diff --git a/backend/api/agents.py b/backend/api/agents.py index 73a562a..2633f5c 100644 --- a/backend/api/agents.py +++ b/backend/api/agents.py @@ -14,7 +14,11 @@ from fastapi import APIRouter, HTTPException, Depends, Body, UploadFile, File, F from pydantic import BaseModel, Field from backend.agents import AgentFactory, WorkspaceManager, get_registry +from backend.agents.agent_workspace import load_agent_workspace_config from backend.agents.skills_manager import SkillsManager +from backend.agents.toolkit_factory import load_agent_profiles +from backend.config.bootstrap_config import get_bootstrap_config_for_run +from backend.llm.models import get_agent_model_info logger = logging.getLogger(__name__) @@ -47,6 +51,14 @@ class InstallExternalSkillRequest(BaseModel): activate: bool = Field(True, description="Whether to enable skill immediately") +class LocalSkillRequest(BaseModel): + skill_name: str = Field(..., description="Local skill name") + + +class LocalSkillContentRequest(BaseModel): + content: str = Field(..., description="Updated SKILL.md content") + + class AgentResponse(BaseModel): """Agent information response.""" agent_id: str @@ -63,6 +75,24 @@ class AgentFileResponse(BaseModel): content: str +class AgentProfileResponse(BaseModel): + agent_id: str + workspace_id: str + profile: Dict[str, Any] + + +class AgentSkillsResponse(BaseModel): + agent_id: str + workspace_id: str + skills: List[Dict[str, Any]] + + +class SkillDetailResponse(BaseModel): + agent_id: str + workspace_id: str + skill: Dict[str, Any] + + # Dependencies def get_agent_factory(): """Get AgentFactory instance.""" @@ -199,6 +229,108 @@ async def get_agent( ) +@router.get("/{agent_id}/profile", response_model=AgentProfileResponse) +async def get_agent_profile( + workspace_id: str, + agent_id: str, + skills_manager: SkillsManager = Depends(get_skills_manager), +): + asset_dir = skills_manager.get_agent_asset_dir(workspace_id, agent_id) + agent_config = load_agent_workspace_config(asset_dir / "agent.yaml") + profiles = load_agent_profiles() + profile = profiles.get(agent_id, {}) + bootstrap = get_bootstrap_config_for_run(skills_manager.project_root, workspace_id) + override = bootstrap.agent_override(agent_id) + active_tool_groups = override.get("active_tool_groups", agent_config.active_tool_groups or profile.get("active_tool_groups", [])) + if not isinstance(active_tool_groups, list): + active_tool_groups = [] + disabled_tool_groups = agent_config.disabled_tool_groups + if disabled_tool_groups: + disabled_set = set(disabled_tool_groups) + active_tool_groups = [group_name for group_name in active_tool_groups if group_name not in disabled_set] + + default_skills = profile.get("skills", []) + if not isinstance(default_skills, list): + default_skills = [] + resolved_skills = skills_manager.resolve_agent_skill_names( + config_name=workspace_id, + agent_id=agent_id, + default_skills=default_skills, + ) + prompt_files = agent_config.prompt_files or ["SOUL.md", "PROFILE.md", "AGENTS.md", "POLICY.md", "MEMORY.md"] + model_name, model_provider = get_agent_model_info(agent_id) + + return AgentProfileResponse( + agent_id=agent_id, + workspace_id=workspace_id, + profile={ + "model_name": model_name, + "model_provider": model_provider, + "prompt_files": prompt_files, + "default_skills": default_skills, + "resolved_skills": resolved_skills, + "active_tool_groups": active_tool_groups, + "disabled_tool_groups": disabled_tool_groups, + "enabled_skills": agent_config.enabled_skills, + "disabled_skills": agent_config.disabled_skills, + }, + ) + + +@router.get("/{agent_id}/skills", response_model=AgentSkillsResponse) +async def get_agent_skills( + workspace_id: str, + agent_id: str, + skills_manager: SkillsManager = Depends(get_skills_manager), +): + agent_asset_dir = skills_manager.get_agent_asset_dir(workspace_id, agent_id) + agent_config = load_agent_workspace_config(agent_asset_dir / "agent.yaml") + resolved_skills = set(skills_manager.resolve_agent_skill_names(config_name=workspace_id, agent_id=agent_id, default_skills=[])) + enabled = set(agent_config.enabled_skills) + disabled = set(agent_config.disabled_skills) + + payload = [] + for item in skills_manager.list_agent_skill_catalog(workspace_id, agent_id): + if item.skill_name in disabled: + status = "disabled" + elif item.skill_name in enabled: + status = "enabled" + elif item.skill_name in resolved_skills: + status = "active" + else: + status = "available" + payload.append({ + "skill_name": item.skill_name, + "name": item.name, + "description": item.description, + "version": item.version, + "source": item.source, + "tools": item.tools, + "status": status, + }) + + return AgentSkillsResponse(agent_id=agent_id, workspace_id=workspace_id, skills=payload) + + +@router.get("/{agent_id}/skills/{skill_name}", response_model=SkillDetailResponse) +async def get_agent_skill_detail( + workspace_id: str, + agent_id: str, + skill_name: str, + skills_manager: SkillsManager = Depends(get_skills_manager), +): + try: + detail = skills_manager.load_agent_skill_document( + config_name=workspace_id, + agent_id=agent_id, + skill_name=skill_name, + ) + except FileNotFoundError: + raise HTTPException(status_code=404, detail=f"Unknown skill: {skill_name}") + + return SkillDetailResponse(agent_id=agent_id, workspace_id=workspace_id, skill=detail) + + @router.delete("/{agent_id}") async def delete_agent( workspace_id: str, @@ -386,6 +518,85 @@ async def install_external_skill( } +@router.post("/{agent_id}/skills/local") +async def create_local_skill( + workspace_id: str, + agent_id: str, + request: LocalSkillRequest, + registry=Depends(get_registry), +): + agent_info = registry.get(agent_id) + if not agent_info or agent_info.workspace_id != workspace_id: + raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found") + + skills_manager = SkillsManager() + try: + skills_manager.create_agent_local_skill( + config_name=workspace_id, + agent_id=agent_id, + skill_name=request.skill_name, + ) + except (ValueError, FileExistsError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) + + return {"message": f"Created local skill '{request.skill_name}' for '{agent_id}'"} + + +@router.put("/{agent_id}/skills/local/{skill_name}") +async def update_local_skill( + workspace_id: str, + agent_id: str, + skill_name: str, + request: LocalSkillContentRequest, + registry=Depends(get_registry), +): + agent_info = registry.get(agent_id) + if not agent_info or agent_info.workspace_id != workspace_id: + raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found") + + skills_manager = SkillsManager() + try: + skills_manager.update_agent_local_skill( + config_name=workspace_id, + agent_id=agent_id, + skill_name=skill_name, + content=request.content, + ) + except (ValueError, FileNotFoundError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) + + return {"message": f"Updated local skill '{skill_name}' for '{agent_id}'"} + + +@router.delete("/{agent_id}/skills/local/{skill_name}") +async def delete_local_skill( + workspace_id: str, + agent_id: str, + skill_name: str, + registry=Depends(get_registry), +): + agent_info = registry.get(agent_id) + if not agent_info or agent_info.workspace_id != workspace_id: + raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found") + + skills_manager = SkillsManager() + try: + skills_manager.delete_agent_local_skill( + config_name=workspace_id, + agent_id=agent_id, + skill_name=skill_name, + ) + skills_manager.forget_agent_skill_overrides( + config_name=workspace_id, + agent_id=agent_id, + skill_names=[skill_name], + ) + except (ValueError, FileNotFoundError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) + + return {"message": f"Deleted local skill '{skill_name}' for '{agent_id}'"} + + @router.post("/{agent_id}/skills/upload") async def upload_external_skill( workspace_id: str, diff --git a/backend/services/gateway_admin_handlers.py b/backend/services/gateway_admin_handlers.py index 424fb11..607ec98 100644 --- a/backend/services/gateway_admin_handlers.py +++ b/backend/services/gateway_admin_handlers.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- -"""Runtime/workspace/skills handlers extracted from the main Gateway module.""" +"""Runtime/workspace/skills handlers extracted from the main Gateway module. + +Deprecated note: + Agent/workspace/skill read-write operations are being migrated to + agent_service REST endpoints. These websocket handlers remain as a + compatibility fallback and should not be considered the primary control + plane path for frontend reads/writes. +""" from __future__ import annotations diff --git a/backend/tests/test_agent_service_app.py b/backend/tests/test_agent_service_app.py index 02d64d4..7978491 100644 --- a/backend/tests/test_agent_service_app.py +++ b/backend/tests/test_agent_service_app.py @@ -6,6 +6,7 @@ from pathlib import Path from fastapi.testclient import TestClient from backend.apps.agent_service import create_app +from backend.api import agents as agents_module def test_agent_service_routes_include_control_plane_endpoints(tmp_path): @@ -25,3 +26,79 @@ def test_agent_service_excludes_runtime_routes(tmp_path): assert "/api/runtime/start" not in paths assert "/api/runtime/gateway/port" not in paths + + +def test_agent_service_read_routes(monkeypatch, tmp_path): + class _FakeSkillsManager: + project_root = tmp_path + + def get_agent_asset_dir(self, config_name, agent_id): + return tmp_path / "runs" / config_name / "agents" / agent_id + + def resolve_agent_skill_names(self, config_name, agent_id, default_skills=None): + return ["demo_skill"] + + def list_agent_skill_catalog(self, config_name, agent_id): + return [ + type( + "Skill", + (), + { + "skill_name": "demo_skill", + "name": "Demo Skill", + "description": "demo", + "version": "1.0.0", + "source": "builtin", + "tools": [], + }, + )() + ] + + def load_agent_skill_document(self, config_name, agent_id, skill_name): + return {"skill_name": skill_name, "content": "# demo"} + + class _FakeWorkspaceManager: + def load_agent_file(self, config_name, agent_id, filename): + return f"{config_name}:{agent_id}:{filename}" + + monkeypatch.setattr(agents_module, "load_agent_profiles", lambda: {"portfolio_manager": {"skills": ["demo_skill"]}}) + monkeypatch.setattr(agents_module, "get_agent_model_info", lambda agent_id: ("deepseek-v3.2", "DASHSCOPE")) + monkeypatch.setattr( + agents_module, + "load_agent_workspace_config", + lambda path: type( + "Cfg", + (), + { + "active_tool_groups": ["portfolio_ops"], + "disabled_tool_groups": [], + "enabled_skills": [], + "disabled_skills": [], + "prompt_files": ["SOUL.md", "MEMORY.md"], + }, + )(), + ) + monkeypatch.setattr( + agents_module, + "get_bootstrap_config_for_run", + lambda project_root, config_name: type("Bootstrap", (), {"agent_override": lambda self, agent_id: {}})(), + ) + + app = create_app(project_root=tmp_path) + app.dependency_overrides[agents_module.get_skills_manager] = lambda: _FakeSkillsManager() + app.dependency_overrides[agents_module.get_workspace_manager] = lambda: _FakeWorkspaceManager() + + with TestClient(app) as client: + profile = client.get("/api/workspaces/demo/agents/portfolio_manager/profile") + skills = client.get("/api/workspaces/demo/agents/portfolio_manager/skills") + detail = client.get("/api/workspaces/demo/agents/portfolio_manager/skills/demo_skill") + workspace_file = client.get("/api/workspaces/demo/agents/portfolio_manager/files/MEMORY.md") + + assert profile.status_code == 200 + assert profile.json()["profile"]["model_name"] == "deepseek-v3.2" + assert skills.status_code == 200 + assert skills.json()["skills"][0]["skill_name"] == "demo_skill" + assert detail.status_code == 200 + assert detail.json()["skill"]["content"] == "# demo" + assert workspace_file.status_code == 200 + assert workspace_file.json()["content"] == "demo:portfolio_manager:MEMORY.md" diff --git a/frontend/src/hooks/useAgentDataRequests.js b/frontend/src/hooks/useAgentDataRequests.js index 28c2f0b..dbe0329 100644 --- a/frontend/src/hooks/useAgentDataRequests.js +++ b/frontend/src/hooks/useAgentDataRequests.js @@ -1,5 +1,18 @@ import { useCallback } from 'react'; -import { uploadAgentSkillZip } from '../services/runtimeApi'; +import { + createAgentLocalSkill, + deleteAgentLocalSkill, + disableAgentSkill, + enableAgentSkill, + fetchAgentProfile, + fetchAgentSkillDetail, + fetchAgentSkills, + fetchAgentWorkspaceFile, + fetchCurrentRuntime, + updateAgentLocalSkill, + updateAgentWorkspaceFile, + uploadAgentSkillZip +} from '../services/runtimeApi'; import { useAgentStore } from '../store/agentStore'; /** @@ -10,12 +23,16 @@ export function useAgentDataRequests(clientRef) { const { selectedSkillAgentId, setSelectedSkillAgentId, + setAgentProfilesByAgent, setIsAgentSkillsLoading, setAgentSkillsFeedback, setAgentSkillsSavingKey, setSkillDetailLoadingKey, + setAgentSkillsByAgent, + setSkillDetailsByName, localSkillDraftsByKey, selectedWorkspaceFile, + setWorkspaceFilesByAgent, setWorkspaceDraftContent, workspaceDraftContent, setWorkspaceFileFeedback, @@ -23,27 +40,88 @@ export function useAgentDataRequests(clientRef) { setIsWorkspaceFileLoading } = useAgentStore(); + const resolveWorkspaceId = useCallback(async () => { + const runtime = await fetchCurrentRuntime(); + const workspaceId = runtime?.run_id; + if (!workspaceId) { + throw new Error('未检测到正在运行的任务'); + } + return workspaceId; + }, []); + const requestAgentSkills = useCallback((agentId) => { const normalized = typeof agentId === 'string' ? agentId.trim() : ''; - if (!normalized || !clientRef.current) return false; + if (!normalized) return false; setIsAgentSkillsLoading(true); setAgentSkillsFeedback(null); - return clientRef.current.send({ type: 'get_agent_skills', agent_id: normalized }); - }, [clientRef, setIsAgentSkillsLoading, setAgentSkillsFeedback]); + void resolveWorkspaceId() + .then((workspaceId) => fetchAgentSkills(workspaceId, normalized)) + .then((payload) => { + setAgentSkillsByAgent((prev) => ({ ...prev, [normalized]: Array.isArray(payload?.skills) ? payload.skills : [] })); + setIsAgentSkillsLoading(false); + }) + .catch(() => { + if (!clientRef.current) { + setIsAgentSkillsLoading(false); + return; + } + console.debug('REST agent skills request failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'get_agent_skills', agent_id: normalized }); + if (!success) { + setIsAgentSkillsLoading(false); + } + }); + return true; + }, [clientRef, resolveWorkspaceId, setAgentSkillsByAgent, setIsAgentSkillsLoading, setAgentSkillsFeedback]); const requestAgentProfile = useCallback((agentId) => { const normalized = typeof agentId === 'string' ? agentId.trim() : ''; - if (!normalized || !clientRef.current) return false; - return clientRef.current.send({ type: 'get_agent_profile', agent_id: normalized }); - }, [clientRef]); + if (!normalized) return false; + void resolveWorkspaceId() + .then((workspaceId) => fetchAgentProfile(workspaceId, normalized)) + .then((payload) => { + setAgentProfilesByAgent((prev) => ({ + ...prev, + [normalized]: payload?.profile && typeof payload.profile === 'object' ? payload.profile : {} + })); + }) + .catch(() => { + if (clientRef.current) { + console.debug('REST agent profile request failed, falling back to websocket compatibility path'); + clientRef.current.send({ type: 'get_agent_profile', agent_id: normalized }); + } + }); + return true; + }, [clientRef, resolveWorkspaceId, setAgentProfilesByAgent]); const requestSkillDetail = useCallback((skillName) => { const normalized = typeof skillName === 'string' ? skillName.trim() : ''; - if (!normalized || !clientRef.current) return false; + if (!normalized) return false; const detailKey = `${selectedSkillAgentId}:${normalized}`; setSkillDetailLoadingKey(detailKey); - return clientRef.current.send({ type: 'get_skill_detail', agent_id: selectedSkillAgentId, skill_name: normalized }); - }, [clientRef, selectedSkillAgentId, setSkillDetailLoadingKey]); + void resolveWorkspaceId() + .then((workspaceId) => fetchAgentSkillDetail(workspaceId, selectedSkillAgentId, normalized)) + .then((payload) => { + setSkillDetailsByName((prev) => ({ ...prev, [detailKey]: payload?.skill || null })); + useAgentStore.getState().setLocalSkillDraftsByKey((prev) => ({ + ...prev, + [detailKey]: typeof payload?.skill?.content === 'string' ? payload.skill.content : '' + })); + setSkillDetailLoadingKey(null); + }) + .catch(() => { + if (!clientRef.current) { + setSkillDetailLoadingKey(null); + return; + } + console.debug('REST skill detail request failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'get_skill_detail', agent_id: selectedSkillAgentId, skill_name: normalized }); + if (!success) { + setSkillDetailLoadingKey(null); + } + }); + return true; + }, [clientRef, resolveWorkspaceId, selectedSkillAgentId, setSkillDetailLoadingKey, setSkillDetailsByName]); const handleCreateLocalSkill = useCallback((skillName) => { const normalized = typeof skillName === 'string' ? skillName.trim() : ''; @@ -51,18 +129,30 @@ export function useAgentDataRequests(clientRef) { setAgentSkillsFeedback({ type: 'error', text: '技能名称不能为空' }); return; } - if (!clientRef.current) { - setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); - return; - } setAgentSkillsSavingKey(`${selectedSkillAgentId}:${normalized}:create`); setAgentSkillsFeedback(null); - const success = clientRef.current.send({ type: 'create_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: normalized }); - if (!success) { - setAgentSkillsSavingKey(null); - setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); - } - }, [clientRef, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); + void resolveWorkspaceId() + .then((workspaceId) => createAgentLocalSkill(workspaceId, selectedSkillAgentId, normalized)) + .then(() => { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'success', text: `已创建本地技能 ${normalized}` }); + requestAgentSkills(selectedSkillAgentId); + requestSkillDetail(normalized); + }) + .catch(() => { + if (!clientRef.current) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); + return; + } + console.debug('REST local skill create failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'create_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: normalized }); + if (!success) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); + } + }); + }, [clientRef, requestAgentSkills, requestSkillDetail, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); const handleLocalSkillDraftChange = useCallback((skillName, content) => { const detailKey = `${selectedSkillAgentId}:${skillName}`; @@ -70,64 +160,110 @@ export function useAgentDataRequests(clientRef) { }, [selectedSkillAgentId]); const handleLocalSkillSave = useCallback((skillName) => { - if (!clientRef.current) { - setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); - return; - } const detailKey = `${selectedSkillAgentId}:${skillName}`; const content = localSkillDraftsByKey[detailKey]; if (typeof content !== 'string') return; setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:content`); setAgentSkillsFeedback(null); - const success = clientRef.current.send({ type: 'update_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: skillName, content }); - if (!success) { - setAgentSkillsSavingKey(null); - setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); - } - }, [clientRef, localSkillDraftsByKey, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); + void resolveWorkspaceId() + .then((workspaceId) => updateAgentLocalSkill(workspaceId, selectedSkillAgentId, skillName, content)) + .then(() => { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 的本地技能 ${skillName} 已保存` }); + requestSkillDetail(skillName); + }) + .catch(() => { + if (!clientRef.current) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); + return; + } + console.debug('REST local skill save failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'update_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: skillName, content }); + if (!success) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); + } + }); + }, [clientRef, localSkillDraftsByKey, requestSkillDetail, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); const handleLocalSkillDelete = useCallback((skillName) => { - if (!clientRef.current) { - setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); - return; - } setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:delete`); setAgentSkillsFeedback(null); - const success = clientRef.current.send({ type: 'delete_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: skillName }); - if (!success) { - setAgentSkillsSavingKey(null); - setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); - } - }, [clientRef, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); + void resolveWorkspaceId() + .then((workspaceId) => deleteAgentLocalSkill(workspaceId, selectedSkillAgentId, skillName)) + .then(() => { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 的本地技能 ${skillName} 已删除` }); + requestAgentSkills(selectedSkillAgentId); + }) + .catch(() => { + if (!clientRef.current) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); + return; + } + console.debug('REST local skill delete failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'delete_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: skillName }); + if (!success) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); + } + }); + }, [clientRef, requestAgentSkills, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); const handleRemoveSharedSkill = useCallback((skillName) => { - if (!clientRef.current) { - setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); - return; - } setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:remove`); setAgentSkillsFeedback(null); - const success = clientRef.current.send({ type: 'remove_agent_skill', agent_id: selectedSkillAgentId, skill_name: skillName }); - if (!success) { - setAgentSkillsSavingKey(null); - setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); - } - }, [clientRef, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); + void resolveWorkspaceId() + .then((workspaceId) => disableAgentSkill(workspaceId, selectedSkillAgentId, skillName)) + .then(() => { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 已移除共享技能 ${skillName}` }); + requestAgentSkills(selectedSkillAgentId); + }) + .catch(() => { + if (!clientRef.current) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); + return; + } + console.debug('REST shared skill remove failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'remove_agent_skill', agent_id: selectedSkillAgentId, skill_name: skillName }); + if (!success) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); + } + }); + }, [clientRef, requestAgentSkills, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); const handleAgentSkillToggle = useCallback((skillName, enabled) => { - if (!clientRef.current) { - setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); - return; - } const agentId = selectedSkillAgentId; setAgentSkillsSavingKey(`${agentId}:${skillName}`); setAgentSkillsFeedback(null); - const success = clientRef.current.send({ type: 'update_agent_skill', agent_id: agentId, skill_name: skillName, enabled }); - if (!success) { - setAgentSkillsSavingKey(null); - setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); - } - }, [clientRef, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); + void resolveWorkspaceId() + .then((workspaceId) => enabled + ? enableAgentSkill(workspaceId, agentId, skillName) + : disableAgentSkill(workspaceId, agentId, skillName)) + .then(() => { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'success', text: `${agentId} ${enabled ? '已启用' : '已禁用'} ${skillName}` }); + requestAgentSkills(agentId); + }) + .catch(() => { + if (!clientRef.current) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); + return; + } + console.debug('REST skill toggle failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'update_agent_skill', agent_id: agentId, skill_name: skillName, enabled }); + if (!success) { + setAgentSkillsSavingKey(null); + setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); + } + }); + }, [clientRef, requestAgentSkills, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]); const handleSkillAgentChange = useCallback((agentId) => { setSelectedSkillAgentId(agentId); @@ -139,11 +275,35 @@ export function useAgentDataRequests(clientRef) { const requestWorkspaceFile = useCallback((agentId, filename) => { const normalizedAgentId = typeof agentId === 'string' ? agentId.trim() : ''; const normalizedFilename = typeof filename === 'string' ? filename.trim() : ''; - if (!normalizedAgentId || !normalizedFilename || !clientRef.current) return false; + if (!normalizedAgentId || !normalizedFilename) return false; setIsWorkspaceFileLoading(true); setWorkspaceFileFeedback(null); - return clientRef.current.send({ type: 'get_agent_workspace_file', agent_id: normalizedAgentId, filename: normalizedFilename }); - }, [clientRef, setIsWorkspaceFileLoading, setWorkspaceFileFeedback]); + void resolveWorkspaceId() + .then((workspaceId) => fetchAgentWorkspaceFile(workspaceId, normalizedAgentId, normalizedFilename)) + .then((payload) => { + setWorkspaceFilesByAgent((prev) => ({ + ...prev, + [normalizedAgentId]: { + ...(prev[normalizedAgentId] || {}), + [normalizedFilename]: typeof payload?.content === 'string' ? payload.content : '' + } + })); + setWorkspaceDraftContent(typeof payload?.content === 'string' ? payload.content : ''); + setIsWorkspaceFileLoading(false); + }) + .catch(() => { + if (!clientRef.current) { + setIsWorkspaceFileLoading(false); + return; + } + console.debug('REST workspace file read failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ type: 'get_agent_workspace_file', agent_id: normalizedAgentId, filename: normalizedFilename }); + if (!success) { + setIsWorkspaceFileLoading(false); + } + }); + return true; + }, [clientRef, resolveWorkspaceId, setIsWorkspaceFileLoading, setWorkspaceDraftContent, setWorkspaceFileFeedback, setWorkspaceFilesByAgent]); const handleWorkspaceFileChange = useCallback((filename) => { useAgentStore.getState().setSelectedWorkspaceFile(filename); @@ -151,24 +311,41 @@ export function useAgentDataRequests(clientRef) { }, [requestWorkspaceFile, selectedSkillAgentId]); const handleWorkspaceFileSave = useCallback(() => { - if (!clientRef.current) { - setWorkspaceFileFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); - return; - } const key = `${selectedSkillAgentId}:${selectedWorkspaceFile}`; setWorkspaceFileSavingKey(key); setWorkspaceFileFeedback(null); - const success = clientRef.current.send({ - type: 'update_agent_workspace_file', - agent_id: selectedSkillAgentId, - filename: selectedWorkspaceFile, - content: workspaceDraftContent - }); - if (!success) { - setWorkspaceFileSavingKey(null); - setWorkspaceFileFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); - } - }, [clientRef, selectedSkillAgentId, selectedWorkspaceFile, setWorkspaceFileFeedback, setWorkspaceFileSavingKey, workspaceDraftContent]); + void resolveWorkspaceId() + .then((workspaceId) => updateAgentWorkspaceFile(workspaceId, selectedSkillAgentId, selectedWorkspaceFile, workspaceDraftContent)) + .then((payload) => { + setWorkspaceFileSavingKey(null); + setWorkspaceFileFeedback({ type: 'success', text: `${selectedSkillAgentId} 的 ${selectedWorkspaceFile} 已保存` }); + setWorkspaceFilesByAgent((prev) => ({ + ...prev, + [selectedSkillAgentId]: { + ...(prev[selectedSkillAgentId] || {}), + [selectedWorkspaceFile]: typeof payload?.content === 'string' ? payload.content : workspaceDraftContent + } + })); + }) + .catch(() => { + if (!clientRef.current) { + setWorkspaceFileSavingKey(null); + setWorkspaceFileFeedback({ type: 'error', text: '连接未就绪,稍后重试' }); + return; + } + console.debug('REST workspace file save failed, falling back to websocket compatibility path'); + const success = clientRef.current.send({ + type: 'update_agent_workspace_file', + agent_id: selectedSkillAgentId, + filename: selectedWorkspaceFile, + content: workspaceDraftContent + }); + if (!success) { + setWorkspaceFileSavingKey(null); + setWorkspaceFileFeedback({ type: 'error', text: '发送失败,请检查连接状态' }); + } + }); + }, [clientRef, resolveWorkspaceId, selectedSkillAgentId, selectedWorkspaceFile, setWorkspaceFileFeedback, setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, workspaceDraftContent]); const handleUploadExternalSkill = useCallback(async (file) => { if (!(file instanceof File)) { diff --git a/frontend/src/services/runtimeApi.js b/frontend/src/services/runtimeApi.js index 3338a37..2fac8dc 100644 --- a/frontend/src/services/runtimeApi.js +++ b/frontend/src/services/runtimeApi.js @@ -129,6 +129,69 @@ export function fetchRuntimeLogs() { return safeFetch(RUNTIME_API_BASE, '/logs'); } +export function fetchAgentProfile(workspaceId, agentId) { + return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/profile`); +} + +export function fetchAgentSkills(workspaceId, agentId) { + return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills`); +} + +export function fetchAgentSkillDetail(workspaceId, agentId, skillName) { + return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skillName)}`); +} + +export function fetchAgentWorkspaceFile(workspaceId, agentId, filename) { + return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/files/${encodeURIComponent(filename)}`); +} + +export function createAgentLocalSkill(workspaceId, agentId, skillName) { + return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/local`, { + method: 'POST', + body: JSON.stringify({ skill_name: skillName }) + }); +} + +export function updateAgentLocalSkill(workspaceId, agentId, skillName, content) { + return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/local/${encodeURIComponent(skillName)}`, { + method: 'PUT', + body: JSON.stringify({ content }) + }); +} + +export function deleteAgentLocalSkill(workspaceId, agentId, skillName) { + return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/local/${encodeURIComponent(skillName)}`, { + method: 'DELETE' + }); +} + +export function enableAgentSkill(workspaceId, agentId, skillName) { + return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skillName)}/enable`, { + method: 'POST' + }); +} + +export function disableAgentSkill(workspaceId, agentId, skillName) { + return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skillName)}/disable`, { + method: 'POST' + }); +} + +export function updateAgentWorkspaceFile(workspaceId, agentId, filename, content) { + return fetch(`${CONTROL_API_BASE}/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/files/${encodeURIComponent(filename)}`, { + method: 'PUT', + headers: { + 'Content-Type': 'text/plain' + }, + body: content + }).then(async (response) => { + if (!response.ok) { + throw new Error(await response.text()); + } + return response.json(); + }); +} + export async function uploadAgentSkillZip({ agentId, file,