fix(agent): hide disabled agents from the chat picker and reject chat calls against them (#105)

This commit is contained in:
matevip 2026-05-12 14:53:14 +08:00
parent 1bfb5f7fc8
commit 691d2b867b
4 changed files with 53 additions and 7 deletions

View File

@ -65,9 +65,26 @@ public class AgentService {
* 按工作区列出 Agent
*/
public List<AgentEntity> listAgentsByWorkspace(Long workspaceId) {
return agentMapper.selectList(new LambdaQueryWrapper<AgentEntity>()
.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<AgentEntity> listAgentsByWorkspace(Long workspaceId, Boolean enabled) {
LambdaQueryWrapper<AgentEntity> q = new LambdaQueryWrapper<AgentEntity>()
.eq(AgentEntity::getWorkspaceId, workspaceId);
if (enabled != null) {
q.eq(AgentEntity::getEnabled, enabled);
}
return agentMapper.selectList(q.orderByDesc(AgentEntity::getCreateTime));
}
public AgentEntity getAgent(Long id) {

View File

@ -55,10 +55,13 @@ public class AgentController {
@GetMapping
@RequireWorkspaceRole("viewer")
public R<List<AgentEntity>> 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.
*
* <p>{@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");

View File

@ -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),

View File

@ -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) {