feat: 添加subagents管理功能
- 实现了应用程序启动期间的理subagents初始化。 - 为subagents的 CRUD 操作和存储库方法创建了单元测试。 - 开发了用于列出、检索、创建、更新和删除子代理的子代理 API 端点。 - 添加了用于在前端管理子代理的 SubAgentsComponent,包括搜索和详细信息视图。
This commit is contained in:
parent
8528dae331
commit
d655cbe640
@ -104,6 +104,6 @@ class DeepContext(BaseContext):
|
||||
default="siliconflow/Pro/deepseek-ai/DeepSeek-V3.2",
|
||||
metadata={
|
||||
"name": "Sub-agent Model",
|
||||
"description": "The model used by sub-agents (e.g., critique-agent, research-agent).",
|
||||
"description": "子智能体的默认模型,会被子智能体的配置覆盖。",
|
||||
},
|
||||
)
|
||||
|
||||
@ -16,6 +16,7 @@ from yuxi.agents.common.middlewares.knowledge_base_middleware import KnowledgeBa
|
||||
from yuxi.agents.common.middlewares.skills_middleware import SkillsMiddleware
|
||||
from yuxi.agents.common.toolkits.buildin.tools import _create_tavily_search
|
||||
from yuxi.services.mcp_service import get_tools_from_all_servers
|
||||
from yuxi.services.subagent_service import get_subagent_specs, resolve_subagent_tools
|
||||
from yuxi.utils import logger
|
||||
|
||||
from .context import DeepContext
|
||||
@ -26,43 +27,6 @@ def _create_fs_backend(rt):
|
||||
return create_agent_composite_backend(rt)
|
||||
|
||||
|
||||
def _get_research_sub_agent(search_tools: list) -> dict:
|
||||
"""Get research sub-agent config with search tools."""
|
||||
return {
|
||||
"name": "research-agent",
|
||||
"description": ("利用搜索工具,用于研究更深入的问题。将调研结果写入到主题研究文件中。"),
|
||||
"system_prompt": (
|
||||
"你是一位专注的研究员。你的工作是根据用户的问题进行研究。"
|
||||
"进行彻底的研究,然后用详细的答案回复用户的问题,只有你的最终答案会被传递给用户。"
|
||||
"除了你的最终信息,他们不会知道任何其他事情,所以你的最终报告应该就是你的最终信息!"
|
||||
"将调研结果保存到主题研究文件中 /sub_research/xxx.md 中。"
|
||||
),
|
||||
"tools": search_tools,
|
||||
}
|
||||
|
||||
|
||||
critique_sub_agent = {
|
||||
"name": "critique-agent",
|
||||
"description": "用于评论最终报告。给这个代理一些关于你希望它如何评论报告的信息。",
|
||||
"system_prompt": (
|
||||
"你是一位专注的编辑。你的任务是评论一份报告。\n\n"
|
||||
"你可以在 `final_report.md` 找到这份报告。\n\n"
|
||||
"你可以在 `question.txt` 找到这份报告的问题/主题。\n\n"
|
||||
"用户可能会要求评论报告的特定方面。请用详细的评论回复用户,指出报告中可以改进的地方。\n\n"
|
||||
"如果有助于你评论报告,你可以使用搜索工具来搜索信息\n\n"
|
||||
"不要自己写入 `final_report.md`。\n\n"
|
||||
"需要检查的事项:\n"
|
||||
"- 检查每个部分的标题是否恰当\n"
|
||||
"- 检查报告的写法是否像论文或教科书——它应该是以文本为主,不要只是一个项目符号列表!\n"
|
||||
"- 检查报告是否全面。如果任何段落或部分过短,或缺少重要细节,请指出来。\n"
|
||||
"- 检查文章是否涵盖了行业的关键领域,确保了整体理解,并且没有遗漏重要部分。\n"
|
||||
"- 检查文章是否深入分析了原因、影响和趋势,提供了有价值的见解\n"
|
||||
"- 检查文章是否紧扣研究主题并直接回答问题\n"
|
||||
"- 检查文章是否结构清晰、语言流畅、易于理解。"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeepAgent(BaseAgent):
|
||||
name = "深度分析智能体"
|
||||
description = "具备规划、深度分析和子智能体协作能力的智能体,可以处理复杂的多步骤任务"
|
||||
@ -103,8 +67,10 @@ class DeepAgent(BaseAgent):
|
||||
all_mcp_tools = await get_tools_from_all_servers()
|
||||
# 合并搜索工具和 MCP 工具
|
||||
|
||||
# Build subagents with search tools
|
||||
research_sub_agent = _get_research_sub_agent(search_tools)
|
||||
# 从数据库加载 subagent specs(工具名称未解析)
|
||||
user_subagents = await get_subagent_specs()
|
||||
# 解析工具名称为实际工具实例
|
||||
user_subagents = resolve_subagent_tools(user_subagents, search_tools + all_mcp_tools)
|
||||
|
||||
# 主 Agent 上下文优化:90k tokens 触发压缩(128k context window 的 70%)
|
||||
summary_middleware = SummaryOffloadMiddleware(
|
||||
@ -127,7 +93,7 @@ class DeepAgent(BaseAgent):
|
||||
subagents_middleware = SubAgentMiddleware(
|
||||
default_model=sub_model,
|
||||
default_tools=search_tools,
|
||||
subagents=[critique_sub_agent, research_sub_agent],
|
||||
subagents=user_subagents,
|
||||
default_middleware=[
|
||||
RuntimeConfigMiddleware(
|
||||
model_context_name="subagents_model",
|
||||
|
||||
112
backend/package/yuxi/repositories/subagent_repository.py
Normal file
112
backend/package/yuxi/repositories/subagent_repository.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""SubAgent 数据访问层"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.storage.postgres.models_business import SubAgent
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
|
||||
class SubAgentRepository:
|
||||
def __init__(self, db_session: AsyncSession):
|
||||
self.db = db_session
|
||||
|
||||
async def list_all(self) -> list[SubAgent]:
|
||||
"""获取所有 SubAgent,按 updated_at 降序"""
|
||||
result = await self.db.execute(select(SubAgent).order_by(SubAgent.updated_at.desc()))
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_name(self, name: str) -> SubAgent | None:
|
||||
"""根据名称获取 SubAgent"""
|
||||
result = await self.db.execute(select(SubAgent).where(SubAgent.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def exists_name(self, name: str) -> bool:
|
||||
"""检查名称是否存在"""
|
||||
return (await self.get_by_name(name)) is not None
|
||||
|
||||
async def create(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
description: str,
|
||||
system_prompt: str,
|
||||
tools: list[str] | None,
|
||||
model: str | None,
|
||||
is_builtin: bool,
|
||||
created_by: str | None,
|
||||
) -> SubAgent:
|
||||
now = utc_now_naive()
|
||||
item = SubAgent(
|
||||
name=name,
|
||||
description=description,
|
||||
system_prompt=system_prompt,
|
||||
tools=tools or [],
|
||||
model=model,
|
||||
is_builtin=is_builtin,
|
||||
created_by=created_by,
|
||||
updated_by=created_by,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
self.db.add(item)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(item)
|
||||
return item
|
||||
|
||||
async def update(
|
||||
self,
|
||||
item: SubAgent,
|
||||
*,
|
||||
description: str | None,
|
||||
system_prompt: str | None,
|
||||
tools: list[str] | None,
|
||||
model: str | None,
|
||||
model_provided: bool = False,
|
||||
updated_by: str | None,
|
||||
) -> SubAgent:
|
||||
if description is not None:
|
||||
item.description = description
|
||||
if system_prompt is not None:
|
||||
item.system_prompt = system_prompt
|
||||
if tools is not None:
|
||||
item.tools = tools
|
||||
if model_provided:
|
||||
item.model = model
|
||||
item.updated_by = updated_by
|
||||
item.updated_at = utc_now_naive()
|
||||
await self.db.commit()
|
||||
await self.db.refresh(item)
|
||||
return item
|
||||
|
||||
async def delete(self, item: SubAgent) -> None:
|
||||
"""删除 SubAgent"""
|
||||
await self.db.delete(item)
|
||||
await self.db.commit()
|
||||
|
||||
async def upsert(self, data: dict[str, Any], created_by: str | None) -> SubAgent:
|
||||
"""Upsert 操作,如果存在则更新,否则创建"""
|
||||
name = data["name"]
|
||||
existing = await self.get_by_name(name)
|
||||
if existing:
|
||||
return await self.update(
|
||||
existing,
|
||||
description=data.get("description", existing.description),
|
||||
system_prompt=data.get("system_prompt", existing.system_prompt),
|
||||
tools=data.get("tools", existing.tools),
|
||||
model=data.get("model", existing.model),
|
||||
model_provided="model" in data,
|
||||
updated_by=created_by,
|
||||
)
|
||||
else:
|
||||
return await self.create(
|
||||
name=name,
|
||||
description=data["description"],
|
||||
system_prompt=data["system_prompt"],
|
||||
tools=data.get("tools"),
|
||||
model=data.get("model"),
|
||||
is_builtin=data.get("is_builtin", False),
|
||||
created_by=created_by,
|
||||
)
|
||||
208
backend/package/yuxi/services/subagent_service.py
Normal file
208
backend/package/yuxi/services/subagent_service.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""SubAgent 服务层"""
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.repositories.subagent_repository import SubAgentRepository
|
||||
from yuxi.services.mcp_service import get_tools_from_all_servers
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
|
||||
# SubAgent specs cache for get_subagent_specs
|
||||
_subagent_specs_cache: list[dict[str, Any]] | None = None
|
||||
_subagent_specs_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _get_session(db: AsyncSession | None = None):
|
||||
"""获取数据库会话的上下文管理器"""
|
||||
if db is not None:
|
||||
yield db
|
||||
else:
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
yield session
|
||||
|
||||
# 内置 SubAgent 配置
|
||||
_DEFAULT_SUBAGENTS = [
|
||||
{
|
||||
"name": "research-agent",
|
||||
"description": "利用搜索工具,用于研究更深入的问题。将调研结果写入到主题研究文件中。",
|
||||
"system_prompt": (
|
||||
"你是一位专注的研究员。你的工作是根据用户的问题进行研究。"
|
||||
"进行彻底的研究,然后用详细的答案回复用户的问题,只有你的最终答案会被传递给用户。"
|
||||
"除了你的最终信息,他们不会知道任何其他事情,所以你的最终报告应该就是你的最终信息!"
|
||||
"将调研结果保存到主题研究文件中 /sub_research/xxx.md 中。"
|
||||
),
|
||||
"tools": ["tavily_search"],
|
||||
"is_builtin": True,
|
||||
},
|
||||
{
|
||||
"name": "critique-agent",
|
||||
"description": "用于评论最终报告。给这个代理一些关于你希望它如何评论报告的信息。",
|
||||
"system_prompt": (
|
||||
"你是一位专注的编辑。你的任务是评论一份报告。\n\n"
|
||||
"你可以在 `final_report.md` 找到这份报告。\n\n"
|
||||
"你可以在 `question.txt` 找到这份报告的问题/主题。\n\n"
|
||||
"用户可能会要求评论报告的特定方面。请用详细的评论回复用户,指出报告中可以改进的地方。\n\n"
|
||||
"如果有助于你评论报告,你可以使用搜索工具来搜索信息\n\n"
|
||||
"不要自己写入 `final_report.md`。\n\n"
|
||||
"需要检查的事项:\n"
|
||||
"- 检查每个部分的标题是否恰当\n"
|
||||
"- 检查报告的写法是否像论文或教科书——它应该是以文本为主,不要只是一个项目符号列表!\n"
|
||||
"- 检查报告是否全面。如果任何段落或部分过短,或缺少重要细节,请指出来。\n"
|
||||
"- 检查文章是否涵盖了行业的关键领域,确保了整体理解,并且没有遗漏重要部分。\n"
|
||||
"- 检查文章是否深入分析了原因、影响和趋势,提供了有价值的见解\n"
|
||||
"- 检查文章是否紧扣研究主题并直接回答问题\n"
|
||||
"- 检查文章是否结构清晰、语言流畅、易于理解。"
|
||||
),
|
||||
"tools": [],
|
||||
"is_builtin": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def init_builtin_subagents() -> None:
|
||||
"""初始化内置 SubAgent(仅创建不存在的)"""
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
repo = SubAgentRepository(session)
|
||||
for data in _DEFAULT_SUBAGENTS:
|
||||
if not await repo.exists_name(data["name"]):
|
||||
await repo.create(
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
system_prompt=data["system_prompt"],
|
||||
tools=data.get("tools", []),
|
||||
model=None,
|
||||
is_builtin=data.get("is_builtin", False),
|
||||
created_by="system",
|
||||
)
|
||||
|
||||
|
||||
async def get_subagent_specs(db: AsyncSession | None = None) -> list[dict[str, Any]]:
|
||||
"""获取所有 subagent specs,用于 SubAgentMiddleware(工具名称未解析)"""
|
||||
global _subagent_specs_cache
|
||||
if _subagent_specs_cache is not None:
|
||||
return deepcopy(_subagent_specs_cache)
|
||||
async with _subagent_specs_lock:
|
||||
if _subagent_specs_cache is not None:
|
||||
return deepcopy(_subagent_specs_cache)
|
||||
async with _get_session(db) as session:
|
||||
repo = SubAgentRepository(session)
|
||||
subagents = await repo.list_all()
|
||||
_subagent_specs_cache = [sa.to_subagent_spec() for sa in subagents]
|
||||
return deepcopy(_subagent_specs_cache)
|
||||
|
||||
|
||||
def invalidate_subagent_specs_cache() -> None:
|
||||
"""清除 subagent specs 缓存"""
|
||||
global _subagent_specs_cache
|
||||
_subagent_specs_cache = None
|
||||
|
||||
|
||||
def resolve_subagent_tools(specs: list[dict[str, Any]], available_tools: list[Any]) -> list[dict[str, Any]]:
|
||||
"""将 subagent specs 中的工具名称解析为实际工具实例"""
|
||||
available_by_name = {tool.name: tool for tool in available_tools if hasattr(tool, "name")}
|
||||
resolved_specs = []
|
||||
for spec in specs:
|
||||
resolved_spec = dict(spec)
|
||||
tool_names = spec.get("tools", [])
|
||||
resolved_spec["tools"] = [
|
||||
available_by_name[name] for name in tool_names if isinstance(name, str) and name in available_by_name
|
||||
]
|
||||
resolved_specs.append(resolved_spec)
|
||||
return resolved_specs
|
||||
|
||||
|
||||
async def _get_available_tools() -> list[Any]:
|
||||
"""获取所有可用的工具实例"""
|
||||
from yuxi.agents.common.toolkits.buildin.tools import _create_tavily_search
|
||||
|
||||
tools = []
|
||||
# 添加 tavily_search 工具
|
||||
tavily = _create_tavily_search()
|
||||
if tavily:
|
||||
tools.append(tavily)
|
||||
# 添加 MCP 工具
|
||||
mcp_tools = await get_tools_from_all_servers()
|
||||
tools.extend(mcp_tools)
|
||||
return tools
|
||||
|
||||
async def get_all_subagents(db: AsyncSession | None = None) -> list[dict[str, Any]]:
|
||||
"""获取所有 SubAgent(含禁用的)"""
|
||||
async with _get_session(db) as session:
|
||||
repo = SubAgentRepository(session)
|
||||
items = await repo.list_all()
|
||||
return [item.to_dict() for item in items]
|
||||
|
||||
|
||||
async def get_subagent(name: str, db: AsyncSession | None = None) -> dict[str, Any] | None:
|
||||
"""获取单个 SubAgent"""
|
||||
async with _get_session(db) as session:
|
||||
repo = SubAgentRepository(session)
|
||||
item = await repo.get_by_name(name)
|
||||
return item.to_dict() if item else None
|
||||
|
||||
|
||||
async def create_subagent(
|
||||
data: dict[str, Any],
|
||||
created_by: str | None,
|
||||
db: AsyncSession | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""创建 SubAgent"""
|
||||
async with _get_session(db) as session:
|
||||
repo = SubAgentRepository(session)
|
||||
item = await repo.create(
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
system_prompt=data["system_prompt"],
|
||||
tools=data.get("tools"),
|
||||
model=data.get("model"),
|
||||
is_builtin=False,
|
||||
created_by=created_by,
|
||||
)
|
||||
invalidate_subagent_specs_cache()
|
||||
return item.to_dict()
|
||||
|
||||
|
||||
async def update_subagent(
|
||||
name: str,
|
||||
data: dict[str, Any],
|
||||
updated_by: str | None,
|
||||
db: AsyncSession | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""更新 SubAgent"""
|
||||
async with _get_session(db) as session:
|
||||
repo = SubAgentRepository(session)
|
||||
item = await repo.get_by_name(name)
|
||||
if not item:
|
||||
return None
|
||||
if item.is_builtin:
|
||||
raise ValueError("内置 SubAgent 不可编辑")
|
||||
item = await repo.update(
|
||||
item,
|
||||
description=data.get("description"),
|
||||
system_prompt=data.get("system_prompt"),
|
||||
tools=data.get("tools"),
|
||||
model=data.get("model"),
|
||||
model_provided="model" in data,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
invalidate_subagent_specs_cache()
|
||||
return item.to_dict()
|
||||
|
||||
|
||||
async def delete_subagent(name: str, db: AsyncSession | None = None) -> bool:
|
||||
"""删除 SubAgent"""
|
||||
async with _get_session(db) as session:
|
||||
repo = SubAgentRepository(session)
|
||||
item = await repo.get_by_name(name)
|
||||
if not item:
|
||||
return False
|
||||
if item.is_builtin:
|
||||
raise ValueError("内置 SubAgent 不可删除")
|
||||
await repo.delete(item)
|
||||
invalidate_subagent_specs_cache()
|
||||
return True
|
||||
@ -547,6 +547,51 @@ class TaskRecord(Base):
|
||||
return data
|
||||
|
||||
|
||||
class SubAgent(Base):
|
||||
"""SubAgent 模型 - 用于动态配置子智能体"""
|
||||
|
||||
__tablename__ = "subagents"
|
||||
|
||||
name = Column(String(128), primary_key=True, comment="唯一标识")
|
||||
description = Column(Text, nullable=False, comment="描述")
|
||||
system_prompt = Column(Text, nullable=False, comment="系统提示词")
|
||||
tools = Column(JSON, nullable=False, default=list, comment="工具名称列表")
|
||||
model = Column(String(128), nullable=True, comment="可选的模型覆盖")
|
||||
|
||||
is_builtin = Column(Boolean, nullable=False, default=False, comment="是否内置")
|
||||
|
||||
created_by = Column(String(100), nullable=True)
|
||||
updated_by = Column(String(100), nullable=True)
|
||||
created_at = Column(DateTime, default=utc_now_naive)
|
||||
updated_at = Column(DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"system_prompt": self.system_prompt,
|
||||
"tools": self.tools or [],
|
||||
"model": self.model,
|
||||
"is_builtin": bool(self.is_builtin),
|
||||
"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_subagent_spec(self) -> dict[str, Any]:
|
||||
"""转换为 SubAgentMiddleware 需要的 spec 格式"""
|
||||
spec = {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"system_prompt": self.system_prompt,
|
||||
"tools": self.tools or [],
|
||||
}
|
||||
if self.model:
|
||||
spec["model"] = self.model
|
||||
return spec
|
||||
|
||||
|
||||
class AgentRun(Base):
|
||||
"""AgentRun table - 运行任务表"""
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ from server.routers.evaluation_router import evaluation
|
||||
from server.routers.mcp_router import mcp
|
||||
from server.routers.mindmap_router import mindmap
|
||||
from server.routers.skill_router import skills
|
||||
from server.routers.subagent_router import subagents_router
|
||||
from server.routers.system_router import system
|
||||
from server.routers.task_router import tasks
|
||||
from server.routers.tool_router import tools
|
||||
@ -29,4 +30,5 @@ 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(subagents_router) # /api/system/subagents/*
|
||||
router.include_router(tools) # /api/system/tools/*
|
||||
|
||||
147
backend/server/routers/subagent_router.py
Normal file
147
backend/server/routers/subagent_router.py
Normal file
@ -0,0 +1,147 @@
|
||||
"""SubAgent 管理路由"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from yuxi.services import subagent_service as service
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils import logger
|
||||
|
||||
subagents_router = APIRouter(prefix="/system/subagents", tags=["subagents"])
|
||||
|
||||
|
||||
class SubAgentCreateRequest(BaseModel):
|
||||
name: str = Field(..., description="唯一标识")
|
||||
description: str = Field(..., description="描述")
|
||||
system_prompt: str = Field(..., description="系统提示词")
|
||||
tools: list[str] = Field(default_factory=list, description="工具名称列表")
|
||||
model: str | None = Field(None, description="可选的模型覆盖")
|
||||
|
||||
|
||||
class SubAgentUpdateRequest(BaseModel):
|
||||
description: str | None = Field(None, description="描述")
|
||||
system_prompt: str | None = Field(None, description="系统提示词")
|
||||
tools: list[str] | None = Field(None, description="工具名称列表")
|
||||
model: str | None = Field(None, description="可选的模型覆盖")
|
||||
|
||||
|
||||
def _raise_from_value_error(e: ValueError) -> None:
|
||||
message = str(e)
|
||||
status_code = 404 if "不存在" in message else 400
|
||||
raise HTTPException(status_code=status_code, detail=message)
|
||||
|
||||
|
||||
def _raise_internal_error(action: str, error: Exception) -> None:
|
||||
logger.exception("SubAgent %s failed: %s", action, error)
|
||||
raise HTTPException(status_code=500, detail=f"{action}失败")
|
||||
|
||||
|
||||
def _is_subagent_name_duplicate_error(error: IntegrityError) -> bool:
|
||||
raw_message = str(getattr(error, "orig", error)).lower()
|
||||
return (
|
||||
"duplicate key" in raw_message
|
||||
and "subagents" in raw_message
|
||||
and ("(name)" in raw_message or "subagents_pkey" in raw_message)
|
||||
)
|
||||
|
||||
|
||||
@subagents_router.get("")
|
||||
async def list_subagents_route(
|
||||
_current_user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取 SubAgent 列表(管理员可读)"""
|
||||
try:
|
||||
items = await service.get_all_subagents(db)
|
||||
return {"success": True, "data": items}
|
||||
except Exception as e:
|
||||
_raise_internal_error("获取列表", e)
|
||||
|
||||
|
||||
@subagents_router.get("/{name}")
|
||||
async def get_subagent_route(
|
||||
name: str,
|
||||
_current_user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取单个 SubAgent(管理员可读)"""
|
||||
try:
|
||||
item = await service.get_subagent(name, db)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail=f"SubAgent '{name}' 不存在")
|
||||
return {"success": True, "data": item}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
_raise_internal_error("获取", e)
|
||||
|
||||
|
||||
@subagents_router.post("")
|
||||
async def create_subagent_route(
|
||||
payload: SubAgentCreateRequest,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建 SubAgent(管理员)"""
|
||||
try:
|
||||
data = payload.model_dump()
|
||||
item = await service.create_subagent(data, created_by=current_user.username, db=db)
|
||||
return {"success": True, "data": item}
|
||||
except IntegrityError as e:
|
||||
if _is_subagent_name_duplicate_error(e):
|
||||
raise HTTPException(status_code=409, detail=f"SubAgent '{payload.name}' 已存在")
|
||||
_raise_internal_error("创建", e)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
_raise_from_value_error(e)
|
||||
except Exception as e:
|
||||
_raise_internal_error("创建", e)
|
||||
|
||||
|
||||
@subagents_router.put("/{name}")
|
||||
async def update_subagent_route(
|
||||
name: str,
|
||||
payload: SubAgentUpdateRequest,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新 SubAgent(管理员)"""
|
||||
try:
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
item = await service.update_subagent(name, data, updated_by=current_user.username, db=db)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail=f"SubAgent '{name}' 不存在")
|
||||
return {"success": True, "data": item}
|
||||
except ValueError as e:
|
||||
_raise_from_value_error(e)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
_raise_internal_error("更新", e)
|
||||
|
||||
|
||||
@subagents_router.delete("/{name}")
|
||||
async def delete_subagent_route(
|
||||
name: str,
|
||||
_current_user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除 SubAgent(管理员)"""
|
||||
try:
|
||||
deleted = await service.delete_subagent(name, db=db)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail=f"SubAgent '{name}' 不存在")
|
||||
return {"success": True}
|
||||
except ValueError as e:
|
||||
_raise_from_value_error(e)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
_raise_internal_error("删除", e)
|
||||
@ -4,6 +4,7 @@ from fastapi import FastAPI
|
||||
|
||||
from yuxi.services.task_service import tasker
|
||||
from yuxi.services.mcp_service import init_mcp_servers
|
||||
from yuxi.services.subagent_service import init_builtin_subagents
|
||||
from yuxi.services.run_queue_service import close_queue_clients, get_redis_client
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.knowledge import knowledge_base
|
||||
@ -28,6 +29,13 @@ async def lifespan(app: FastAPI):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize MCP servers during startup: {e}")
|
||||
|
||||
# 初始化内置 SubAgent
|
||||
try:
|
||||
await init_builtin_subagents()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize builtin subagents during startup: {e}")
|
||||
raise
|
||||
|
||||
# 初始化知识库管理器
|
||||
try:
|
||||
await knowledge_base.initialize()
|
||||
|
||||
553
backend/test/test_subagent.py
Normal file
553
backend/test/test_subagent.py
Normal file
@ -0,0 +1,553 @@
|
||||
"""SubAgent 单元测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from yuxi.storage.postgres.models_business import SubAgent
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Router Tests
|
||||
# =============================================================================
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from server.routers.subagent_router import subagents_router
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
|
||||
|
||||
def _build_app() -> FastAPI:
|
||||
app = FastAPI()
|
||||
app.include_router(subagents_router, prefix="/api")
|
||||
|
||||
async def fake_db():
|
||||
return None
|
||||
|
||||
async def fake_admin_user():
|
||||
return User(
|
||||
username="admin",
|
||||
user_id="admin",
|
||||
password_hash="x",
|
||||
role="admin",
|
||||
)
|
||||
|
||||
app.dependency_overrides[get_db] = fake_db
|
||||
app.dependency_overrides[get_admin_user] = fake_admin_user
|
||||
return app
|
||||
|
||||
|
||||
def test_list_subagents_returns_data(monkeypatch):
|
||||
async def fake_get_all_subagents(_db):
|
||||
return [
|
||||
{
|
||||
"name": "research-agent",
|
||||
"description": "Test research agent",
|
||||
"system_prompt": "You are a researcher",
|
||||
"tools": ["tavily_search"],
|
||||
"model": None,
|
||||
"is_builtin": True,
|
||||
"created_by": "system",
|
||||
"updated_by": "system",
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.get_all_subagents", fake_get_all_subagents)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/system/subagents")
|
||||
assert resp.status_code == 200, resp.text
|
||||
payload = resp.json()
|
||||
assert payload["success"] is True
|
||||
assert payload["data"][0]["name"] == "research-agent"
|
||||
assert payload["data"][0]["is_builtin"] is True
|
||||
|
||||
|
||||
def test_get_single_subagent(monkeypatch):
|
||||
async def fake_get_subagent(name, db=None):
|
||||
if name == "research-agent":
|
||||
return {
|
||||
"name": "research-agent",
|
||||
"description": "Test research agent",
|
||||
"system_prompt": "You are a researcher",
|
||||
"tools": ["tavily_search"],
|
||||
"model": None,
|
||||
"is_builtin": True,
|
||||
"created_by": "system",
|
||||
"updated_by": "system",
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.get_subagent", fake_get_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/system/subagents/research-agent")
|
||||
assert resp.status_code == 200, resp.text
|
||||
payload = resp.json()
|
||||
assert payload["success"] is True
|
||||
assert payload["data"]["name"] == "research-agent"
|
||||
|
||||
|
||||
def test_get_single_subagent_not_found(monkeypatch):
|
||||
async def fake_get_subagent(name, db=None):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.get_subagent", fake_get_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/system/subagents/nonexistent")
|
||||
assert resp.status_code == 404, resp.text
|
||||
|
||||
|
||||
def test_create_subagent(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_create_subagent(data, created_by, db=None):
|
||||
captured["data"] = data
|
||||
captured["created_by"] = created_by
|
||||
return {
|
||||
"name": data["name"],
|
||||
"description": data["description"],
|
||||
"system_prompt": data["system_prompt"],
|
||||
"tools": data.get("tools", []),
|
||||
"model": data.get("model"),
|
||||
"is_builtin": False,
|
||||
"created_by": created_by,
|
||||
"updated_by": created_by,
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.create_subagent", fake_create_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.post(
|
||||
"/api/system/subagents",
|
||||
json={
|
||||
"name": "my-agent",
|
||||
"description": "My custom agent",
|
||||
"system_prompt": "You are a helpful assistant",
|
||||
"tools": ["tool_a", "tool_b"],
|
||||
"model": None,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
payload = resp.json()
|
||||
assert payload["success"] is True
|
||||
assert captured["data"]["name"] == "my-agent"
|
||||
assert captured["created_by"] == "admin"
|
||||
|
||||
|
||||
def test_create_subagent_duplicate_returns_409(monkeypatch):
|
||||
async def fake_create_subagent(data, created_by, db=None):
|
||||
raise IntegrityError(
|
||||
"duplicate",
|
||||
{},
|
||||
Exception('duplicate key value violates unique constraint "subagents_pkey"'),
|
||||
)
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.create_subagent", fake_create_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.post(
|
||||
"/api/system/subagents",
|
||||
json={
|
||||
"name": "my-agent",
|
||||
"description": "My custom agent",
|
||||
"system_prompt": "You are a helpful assistant",
|
||||
"tools": [],
|
||||
"model": None,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 409, resp.text
|
||||
|
||||
|
||||
def test_update_subagent(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_update_subagent(name, data, updated_by, db=None):
|
||||
captured["name"] = name
|
||||
captured["data"] = data
|
||||
captured["updated_by"] = updated_by
|
||||
return {
|
||||
"name": name,
|
||||
"description": data.get("description", "Updated description"),
|
||||
"system_prompt": data.get("system_prompt", "Updated prompt"),
|
||||
"tools": data.get("tools", []),
|
||||
"model": data.get("model"),
|
||||
"is_builtin": False,
|
||||
"created_by": "admin",
|
||||
"updated_by": updated_by,
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.update_subagent", fake_update_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.put(
|
||||
"/api/system/subagents/my-agent",
|
||||
json={
|
||||
"description": "Updated description",
|
||||
"system_prompt": "Updated prompt",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
payload = resp.json()
|
||||
assert payload["success"] is True
|
||||
assert captured["name"] == "my-agent"
|
||||
assert captured["updated_by"] == "admin"
|
||||
|
||||
|
||||
def test_update_builtin_subagent_fails(monkeypatch):
|
||||
async def fake_update_subagent(name, data, updated_by, db=None):
|
||||
raise ValueError("内置 SubAgent 不可编辑")
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.update_subagent", fake_update_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.put(
|
||||
"/api/system/subagents/research-agent",
|
||||
json={"description": "Try to update builtin"},
|
||||
)
|
||||
assert resp.status_code == 400, resp.text
|
||||
|
||||
|
||||
def test_delete_subagent(monkeypatch):
|
||||
deleted_name = {"name": None}
|
||||
|
||||
async def fake_delete_subagent(name, db=None):
|
||||
deleted_name["name"] = name
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.delete_subagent", fake_delete_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.delete("/api/system/subagents/my-agent")
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert deleted_name["name"] == "my-agent"
|
||||
|
||||
|
||||
def test_delete_builtin_subagent_fails(monkeypatch):
|
||||
async def fake_delete_subagent(name, db=None):
|
||||
raise ValueError("内置 SubAgent 不可删除")
|
||||
|
||||
monkeypatch.setattr("server.routers.subagent_router.service.delete_subagent", fake_delete_subagent)
|
||||
|
||||
app = _build_app()
|
||||
client = TestClient(app)
|
||||
resp = client.delete("/api/system/subagents/research-agent")
|
||||
assert resp.status_code == 400, resp.text
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Repository Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestSubAgentRepository:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_all(self):
|
||||
from yuxi.repositories.subagent_repository import SubAgentRepository
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.all.return_value = [
|
||||
SubAgent(
|
||||
name="test-agent",
|
||||
description="Test agent",
|
||||
system_prompt="You are a test",
|
||||
tools=["tool_a"],
|
||||
model=None,
|
||||
is_builtin=False,
|
||||
created_by="admin",
|
||||
updated_by="admin",
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
]
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
repo = SubAgentRepository(mock_db)
|
||||
result = await repo.list_all()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "test-agent"
|
||||
mock_db.execute.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_name_found(self):
|
||||
from yuxi.repositories.subagent_repository import SubAgentRepository
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = SubAgent(
|
||||
name="test-agent",
|
||||
description="Test agent",
|
||||
system_prompt="You are a test",
|
||||
tools=[],
|
||||
model=None,
|
||||
is_builtin=False,
|
||||
created_by="admin",
|
||||
updated_by="admin",
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
repo = SubAgentRepository(mock_db)
|
||||
result = await repo.get_by_name("test-agent")
|
||||
|
||||
assert result is not None
|
||||
assert result.name == "test-agent"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_name_not_found(self):
|
||||
from yuxi.repositories.subagent_repository import SubAgentRepository
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
repo = SubAgentRepository(mock_db)
|
||||
result = await repo.get_by_name("nonexistent")
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_can_clear_model_when_provided(self):
|
||||
from yuxi.repositories.subagent_repository import SubAgentRepository
|
||||
|
||||
mock_db = AsyncMock()
|
||||
repo = SubAgentRepository(mock_db)
|
||||
item = SubAgent(
|
||||
name="test-agent",
|
||||
description="Test agent",
|
||||
system_prompt="You are a test",
|
||||
tools=[],
|
||||
model="gpt-4",
|
||||
is_builtin=False,
|
||||
created_by="admin",
|
||||
updated_by="admin",
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
|
||||
await repo.update(
|
||||
item,
|
||||
description=None,
|
||||
system_prompt=None,
|
||||
tools=None,
|
||||
model=None,
|
||||
model_provided=True,
|
||||
updated_by="admin",
|
||||
)
|
||||
|
||||
assert item.model is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Service Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestSubAgentService:
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_builtin_subagents_creates_agents(self, monkeypatch):
|
||||
from yuxi.services import subagent_service as service_module
|
||||
|
||||
created_agents = []
|
||||
|
||||
class MockRepo:
|
||||
def __init__(self, session):
|
||||
pass
|
||||
|
||||
async def exists_name(self, name):
|
||||
return False
|
||||
|
||||
async def create(self, **kwargs):
|
||||
created_agents.append(kwargs)
|
||||
return MagicMock()
|
||||
|
||||
@asynccontextmanager
|
||||
async def mock_session_context(*args, **kwargs):
|
||||
yield MagicMock()
|
||||
|
||||
class MockPgManager:
|
||||
get_async_session_context = mock_session_context
|
||||
|
||||
monkeypatch.setattr(service_module, "SubAgentRepository", MockRepo)
|
||||
monkeypatch.setattr(service_module, "pg_manager", MockPgManager())
|
||||
|
||||
await service_module.init_builtin_subagents()
|
||||
|
||||
assert len(created_agents) == 2
|
||||
agent_names = [a["name"] for a in created_agents]
|
||||
assert "research-agent" in agent_names
|
||||
assert "critique-agent" in agent_names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_subagent_specs_returns_list(self, monkeypatch):
|
||||
from yuxi.services import subagent_service as service_module
|
||||
|
||||
mock_spec = {
|
||||
"name": "test-agent",
|
||||
"description": "Test",
|
||||
"system_prompt": "You are a test",
|
||||
"tools": ["tool_a"],
|
||||
}
|
||||
|
||||
class MockSubAgent:
|
||||
def to_subagent_spec(self):
|
||||
return mock_spec
|
||||
|
||||
class MockRepo:
|
||||
def __init__(self, session):
|
||||
pass
|
||||
|
||||
async def list_all(self):
|
||||
return [MockSubAgent()]
|
||||
|
||||
@asynccontextmanager
|
||||
async def mock_session_context(*args, **kwargs):
|
||||
yield MagicMock()
|
||||
|
||||
class MockPgManager:
|
||||
get_async_session_context = mock_session_context
|
||||
|
||||
monkeypatch.setattr(service_module, "SubAgentRepository", MockRepo)
|
||||
monkeypatch.setattr(service_module, "pg_manager", MockPgManager())
|
||||
monkeypatch.setattr(service_module, "_get_available_tools", AsyncMock(return_value=[]))
|
||||
|
||||
result = await service_module.get_subagent_specs()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "test-agent"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_subagent_specs_returns_defensive_copy(self, monkeypatch):
|
||||
from yuxi.services import subagent_service as service_module
|
||||
|
||||
service_module._subagent_specs_cache = [
|
||||
{
|
||||
"name": "test-agent",
|
||||
"description": "Test",
|
||||
"system_prompt": "You are a test",
|
||||
"tools": ["tool_a"],
|
||||
}
|
||||
]
|
||||
|
||||
first = await service_module.get_subagent_specs()
|
||||
first[0]["tools"].append("tool_b")
|
||||
second = await service_module.get_subagent_specs()
|
||||
|
||||
assert second[0]["tools"] == ["tool_a"]
|
||||
service_module.invalidate_subagent_specs_cache()
|
||||
|
||||
def test_resolve_subagent_tools_does_not_mutate_input(self):
|
||||
from yuxi.services import subagent_service as service_module
|
||||
|
||||
mock_tool = MagicMock()
|
||||
mock_tool.name = "tool_a"
|
||||
specs = [
|
||||
{
|
||||
"name": "test-agent",
|
||||
"description": "Test",
|
||||
"system_prompt": "You are a test",
|
||||
"tools": ["tool_a"],
|
||||
}
|
||||
]
|
||||
|
||||
resolved = service_module.resolve_subagent_tools(specs, [mock_tool])
|
||||
|
||||
assert specs[0]["tools"] == ["tool_a"]
|
||||
assert resolved[0]["tools"] == [mock_tool]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Model Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestSubAgentModel:
|
||||
def test_to_dict(self):
|
||||
now = utc_now_naive()
|
||||
agent = SubAgent(
|
||||
name="test-agent",
|
||||
description="Test agent",
|
||||
system_prompt="You are a test",
|
||||
tools=["tool_a", "tool_b"],
|
||||
model="gpt-4",
|
||||
is_builtin=False,
|
||||
created_by="admin",
|
||||
updated_by="admin",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
result = agent.to_dict()
|
||||
|
||||
assert result["name"] == "test-agent"
|
||||
assert result["description"] == "Test agent"
|
||||
assert result["system_prompt"] == "You are a test"
|
||||
assert result["tools"] == ["tool_a", "tool_b"]
|
||||
assert result["model"] == "gpt-4"
|
||||
assert result["is_builtin"] is False
|
||||
assert result["created_by"] == "admin"
|
||||
|
||||
def test_to_subagent_spec(self):
|
||||
agent = SubAgent(
|
||||
name="test-agent",
|
||||
description="Test agent",
|
||||
system_prompt="You are a test",
|
||||
tools=["tool_a"],
|
||||
model="gpt-4",
|
||||
is_builtin=False,
|
||||
created_by="admin",
|
||||
updated_by="admin",
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
|
||||
spec = agent.to_subagent_spec()
|
||||
|
||||
assert spec["name"] == "test-agent"
|
||||
assert spec["description"] == "Test agent"
|
||||
assert spec["system_prompt"] == "You are a test"
|
||||
assert spec["tools"] == ["tool_a"]
|
||||
assert spec["model"] == "gpt-4"
|
||||
|
||||
def test_to_subagent_spec_no_model(self):
|
||||
agent = SubAgent(
|
||||
name="test-agent",
|
||||
description="Test agent",
|
||||
system_prompt="You are a test",
|
||||
tools=[],
|
||||
model=None,
|
||||
is_builtin=False,
|
||||
created_by="admin",
|
||||
updated_by="admin",
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
|
||||
spec = agent.to_subagent_spec()
|
||||
|
||||
assert "model" not in spec
|
||||
@ -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 './subagent_api' // SubAgent API
|
||||
export * from './tool_api' // 工具 API
|
||||
|
||||
// 导出基础工具函数
|
||||
|
||||
71
web/src/apis/subagent_api.js
Normal file
71
web/src/apis/subagent_api.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
|
||||
/**
|
||||
* SubAgent 管理 API 模块
|
||||
* 包含 SubAgent 的增删改查功能
|
||||
*/
|
||||
|
||||
const BASE_URL = '/api/system/subagents'
|
||||
|
||||
// =============================================================================
|
||||
// === SubAgent CRUD ===
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 获取所有 SubAgent 配置
|
||||
* @returns {Promise} - SubAgent 列表
|
||||
*/
|
||||
export const getSubAgents = async () => {
|
||||
return apiAdminGet(BASE_URL)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个 SubAgent 配置
|
||||
* @param {string} name - SubAgent 名称
|
||||
* @returns {Promise} - SubAgent 配置
|
||||
*/
|
||||
export const getSubAgent = async (name) => {
|
||||
return apiAdminGet(`${BASE_URL}/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的 SubAgent
|
||||
* @param {Object} data - SubAgent 配置数据
|
||||
* @returns {Promise} - 创建结果
|
||||
*/
|
||||
export const createSubAgent = async (data) => {
|
||||
return apiAdminPost(BASE_URL, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 SubAgent 配置
|
||||
* @param {string} name - SubAgent 名称
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} - 更新结果
|
||||
*/
|
||||
export const updateSubAgent = async (name, data) => {
|
||||
return apiAdminPut(`${BASE_URL}/${encodeURIComponent(name)}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 SubAgent
|
||||
* @param {string} name - SubAgent 名称
|
||||
* @returns {Promise} - 删除结果
|
||||
*/
|
||||
export const deleteSubAgent = async (name) => {
|
||||
return apiAdminDelete(`${BASE_URL}/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// === 导出为对象形式(兼容现有代码风格)===
|
||||
// =============================================================================
|
||||
|
||||
export const subagentApi = {
|
||||
getSubAgents,
|
||||
getSubAgent,
|
||||
createSubAgent,
|
||||
updateSubAgent,
|
||||
deleteSubAgent,
|
||||
}
|
||||
|
||||
export default subagentApi
|
||||
26
web/src/assets/icons/subagents.svg
Normal file
26
web/src/assets/icons/subagents.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<!-- Center agent -->
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
|
||||
<!-- Sub-agents -->
|
||||
<circle cx="5" cy="5" r="2" />
|
||||
<circle cx="19" cy="5" r="2" />
|
||||
<circle cx="5" cy="19" r="2" />
|
||||
<circle cx="19" cy="19" r="2" />
|
||||
|
||||
<!-- Connections -->
|
||||
<line x1="9.5" y1="9.5" x2="6.5" y2="6.5" />
|
||||
<line x1="14.5" y1="9.5" x2="17.5" y2="6.5" />
|
||||
<line x1="9.5" y1="14.5" x2="6.5" y2="17.5" />
|
||||
<line x1="14.5" y1="14.5" x2="17.5" y2="17.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 645 B |
@ -48,7 +48,6 @@
|
||||
/>
|
||||
</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>
|
||||
|
||||
573
web/src/components/SubAgentsComponent.vue
Normal file
573
web/src/components/SubAgentsComponent.vue
Normal file
@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<div class="subagents-component 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 }">
|
||||
<!-- 左侧:SubAgent 列表 -->
|
||||
<div class="sidebar-list">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索 SubAgent..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<!-- SubAgent 列表 -->
|
||||
<div class="list-container">
|
||||
<div v-if="filteredSubAgents.length === 0" class="empty-text">
|
||||
<a-empty :image="false" :description="searchQuery ? '无匹配 SubAgent' : '暂无 SubAgent'" />
|
||||
</div>
|
||||
<template v-for="(agent, index) in filteredSubAgents" :key="agent.name">
|
||||
<div
|
||||
class="list-item"
|
||||
:class="{ active: currentAgent?.name === agent.name }"
|
||||
@click="selectAgent(agent)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<img :src="subagentsIcon" alt="SubAgent" class="agent-icon-svg" />
|
||||
<span class="item-name">{{ agent.name }}</span>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<span class="item-desc">{{ agent.description || '暂无描述' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index < filteredSubAgents.length - 1" class="list-separator"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:详情面板 -->
|
||||
<div class="main-panel">
|
||||
<div v-if="!currentAgent" class="unselected-state">
|
||||
<div class="hint-box">
|
||||
<Bot :size="40" class="text-muted" />
|
||||
<p>请在左侧选择 SubAgent 进行操作</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-top-bar">
|
||||
<h2 style="min-height: 32px">
|
||||
<img :src="subagentsIcon" alt="SubAgent" class="agent-icon-lg-svg" />
|
||||
<span><strong>{{ currentAgent.name }}</strong></span>
|
||||
</h2>
|
||||
<div class="panel-actions">
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="showEditModal(currentAgent)"
|
||||
class="lucide-icon-btn"
|
||||
v-if="!currentAgent.is_builtin"
|
||||
>
|
||||
<Pencil :size="14" />
|
||||
<span>编辑</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
danger
|
||||
ghost
|
||||
:disabled="currentAgent.is_builtin"
|
||||
@click="confirmDeleteAgent(currentAgent)"
|
||||
class="lucide-icon-btn"
|
||||
v-if="!currentAgent.is_builtin"
|
||||
>
|
||||
<Trash2 :size="14" />
|
||||
<span>删除</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
<a-tabs v-model:activeKey="detailTab" class="detail-tabs">
|
||||
<a-tab-pane key="general">
|
||||
<template #tab>
|
||||
<span class="tab-title"><Info :size="14" />信息</span>
|
||||
</template>
|
||||
<div class="tab-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item" v-if="currentAgent.description">
|
||||
<label>描述</label>
|
||||
<span>{{ currentAgent.description }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="currentAgent.model">
|
||||
<label>模型覆盖</label>
|
||||
<span>{{ currentAgent.model }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="currentAgent.tools && currentAgent.tools.length > 0">
|
||||
<label>工具</label>
|
||||
<span>
|
||||
<a-tag v-for="tool in currentAgent.tools" :key="tool" size="small">
|
||||
{{ tool }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="currentAgent.is_builtin">
|
||||
<label>类型</label>
|
||||
<span><a-tag color="blue">内置</a-tag></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>创建时间</label>
|
||||
<span>{{ formatTime(currentAgent.created_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>更新时间</label>
|
||||
<span>{{ formatTime(currentAgent.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="currentAgent.created_by">
|
||||
<label>创建人</label>
|
||||
<span>{{ currentAgent.created_by }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="prompt">
|
||||
<template #tab>
|
||||
<span class="tab-title"><MessageSquare :size="14" />系统提示词</span>
|
||||
</template>
|
||||
<div class="tab-content">
|
||||
<div class="prompt-display">
|
||||
<pre>{{ currentAgent.system_prompt }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑 SubAgent 模态框 -->
|
||||
<a-modal
|
||||
v-model:open="formModalVisible"
|
||||
:title="editMode ? '编辑 SubAgent' : '添加 SubAgent'"
|
||||
@ok="handleFormSubmit"
|
||||
:confirmLoading="formLoading"
|
||||
@cancel="formModalVisible = false"
|
||||
:maskClosable="false"
|
||||
width="600px"
|
||||
class="subagent-modal"
|
||||
>
|
||||
<a-form layout="vertical" class="subagent-form">
|
||||
<a-form-item label="名称" required class="form-item">
|
||||
<a-input
|
||||
v-model:value="form.name"
|
||||
placeholder="请输入 SubAgent 名称(唯一标识)"
|
||||
:disabled="editMode"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" class="form-item">
|
||||
<a-input v-model:value="form.description" placeholder="请输入 SubAgent 描述" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="系统提示词" required class="form-item">
|
||||
<a-textarea
|
||||
v-model:value="form.system_prompt"
|
||||
placeholder="请输入系统提示词"
|
||||
:rows="6"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="工具" class="form-item">
|
||||
<a-select
|
||||
v-model:value="form.tools"
|
||||
mode="tags"
|
||||
placeholder="选择或输入工具名称"
|
||||
style="width: 100%"
|
||||
:options="availableTools"
|
||||
@focus="fetchAvailableTools"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="模型覆盖(可选)" class="form-item">
|
||||
<div class="model-override-row">
|
||||
<ModelSelectorComponent
|
||||
:model_spec="form.model"
|
||||
placeholder="请选择模型"
|
||||
class="model-selector-full"
|
||||
@select-model="handleModelSelect"
|
||||
/>
|
||||
<a-button v-if="form.model" type="link" size="small" @click="form.model = ''">
|
||||
清空
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
Search,
|
||||
Bot,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Info,
|
||||
MessageSquare
|
||||
} from 'lucide-vue-next'
|
||||
import { subagentApi } from '@/apis/subagent_api'
|
||||
import { toolApi } from '@/apis/tool_api'
|
||||
import { formatFullDateTime } from '@/utils/time'
|
||||
import subagentsIcon from '@/assets/icons/subagents.svg'
|
||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const subagents = ref([])
|
||||
const searchQuery = ref('')
|
||||
const currentAgent = ref(null)
|
||||
const detailTab = ref('general')
|
||||
const availableTools = ref([])
|
||||
|
||||
// 表单相关
|
||||
const formModalVisible = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const editMode = ref(false)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
system_prompt: '',
|
||||
tools: [],
|
||||
model: ''
|
||||
})
|
||||
|
||||
const getSortedSubAgents = (items) => {
|
||||
return [...items].sort((a, b) => {
|
||||
// 内置的排前面
|
||||
if (a.is_builtin !== b.is_builtin) {
|
||||
return a.is_builtin ? -1 : 1
|
||||
}
|
||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
})
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const filteredSubAgents = computed(() => {
|
||||
const sorted = getSortedSubAgents(subagents.value)
|
||||
if (!searchQuery.value) return sorted
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
return sorted.filter(
|
||||
(a) => a.name.toLowerCase().includes(q) || (a.description || '').toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
// 获取 SubAgent 列表
|
||||
const fetchSubAgents = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const result = await subagentApi.getSubAgents()
|
||||
if (result.success) {
|
||||
subagents.value = result.data || []
|
||||
// 保持选中状态
|
||||
if (currentAgent.value) {
|
||||
const latest = subagents.value.find((a) => a.name === currentAgent.value.name)
|
||||
if (latest) {
|
||||
currentAgent.value = latest
|
||||
} else {
|
||||
currentAgent.value = null
|
||||
}
|
||||
}
|
||||
// 默认选中第一项(切换到 tab 且当前未选中时)
|
||||
if (!currentAgent.value && subagents.value.length > 0) {
|
||||
currentAgent.value = getSortedSubAgents(subagents.value)[0]
|
||||
detailTab.value = 'general'
|
||||
}
|
||||
} else {
|
||||
error.value = result.message || '获取列表失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取 SubAgent 列表失败:', err)
|
||||
error.value = err.message || '获取列表失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取可选工具列表
|
||||
const fetchAvailableTools = async () => {
|
||||
if (availableTools.value.length > 0) return
|
||||
try {
|
||||
const result = await toolApi.getToolOptions()
|
||||
if (result.success && result.data) {
|
||||
availableTools.value = result.data
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取工具选项失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => formatFullDateTime(timeStr)
|
||||
|
||||
const handleModelSelect = (spec) => {
|
||||
form.model = spec || ''
|
||||
}
|
||||
|
||||
// 选择 SubAgent
|
||||
const selectAgent = (agent) => {
|
||||
currentAgent.value = agent
|
||||
detailTab.value = 'general'
|
||||
}
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = () => {
|
||||
editMode.value = false
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
description: '',
|
||||
system_prompt: '',
|
||||
tools: [],
|
||||
model: ''
|
||||
})
|
||||
formModalVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑模态框
|
||||
const showEditModal = async (agent) => {
|
||||
try {
|
||||
const result = await subagentApi.getSubAgent(agent.name)
|
||||
if (result.success && result.data) {
|
||||
editMode.value = true
|
||||
Object.assign(form, {
|
||||
name: result.data.name,
|
||||
description: result.data.description || '',
|
||||
system_prompt: result.data.system_prompt || '',
|
||||
tools: result.data.tools || [],
|
||||
model: result.data.model || ''
|
||||
})
|
||||
formModalVisible.value = true
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取 SubAgent 详情失败,回退使用列表数据:', err)
|
||||
}
|
||||
// 回退:使用列表数据
|
||||
editMode.value = true
|
||||
Object.assign(form, {
|
||||
name: agent.name,
|
||||
description: agent.description || '',
|
||||
system_prompt: agent.system_prompt || '',
|
||||
tools: agent.tools || [],
|
||||
model: agent.model || ''
|
||||
})
|
||||
formModalVisible.value = true
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
const handleFormSubmit = async () => {
|
||||
try {
|
||||
// 校验
|
||||
if (!form.name?.trim()) {
|
||||
message.error('名称不能为空')
|
||||
return
|
||||
}
|
||||
if (!form.system_prompt?.trim()) {
|
||||
message.error('系统提示词不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
|
||||
const data = {
|
||||
name: form.name.trim(),
|
||||
description: form.description || '',
|
||||
system_prompt: form.system_prompt,
|
||||
tools: form.tools || [],
|
||||
model: form.model || null
|
||||
}
|
||||
|
||||
if (editMode.value) {
|
||||
const result = await subagentApi.updateSubAgent(form.name, data)
|
||||
if (result.success) {
|
||||
message.success('SubAgent 更新成功')
|
||||
} else {
|
||||
message.error(result.message || '更新失败')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const result = await subagentApi.createSubAgent(data)
|
||||
if (result.success) {
|
||||
message.success('SubAgent 创建成功')
|
||||
} else {
|
||||
message.error(result.message || '创建失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
formModalVisible.value = false
|
||||
await fetchSubAgents()
|
||||
} catch (err) {
|
||||
console.error('操作失败:', err)
|
||||
message.error(err.message || '操作失败')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除 SubAgent
|
||||
const confirmDeleteAgent = (agent) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除 SubAgent',
|
||||
content: `确定要删除 SubAgent "${agent.name}" 吗?此操作不可撤销。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
const result = await subagentApi.deleteSubAgent(agent.name)
|
||||
if (result.success) {
|
||||
message.success('SubAgent 删除成功')
|
||||
if (currentAgent.value?.name === agent.name) {
|
||||
currentAgent.value = null
|
||||
}
|
||||
await fetchSubAgents()
|
||||
} else {
|
||||
message.error(result.message || '删除失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除失败:', err)
|
||||
message.error(err.message || '删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchSubAgents()
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
fetchSubAgents,
|
||||
showAddModal
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '@/assets/css/extensions.less';
|
||||
|
||||
.list-item {
|
||||
.agent-icon-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--gray-500);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.builtin-tag {
|
||||
background: var(--color-primary-50);
|
||||
border: none;
|
||||
color: var(--color-primary-600);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 12px;
|
||||
color: var(--gray-400);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agent-icon-lg-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--gray-700);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.model-override-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-selector-full {
|
||||
flex: 1;
|
||||
|
||||
:deep(.model-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧面板 */
|
||||
.main-panel {
|
||||
.detail-tabs {
|
||||
.tab-content {
|
||||
padding: 16px;
|
||||
min-height: 300px;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
color: var(--gray-500);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-display {
|
||||
background: var(--gray-50);
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.subagent-modal {
|
||||
.subagent-form {
|
||||
.form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -5,6 +5,7 @@
|
||||
<a-tab-pane key="tools" tab="工具" />
|
||||
<a-tab-pane key="skills" tab="Skills 管理" />
|
||||
<a-tab-pane key="mcp" tab="MCP 服务器" />
|
||||
<a-tab-pane key="subagents" tab="Subagents 管理" />
|
||||
</a-tabs>
|
||||
<div class="header-actions">
|
||||
<!-- Skills Tab 的按钮 -->
|
||||
@ -43,6 +44,17 @@
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- Subagents Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'subagents'">
|
||||
<a-button type="primary" @click="handleSubagentAdd" class="lucide-icon-btn">
|
||||
<Plus :size="14" />
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-button @click="handleSubagentRefresh" :disabled="subagentsLoading" class="lucide-icon-btn">
|
||||
<RotateCw :size="14" />
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -60,6 +72,9 @@
|
||||
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
||||
<McpServersComponent ref="mcpRef" @add="handleMcpAdd" @refresh="handleMcpRefresh" />
|
||||
</div>
|
||||
<div v-show="activeTab === 'subagents'" class="tab-panel">
|
||||
<SubAgentsComponent ref="subagentsRef" @add="handleSubagentAdd" @refresh="handleSubagentRefresh" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -71,6 +86,7 @@ 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'
|
||||
import SubAgentsComponent from '@/components/SubAgentsComponent.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const activeTab = ref('tools')
|
||||
@ -80,7 +96,7 @@ const skillsRef = ref(null)
|
||||
watch(
|
||||
() => route.query,
|
||||
(query) => {
|
||||
if (query.tab && ['tools', 'skills', 'mcp'].includes(query.tab)) {
|
||||
if (query.tab && ['tools', 'skills', 'mcp', 'subagents'].includes(query.tab)) {
|
||||
activeTab.value = query.tab
|
||||
}
|
||||
},
|
||||
@ -88,12 +104,14 @@ watch(
|
||||
)
|
||||
const toolsRef = ref(null)
|
||||
const mcpRef = ref(null)
|
||||
const subagentsRef = ref(null)
|
||||
|
||||
// Skills 相关状态(从子组件透传)
|
||||
const skillsLoading = ref(false)
|
||||
const skillsImporting = ref(false)
|
||||
const toolsLoading = ref(false)
|
||||
const mcpLoading = ref(false)
|
||||
const subagentsLoading = ref(false)
|
||||
|
||||
// 暴露给子组件的状态更新
|
||||
const updateSkillsState = (loading, importing) => {
|
||||
@ -109,6 +127,10 @@ const updateMcpState = (loading) => {
|
||||
mcpLoading.value = loading
|
||||
}
|
||||
|
||||
const updateSubagentsState = (loading) => {
|
||||
subagentsLoading.value = loading
|
||||
}
|
||||
|
||||
// Skills 事件处理
|
||||
const handleSkillsImport = () => {
|
||||
// 导入完成后自动刷新
|
||||
@ -150,6 +172,22 @@ const handleMcpRefresh = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Subagents 事件处理
|
||||
const handleSubagentAdd = () => {
|
||||
if (subagentsRef.value?.showAddModal) {
|
||||
subagentsRef.value.showAddModal()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubagentRefresh = () => {
|
||||
if (subagentsRef.value?.fetchSubAgents) {
|
||||
updateSubagentsState(true)
|
||||
subagentsRef.value.fetchSubAgents().finally(() => {
|
||||
updateSubagentsState(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理导入上传
|
||||
const handleImportUpload = async ({ file, onSuccess, onError }) => {
|
||||
if (skillsRef.value?.handleImportUpload) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user