package services import ( "encoding/json" "fmt" "log" "strings" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // La regla que separa "asistente" de "incidente": preparar es libre, entregar // necesita una persona — cuando del otro lado no está el dueño. // RequiereAprobacion decide si esta llamada se ejecuta o se encola. // // El eje es quién está del otro lado, no qué tan peligrosa suena la acción. // En un canal interno el dueño ya autorizó al escribirlo, y mandarlo a // aprobar su propio pedido es fricción sin seguridad. En el widget público // escribe cualquiera: ahí toda acción hacia afuera espera. func RequiereAprobacion(sesionInterna bool, name string) bool { if sesionInterna { return false } switch name { case "buscar_conocimiento", "leer_bandeja": return false // lectura: no sale nada del negocio } return true } func tipoDeAccion(name string) string { switch { case name == "enviar_correo": return "correo" case strings.HasPrefix(name, "generar_documento"): return "documento" } return "herramienta" } // EncolarAccion deja la acción esperando y avisa al dueño. Devuelve lo que el // modelo le va a contar al cliente: nunca "fue rechazado" ni "está pendiente // de aprobación interna" — el cliente no tiene por qué conocer la cocina. func EncolarAccion(agenteID uint, sessionID, name string, args map[string]interface{}) string { agente, err := models.GetUmindAgenteByID(agenteID) if err != nil { return `{"error": "no se pudo registrar la solicitud"}` } argsJSON, _ := json.Marshal(args) accion := &models.UmindAccionPendiente{ AgenteID: agenteID, TenantID: agente.TenantID, Tipo: tipoDeAccion(name), Herramienta: name, ArgumentosJSON: string(argsJSON), Resumen: resumirAccion(name, args), SessionID: sessionID, Estado: "pendiente", } if err := models.CreateUmindAccion(accion); err != nil { log.Printf("[UMIND_APROB] no se pudo encolar la acción %q del agente %d: %v", name, agenteID, err) return `{"error": "no se pudo registrar la solicitud, intentá más tarde"}` } go avisarAccionPendiente(agente, accion) b, _ := json.Marshal(map[string]interface{}{ "ok": true, "nota": "La solicitud quedó registrada y una persona del equipo la va a revisar. Decíselo al cliente de forma natural, sin mencionar aprobaciones ni procesos internos.", }) return string(b) } // resumirAccion arma la línea que va a leer quien aprueba. func resumirAccion(name string, args map[string]interface{}) string { switch name { case "enviar_correo": return fmt.Sprintf("Enviar un correo a %v con asunto %q", args["destinatario"], args["asunto"]) case "generar_documento": return fmt.Sprintf("Generar y entregar un documento (%v) para %v", args["tipo"], args["cliente"]) } partes := make([]string, 0, len(args)) for k, v := range args { partes = append(partes, fmt.Sprintf("%s: %v", k, v)) } return fmt.Sprintf("Ejecutar %q con %s", name, strings.Join(partes, ", ")) } // EjecutarAccionAprobada corre el payload guardado, tal cual se aprobó. func EjecutarAccionAprobada(accion *models.UmindAccionPendiente, quien string) error { if !models.ReclamarAccion(accion.ID) { return fmt.Errorf("esa solicitud ya fue resuelta") } var args map[string]interface{} if err := json.Unmarshal([]byte(accion.ArgumentosJSON), &args); err != nil { _ = models.CerrarAccion(accion.ID, "fallida", quien, "los datos guardados no se pudieron leer", "") return err } // Se ejecuta como sesión interna porque ya pasó por una persona; ese es // exactamente el permiso que la aprobación otorga. resultado := ejecutarHerramienta(accion.AgenteID, accion.SessionID, true, accion.Herramienta, args) estado := "ejecutada" if strings.Contains(resultado, `"error"`) { estado = "fallida" } if err := models.CerrarAccion(accion.ID, estado, quien, "", resultado); err != nil { return err } if estado == "fallida" { models.RegistrarEventoUmind(accion.AgenteID, "error", "aprobacion", "Una acción aprobada falló al ejecutarse: "+accion.Resumen, resultado) return fmt.Errorf("la acción se aprobó pero falló al ejecutarse") } return nil } // avisarAccionPendiente le dice al dueño que tiene algo para revisar. El // correo lleva un enlace al panel, con login: un enlace que ejecuta algo // irreversible sin autenticar es un enlace que reenviado por error firma. func avisarAccionPendiente(agente *models.UmindAgente, accion *models.UmindAccionPendiente) { tenant, err := models.GetUmindTenantByID(agente.TenantID) if err != nil || tenant.ClienteID == nil { return } usuarios, err := models.GetPortalUsersByClienteID(*tenant.ClienteID) if err != nil { return } url := GetPublicURL() + "/portal/studio" for _, u := range usuarios { models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "portal_user", UsuarioID: u.ID, Titulo: "Tu asistente necesita tu visto bueno", Cuerpo: accion.Resumen, Icono: "✋", Url: url, }) if u.Email == "" { continue } html := fmt.Sprintf(`
uMind · %s
%s
RevisarloSi no lo revisás, vence en 7 días y no se hace nada.