package controlplane import ( "errors" "fmt" "time" ) // AgentDeploymentStatus summarizes controller-owned LLM, Skill and MCP application state. type AgentDeploymentStatus struct { AgentID string `json:"agentID"` LLMProfile string `json:"llmProfile"` LLMStatus string `json:"llmStatus"` SkillTotal int `json:"skillTotal"` SkillApplied int `json:"skillApplied"` SkillFailed int `json:"skillFailed"` MCPStatus string `json:"mcpStatus"` MCPError string `json:"mcpError"` LastUpdatedAt string `json:"lastUpdatedAt"` } // EnableDesktopFilesystem deploys the intentionally narrow built-in filesystem MCP. func (c *ControlPlane) EnableDesktopFilesystem(agentIDs []string) error { if len(agentIDs) == 0 { return errors.New("请选择至少一个 Agent") } now := time.Now().UTC().Format(time.RFC3339Nano) for _, agentID := range agentIDs { var exists int if err := c.db.QueryRow("SELECT COUNT(*) FROM managed_agents WHERE agent_id=?", agentID).Scan(&exists); err != nil || exists != 1 { return errors.New("目标 Agent 不存在") } if _, err := c.db.Exec("INSERT INTO agent_mcp_assignments(agent_id, tool_id, deployment_version, apply_status, last_error, updated_at) VALUES (?, 'desktop-filesystem', 1, 'pending', '', ?) ON CONFLICT(agent_id, tool_id) DO UPDATE SET deployment_version=agent_mcp_assignments.deployment_version + 1, apply_status='pending', last_error='', updated_at=excluded.updated_at", agentID, now); err != nil { return err } c.recordAudit("mcp.deployment_requested", "controller", "agent:"+agentID+":desktop-filesystem", "pending", "已请求部署受控桌面文件工具。") c.pushMCP(agentID) } return nil } func (c *ControlPlane) pushMCP(agentID string) { client := c.clientFor(agentID) if client == nil { return } var version int if err := c.db.QueryRow("SELECT deployment_version FROM agent_mcp_assignments WHERE agent_id=? AND tool_id='desktop-filesystem'", agentID).Scan(&version); err != nil { return } if client.send(map[string]any{"type": "mcp_snapshot", "tools": []map[string]any{{"id": "desktop-filesystem", "deployment_version": version, "enabled": true, "scope": "desktop-directories"}}}) == nil { _, _ = c.db.Exec("UPDATE agent_mcp_assignments SET apply_status='sent', updated_at=? WHERE agent_id=? AND tool_id='desktop-filesystem' AND deployment_version=?", time.Now().UTC().Format(time.RFC3339Nano), agentID, version) c.recordAudit("mcp.snapshot_sent", "controller", "agent:"+agentID+":desktop-filesystem", "sent", "已通过认证 WSS 下发受控桌面文件工具。") } } func (c *ControlPlane) applyMCPAck(agentID string, message map[string]any) { toolID, _ := message["tool_id"].(string) version, _ := message["deployment_version"].(float64) status, _ := message["status"].(string) errorText, _ := message["error"].(string) if toolID != "desktop-filesystem" || version < 1 || (status != "applied" && status != "failed") { return } result, _ := c.db.Exec("UPDATE agent_mcp_assignments SET apply_status=?, last_error=?, updated_at=? WHERE agent_id=? AND tool_id=? AND deployment_version=?", status, limit(sanitizeDisplayText(errorText), 1000), time.Now().UTC().Format(time.RFC3339Nano), agentID, toolID, int(version)) if changed, _ := result.RowsAffected(); changed == 0 { return // Stale acknowledgement from an earlier deployment; preserve newer state. } c.recordAudit("mcp.deployment_ack", "agent:"+agentID, "agent:"+agentID+":desktop-filesystem", status, errorText) } // AgentDeploymentStatuses provides the Agent page with consolidated deployment state. func (c *ControlPlane) AgentDeploymentStatuses() ([]AgentDeploymentStatus, error) { rows, err := c.db.Query(`SELECT a.agent_id, COALESCE(p.name, ''), COALESCE(l.apply_status, '未部署'), COALESCE((SELECT COUNT(*) FROM agent_skill_assignments s WHERE s.agent_id = a.agent_id), 0), COALESCE((SELECT COUNT(*) FROM agent_skill_assignments s WHERE s.agent_id = a.agent_id AND s.apply_status = 'applied'), 0), COALESCE((SELECT COUNT(*) FROM agent_skill_assignments s WHERE s.agent_id = a.agent_id AND s.apply_status = 'failed'), 0), COALESCE(m.apply_status, '未启用'), COALESCE(m.last_error, ''), COALESCE(MAX(l.updated_at, m.updated_at), '') FROM managed_agents a LEFT JOIN agent_llm_assignments l ON l.agent_id = a.agent_id LEFT JOIN llm_profiles p ON p.profile_id = l.profile_id LEFT JOIN agent_mcp_assignments m ON m.agent_id = a.agent_id AND m.tool_id = 'desktop-filesystem' GROUP BY a.agent_id, p.name, l.apply_status, m.apply_status, m.last_error`) if err != nil { return nil, fmt.Errorf("无法读取 Agent 部署状态: %w", err) } defer rows.Close() statuses := make([]AgentDeploymentStatus, 0) for rows.Next() { var status AgentDeploymentStatus if err := rows.Scan(&status.AgentID, &status.LLMProfile, &status.LLMStatus, &status.SkillTotal, &status.SkillApplied, &status.SkillFailed, &status.MCPStatus, &status.MCPError, &status.LastUpdatedAt); err != nil { return nil, err } statuses = append(statuses, status) } return statuses, rows.Err() }