feat: 重构扩展管理中的 SubAgent 与 MCP 交互,统一为类似 Skills 的方式。
This commit is contained in:
parent
33320022af
commit
b7e10c6666
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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",
|
||||
"""
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
67
backend/test/test_mcp_router.py
Normal file
67
backend/test/test_mcp_router.py
Normal 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
|
||||
@ -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
|
||||
# =============================================================================
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
MCP(Model Context Protocol)是扩展智能体能力的重要方式。系统支持通过管理界面动态配置 MCP 服务器,无需修改代码。
|
||||
|
||||
内置 MCP 服务器以代码为事实源:系统启动时会自动补齐缺失项,并用代码中的最新连接与展示字段覆盖数据库定义;是否“已添加”以及工具级禁用列表仍保留数据库状态。
|
||||
|
||||
## 支持的传输协议
|
||||
|
||||
| 协议 | 说明 | 适用场景 |
|
||||
@ -37,6 +39,13 @@ MCP(Model Context Protocol)是扩展智能体能力的重要方式。系统
|
||||
}
|
||||
```
|
||||
|
||||
## 服务器管理
|
||||
|
||||
管理界面使用“添加 / 移除”语义管理 MCP 服务器:
|
||||
|
||||
- 已添加:`enabled=true`,会加载到运行时缓存并可供 Agent 使用
|
||||
- 可添加:`enabled=false`,记录保留但不会进入运行时
|
||||
|
||||
## 工具管理
|
||||
|
||||
MCP 工具支持粒度控制:管理员可以单独启用或禁用某个 MCP 服务器下的特定工具,实现精细化的权限管理。
|
||||
|
||||
@ -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} | 删除 |
|
||||
|
||||
所有接口均要求管理员权限。
|
||||
|
||||
@ -48,6 +48,7 @@
|
||||
- 重构内置 Skills 安装机制:内置 skill 改为在管理页以“未安装”状态展示,支持按需安装、基于 `version + content_hash` 的更新提示与覆盖确认,并对已安装内置 skill 禁止在线文件编辑
|
||||
- 新增知识库 PDF、图片的预览功能
|
||||
- 优化扩展页工具列表筛选区:将“全部分类”筛选收纳为搜索框右侧的紧凑下拉入口,并复用扩展页侧栏工具条样式,避免影响其他管理组件布局
|
||||
- 重构扩展管理中的 SubAgent 与 MCP 交互,统一为类似 Skills 的“已添加 / 可添加”列表,不再使用服务器级开关切换,为 SubAgent 增加 `enabled` 状态,并让运行时只加载已添加项,调整内置 SubAgent / MCP 的启动同步逻辑,使用代码中的最新定义覆盖数据库展示字段,同时保留启用状态与 MCP 工具禁用状态;统一扩展详情区胶囊操作按钮样式到公共 `extensions.less`,并将内置 MCP 在详情页中的危险操作从禁用“删除”改为可执行的“移除”
|
||||
|
||||
<!-- 添加到这里 -->
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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; /* 强调色-标准 */
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 || []
|
||||
}
|
||||
|
||||
@ -300,7 +300,7 @@ onUnmounted(() => {
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
background: var(--shadow-1);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}" 吗?此操作不可撤销。`,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user