From 691d2b867b0d505724542b073c5b10b62c48873c Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 12 May 2026 14:53:14 +0800 Subject: [PATCH] fix(agent): hide disabled agents from the chat picker and reject chat calls against them (#105) --- .../java/vip/mate/agent/AgentService.java | 23 ++++++++++++++--- .../agent/controller/AgentController.java | 25 +++++++++++++++++-- mateclaw-ui/src/api/index.ts | 7 +++++- mateclaw-ui/src/views/ChatConsole.vue | 5 +++- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java index 45182bf8..c101eb6c 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java @@ -65,9 +65,26 @@ public class AgentService { * 按工作区列出 Agent */ public List listAgentsByWorkspace(Long workspaceId) { - return agentMapper.selectList(new LambdaQueryWrapper() - .eq(AgentEntity::getWorkspaceId, workspaceId) - .orderByDesc(AgentEntity::getCreateTime)); + return listAgentsByWorkspace(workspaceId, null); + } + + /** + * 按工作区列出 Agent,可选过滤启用状态。 + * + * @param enabled non-null restricts the result set to agents whose + * {@code enabled} column matches the given value. + * Pass {@code true} from chat selectors so disabled + * agents disappear from the picker; the admin + * management page passes {@code null} to keep + * disabled rows visible for re-enabling. + */ + public List listAgentsByWorkspace(Long workspaceId, Boolean enabled) { + LambdaQueryWrapper q = new LambdaQueryWrapper() + .eq(AgentEntity::getWorkspaceId, workspaceId); + if (enabled != null) { + q.eq(AgentEntity::getEnabled, enabled); + } + return agentMapper.selectList(q.orderByDesc(AgentEntity::getCreateTime)); } public AgentEntity getAgent(Long id) { diff --git a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java index 9f04b02f..f2d6aac9 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java @@ -55,10 +55,13 @@ public class AgentController { @GetMapping @RequireWorkspaceRole("viewer") public R> list( - @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, + @RequestParam(value = "enabled", required = false) Boolean enabled) { // 无 header 时强制使用默认 workspace,不返回全局数据 long wsId = workspaceId != null ? workspaceId : 1L; - return R.ok(agentService.listAgentsByWorkspace(wsId)); + // enabled=true: chat selectors hide disabled agents. + // enabled=null: admin management page sees enabled + disabled. + return R.ok(agentService.listAgentsByWorkspace(wsId, enabled)); } @Operation(summary = "获取Agent详情") @@ -187,6 +190,7 @@ public class AgentController { @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { AgentEntity agent = agentService.getAgent(id); verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); + verifyAgentEnabled(agent); // RFC-058 PR-1: Utf8SseEmitter 显式 charset=UTF-8,防止中文 SSE 乱码 SseEmitter emitter = new Utf8SseEmitter(5 * 60 * 1000L); @@ -226,6 +230,7 @@ public class AgentController { @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { AgentEntity agent = agentService.getAgent(id); verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); + verifyAgentEnabled(agent); return R.ok(agentService.chat(id, request.getMessage(), request.getConversationId())); } @@ -238,6 +243,7 @@ public class AgentController { @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { AgentEntity agent = agentService.getAgent(id); verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); + verifyAgentEnabled(agent); return R.ok(agentService.execute(id, request.getMessage(), request.getConversationId())); } @@ -268,6 +274,21 @@ public class AgentController { } } + /** + * Block runtime calls against an agent flagged as disabled. + * + *

{@code AgentService#getOrBuildAgent} also checks the flag, but only on + * a cache miss — once the {@code BaseAgent} instance is warm, a flip to + * disabled would silently keep serving requests until something else + * invalidates the cache. Enforcing here at the controller closes that gap + * for every external entry point. + */ + private void verifyAgentEnabled(AgentEntity agent) { + if (agent != null && !Boolean.TRUE.equals(agent.getEnabled())) { + throw new MateClawException("err.agent.disabled", "Agent 已禁用: " + agent.getName()); + } + } + private Long resolveUserId(Authentication auth) { if (auth == null) { throw new MateClawException("err.auth.unauthenticated", 401, "Not authenticated"); diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 5f708ccf..0da4b940 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -93,7 +93,12 @@ export const authApi = { // ==================== Agent ==================== export const agentApi = { - list: () => http.get('/agents'), + /** + * @param params.enabled when `true`, restricts the result to enabled agents + * (used by chat selectors so disabled agents disappear from the picker). + * Omit to receive enabled + disabled (admin management page). + */ + list: (params?: { enabled?: boolean }) => http.get('/agents', { params }), get: (id: string | number) => http.get(`/agents/${id}`), create: (data: any) => http.post('/agents', data), update: (id: string | number, data: any) => http.put(`/agents/${id}`, data), diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue index a29eed38..bcba6d5c 100644 --- a/mateclaw-ui/src/views/ChatConsole.vue +++ b/mateclaw-ui/src/views/ChatConsole.vue @@ -1163,7 +1163,10 @@ watch(selectedAgentId, async (id) => { // ============ 方法 ============ async function loadAgents() { try { - const res: any = await agentApi.list() + // Hide disabled agents from the picker — they cannot be chatted with + // (the chat endpoints reject disabled agents), so showing them invites + // a confusing failure path. The admin Agents view passes no filter. + const res: any = await agentApi.list({ enabled: true }) agents.value = res.data || [] // 只有在 URL 没有指定 agentId 且当前无选中时,才默认选第一个 if (agents.value.length > 0 && !selectedAgentId.value && !route.query.agentId) {