feat: 重构扩展管理中的 SubAgent 与 MCP 交互,统一为类似 Skills 的方式。

This commit is contained in:
Wenjie Zhang 2026-03-29 13:47:41 +08:00
parent 33320022af
commit b7e10c6666
26 changed files with 770 additions and 413 deletions

View File

@ -18,9 +18,16 @@ class SubAgentRepository:
result = await self.db.execute(select(SubAgent).order_by(SubAgent.updated_at.desc()))
return list(result.scalars().all())
async def list_enabled(self) -> list[SubAgent]:
"""获取已启用的 SubAgent。"""
result = await self.db.execute(
select(SubAgent).where(SubAgent.enabled.is_(True)).order_by(SubAgent.updated_at.desc())
)
return list(result.scalars().all())
async def list_all_specs(self) -> list[dict[str, Any]]:
"""获取所有 SubAgent 运行规格,按 updated_at 降序"""
items = await self.list_all()
"""获取已启用的 SubAgent 运行规格,按 updated_at 降序。"""
items = await self.list_enabled()
return [item.to_subagent_spec() for item in items]
async def get_by_name(self, name: str) -> SubAgent | None:
@ -53,6 +60,7 @@ class SubAgentRepository:
system_prompt=system_prompt,
tools=tools or [],
model=model,
enabled=True,
is_builtin=is_builtin,
created_by=created_by,
updated_by=created_by,

View File

@ -55,6 +55,20 @@ _DEFAULT_MCP_SERVERS = {
},
}
_SYNCED_MCP_FIELDS = (
"description",
"transport",
"url",
"command",
"args",
"env",
"headers",
"timeout",
"sse_read_timeout",
"tags",
"icon",
)
# =============================================================================
# === Core Logic (Moved from agents/common/mcp.py) ===
# =============================================================================
@ -168,9 +182,20 @@ async def init_mcp_servers() -> None:
)
session.add(server)
logger.info(f"Added built-in MCP server '{name}' to database")
else:
changed = False
for field in _SYNCED_MCP_FIELDS:
next_value = config.get(field)
if getattr(existing, field) != next_value:
setattr(existing, field, next_value)
changed = True
if changed:
existing.updated_by = "system"
# Commit if any new servers were added (check session state)
if session.new:
await session.commit()
elif session.dirty:
await session.commit()
# Load configurations from database to cache
await load_mcp_servers_from_db()
@ -491,13 +516,13 @@ async def delete_mcp_server(db: AsyncSession, name: str) -> bool:
# =============================================================================
async def toggle_server_enabled(db: AsyncSession, name: str, updated_by: str = None) -> tuple[bool, MCPServer]:
"""Toggle server enabled status."""
async def set_server_enabled(db: AsyncSession, name: str, enabled: bool, updated_by: str = None) -> tuple[bool, MCPServer]:
"""Set server enabled status."""
server = await get_mcp_server(db, name)
if not server:
raise ValueError(f"Server '{name}' does not exist")
server.enabled = 0 if server.enabled else 1
server.enabled = 1 if enabled else 0
if updated_by is not None:
server.updated_by = updated_by
await db.commit()
@ -507,7 +532,7 @@ async def toggle_server_enabled(db: AsyncSession, name: str, updated_by: str = N
server_config = server.to_mcp_config() if is_enabled else None
await sync_mcp_server_to_cache(name, server_config)
logger.info(f"Toggled MCP server '{name}' enabled={is_enabled}")
logger.info(f"Set MCP server '{name}' enabled={is_enabled}")
return is_enabled, server

View File

@ -34,7 +34,7 @@ _DEFAULT_SUBAGENTS = [
"你是一位专注的研究员。你的工作是根据用户的问题进行研究。"
"进行彻底的研究,然后用详细的答案回复用户的问题,只有你的最终答案会被传递给用户。"
"除了你的最终信息,他们不会知道任何其他事情,所以你的最终报告应该就是你的最终信息!"
"将调研结果保存到主题研究文件中 /sub_research/xxx.md 中。"
"将调研结果保存到主题研究文件中 sub_research/xxx.md 中。"
),
"tools": ["tavily_search"],
"is_builtin": True,
@ -63,13 +63,16 @@ _DEFAULT_SUBAGENTS = [
},
]
_SYNCED_SUBAGENT_FIELDS = ("description", "system_prompt", "tools", "model", "is_builtin")
async def init_builtin_subagents() -> None:
"""初始化内置 SubAgent(仅创建不存在的)"""
"""初始化内置 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"]):
item = await repo.get_by_name(data["name"])
if item is None:
await repo.create(
name=data["name"],
description=data["description"],
@ -79,6 +82,19 @@ async def init_builtin_subagents() -> None:
is_builtin=data.get("is_builtin", False),
created_by="system",
)
continue
changed = False
for field in _SYNCED_SUBAGENT_FIELDS:
next_value = data.get(field)
current_value = getattr(item, field)
if current_value != next_value:
setattr(item, field, deepcopy(next_value))
changed = True
if changed:
item.updated_by = "system"
await session.commit()
clear_specs_cache()
async def get_subagent_specs(db: AsyncSession | None = None) -> list[dict[str, Any]]:
@ -208,3 +224,24 @@ async def delete_subagent(name: str, db: AsyncSession | None = None) -> bool:
await repo.delete(item)
clear_specs_cache()
return True
async def set_subagent_enabled(
name: str,
enabled: bool,
*,
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
item.enabled = enabled
item.updated_by = updated_by
await session.commit()
await session.refresh(item)
clear_specs_cache()
return item.to_dict()

View File

@ -191,6 +191,7 @@ class PostgresManager(metaclass=SingletonMeta):
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS version VARCHAR(64)",
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS is_builtin BOOLEAN NOT NULL DEFAULT FALSE",
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS content_hash VARCHAR(128)",
"ALTER TABLE IF EXISTS subagents ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT TRUE",
"ALTER TABLE IF EXISTS conversations ADD COLUMN IF NOT EXISTS is_pinned BOOLEAN NOT NULL DEFAULT FALSE",
"ALTER TABLE IF EXISTS mcp_servers ADD COLUMN IF NOT EXISTS env JSONB",
"""

View File

