feat: conexiones OAuth (Gmail/Outlook) para el agente + rediseño del orquestador
Backend: - UmindConexion: cuenta de correo conectada por tenant vía OAuth2 (golang.org/x/oauth2, promovida de indirecta a directa), tokens cifrados en reposo con el mismo AES-GCM+APP_KEY que ya usan tools/canales. - Flujo completo: /app/umind/conexiones/conectar redirige a Google/Microsoft, /callback/:proveedor intercambia el code (state autoverificable por HMAC, sin tabla de estados pendientes), refresh on-demand antes de cada uso. - Dos tools nuevas para el agente (enviar_correo/leer_bandeja) que aparecen solo si el tenant tiene una conexión activa, vía Gmail API / Microsoft Graph directo (sin el SDK pesado de Google). - Requiere que el dueño del proyecto cree las apps OAuth en Google Cloud Console / Azure y cargue GOOGLE_OAUTH_CLIENT_ID/SECRET y MS_OAUTH_CLIENT_ID/SECRET — sin eso los botones de conectar fallan con un mensaje claro, no en silencio. Frontend: rediseño del orquestador — layout de sidebar fijo (reemplaza el navbar + lista de página completa), modo oscuro vía prefers-color-scheme, tabs en pill, y la nueva tab "Conexiones". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b2f6b518b6
commit
5ba41786d6
@@ -82,9 +82,51 @@ func umindTools(tenantID uint) []agentTool {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if conexion, err := models.GetUmindConexionActiva(tenantID); err == nil && conexion != nil {
|
||||
tools = append(tools, umindEmailTools()...)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// umindEmailTools son las tools de correo, disponibles solo cuando el
|
||||
// tenant tiene una cuenta conectada (UmindConexion activa) — nombres
|
||||
// genéricos porque al modelo no le importa si detrás hay Gmail u Outlook.
|
||||
func umindEmailTools() []agentTool {
|
||||
return []agentTool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: agentToolFunc{
|
||||
Name: "enviar_correo",
|
||||
Description: "Envía un correo electrónico desde la cuenta de correo conectada del negocio.",
|
||||
Parameters: agentToolParam{
|
||||
Type: "object",
|
||||
Properties: map[string]agentToolParam{
|
||||
"destinatario": {Type: "string", Description: "Email del destinatario"},
|
||||
"asunto": {Type: "string", Description: "Asunto del correo"},
|
||||
"cuerpo": {Type: "string", Description: "Cuerpo del correo en texto plano"},
|
||||
},
|
||||
Required: []string{"destinatario", "asunto", "cuerpo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "function",
|
||||
Function: agentToolFunc{
|
||||
Name: "leer_bandeja",
|
||||
Description: "Busca correos recibidos en la bandeja conectada del negocio (ej. revisar si llegó un comprobante o la respuesta de un cliente).",
|
||||
Parameters: agentToolParam{
|
||||
Type: "object",
|
||||
Properties: map[string]agentToolParam{
|
||||
"consulta": {Type: "string", Description: "Qué buscar: remitente, palabras clave del asunto o del cuerpo"},
|
||||
},
|
||||
Required: []string{"consulta"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// executeUmindTool ejecuta buscar_conocimiento (RAG interno) o, si el nombre
|
||||
// no matchea, busca una UmindHerramienta custom del tenant y hace el POST al
|
||||
// webhook configurado. Devuelve el resultado ya serializado, en el mismo
|
||||
@@ -111,6 +153,10 @@ func executeUmindTool(tenantID uint, name string, args map[string]interface{}) s
|
||||
return string(b)
|
||||
}
|
||||
|
||||
if name == "enviar_correo" || name == "leer_bandeja" {
|
||||
return executeUmindEmailTool(tenantID, name, args)
|
||||
}
|
||||
|
||||
herramienta, err := models.GetUmindHerramientaByNombre(tenantID, name)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"error": "herramienta desconocida: %s"}`, name)
|
||||
@@ -131,6 +177,60 @@ func executeUmindTool(tenantID uint, name string, args map[string]interface{}) s
|
||||
return resultado
|
||||
}
|
||||
|
||||
// executeUmindEmailTool despacha enviar_correo/leer_bandeja a Gmail o
|
||||
// Microsoft Graph según el proveedor de la conexión activa del tenant,
|
||||
// refrescando el token primero si hace falta.
|
||||
func executeUmindEmailTool(tenantID uint, name string, args map[string]interface{}) string {
|
||||
conexion, err := models.GetUmindConexionActiva(tenantID)
|
||||
if err != nil {
|
||||
return `{"error": "no hay ninguna cuenta de correo conectada"}`
|
||||
}
|
||||
if err := RefrescarSiVence(conexion); err != nil {
|
||||
log.Printf("[UMIND] Error refrescando token OAuth (conexión %d): %v", conexion.ID, err)
|
||||
return `{"error": "no se pudo usar la cuenta de correo conectada, intenta más tarde"}`
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "enviar_correo":
|
||||
destinatario, _ := args["destinatario"].(string)
|
||||
asunto, _ := args["asunto"].(string)
|
||||
cuerpo, _ := args["cuerpo"].(string)
|
||||
if strings.TrimSpace(destinatario) == "" || strings.TrimSpace(cuerpo) == "" {
|
||||
return `{"error": "destinatario y cuerpo son requeridos"}`
|
||||
}
|
||||
var envErr error
|
||||
if conexion.Proveedor == UmindOAuthGoogle {
|
||||
envErr = EnviarCorreoGoogle(conexion, destinatario, asunto, cuerpo)
|
||||
} else {
|
||||
envErr = EnviarCorreoMicrosoft(conexion, destinatario, asunto, cuerpo)
|
||||
}
|
||||
if envErr != nil {
|
||||
log.Printf("[UMIND] Error enviando correo (tenant %d): %v", tenantID, envErr)
|
||||
return `{"error": "no se pudo enviar el correo"}`
|
||||
}
|
||||
return `{"ok": true}`
|
||||
|
||||
case "leer_bandeja":
|
||||
consulta, _ := args["consulta"].(string)
|
||||
var resultados []CorreoResumen
|
||||
var lecErr error
|
||||
if conexion.Proveedor == UmindOAuthGoogle {
|
||||
resultados, lecErr = LeerBandejaGoogle(conexion, consulta, 5)
|
||||
} else {
|
||||
resultados, lecErr = LeerBandejaMicrosoft(conexion, consulta, 5)
|
||||
}
|
||||
if lecErr != nil {
|
||||
log.Printf("[UMIND] Error leyendo bandeja (tenant %d): %v", tenantID, lecErr)
|
||||
return `{"error": "no se pudo leer la bandeja"}`
|
||||
}
|
||||
b, _ := json.Marshal(map[string]interface{}{"resultados": resultados})
|
||||
return string(b)
|
||||
|
||||
default:
|
||||
return `{"error": "herramienta desconocida"}`
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessWidgetMessage procesa un mensaje del widget de uMind y devuelve la
|
||||
// respuesta del agente. Es el equivalente de ProcessAgentMessage pero
|
||||
// multi-tenant y con un toolset acotado a RAG (sin herramientas internas).
|
||||
|
||||
Reference in New Issue
Block a user