SDD — ReenvioZoho (Python)¶
Ficha técnica
| Campo | Valor |
|---|---|
| Repositorio / carpeta | python/ReenvioZoho |
| Plataforma | Python 3.14 + Selenium (Chrome) |
| Servidor | VM-ROBOTS (Windows Server), cuenta robots_IT |
| Disparo | Programador de tareas, tarea PY_main |
| Frecuencia | Cada 15 min, lun-vie 08:35–17:30 (36 ejecuciones/día) |
| Proceso de negocio | PDD |
1. Arquitectura general¶
El robot tiene dos mitades. La primera solo lee por la API oficial de SalesIQ y arma un plan de reparto. La segunda ejecuta la asignación desde el navegador, porque la API oficial no permite asignar chats perdidos. Si la primera mitad falla, el navegador ni se abre.
flowchart LR
TS[Programador de tareas<br/>PY_main] --> M[main.py]
M --> L[bloqueo.py<br/>candado]
M --> R[registro.py]
R --> SQL[(SQL Server<br/>automatizaciones)]
M --> P[planificador.py]
P --> S[salesiq.py]
S -->|OAuth, solo lectura| API[API oficial SalesIQ v2]
M --> N[navegador.py]
N -->|lanza| CH[Chrome<br/>perfil propio]
N -->|debuggerAddress| CH
CH -->|PUT assign + CSRF| WEB[API web interna SalesIQ]
M --> T[telegram.py] -->|solo fallos| TG[Telegram]
2. Componentes¶
| Archivo / módulo | Responsabilidad |
|---|---|
main.py |
Orquestador: candado, registro, reintentos, decide éxito/falla y si se notifica |
config.py |
Lee el .env, valida que no falten variables y configura el logging |
salesiq.py |
Cliente de la API oficial: token OAuth, operadoras y chats perdidos |
planificador.py |
Filtros de negocio, memoria de chats asignados y reparto round-robin |
navegador.py |
Lanza Chrome, conecta Selenium, inicia sesión y ejecuta el JS de asignación |
registro.py |
Registra la ejecución y sus métricas en SQL Server (reutilizable) |
bloqueo.py |
Candado de instancia única y limpieza de Chrome huérfanos (reutilizable) |
telegram.py |
Envío de avisos; nunca lanza excepción |
.env |
Credenciales y parámetros. No se versiona; ver .env.example |
3. Flujo detallado¶
- Candado.
main.pytomaC:\automatizaciones\locks\python-reenviozoho.lock. Si otra corrida sigue viva, esta se sale con código 0 sin registrar nada. Si el PID del candado ya no existe, lo reclama. - Registro. Abre un renglón en
ejecucionesconresultado = 'en_curso'. Con--simularno se registra. - Validación.
config.validar()confirma credenciales, departamento y lista de operadoras. Si falta algo: código 2 y Telegram. - Paso 1 — planificación (
planificar()):- Token OAuth con el refresh token.
- Operadoras del portal; se quedan las de
ALLOWED_OPERATORSconstatus_code = 1yis_enabled = true. - Chats
status=misseddelDEPARTMENT_ID. - Filtros: últimas
MAX_HORAS_ANTIGUEDAD(72) horas, fuera los que están en la memoria de asignados, fuera los que ya tienen owner/attender humano. - Orden del más antiguo al más reciente y reparto round-robin. Devuelve un objeto
Plan.
- Si no hay plan (
SIN_CHATS,SIN_OPERADORES,DRY_RUNo error), termina sin abrir el navegador. - Limpieza.
matar_huerfanos()cierra los Chrome que quedaron vivos con el perfil del robot. - Paso 2 — navegador (
Navegador):- Lanza
chrome.execon--remote-debugging-port(puerto libre al azar) y el perfil del robot. - Espera a que el puerto responda (
/json/version, hasta 40 s). - Conecta Selenium por
debuggerAddress. - Abre el portal; si no hay sesión, hace login con
ZOHO_USER/ZOHO_PASSWORD.
- Lanza
- Asignación. Lee el token CSRF con
get_cookie()y ejecuta el JS, que hace unPUTsíncrono a/web/v2/{screen}/conversations/assignpor cada operadora. Hasta 3 intentos si el estado es reintentable. Los ids asignados se guardan en la memoria después de cada intento, aunque sea parcial. - Cierre. Escribe las tres métricas, cierra el renglón como
exitoofallay, solo si es falla, avisa por Telegram. Al salir delwith, cierra Chrome por CDP y limpia por perfil.
4. Sistemas y dependencias¶
| Sistema | Uso | Acceso | Notas |
|---|---|---|---|
| API oficial SalesIQ v2 | Operadoras y chats perdidos | API (OAuth) | Solo lectura |
| API web interna SalesIQ | Asignar chats | Web (cookie de sesión + CSRF) | Endpoint no documentado; puede cambiar sin aviso |
| Zoho Accounts | Token OAuth y login del portal | API / web | DC .com |
| Google Chrome + ChromeDriver | Sesión del portal | Selenium | ChromeDriver lo resuelve Selenium Manager según la versión de Chrome |
SQL Server automatizaciones |
Ejecuciones y métricas | BD (autenticación de Windows) | localhost\SQLEXPRESS, driver ODBC detectado solo |
| Telegram Bot API | Avisos de falla | API | — |
5. Credenciales y configuración¶
No se escriben valores aquí; solo dónde están.
| Nombre | Tipo | Dónde se guarda |
|---|---|---|
ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, ZOHO_REFRESH_TOKEN |
OAuth self-client | .env del servidor / Bitwarden |
ZOHO_USER, ZOHO_PASSWORD |
Usuario y contraseña del portal | .env del servidor / Bitwarden |
TELEGRAM_TOKEN, TELEGRAM_CHAT_ID |
Token de bot | .env del servidor / Bitwarden |
| SQL Server | Autenticación de Windows | Cuenta robots_IT, sin credencial aparte |
El refresh token debe tener los scopes SalesIQ.conversations.READ, SalesIQ.conversations.UPDATE
y SalesIQ.operators.READ. Se genera en api-console.zoho.com.
Parámetros de negocio en el .env: SALESIQ_SCREEN_NAME, SALESIQ_DEPARTMENT_ID,
ALLOWED_OPERATORS (correos separados por coma), MAX_HORAS_ANTIGUEDAD, DRY_RUN.
6. Estados y datos persistentes¶
| Recurso | Ruta | Para qué |
|---|---|---|
| Memoria de asignados | C:\Robot\logs\salesiq_asignados.json |
Ids ya repartidos; se purgan a los 7 días |
| Perfil de Chrome | C:\automatizaciones\chrome_profile_Zoho |
Conserva la sesión del portal entre corridas |
| Candado | C:\automatizaciones\locks\python-reenviozoho.lock |
PID e inicio de la corrida activa |
| Log del robot | C:\Robot\logs\salesiq_robot.log |
Bitácora completa en UTF-8 |
| Log de ChromeDriver | C:\Robot\logs\chromedriver_verbose.log |
Diagnóstico de arranque del navegador |
| Tablas SQL | ejecuciones, metricas |
Historial y cifras por corrida |
7. Manejo de errores y reintentos¶
| Estado final | Resultado en SQL | ¿Reintenta? | ¿Telegram? |
|---|---|---|---|
OK_ASIGNADO |
exito | — | No |
SIN_CHATS |
exito | — | No |
SIN_OPERADORES |
exito | — | No |
DRY_RUN |
no se registra | — | No |
ERROR_PARCIAL |
falla | Sí, hasta 3 | Sí |
ERROR_TOTAL |
falla | Sí, hasta 3 | Sí |
ERROR_SESION |
falla | No | Sí |
ERROR_API (paso 1) |
falla | No | Sí |
| Configuración inválida | falla (código 2) | No | Sí |
| Excepción no manejada | falla (código 1) | No | Sí |
En la API, los 401/403 y demás 4xx no se reintentan; los 5xx y errores de red sí son reintentables. Ante un error HTTP, el cuerpo de la respuesta de Zoho queda en el log, que es donde explica el motivo.
8. Monitoreo y notificaciones¶
- SQL Server: cada corrida en
ejecuciones(automatizacion_id = 'python-reenviozoho',origenscheduleromanual) con tres métricas:chats_encontrados(los que entraron al plan, ya filtrados),chats_asignadosychats_no_asignados. Un renglón que se queda enen_cursosignifica que el proceso murió sin cerrar. - Metabase: vistas para gerente (chats y horas ahorradas al mes), jefe (ejecuciones del día contra las 36 esperadas,
fallas silenciosas por
chats_no_asignadosalto) y desarrollador (detalle de las últimas corridas). - Telegram: solo fallos. Las corridas sin chats o exitosas no avisan.
9. Restricciones conocidas y trampas¶
La API oficial no sirve para asignar chats perdidos
/conversations/{id}/transfer responde 400 Chat is not connected (código 2046): solo opera sobre
chats en curso. Por eso existe la mitad del navegador. Se probó el 2026-09-16.
Paginación inconsistente entre endpoints
/operators solo acepta limit (con index da 500, con page da 400, sin parámetros recorta a 20 registros).
/conversations sí acepta limit + index. salesiq.listar() prueba estrategias en orden y avisa en el log
si el resultado parece un tope de la API. Cuando sin paginación devolvía 20, operadoras reales quedaban fuera del reparto.
El token CSRF cambió de nombre
La cookie pasó de CT_CSRF_TOKEN a LS_CSRF_TOKEN. Se lee con get_cookie() probando ambos nombres.
Si vuelve a cambiar, el error lo dice: lista los nombres de todas las cookies presentes.
Chrome lo lanza el robot, no ChromeDriver
En el servidor, Chrome se relanza a sí mismo al arrancar (actualizaciones pendientes, desescalación de privilegios)
y ChromeDriver pierde el proceso: session not created: Chrome instance exited. Por eso se lanza con subprocess
y Selenium se engancha por puerto (debuggerAddress). En este modo driver.quit() no cierra Chrome;
lo cierra Navegador.__exit__ por CDP y por perfil.
Un perfil de Chrome por robot
Chrome solo permite una instancia por perfil (ProcessSingleton). Si otro proceso, o una ventana abierta a mano,
tiene el perfil tomado, el Chrome del robot se cierra al instante. No compartir el perfil con robots de Rocketbot.
psutil es obligatorio en la práctica
Sin psutil instalado en el mismo Python 3.14 que ejecuta la tarea, matar_huerfanos() solo avisa y no limpia,
y los Chrome huérfanos se acumulan hasta bloquear el perfil.
Marcador de limpieza
main.py llama a matar_huerfanos() con el nombre de la carpeta del perfil, que se busca como texto en la línea
de comandos de cada Chrome. Si el nombre es genérico (por ejemplo chrome_profile), puede coincidir con perfiles de
Rocketbot y cerrarles el navegador. Con chrome_profile_Zoho el riesgo es bajo; lo más seguro es pasar la ruta completa.
Sin operadoras disponibles no hay ninguna señal
SIN_OPERADORES cuenta como éxito, no avisa y deja las tres métricas en 0, porque el plan se corta antes de consultar
los chats. Si nadie está disponible por horas, los chats se acumulan y ni Telegram ni Metabase lo muestran.
Programador de tareas
La tarea debe correr con la misma cuenta que creó el perfil, sin "Ejecutar con los privilegios más altos" y solo con la sesión iniciada: el navegador es visible y necesita escritorio.
Actualizaciones de Chrome
Selenium Manager descarga el ChromeDriver que corresponde a cada versión de Chrome. Los robots de Rocketbot usan un ChromeDriver fijo y pueden romperse con la misma actualización.
10. Operación (runbook)¶
Telegram: session not created / Chrome instance exited
- Revisa si hay Chrome vivos con el perfil del robot:
Get-CimInstance Win32_Process -Filter "name='chrome.exe'" | Where-Object CommandLine -like "*chrome_profile_Zoho*" - Ciérralos (
Stop-Process -Id <PID>) y confirma quepsutilestá instalado en el Python 3.14. - Abre
chrome://settings/help: si hay una actualización a medias, deja que termine y reinicia Chrome. - Revisa el final de
C:\Robot\logs\chromedriver_verbose.log.
Falla de inicio de sesión (ERROR_SESION)
- Confirma que
ZOHO_USERyZOHO_PASSWORDtienen valor en el.envdel servidor. - Corre a mano (
python main.py --produccion --origen manual) y observa la ventana: verificación en dos pasos, captcha o una pantalla intermedia de Zoho detienen el login. - Si el log dice que no encontró el token CSRF, revisa la lista de cookies que imprime.
Hay chats perdidos sin asignar y no llegó ningún aviso
- Busca en el log el bloque
Estado de los operadores configurados. - Si ninguna aparece con
OK, nadie estaba disponible (SIN_OPERADORES): es el caso que no avisa. - Si una aparece como
NO ENCONTRADO EN EL PORTAL, revisa su correo enALLOWED_OPERATORS.
Faltan ejecuciones contra las 36 esperadas
- Revisa el historial de la tarea
PY_mainen el Programador de tareas. - Busca renglones en
en_cursoenejecuciones: el proceso murió sin cerrar. - Si el log dice
Ya hay una corrida activa, revisa el candado enC:\automatizaciones\locks.
Error HTTP de la API de SalesIQ
- El log trae el código y el cuerpo de la respuesta de Zoho.
- 401/403: refresh token revocado o sin scopes; regenerarlo en
api-console.zoho.com. - 500 con
Operation Failed(código 1000): suele ser un parámetro de paginación no aceptado.
11. Historial de cambios¶
| Versión | Fecha | Cambio | Autor |
|---|---|---|---|
| 1.0 | 2026-09-16 | Migración desde Rocketbot a Python puro | Angel |
| 1.1 | 2026-09-17 | Registro en SQL Server con métricas; Telegram solo en fallos | Angel |
| 1.2 | 2026-09-22 | Candado de instancia única y limpieza de Chrome huérfanos | Angel |
| 1.3 | 2026-10-01 | Chrome lanzado por el robot y conexión por debuggerAddress |
Angel |