@ -567,6 +567,7 @@ class SubAgent(Base):
system_prompt = Column(Text, nullable=False, comment="系统提示词")
tools = Column(JSON, nullable=False, default=list, comment="工具名称列表")
model = Column(String(128), nullable=True, comment="可选的模型覆盖")
enabled = Column(Boolean, nullable=False, default=True, comment="是否启用")
is_builtin = Column(Boolean, nullable=False, default=False, comment="是否内置")
@ -582,6 +583,7 @@ class SubAgent(Base):
"system_prompt": self.system_prompt,
"tools": self.tools or [],
"model": self.model,
"enabled": bool(self.enabled),
"is_builtin": bool(self.is_builtin),
"created_by": self.created_by,
"updated_by": self.updated_by,

View File

@ -11,7 +11,7 @@ from yuxi.services.mcp_service import (
get_all_mcp_servers,
get_all_mcp_tools,
get_mcp_server,
toggle_server_enabled,
set_server_enabled,
toggle_tool_enabled,
update_mcp_server,
)
@ -56,6 +56,10 @@ class UpdateMcpServerRequest(BaseModel):
icon: str | None = Field(None, description="图标emoji")
class UpdateMcpServerStatusRequest(BaseModel):
enabled: bool = Field(..., description="是否启用")
# =============================================================================
# === Helpers ===
# =============================================================================
@ -246,19 +250,21 @@ async def test_mcp_server(
raise HTTPException(status_code=500, detail=str(e))
@mcp.put("/{name}/toggle")
async def toggle_mcp_server_route(
@mcp.put("/{name}/status")
async def update_mcp_server_status_route(
name: str,
request: UpdateMcpServerStatusRequest,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""切换 MCP 服务器启用状态"""
"""更新 MCP 服务器启用状态"""
try:
is_enabled, server = await toggle_server_enabled(db, name, current_user.username)
is_enabled, server = await set_server_enabled(db, name, request.enabled, current_user.username)
return {
"success": True,
"enabled": is_enabled,
"message": f"MCP '{name}'{'启用' if is_enabled else '禁用'}",
"data": server.to_dict(),
"message": f"MCP '{name}'{'添加' if is_enabled else '移除'}",
}
except ValueError as ve:
raise HTTPException(status_code=404, detail=str(ve))

View File

@ -31,6 +31,10 @@ class SubAgentUpdateRequest(BaseModel):
model: str | None = Field(None, description="可选的模型覆盖")
class SubAgentStatusRequest(BaseModel):
enabled: bool = Field(..., description="是否启用")
def _raise_from_value_error(e: ValueError) -> None:
message = str(e)
status_code = 404 if "不存在" in message else 400
@ -145,3 +149,26 @@ async def delete_subagent_route(
raise
except Exception as e:
_raise_internal_error("删除", e)
@subagents_router.put("/{name}/status")
async def update_subagent_status_route(
name: str,
payload: SubAgentStatusRequest,
current_user: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
"""更新 SubAgent 启用状态(管理员)。"""
try:
item = await service.set_subagent_enabled(name, payload.enabled, updated_by=current_user.username, db=db)
if not item:
raise HTTPException(status_code=404, detail=f"SubAgent '{name}' 不存在")
return {
"success": True,
"data": item,
"message": f"SubAgent '{name}'{'添加' if payload.enabled else '移除'}",
}
except HTTPException:
raise
except Exception as e:
_raise_internal_error("更新状态", e)

View File

@ -0,0 +1,67 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from server.routers.mcp_router import mcp
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(mcp, 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_update_mcp_server_status(monkeypatch):
captured = {}
class DummyServer:
def __init__(self, enabled):
self.enabled = enabled
def to_dict(self):
return {"name": "sequentialthinking", "enabled": self.enabled}
async def fake_set_server_enabled(db, name, enabled, updated_by=None):
captured["name"] = name
captured["enabled"] = enabled
captured["updated_by"] = updated_by
return enabled, DummyServer(enabled)
monkeypatch.setattr("server.routers.mcp_router.set_server_enabled", fake_set_server_enabled)
client = TestClient(_build_app())
resp = client.put("/api/system/mcp-servers/sequentialthinking/status", json={"enabled": False})
assert resp.status_code == 200, resp.text
payload = resp.json()
assert payload["success"] is True
assert payload["enabled"] is False
assert payload["data"]["enabled"] is False
assert captured == {"name": "sequentialthinking", "enabled": False, "updated_by": "admin"}
def test_update_mcp_server_status_not_found(monkeypatch):
async def fake_set_server_enabled(db, name, enabled, updated_by=None):
raise ValueError(f"Server '{name}' does not exist")
monkeypatch.setattr("server.routers.mcp_router.set_server_enabled", fake_set_server_enabled)
client = TestClient(_build_app())
resp = client.put("/api/system/mcp-servers/missing/status", json={"enabled": True})
assert resp.status_code == 404, resp.text

View File

@ -53,6 +53,7 @@ def test_list_subagents_returns_data(monkeypatch):
"system_prompt": "You are a researcher",
"tools": ["tavily_search"],
"model": None,
"enabled": True,
"is_builtin": True,
"created_by": "system",
"updated_by": "system",
@ -82,6 +83,7 @@ def test_get_single_subagent(monkeypatch):
"system_prompt": "You are a researcher",
"tools": ["tavily_search"],
"model": None,
"enabled": True,
"is_builtin": True,
"created_by": "system",
"updated_by": "system",
@ -125,6 +127,7 @@ def test_create_subagent(monkeypatch):
"system_prompt": data["system_prompt"],
"tools": data.get("tools", []),
"model": data.get("model"),
"enabled": True,
"is_builtin": False,
"created_by": created_by,
"updated_by": created_by,
@ -191,6 +194,7 @@ def test_update_subagent(monkeypatch):
"system_prompt": data.get("system_prompt", "Updated prompt"),
"tools": data.get("tools", []),
"model": data.get("model"),
"enabled": True,
"is_builtin": False,
"created_by": "admin",
"updated_by": updated_by,
@ -259,6 +263,51 @@ def test_delete_builtin_subagent_fails(monkeypatch):
assert resp.status_code == 400, resp.text
def test_update_subagent_status(monkeypatch):
captured = {}
async def fake_set_subagent_enabled(name, enabled, updated_by, db=None):
captured["name"] = name
captured["enabled"] = enabled
captured["updated_by"] = updated_by
return {
"name": name,
"description": "Test",
"system_prompt": "Prompt",
"tools": [],
"model": None,
"enabled": enabled,
"is_builtin": True,
"created_by": "system",
"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.set_subagent_enabled", fake_set_subagent_enabled)
app = _build_app()
client = TestClient(app)
resp = client.put("/api/system/subagents/research-agent/status", json={"enabled": False})
assert resp.status_code == 200, resp.text
payload = resp.json()
assert payload["success"] is True
assert payload["data"]["enabled"] is False
assert captured == {"name": "research-agent", "enabled": False, "updated_by": "admin"}
def test_update_subagent_status_not_found(monkeypatch):
async def fake_set_subagent_enabled(name, enabled, updated_by, db=None):
return None
monkeypatch.setattr("server.routers.subagent_router.service.set_subagent_enabled", fake_set_subagent_enabled)
app = _build_app()
client = TestClient(app)
resp = client.put("/api/system/subagents/missing/status", json={"enabled": True})
assert resp.status_code == 404, resp.text
# =============================================================================
# Repository Tests
# =============================================================================

View File

@ -2,6 +2,8 @@
MCPModel Context Protocol是扩展智能体能力的重要方式。系统支持通过管理界面动态配置 MCP 服务器,无需修改代码。
内置 MCP 服务器以代码为事实源:系统启动时会自动补齐缺失项,并用代码中的最新连接与展示字段覆盖数据库定义;是否“已添加”以及工具级禁用列表仍保留数据库状态。
## 支持的传输协议
| 协议 | 说明 | 适用场景 |
@ -37,6 +39,13 @@ MCPModel Context Protocol是扩展智能体能力的重要方式。系统
}
```
## 服务器管理
管理界面使用“添加 / 移除”语义管理 MCP 服务器:
- 已添加:`enabled=true`,会加载到运行时缓存并可供 Agent 使用
- 可添加:`enabled=false`,记录保留但不会进入运行时
## 工具管理
MCP 工具支持粒度控制:管理员可以单独启用或禁用某个 MCP 服务器下的特定工具,实现精细化的权限管理。

View File

@ -30,6 +30,7 @@ SubAgent 是 Deep Agent 的可调用子智能体配置。你可以在管理界
| system_prompt | 子智能体行为约束 |
| tools | 可用工具名称列表(从工具系统选择/输入) |
| model | 可选,子智能体模型覆盖 |
| enabled | 是否已添加到运行时 |
| is_builtin | 是否内置(内置项不可编辑、不可删除) |
### 配置建议
@ -67,10 +68,8 @@ graph = create_agent(
启用规则可以总结为:
- 未接入 `SubAgentMiddleware`SubAgent 功能不生效(即使数据库里已配置)。
- 已接入 `SubAgentMiddleware`SubAgent 默认启用,不需要额外开关。
- 可调用范围由 `subagents` 参数决定,通常来自数据库读取结果。
注意:当前 `SubAgent` 模型中没有独立的 `enabled` 字段,因此“是否启用”由是否纳入 middleware 的 `subagents` 列表决定。
- 已接入 `SubAgentMiddleware`:只有 `enabled=true` 且被 Agent 配置选中的 SubAgent 才可调用。
- 可调用范围由 Agent 配置中的 `subagents` 列表与数据库启用状态共同决定。
## 开发者视角
@ -116,7 +115,7 @@ Deep Agent 构图时会动态加载 SubAgent
### 内置 SubAgent 初始化
系统启动时会自动确保内置 SubAgent 存在:
系统启动时会自动确保内置 SubAgent 存在,并用代码中的最新定义覆盖数据库中的展示字段
- research-agent
- critique-agent
@ -130,6 +129,7 @@ Deep Agent 构图时会动态加载 SubAgent
- 内置 SubAgent 不可编辑。
- 内置 SubAgent 不可删除。
- 内置 SubAgent 可以移除/重新添加(通过 `enabled` 控制)。
### API 概览
@ -141,6 +141,7 @@ Deep Agent 构图时会动态加载 SubAgent
| GET | /api/system/subagents/{name} | 详情 |
| POST | /api/system/subagents | 创建 |
| PUT | /api/system/subagents/{name} | 更新 |
| PUT | /api/system/subagents/{name}/status | 更新添加状态 |
| DELETE | /api/system/subagents/{name} | 删除 |
所有接口均要求管理员权限。

View File

@ -48,6 +48,7 @@
- 重构内置 Skills 安装机制:内置 skill 改为在管理页以“未安装”状态展示,支持按需安装、基于 `version + content_hash` 的更新提示与覆盖确认,并对已安装内置 skill 禁止在线文件编辑
- 新增知识库 PDF、图片的预览功能
- 优化扩展页工具列表筛选区:将“全部分类”筛选收纳为搜索框右侧的紧凑下拉入口,并复用扩展页侧栏工具条样式,避免影响其他管理组件布局
- 重构扩展管理中的 SubAgent 与 MCP 交互,统一为类似 Skills 的“已添加 / 可添加”列表,不再使用服务器级开关切换,为 SubAgent 增加 `enabled` 状态,并让运行时只加载已添加项,调整内置 SubAgent / MCP 的启动同步逻辑,使用代码中的最新定义覆盖数据库展示字段,同时保留启用状态与 MCP 工具禁用状态;统一扩展详情区胶囊操作按钮样式到公共 `extensions.less`,并将内置 MCP 在详情页中的危险操作从禁用“删除”改为可执行的“移除”
<!-- 添加到这里 -->

View File

@ -70,12 +70,13 @@ export const testMcpServer = async (name) => {
}
/**
* 切换 MCP 服务器启用状态
* 更新 MCP 服务器启用状态
* @param {string} name - 服务器名称
* @param {boolean} enabled - 是否启用
* @returns {Promise} - 切换结果
*/
export const toggleMcpServer = async (name) => {
return apiAdminPut(`${BASE_URL}/${encodeURIComponent(name)}/toggle`, {})
export const updateMcpServerStatus = async (name, enabled) => {
return apiAdminPut(`${BASE_URL}/${encodeURIComponent(name)}/status`, { enabled })
}
// =============================================================================
@ -124,7 +125,7 @@ export const mcpApi = {
updateMcpServer,
deleteMcpServer,
testMcpServer,
toggleMcpServer,
updateMcpServerStatus,
getMcpServerTools,
refreshMcpServerTools,
toggleMcpServerTool

View File

@ -56,6 +56,10 @@ export const deleteSubAgent = async (name) => {
return apiAdminDelete(`${BASE_URL}/${encodeURIComponent(name)}`)
}
export const updateSubAgentStatus = async (name, enabled) => {
return apiAdminPut(`${BASE_URL}/${encodeURIComponent(name)}/status`, { enabled })
}
// =============================================================================
// === 导出为对象形式(兼容现有代码风格)===
// =============================================================================
@ -65,7 +69,8 @@ export const subagentApi = {
getSubAgent,
createSubAgent,
updateSubAgent,
deleteSubAgent
deleteSubAgent,
updateSubAgentStatus
}
export default subagentApi

View File

@ -49,36 +49,42 @@
--main-color: #016179;
--main-bright: #0188a6;
--color-secondary-10: #fafcfd; /* 次要色-最浅 */
--color-secondary-50: #f5f7f7; /* 次要色-最浅 */
--color-secondary-100: #eff2f2; /* 次要色-浅 */
--color-secondary-500: #4e616d; /* 次要色-标准 */
--color-secondary-700: #3a4a56; /* 次要色-深 */
--color-secondary-900: #2c3843; /* 次要色-最深 */
--color-success-10: #fafff2; /* 成功色-极浅 */
--color-success-50: #f6ffed; /* 成功色-最浅 */
--color-success-100: #b7eb8f; /* 成功色-浅 */
--color-success-500: #52c41a; /* 成功色-标准 */
--color-success-700: #389e0d; /* 成功色-深 */
--color-success-900: #135200; /* 成功色-最深 */
--color-error-10: #fffaf9; /* 错误色-极浅 */
--color-error-50: #fff2f0; /* 错误色-最浅 */
--color-error-100: #ffccc7; /* 错误色-浅 */
--color-error-500: #ff4d4f; /* 错误色-标准 */
--color-error-700: #cf1322; /* 错误色-深 */
--color-error-900: #820014; /* 错误色-最深 */
--color-warning-10: #fffef5; /* 警告色-极浅 */
--color-warning-50: #fffbe6; /* 警告色-最浅 */
--color-warning-100: #ffe58f; /* 警告色-浅 */
--color-warning-500: #faad14; /* 警告色-标准 */
--color-warning-700: #d48806; /* 警告色-深 */
--color-warning-900: #ad6800; /* 警告色-最深 */
--color-info-10: #f5fbff; /* 信息色-极浅 */
--color-info-50: #e6f7ff; /* 信息色-最浅 */
--color-info-100: #bae7ff; /* 信息色-浅 */
--color-info-500: #1890ff; /* 信息色-标准 */
--color-info-700: #096dd9; /* 信息色-深 */
--color-info-900: #0050b3; /* 信息色-最深 */
--color-accent-10: #f5fffe; /* 强调色-极浅 */
--color-accent-50: #e6fffb; /* 强调色-最浅 */
--color-accent-100: #87e8de; /* 强调色-浅 */
--color-accent-500: #13c2c2; /* 强调色-标准 */

View File

@ -31,11 +31,10 @@
flex-shrink: 0;
.sidebar-toolbar {
padding: 8px 12px;
padding: 8px 12px 0;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--gray-150);
.search-box {
flex: 1;
@ -137,11 +136,11 @@
background-color: var(--gray-0);
display: flex;
flex-direction: column;
gap: 6px;
gap: 12px;
&:hover {
border-color: @border-color;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 2px var(--shadow-1);
}
&.active {
@ -212,6 +211,263 @@
gap: 8px;
}
.list-section-title {
padding: 10px 14px 6px;
color: var(--gray-500);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.4px;
text-transform: uppercase;
}
.extension-list-item {
.item-main-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.item-header {
min-width: 0;
margin-bottom: 0;
}
.item-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-status {
position: relative;
flex-shrink: 0;
}
.item-details {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.item-desc {
flex: 1;
min-width: 0;
}
.item-tags {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.status-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 9px;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-600);
font-size: 11px;
font-weight: 600;
&.warning {
background: var(--color-warning-50);
color: var(--color-warning-900);
}
}
.status-chip-success {
background: var(--color-success-50);
color: var(--color-success-700);
}
.inline-hover-action {
display: none;
min-height: 24px;
padding: 0 9px;
border: 0;
border-radius: 999px;
background: var(--color-error-50);
color: var(--color-error-700);
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
&:hover .inline-hover-action {
display: inline-flex;
align-items: center;
justify-content: center;
}
&:hover .status-chip {
display: none;
}
.source-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 20px;
padding: 0 8px;
border-radius: 999px;
background: var(--color-info-50);
color: var(--color-info-700);
font-size: 11px;
font-weight: 600;
line-height: 1;
&.builtin {
background: var(--gray-50);
color: var(--gray-600);
}
}
}
.skill-inline-action {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
min-width: 52px;
min-height: 24px;
padding: 0 9px;
border: 1px solid transparent;
box-shadow: none;
font-size: 11px;
font-weight: 600;
line-height: 1;
transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer;
appearance: none;
&:hover,
&:focus {
outline: none;
}
&.skill-inline-action-primary {
// border-color: var(--main-100);
background: var(--main-50);
color: var(--main-700);
}
&.skill-inline-action-secondary {
border-color: var(--main-100);
background: var(--main-30);
color: var(--main-700);
}
&.skill-inline-action-primary:hover,
&.skill-inline-action-primary:focus {
border-color: var(--main-200);
background: var(--main-50);
color: var(--main-800);
}
&.skill-inline-action-secondary:hover,
&.skill-inline-action-secondary:focus {
border-color: var(--main-200);
background: var(--main-50);
color: var(--main-800);
}
}
.extension-panel-action {
min-height: 30px;
padding: 0 12px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
border-style: solid;
border-width: 1px;
box-shadow: none;
font-weight: 500;
transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer;
appearance: none;
&:hover,
&:focus {
outline: none;
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
&.extension-panel-action-primary {
border-color: transparent;
background: var(--main-600);
color: var(--main-0);
}
&.extension-panel-action-primary:hover,
&.extension-panel-action-primary:focus {
background: var(--main-700);
color: var(--main-0);
}
&.extension-panel-action-secondary {
border-color: var(--gray-200);
background: var(--gray-25);
color: var(--gray-700);
&:hover,
&:focus {
border-color: var(--gray-300);
color: var(--gray-900);
background: var(--gray-0);
}
}
&.extension-panel-action-danger {
border-color: var(--color-error-100);
background: var(--color-error-50);
color: var(--color-error-700);
&:hover,
&:focus {
border-color: var(--color-error-100);
background: var(--color-error-100);
color: var(--color-error-900);
}
}
}
.panel-status-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-600);
font-size: 12px;
font-weight: 600;
line-height: 1;
&.warning {
background: var(--color-warning-50);
color: var(--color-warning-900);
}
}
.item-meta {
font-size: 12px;
color: var(--gray-700);

View File

@ -648,11 +648,13 @@ const loadSubagentOptions = async (force = false) => {
try {
const result = await subagentApi.getSubAgents()
const rows = result?.data || []
liveSubagentOptions.value = rows.map((item) => ({
id: item.name,
name: item.name,
description: item.description || ''
}))
liveSubagentOptions.value = rows
.filter((item) => item?.enabled !== false)
.map((item) => ({
id: item.name,
name: item.name,
description: item.description || ''
}))
} catch (error) {
console.warn('加载 Subagents 列表失败:', error)
}
@ -739,7 +741,8 @@ const getConfigOptions = (value) => {
return liveSkillOptions.value.length > 0 ? liveSkillOptions.value : value?.options || []
}
if (value?.template_metadata?.kind === 'subagents') {
return liveSubagentOptions.value.length > 0 ? liveSubagentOptions.value : value?.options || []
const options = liveSubagentOptions.value.length > 0 ? liveSubagentOptions.value : value?.options || []
return options.filter((option) => option?.enabled !== false)
}
return value?.options || []
}

View File

@ -300,7 +300,7 @@ onUnmounted(() => {
box-shadow: none;
&:hover {
background: rgba(0, 0, 0, 0.05);
background: var(--shadow-1);
color: var(--primary-color);
}
}

View File

@ -27,31 +27,66 @@
<!-- 服务器列表 -->
<div class="list-container">
<div v-if="filteredServers.length === 0" class="empty-text">
<div v-if="!filteredEnabledServers.length && !filteredDisabledServers.length" class="empty-text">
<a-empty :image="false" :description="searchQuery ? '无匹配服务器' : '暂无服务器'" />
</div>
<template v-for="(server, index) in filteredServers" :key="server.name">
<div v-if="filteredEnabledServers.length" class="list-section-title">已添加</div>
<template v-for="(server, index) in filteredEnabledServers" :key="`enabled-${server.name}`">
<div
class="list-item"
:class="{ active: currentServer?.name === server.name, disabled: !server.enabled }"
class="list-item extension-list-item"
:class="{ active: currentServer?.name === server.name }"
@click="selectServer(server)"
>
<div class="item-header">
<span class="server-icon">{{ server.icon || '🔌' }}</span>
<span class="item-name">{{ server.name }}</span>
<a-switch
size="small"
:checked="server.enabled"
@change="handleToggleServer(server)"
@click.stop
:loading="toggleLoading === server.name"
/>
<div class="item-main-row">
<div class="item-header">
<span class="server-icon">{{ server.icon || '🔌' }}</span>
<span class="item-name">{{ server.name }}</span>
</div>
<div class="item-status">
<span class="status-chip status-chip-success">已添加</span>
<button type="button" class="inline-hover-action" @click.stop="handleSetServerEnabled(server, false)">
移除
</button>
</div>
</div>
<div class="item-details">
<span class="item-desc">{{ server.description || '暂无描述' }}</span>
<div class="item-tags">
<span v-if="server.created_by === 'system'" class="source-tag builtin">内置</span>
</div>
</div>
</div>
<div v-if="index < filteredServers.length - 1" class="list-separator"></div>
<div
v-if="index < filteredEnabledServers.length - 1 || filteredDisabledServers.length > 0"
class="list-separator"
></div>
</template>
<div v-if="filteredDisabledServers.length" class="list-section-title">可添加</div>
<template v-for="(server, index) in filteredDisabledServers" :key="`disabled-${server.name}`">
<div
class="list-item extension-list-item"
:class="{ active: currentServer?.name === server.name, disabled: true }"
@click="selectServer(server)"
>
<div class="item-main-row">
<div class="item-header">
<span class="server-icon">{{ server.icon || '🔌' }}</span>
<span class="item-name">{{ server.name }}</span>
</div>
<div class="item-status">
<button type="button" class="skill-inline-action skill-inline-action-primary" @click.stop="handleSetServerEnabled(server, true)">
添加
</button>
</div>
</div>
<div class="item-details">
<span class="item-desc">{{ server.description || '暂无描述' }}</span>
<div class="item-tags">
<span v-if="server.created_by === 'system'" class="source-tag builtin">内置</span>
</div>
</div>
</div>
<div v-if="index < filteredDisabledServers.length - 1" class="list-separator"></div>
</template>
</div>
</div>
@ -75,34 +110,36 @@
</h2>
<div class="panel-actions">
<a-space :size="8">
<a-button
size="small"
<button
type="button"
@click="handleTestServer(currentServer)"
:loading="testLoading === currentServer.name"
class="lucide-icon-btn"
:disabled="testLoading === currentServer.name"
class="lucide-icon-btn extension-panel-action extension-panel-action-secondary"
>
<Zap :size="14" v-if="testLoading !== currentServer.name" />
<span>测试</span>
</a-button>
<a-button
size="small"
</button>
<button
type="button"
@click="showEditModal(currentServer)"
class="lucide-icon-btn"
class="lucide-icon-btn extension-panel-action extension-panel-action-secondary"
>
<Pencil :size="14" />
<span>编辑</span>
</a-button>
<a-button
size="small"
danger
ghost
:disabled="currentServer.created_by === 'system'"
@click="confirmDeleteServer(currentServer)"
class="lucide-icon-btn"
</button>
<button
type="button"
@click="handleDangerAction(currentServer)"
:class="[
'lucide-icon-btn',
'extension-panel-action',
getServerActionTone(currentServer)
]"
>
<Trash2 :size="14" />
<span>删除</span>
</a-button>
<Plus v-if="currentServer.enabled === false" :size="14" />
<Trash2 v-else :size="14" />
<span>{{ getServerActionLabel(currentServer) }}</span>
</button>
</a-space>
</div>
</div>
@ -452,6 +489,7 @@ import { message, notification, Modal } from 'ant-design-vue'
import {
Search,
Plug,
Plus,
Zap,
Pencil,
Trash2,
@ -518,6 +556,9 @@ const filteredServers = computed(() => {
)
})
const filteredEnabledServers = computed(() => filteredServers.value.filter((item) => !!item.enabled))
const filteredDisabledServers = computed(() => filteredServers.value.filter((item) => !item.enabled))
const isStdioTransport = computed(
() =>
String(form.transport || '')
@ -544,9 +585,11 @@ const fetchServers = async () => {
const result = await mcpApi.getMcpServers()
if (result.success) {
servers.value = result.data || []
//
if (!currentServer.value && servers.value.length > 0) {
selectServer(filteredServers.value[0])
const defaultList = filteredEnabledServers.value.length
? filteredEnabledServers.value
: filteredDisabledServers.value
if (!currentServer.value && defaultList.length > 0) {
selectServer(defaultList[0])
} else if (currentServer.value) {
const latest = servers.value.find((s) => s.name === currentServer.value.name)
if (latest) {
@ -790,19 +833,22 @@ const handleFormSubmit = async () => {
}
}
//
const handleToggleServer = async (server) => {
//
const handleSetServerEnabled = async (server, enabled) => {
try {
toggleLoading.value = server.name
const result = await mcpApi.toggleMcpServer(server.name)
const result = await mcpApi.updateMcpServerStatus(server.name, enabled)
if (result.success) {
message.success(result.message)
message.success(result.message || `MCP 已${enabled ? '添加' : '移除'}`)
await fetchServers()
if (!enabled && currentServer.value?.name === server.name) {
tools.value = []
}
} else {
notification.error({ message: result.message || '操作失败' })
}
} catch (err) {
console.error('切换状态失败:', err)
console.error('更新状态失败:', err)
notification.error({ message: err.message || '操作失败' })
} finally {
toggleLoading.value = null
@ -827,17 +873,33 @@ const handleTestServer = async (server) => {
}
}
//
const confirmDeleteServer = (server) => {
// system
if (server.created_by === 'system') {
notification.warning({
message: '无法删除系统服务器',
description: '系统内置的 MCP 服务器无法删除,如需停用可切换禁用开关。'
})
const handleDangerAction = async (server) => {
if (server.enabled === false) {
await handleSetServerEnabled(server, true)
return
}
if (server.created_by === 'system') {
await handleSetServerEnabled(server, false)
return
}
confirmDeleteServer(server)
}
const getServerActionLabel = (server) => {
if (server?.enabled === false) {
return '添加'
}
return server?.created_by === 'system' ? '移除' : '删除'
}
const getServerActionTone = (server) => {
return server?.enabled === false
? 'extension-panel-action-primary'
: 'extension-panel-action-danger'
}
//
const confirmDeleteServer = (server) => {
Modal.confirm({
title: '确认删除服务器',
content: `确定要删除服务器 "${server.name}" 吗?此操作不可撤销。`,

View File

@ -805,7 +805,7 @@ const testCustomProvider = async (providerId, modelName) => {
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 3px var(--shadow-1);
.card-header {
display: flex;

View File

@ -1312,7 +1312,7 @@ onMounted(() => {
&:hover {
border-color: var(--gray-300);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 4px var(--shadow-1);
}
}
}

View File

@ -24,10 +24,10 @@
>
<a-empty :image="false" description="无匹配技能" />
</div>
<div v-if="filteredInstalledSkills.length" class="list-section-title">安装 Skills</div>
<div v-if="filteredInstalledSkills.length" class="list-section-title">添加 Skills</div>
<template v-for="(skill, index) in filteredInstalledSkills" :key="`installed-${skill.slug}`">
<div
class="list-item skill-list-item"
class="list-item extension-list-item"
:class="{ active: currentSkill?.slug === skill.slug }"
@click="selectSkill(skill)"
>
@ -37,40 +37,16 @@
<span class="item-name">{{ skill.name }}</span>
</div>
<div class="item-status">
<button
v-if="skill.status === 'update_available'"
type="button"
class="skill-inline-action skill-inline-action-secondary"
@click.stop="handleUpdateBuiltin(skill)"
>
更新
<span class="status-chip status-chip-success">已添加</span>
<button type="button" class="inline-hover-action" @click.stop="confirmDeleteSkill(skill)">
移除
</button>
<span
v-else-if="skill.statusLabel"
class="status-chip"
:class="{ warning: skill.statusTone === 'warning' }"
>
{{ skill.statusLabel }}
</span>
</div>
</div>
<div class="item-details">
<span class="item-desc">{{ skill.description || '暂无描述' }}</span>
<div class="item-tags">
<span class="source-tag" :class="{ builtin: skill.sourceType === 'builtin' }">
{{ skill.sourceLabel }}
</span>
</div>
<div class="item-badges">
<span
v-if="skill.tool_dependencies?.length"
class="dot-badge blue"
title="工具依赖"
></span>
<span
v-if="skill.mcp_dependencies?.length"
class="dot-badge green"
title="MCP依赖"
></span>
<span class="source-tag" :class="{ builtin: skill.sourceType === 'builtin' }">{{ skill.sourceLabel }}</span>
</div>
</div>
</div>
@ -80,10 +56,10 @@
></div>
</template>
<div v-if="filteredUninstalledBuiltinSkills.length" class="list-section-title">未安装 Skills</div>
<div v-if="filteredUninstalledBuiltinSkills.length" class="list-section-title">可添加 Skills</div>
<template v-for="(skill, index) in filteredUninstalledBuiltinSkills" :key="`builtin-${skill.slug}`">
<div
class="list-item skill-list-item"
class="list-item extension-list-item"
:class="{ active: currentSkill?.slug === skill.slug }"
@click="selectSkill(skill)"
>
@ -102,22 +78,11 @@
</button>
</div>
</div>
<div class="item-details item-details-inline">
<div class="item-details">
<span class="item-desc">{{ skill.description || '暂无描述' }}</span>
<div class="item-tags">
<span class="source-tag builtin">内置</span>
</div>
<div class="item-badges">
<span
v-if="skill.installed_record?.tool_dependencies?.length || skill.tool_dependencies?.length"
class="dot-badge blue"
title="工具依赖"
></span>
<span
v-if="skill.installed_record?.mcp_dependencies?.length || skill.mcp_dependencies?.length"
class="dot-badge green"
title="MCP依赖"
></span>
</div>
</div>
</div>
<div v-if="index < filteredUninstalledBuiltinSkills.length - 1" class="list-separator"></div>
@ -153,7 +118,7 @@
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'not_installed'"
type="button"
@click="handleInstallBuiltin(currentSkill)"
class="lucide-icon-btn skill-panel-action skill-panel-action-primary"
class="lucide-icon-btn extension-panel-action extension-panel-action-primary"
>
<span>安装</span>
</button>
@ -161,7 +126,7 @@
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'update_available'"
type="button"
@click="handleUpdateBuiltin(currentSkill)"
class="lucide-icon-btn skill-panel-action skill-panel-action-secondary"
class="lucide-icon-btn extension-panel-action extension-panel-action-secondary"
>
<span>更新</span>
</button>
@ -169,7 +134,7 @@
v-if="isInstalledSkill"
type="button"
@click="handleExport"
class="lucide-icon-btn skill-panel-action skill-panel-action-secondary"
class="lucide-icon-btn extension-panel-action extension-panel-action-secondary"
>
<Download :size="14" />
<span>导出</span>
@ -178,7 +143,7 @@
v-if="isInstalledSkill"
type="button"
@click="confirmDeleteSkill"
class="lucide-icon-btn skill-panel-action skill-panel-action-danger"
class="lucide-icon-btn extension-panel-action extension-panel-action-danger"
>
<Trash2 :size="14" />
<span>{{ isBuiltinInstalledSkill ? '卸载' : '删除' }}</span>
@ -478,10 +443,6 @@ const isBuiltinInstalledSkill = computed(() => {
return !!(isInstalledSkill.value && (currentSkill.value?.is_builtin || currentSkill.value?.installed_record))
})
const currentSkillDeleteActionText = computed(() => {
return isBuiltinInstalledSkill.value ? '卸载' : '删除'
})
const currentSkillStatusLabel = computed(() => {
const skill = currentSkill.value
if (!skill) return ''
@ -781,25 +742,34 @@ const handleCreateNode = async () => {
}
}
const confirmDeleteSkill = () => {
if (!currentSkill.value || !isInstalledSkill.value) return
const actionText = currentSkillDeleteActionText.value
const detailText = isBuiltinInstalledSkill.value
const confirmDeleteSkill = (targetSkill = null) => {
const target = targetSkill || currentSkill.value
if (!target) return
const installed =
!!(target && (target.installed_record || target.dir_path || target.is_builtin || target.sourceType))
if (!installed) return
const isBuiltinTarget = !!(target?.is_builtin || target?.installed_record || target?.sourceType === 'builtin')
const actionText = isBuiltinTarget ? '卸载' : '删除'
const detailText = isBuiltinTarget
? '卸载后会移除已安装文件和数据库记录,但仍可从“未安装 Skills”中重新安装。'
: '删除后无法恢复,所有文件和配置将永久消失。'
Modal.confirm({
title: `确认${actionText}技能「${currentSkill.value.slug}」?`,
title: `确认${actionText}技能「${target.slug}」?`,
content: detailText,
okText: `确认${actionText}`,
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await skillApi.deleteSkill(currentSkill.value.slug)
await skillApi.deleteSkill(target.slug)
message.success(`${actionText}`)
currentSkill.value = null
treeData.value = []
resetFileState()
if (currentSkill.value?.slug === target.slug) {
currentSkill.value = null
treeData.value = []
resetFileState()
}
await fetchSkills()
} catch {
message.error(`${actionText}失败`)
@ -879,248 +849,6 @@ defineExpose({
<style scoped lang="less">
@import '@/assets/css/extensions.less';
.list-section-title {
padding: 10px 14px 6px;
color: var(--gray-500);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.4px;
text-transform: uppercase;
}
.skill-list-item {
.item-main-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.item-header {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
margin-bottom: 0;
.item-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.item-status {
flex-shrink: 0;
}
.status-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 9px;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-600);
font-size: 11px;
font-weight: 600;
line-height: 1;
&.warning {
background: var(--color-warning-50);
color: var(--color-warning-900);
}
}
.item-details {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
.item-tags {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.source-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 20px;
padding: 0 8px;
border-radius: 999px;
background: var(--color-info-50);
color: var(--color-info-700);
font-size: 11px;
font-weight: 600;
line-height: 1;
flex-shrink: 0;
&.builtin {
background: var(--color-primary-50);
color: var(--color-primary-700);
}
}
.item-badges {
display: flex;
gap: 4px;
flex-shrink: 0;
.dot-badge {
width: 6px;
height: 6px;
border-radius: 50%;
&.blue {
background-color: var(--color-info-500);
}
&.green {
background-color: var(--color-success-500);
}
}
}
}
}
.skill-inline-action {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
min-width: 52px;
height: 24px;
padding: 0 9px;
border-style: solid;
border-width: 1px;
box-shadow: none;
font-size: 12px;
font-weight: 600;
line-height: 1;
transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer;
appearance: none;
&.skill-inline-action-primary {
border-color: transparent;
background: var(--main-600);
color: var(--main-0);
}
&.skill-inline-action-secondary {
border-color: var(--main-100);
background: var(--main-30);
color: var(--main-700);
}
&:hover,
&:focus {
outline: none;
}
&.skill-inline-action-primary:hover,
&.skill-inline-action-primary:focus {
background: var(--main-700);
color: var(--main-0);
}
&.skill-inline-action-secondary:hover,
&.skill-inline-action-secondary:focus {
border-color: var(--main-200);
background: var(--main-50);
color: var(--main-800);
}
}
.skill-panel-action {
min-height: 30px;
padding: 0 12px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
border-style: solid;
border-width: 1px;
box-shadow: none;
font-weight: 500;
transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer;
appearance: none;
&:hover,
&:focus {
outline: none;
}
&.skill-panel-action-primary {
border-color: transparent;
background: var(--main-600);
color: var(--main-0);
}
&.skill-panel-action-primary:hover,
&.skill-panel-action-primary:focus {
background: var(--main-700);
color: var(--main-0);
}
&.skill-panel-action-secondary {
border-color: var(--gray-200);
background: var(--gray-25);
color: var(--gray-700);
&:hover,
&:focus {
border-color: var(--gray-300);
color: var(--gray-900);
background: var(--gray-0);
}
}
&.skill-panel-action-danger {
border-color: var(--color-error-100);
background: var(--color-error-50);
color: var(--color-error-700);
&:hover,
&:focus {
border-color: var(--color-error-100);
background: var(--color-error-100);
color: var(--color-error-900);
}
}
}
.panel-status-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-600);
font-size: 12px;
font-weight: 600;
line-height: 1;
&.warning {
background: var(--color-warning-50);
color: var(--color-warning-900);
}
}
.builtin-uninstalled-state {
padding: 24px;
h3 {

View File

@ -20,27 +20,67 @@
<!-- SubAgent 列表 -->
<div class="list-container">
<div v-if="filteredSubAgents.length === 0" class="empty-text">
<a-empty
:image="false"
:description="searchQuery ? '无匹配 SubAgent' : '暂无 SubAgent'"
/>
<div v-if="!filteredEnabledSubAgents.length && !filteredDisabledSubAgents.length" class="empty-text">
<a-empty :image="false" :description="searchQuery ? '无匹配 SubAgent' : '暂无 SubAgent'" />
</div>
<template v-for="(agent, index) in filteredSubAgents" :key="agent.name">
<div v-if="filteredEnabledSubAgents.length" class="list-section-title">已添加</div>
<template v-for="(agent, index) in filteredEnabledSubAgents" :key="`enabled-${agent.name}`">
<div
class="list-item"
class="list-item extension-list-item"
:class="{ active: currentAgent?.name === agent.name }"
@click="selectAgent(agent)"
>
<div class="item-header">
<Bot :size="16" class="item-icon" />
<span class="item-name">{{ agent.name }}</span>
<div class="item-main-row">
<div class="item-header">
<Bot :size="16" class="item-icon" />
<span class="item-name">{{ agent.name }}</span>
</div>
<div class="item-status">
<span class="status-chip status-chip-success">已添加</span>
<button type="button" class="inline-hover-action danger" @click.stop="handleSetAgentEnabled(agent, false)">
移除
</button>
</div>
</div>
<div class="item-details">
<span class="item-desc">{{ agent.description || '暂无描述' }}</span>
<div class="item-tags">
<span v-if="agent.is_builtin" class="source-tag builtin">内置</span>
</div>
</div>
</div>
<div v-if="index < filteredSubAgents.length - 1" class="list-separator"></div>
<div
v-if="index < filteredEnabledSubAgents.length - 1 || filteredDisabledSubAgents.length > 0"
class="list-separator"
></div>
</template>
<div v-if="filteredDisabledSubAgents.length" class="list-section-title">可添加</div>
<template v-for="(agent, index) in filteredDisabledSubAgents" :key="`disabled-${agent.name}`">
<div
class="list-item extension-list-item"
:class="{ active: currentAgent?.name === agent.name }"
@click="selectAgent(agent)"
>
<div class="item-main-row">
<div class="item-header">
<Bot :size="16" class="item-icon" />
<span class="item-name">{{ agent.name }}</span>
</div>
<div class="item-status">
<button type="button" class="skill-inline-action skill-inline-action-primary" @click.stop="handleSetAgentEnabled(agent, true)">
添加
</button>
</div>
</div>
<div class="item-details">
<span class="item-desc">{{ agent.description || '暂无描述' }}</span>
<div class="item-tags">
<span v-if="agent.is_builtin" class="source-tag builtin">内置</span>
</div>
</div>
</div>
<div v-if="index < filteredDisabledSubAgents.length - 1" class="list-separator"></div>
</template>
</div>
</div>
@ -132,13 +172,15 @@
</div>
</div>
<div class="detail-section" v-if="currentAgent.is_builtin">
<div class="detail-section" v-if="currentAgent.is_builtin || currentAgent.enabled === false">
<div class="section-header">
<Info :size="14" />
<span>类型</span>
</div>
<div class="section-content">
<a-tag color="blue">内置</a-tag>
<a-tag v-if="currentAgent.is_builtin" color="blue">内置</a-tag>
<a-tag v-else color="default">自定义</a-tag>
<a-tag v-if="currentAgent.enabled === false" color="default">未添加</a-tag>
</div>
</div>
@ -283,6 +325,9 @@ const filteredSubAgents = computed(() => {
)
})
const filteredEnabledSubAgents = computed(() => filteredSubAgents.value.filter((item) => item.enabled !== false))
const filteredDisabledSubAgents = computed(() => filteredSubAgents.value.filter((item) => item.enabled === false))
// SubAgent
const fetchSubAgents = async () => {
try {
@ -300,9 +345,12 @@ const fetchSubAgents = async () => {
currentAgent.value = null
}
}
//
if (!currentAgent.value && subagents.value.length > 0) {
currentAgent.value = getSortedSubAgents(subagents.value)[0]
//
const defaultList = filteredEnabledSubAgents.value.length
? filteredEnabledSubAgents.value
: filteredDisabledSubAgents.value
if (!currentAgent.value && defaultList.length > 0) {
currentAgent.value = defaultList[0]
}
} else {
error.value = result.message || '获取列表失败'
@ -335,6 +383,21 @@ const handleModelSelect = (spec) => {
form.model = spec || ''
}
const handleSetAgentEnabled = async (agent, enabled) => {
try {
const result = await subagentApi.updateSubAgentStatus(agent.name, enabled)
if (result.success) {
message.success(result.message || `SubAgent 已${enabled ? '添加' : '移除'}`)
await fetchSubAgents()
} else {
message.error(result.message || '操作失败')
}
} catch (err) {
console.error('更新状态失败:', err)
message.error(err.message || '操作失败')
}
}
// SubAgent
const selectAgent = (agent) => {
currentAgent.value = agent

View File

@ -585,7 +585,7 @@ onMounted(async () => {
padding-bottom: 6px;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 3px var(--shadow-1);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);

View File

@ -135,7 +135,7 @@ const getSatisfactionClass = () => {
&:hover {
border-color: var(--gray-200);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 3px 0 var(--shadow-1);
}
&.primary {

View File

@ -3,9 +3,9 @@
<div class="extensions-header">
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
<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-tab-pane key="skills" tab="Skills 管理" />
</a-tabs>
<div class="header-actions">
<!-- Skills Tab 的按钮 -->
@ -37,7 +37,7 @@
<template v-else-if="activeTab === 'mcp'">
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
<Plus :size="14" />
<span>添加服务器</span>
<span>添加 MCP</span>
</a-button>
<a-button @click="handleMcpRefresh" :disabled="mcpLoading" class="lucide-icon-btn">
<RotateCw :size="14" />