feat: 添加subagents管理功能

- 实现了应用程序启动期间的理subagents初始化。
- 为subagents的 CRUD 操作和存储库方法创建了单元测试。
- 开发了用于列出、检索、创建、更新和删除子代理的子代理 API 端点。
- 添加了用于在前端管理子代理的 SubAgentsComponent,包括搜索和详细信息视图。
This commit is contained in:
Wenjie Zhang 2026-03-19 02:11:52 +08:00
parent 8528dae331
commit d655cbe640
15 changed files with 1792 additions and 43 deletions

View File

@ -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": "子智能体的默认模型,会被子智能体的配置覆盖。",
},
)

View File

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

View 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,
)

View 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

View File

@ -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 - 运行任务表"""

View File

@ -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/*

View 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)

View File

@ -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()

View 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

View File

@ -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
// 导出基础工具函数

View 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

View 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

View File

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

View 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>

View File

@ -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) {