diff --git a/backend/package/yuxi/repositories/subagent_repository.py b/backend/package/yuxi/repositories/subagent_repository.py index 73a91fae..9ca994c8 100644 --- a/backend/package/yuxi/repositories/subagent_repository.py +++ b/backend/package/yuxi/repositories/subagent_repository.py @@ -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, diff --git a/backend/package/yuxi/services/mcp_service.py b/backend/package/yuxi/services/mcp_service.py index 27e1068c..6f8979cb 100644 --- a/backend/package/yuxi/services/mcp_service.py +++ b/backend/package/yuxi/services/mcp_service.py @@ -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 diff --git a/backend/package/yuxi/services/subagent_service.py b/backend/package/yuxi/services/subagent_service.py index 9d1456c1..8a6b3a88 100644 --- a/backend/package/yuxi/services/subagent_service.py +++ b/backend/package/yuxi/services/subagent_service.py @@ -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() diff --git a/backend/package/yuxi/storage/postgres/manager.py b/backend/package/yuxi/storage/postgres/manager.py index 50095ea1..8c88c622 100644 --- a/backend/package/yuxi/storage/postgres/manager.py +++ b/backend/package/yuxi/storage/postgres/manager.py @@ -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", """ diff --git a/backend/package/yuxi/storage/postgres/models_business.py b/backend/package/yuxi/storage/postgres/models_business.py index e1147c69..d5a290f4 100644 --- a/backend/package/yuxi/storage/postgres/models_business.py +++ b/backend/package/yuxi/storage/postgres/models_business.py @@ -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, diff --git a/backend/server/routers/mcp_router.py b/backend/server/routers/mcp_router.py index e04cd318..7d6be44e 100644 --- a/backend/server/routers/mcp_router.py +++ b/backend/server/routers/mcp_router.py @@ -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)) diff --git a/backend/server/routers/subagent_router.py b/backend/server/routers/subagent_router.py index 621b7e7f..f535006c 100644 --- a/backend/server/routers/subagent_router.py +++ b/backend/server/routers/subagent_router.py @@ -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) diff --git a/backend/test/test_mcp_router.py b/backend/test/test_mcp_router.py new file mode 100644 index 00000000..23c166c2 --- /dev/null +++ b/backend/test/test_mcp_router.py @@ -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 diff --git a/backend/test/test_subagent.py b/backend/test/test_subagent.py index cbffa029..8e6deb4f 100644 --- a/backend/test/test_subagent.py +++ b/backend/test/test_subagent.py @@ -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 # ============================================================================= diff --git a/docs/agents/mcp-integration.md b/docs/agents/mcp-integration.md index 298d0049..6f68cf2d 100644 --- a/docs/agents/mcp-integration.md +++ b/docs/agents/mcp-integration.md @@ -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 服务器下的特定工具,实现精细化的权限管理。 diff --git a/docs/agents/subagents-management.md b/docs/agents/subagents-management.md index 0f4faf23..8b6eff87 100644 --- a/docs/agents/subagents-management.md +++ b/docs/agents/subagents-management.md @@ -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} | 删除 | 所有接口均要求管理员权限。 diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index f79ac37a..843620a8 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -48,6 +48,7 @@ - 重构内置 Skills 安装机制:内置 skill 改为在管理页以“未安装”状态展示,支持按需安装、基于 `version + content_hash` 的更新提示与覆盖确认,并对已安装内置 skill 禁止在线文件编辑 - 新增知识库 PDF、图片的预览功能 - 优化扩展页工具列表筛选区:将“全部分类”筛选收纳为搜索框右侧的紧凑下拉入口,并复用扩展页侧栏工具条样式,避免影响其他管理组件布局 +- 重构扩展管理中的 SubAgent 与 MCP 交互,统一为类似 Skills 的“已添加 / 可添加”列表,不再使用服务器级开关切换,为 SubAgent 增加 `enabled` 状态,并让运行时只加载已添加项,调整内置 SubAgent / MCP 的启动同步逻辑,使用代码中的最新定义覆盖数据库展示字段,同时保留启用状态与 MCP 工具禁用状态;统一扩展详情区胶囊操作按钮样式到公共 `extensions.less`,并将内置 MCP 在详情页中的危险操作从禁用“删除”改为可执行的“移除” diff --git a/web/src/apis/mcp_api.js b/web/src/apis/mcp_api.js index 008266c8..c0a58745 100644 --- a/web/src/apis/mcp_api.js +++ b/web/src/apis/mcp_api.js @@ -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 diff --git a/web/src/apis/subagent_api.js b/web/src/apis/subagent_api.js index 00789112..3fb9aa56 100644 --- a/web/src/apis/subagent_api.js +++ b/web/src/apis/subagent_api.js @@ -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 diff --git a/web/src/assets/css/base.css b/web/src/assets/css/base.css index 242a5ac5..0769a6b1 100644 --- a/web/src/assets/css/base.css +++ b/web/src/assets/css/base.css @@ -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; /* 强调色-标准 */ diff --git a/web/src/assets/css/extensions.less b/web/src/assets/css/extensions.less index 2e3ad3d7..948d8ddf 100644 --- a/web/src/assets/css/extensions.less +++ b/web/src/assets/css/extensions.less @@ -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); diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index 151a7c37..1c4780b1 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -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 || [] } diff --git a/web/src/components/KnowledgeGraphSection.vue b/web/src/components/KnowledgeGraphSection.vue index 9e30e55e..4b9d757a 100644 --- a/web/src/components/KnowledgeGraphSection.vue +++ b/web/src/components/KnowledgeGraphSection.vue @@ -300,7 +300,7 @@ onUnmounted(() => { box-shadow: none; &:hover { - background: rgba(0, 0, 0, 0.05); + background: var(--shadow-1); color: var(--primary-color); } } diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue index 5800dd1f..619c8537 100644 --- a/web/src/components/McpServersComponent.vue +++ b/web/src/components/McpServersComponent.vue @@ -27,31 +27,66 @@
-
+
-