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.skill_router import skills
|
||||||
from server.routers.system_router import system
|
from server.routers.system_router import system
|
||||||
from server.routers.task_router import tasks
|
from server.routers.task_router import tasks
|
||||||
|
from server.routers.tool_router import tools
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -28,3 +29,4 @@ router.include_router(graph) # /api/graph/*
|
|||||||
router.include_router(tasks) # /api/tasks/*
|
router.include_router(tasks) # /api/tasks/*
|
||||||
router.include_router(mcp) # /api/system/mcp-servers/*
|
router.include_router(mcp) # /api/system/mcp-servers/*
|
||||||
router.include_router(skills) # /api/system/skills/*
|
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 - 核心基类
|
# Base classes - 核心基类
|
||||||
from src.agents.common.base import BaseAgent
|
from src.agents.common.base import BaseAgent
|
||||||
from src.agents.common.context import BaseContext
|
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
|
from src.agents.common.state import BaseState
|
||||||
|
|
||||||
# Tools - 核心工具函数
|
# 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)
|
# MCP - Agent 层统一入口(自动过滤 disabled_tools)
|
||||||
from src.services.mcp_service import get_enabled_mcp_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.services.mcp_service import get_mcp_server_names
|
||||||
from src.utils import logger
|
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)
|
@dataclass(kw_only=True)
|
||||||
|
|||||||
@ -12,7 +12,8 @@ from langchain_core.messages import SystemMessage, ToolMessage
|
|||||||
from langgraph.types import Command
|
from langgraph.types import Command
|
||||||
|
|
||||||
from src.agents.common import load_chat_model
|
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.mcp_service import get_enabled_mcp_tools
|
||||||
from src.services.skill_resolver import (
|
from src.services.skill_resolver import (
|
||||||
SkillSessionSnapshot,
|
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
|
from typing import Annotated, Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from langchain.tools import tool
|
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
|
|
||||||
from src import config, graph_base
|
from src import config, graph_base
|
||||||
from src.agents.common.toolkits.kbs import get_kb_based_tools
|
from src.agents.common.toolkits import tool
|
||||||
from src.services.mcp_service import get_enabled_mcp_tools
|
|
||||||
from src.storage.minio import aupload_file_to_minio
|
from src.storage.minio import aupload_file_to_minio
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
||||||
@ -24,12 +22,15 @@ def get_tavily_search():
|
|||||||
from langchain_tavily import TavilySearch
|
from langchain_tavily import TavilySearch
|
||||||
|
|
||||||
_tavily_search_instance = 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
|
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:
|
def calculator(a: float, b: float, operation: str) -> float:
|
||||||
|
"""计算器:对给定的2个数字进行基本数学运算"""
|
||||||
try:
|
try:
|
||||||
if operation == "add":
|
if operation == "add":
|
||||||
return a + b
|
return a + b
|
||||||
@ -48,7 +49,7 @@ def calculator(a: float, b: float, operation: str) -> float:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(category="buildin", tags=["图片", "测试"], display_name="文生图测试")
|
||||||
async def text_to_img_demo(text: str) -> str:
|
async def text_to_img_demo(text: str) -> str:
|
||||||
"""【测试用】使用模型生成图片, 会返回图片的URL"""
|
"""【测试用】使用模型生成图片, 会返回图片的URL"""
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ async def text_to_img_demo(text: str) -> str:
|
|||||||
return image_url
|
return image_url
|
||||||
|
|
||||||
|
|
||||||
@tool(name_or_callable="human_in_the_loop_debug", description="请求人工审批工具,用于在执行重要操作前获得人类确认。")
|
@tool(category="debug", tags=["内置", "审批"], display_name="人工审批")
|
||||||
def get_approved_user_goal(
|
def get_approved_user_goal(
|
||||||
operation_description: str,
|
operation_description: str,
|
||||||
) -> dict:
|
) -> 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:
|
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
|
||||||
"""使用这个工具可以查询知识图谱中包含的三元组信息。关键词(query),使用可能帮助回答这个问题的关键词进行查询,不要直接使用用户的原始输入去查询。"""
|
"""使用这个工具可以查询知识图谱中包含的三元组信息。关键词(query),使用可能帮助回答这个问题的关键词进行查询,不要直接使用用户的原始输入去查询。"""
|
||||||
try:
|
try:
|
||||||
@ -145,59 +146,8 @@ def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge
|
|||||||
return f"知识图谱查询失败: {str(e)}"
|
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:
|
def get_buildin_tools() -> list:
|
||||||
"""注册静态工具"""
|
"""获取内置工具列表"""
|
||||||
static_tools = [
|
static_tools = [
|
||||||
query_knowledge_graph,
|
query_knowledge_graph,
|
||||||
get_approved_user_goal,
|
get_approved_user_goal,
|
||||||
@ -205,42 +155,7 @@ def get_buildin_tools() -> list:
|
|||||||
text_to_img_demo,
|
text_to_img_demo,
|
||||||
]
|
]
|
||||||
|
|
||||||
# subagents 工具
|
# 始终添加 tavily_search,无论是否配置 API_KEY(调用时会出错)
|
||||||
from .subagents import calc_agent_tool
|
static_tools.append(get_tavily_search())
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return static_tools
|
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
|
from .tools import get_mysql_tools
|
||||||
|
|
||||||
__all__ = ["get_mysql_tools"]
|
__all__ = ["get_mysql_tools"]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from langchain.tools import tool
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from src.agents.common.toolkits import tool
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
||||||
from .connection import (
|
from .connection import (
|
||||||
@ -52,7 +52,7 @@ class TableListModel(BaseModel):
|
|||||||
pass
|
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:
|
def mysql_list_tables() -> str:
|
||||||
"""【查询表名及说明】获取数据库中的所有表名
|
"""【查询表名及说明】获取数据库中的所有表名
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ class TableDescribeModel(BaseModel):
|
|||||||
table_name: str = Field(description="要查询的表名", example="users")
|
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:
|
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)
|
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(
|
def mysql_query(
|
||||||
sql: Annotated[str, "要执行的SQL查询语句(只能是SELECT语句)"],
|
sql: Annotated[str, "要执行的SQL查询语句(只能是SELECT语句)"],
|
||||||
timeout: Annotated[int | None, "查询超时时间(秒),默认60秒,最大600秒"] = 60,
|
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 import BaseAgent, load_chat_model
|
||||||
from src.agents.common.backends import create_agent_composite_backend
|
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.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.services.mcp_service import get_tools_from_all_servers
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
||||||
|
|||||||
@ -80,7 +80,7 @@ def validate_skill_slug(slug: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _get_buildin_tool_names() -> list[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()]
|
return [tool.name for tool in get_buildin_tools()]
|
||||||
|
|
||||||
@ -91,11 +91,19 @@ def get_skills_root_dir() -> Path:
|
|||||||
return root
|
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)
|
repo = SkillRepository(db)
|
||||||
items = await repo.list_all()
|
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 {
|
return {
|
||||||
"tools": _get_buildin_tool_names(),
|
"tools": tool_list,
|
||||||
"mcps": get_mcp_server_names(),
|
"mcps": get_mcp_server_names(),
|
||||||
"skills": [item.slug for item in items],
|
"skills": [item.slug for item in items],
|
||||||
}
|
}
|
||||||
@ -106,6 +114,14 @@ async def list_skills(db: AsyncSession) -> list[Skill]:
|
|||||||
return await repo.list_all()
|
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(
|
def _validate_dependencies(
|
||||||
*,
|
*,
|
||||||
slug: str,
|
slug: str,
|
||||||
@ -118,7 +134,8 @@ def _validate_dependencies(
|
|||||||
mcps = _normalize_string_list(mcp_dependencies)
|
mcps = _normalize_string_list(mcp_dependencies)
|
||||||
skills = _normalize_string_list(skill_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]
|
invalid_tools = [name for name in tools if name not in available_tools]
|
||||||
if invalid_tools:
|
if invalid_tools:
|
||||||
raise ValueError(f"存在无效工具依赖: {', '.join(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 './department_api' // 部门管理API
|
||||||
export * from './mcp_api' // MCP API
|
export * from './mcp_api' // MCP API
|
||||||
export * from './skill_api' // Skills API
|
export * from './skill_api' // Skills API
|
||||||
|
export * from './tool_api' // 工具 API
|
||||||
|
|
||||||
// 导出基础工具函数
|
// 导出基础工具函数
|
||||||
export {
|
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 */
|
||||||
.main-panel {
|
.main-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -373,6 +373,7 @@ import { useAgentStore } from '@/stores/agent'
|
|||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useDatabaseStore } from '@/stores/database'
|
import { useDatabaseStore } from '@/stores/database'
|
||||||
import { skillApi } from '@/apis/skill_api'
|
import { skillApi } from '@/apis/skill_api'
|
||||||
|
import { toolApi } from '@/apis/tool_api'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
@ -401,6 +402,7 @@ watch(
|
|||||||
if (val) {
|
if (val) {
|
||||||
databaseStore.loadDatabases().catch(() => {})
|
databaseStore.loadDatabases().catch(() => {})
|
||||||
loadLiveSkillOptions().catch(() => {})
|
loadLiveSkillOptions().catch(() => {})
|
||||||
|
loadToolOptions().catch(() => {})
|
||||||
if (selectedAgentId.value) {
|
if (selectedAgentId.value) {
|
||||||
try {
|
try {
|
||||||
await agentStore.fetchAgentDetail(selectedAgentId.value, true)
|
await agentStore.fetchAgentDetail(selectedAgentId.value, true)
|
||||||
@ -433,6 +435,7 @@ const selectionSearchText = ref('')
|
|||||||
const systemPromptEditMode = ref(false)
|
const systemPromptEditMode = ref(false)
|
||||||
const activeTab = ref('basic')
|
const activeTab = ref('basic')
|
||||||
const liveSkillOptions = ref([])
|
const liveSkillOptions = ref([])
|
||||||
|
const toolOptionsFromApi = ref([])
|
||||||
|
|
||||||
const isEmptyConfig = computed(() => {
|
const isEmptyConfig = computed(() => {
|
||||||
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0
|
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) => {
|
const getConfigOptions = (value) => {
|
||||||
if (value?.template_metadata?.kind === 'tools') {
|
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') {
|
if (value?.template_metadata?.kind === 'knowledges') {
|
||||||
return databaseStore.databases || []
|
return databaseStore.databases || []
|
||||||
@ -644,13 +667,9 @@ const getOptionLabelFromValue = (key, val) => {
|
|||||||
|
|
||||||
const openSelectionModal = async (key) => {
|
const openSelectionModal = async (key) => {
|
||||||
currentConfigKey.value = key
|
currentConfigKey.value = key
|
||||||
// 如果是工具,可能需要刷新
|
// 如果是工具,从 API 刷新工具列表
|
||||||
if (configurableItems.value[key]?.template_metadata?.kind === 'tools' && selectedAgentId.value) {
|
if (configurableItems.value[key]?.template_metadata?.kind === 'tools') {
|
||||||
try {
|
await loadToolOptions()
|
||||||
await agentStore.fetchAgentDetail(selectedAgentId.value, true)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('刷新工具列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 如果是知识库,需要获取知识库列表
|
// 如果是知识库,需要获取知识库列表
|
||||||
if (configurableItems.value[key]?.template_metadata?.kind === 'knowledges') {
|
if (configurableItems.value[key]?.template_metadata?.kind === 'knowledges') {
|
||||||
|
|||||||
@ -25,29 +25,30 @@
|
|||||||
<div v-if="filteredServers.length === 0" class="empty-text">
|
<div v-if="filteredServers.length === 0" class="empty-text">
|
||||||
<a-empty :image="false" :description="searchQuery ? '无匹配服务器' : '暂无服务器'" />
|
<a-empty :image="false" :description="searchQuery ? '无匹配服务器' : '暂无服务器'" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<template v-for="(server, index) in filteredServers" :key="server.name">
|
||||||
v-for="server in filteredServers"
|
<div
|
||||||
:key="server.name"
|
class="list-item"
|
||||||
class="list-item"
|
:class="{ active: currentServer?.name === server.name, disabled: !server.enabled }"
|
||||||
:class="{ active: currentServer?.name === server.name, disabled: !server.enabled }"
|
@click="selectServer(server)"
|
||||||
@click="selectServer(server)"
|
>
|
||||||
>
|
<div class="item-header">
|
||||||
<div class="item-header">
|
<span class="server-icon">{{ server.icon || '🔌' }}</span>
|
||||||
<span class="server-icon">{{ server.icon || '🔌' }}</span>
|
<span class="item-name">{{ server.name }}</span>
|
||||||
<span class="item-name">{{ server.name }}</span>
|
<a-switch
|
||||||
<a-switch
|
size="small"
|
||||||
size="small"
|
:checked="server.enabled"
|
||||||
:checked="server.enabled"
|
@change="handleToggleServer(server)"
|
||||||
@change="handleToggleServer(server)"
|
@click.stop
|
||||||
@click.stop
|
:loading="toggleLoading === server.name"
|
||||||
: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>
|
||||||
<div class="item-details">
|
<div v-if="index < filteredServers.length - 1" class="list-separator"></div>
|
||||||
<a-tag size="small" class="transport-tag">{{ server.transport }}</a-tag>
|
</template>
|
||||||
<span class="item-desc">{{ server.description || '暂无描述' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -16,25 +16,26 @@
|
|||||||
<div v-if="filteredSkills.length === 0" class="empty-text">
|
<div v-if="filteredSkills.length === 0" class="empty-text">
|
||||||
<a-empty :image="false" description="无匹配技能" />
|
<a-empty :image="false" description="无匹配技能" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<template v-for="(skill, index) in filteredSkills" :key="skill.slug">
|
||||||
v-for="skill in filteredSkills"
|
<div
|
||||||
:key="skill.slug"
|
class="list-item"
|
||||||
class="list-item"
|
:class="{ active: currentSkill?.slug === skill.slug }"
|
||||||
:class="{ active: currentSkill?.slug === skill.slug }"
|
@click="selectSkill(skill)"
|
||||||
@click="selectSkill(skill)"
|
>
|
||||||
>
|
<div class="item-header">
|
||||||
<div class="item-header">
|
<Box :size="16" class="item-icon" />
|
||||||
<Box :size="16" class="item-icon" />
|
<span class="item-name">{{ skill.name }}</span>
|
||||||
<span class="item-name">{{ skill.name }}</span>
|
</div>
|
||||||
</div>
|
<div class="item-details">
|
||||||
<div class="item-details">
|
<span class="item-slug">{{ skill.slug }}</span>
|
||||||
<span class="item-slug">{{ skill.slug }}</span>
|
<div class="item-badges">
|
||||||
<div class="item-badges">
|
<span v-if="skill.tool_dependencies?.length" class="dot-badge blue" title="工具依赖"></span>
|
||||||
<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>
|
||||||
<span v-if="skill.mcp_dependencies?.length" class="dot-badge green" title="MCP依赖"></span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="index < filteredSkills.length - 1" class="list-separator"></div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -232,7 +233,9 @@ const canSave = computed(() => {
|
|||||||
|
|
||||||
const formatRelativeTime = (time) => time ? dayjs(time).fromNow() : '-'
|
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 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 })))
|
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">
|
<div class="extensions-header">
|
||||||
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
|
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
|
||||||
<a-tab-pane key="skills" tab="Skills 管理" />
|
<a-tab-pane key="skills" tab="Skills 管理" />
|
||||||
|
<a-tab-pane key="tools" tab="工具" />
|
||||||
<a-tab-pane key="mcp" tab="MCP 服务器" />
|
<a-tab-pane key="mcp" tab="MCP 服务器" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
@ -24,6 +25,13 @@
|
|||||||
<span>刷新</span>
|
<span>刷新</span>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</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 的按钮 -->
|
<!-- MCP Tab 的按钮 -->
|
||||||
<template v-else-if="activeTab === 'mcp'">
|
<template v-else-if="activeTab === 'mcp'">
|
||||||
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
||||||
@ -46,6 +54,12 @@
|
|||||||
@refresh="handleSkillsRefresh"
|
@refresh="handleSkillsRefresh"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="activeTab === 'tools'" class="tab-panel">
|
||||||
|
<ToolsManagerComponent
|
||||||
|
ref="toolsRef"
|
||||||
|
@refresh="handleToolsRefresh"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
||||||
<McpServersComponent
|
<McpServersComponent
|
||||||
ref="mcpRef"
|
ref="mcpRef"
|
||||||
@ -61,15 +75,18 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Upload, RotateCw, Plus } from 'lucide-vue-next'
|
import { Upload, RotateCw, Plus } from 'lucide-vue-next'
|
||||||
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
||||||
|
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
|
||||||
import McpServersComponent from '@/components/McpServersComponent.vue'
|
import McpServersComponent from '@/components/McpServersComponent.vue'
|
||||||
|
|
||||||
const activeTab = ref('skills')
|
const activeTab = ref('skills')
|
||||||
const skillsRef = ref(null)
|
const skillsRef = ref(null)
|
||||||
|
const toolsRef = ref(null)
|
||||||
const mcpRef = ref(null)
|
const mcpRef = ref(null)
|
||||||
|
|
||||||
// Skills 相关状态(从子组件透传)
|
// Skills 相关状态(从子组件透传)
|
||||||
const skillsLoading = ref(false)
|
const skillsLoading = ref(false)
|
||||||
const skillsImporting = ref(false)
|
const skillsImporting = ref(false)
|
||||||
|
const toolsLoading = ref(false)
|
||||||
const mcpLoading = ref(false)
|
const mcpLoading = ref(false)
|
||||||
|
|
||||||
// 暴露给子组件的状态更新
|
// 暴露给子组件的状态更新
|
||||||
@ -78,6 +95,10 @@ const updateSkillsState = (loading, importing) => {
|
|||||||
skillsImporting.value = importing
|
skillsImporting.value = importing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateToolsState = (loading) => {
|
||||||
|
toolsLoading.value = loading
|
||||||
|
}
|
||||||
|
|
||||||
const updateMcpState = (loading) => {
|
const updateMcpState = (loading) => {
|
||||||
mcpLoading.value = 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 事件处理
|
// MCP 事件处理
|
||||||
const handleMcpAdd = () => {
|
const handleMcpAdd = () => {
|
||||||
if (mcpRef.value?.showAddModal) {
|
if (mcpRef.value?.showAddModal) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user