Visión
El Menú Correo integra la bandeja de Zoho Mail del miembro en el workspace, con asistencia vía Gemma 4 y automatización de borradores.
- F0 (estrenada el 31-07-2026): digest semanal sobre el buzón (cambios, ruido filtrado, alertas de CreaRack).
- F1 (12-08-2026): chat multiturno con la secretaria — el modelo puede buscar mensajes en vivo y leer cuerpos contra el OAuth per-user del miembro, contextualizando con el informe F0. Conversación efímera (no persiste).
- F2 (12-08-2026): cola de peticiones — el miembro encola encargos (“respóndele que X”); la pasada desatendida de
/correolos atiende, genera borradores y deja deep-link a Zoho Compose. El envío es siempre manual (grill Q8).
Arquitectura F1 — Chat
Endpoint: POST /api/correo/ask
Identidad: solo navegador (CF Access). Los chats son de personas, no de scripts.
Flujo:
- Cliente envía
{question, history?}(pregunta + historial previo de la sesión, máx 12 mensajes). - Worker carga el contexto principal: el informe más reciente (
correo_digests) del miembro — resumen, ruido filtrado, ítems. - Decodificación restringida (patrón canónico Auto-Plan/Oráculo): Gemma 4 recibe un prompt que especifica 3 acciones posibles en JSON:
{"action":"responder","answer":"..."}— respuesta final.{"action":"buscar","query":"..." | "from":"..." | "subject":"..."}— busca en el buzón real.{"action":"leer","messageId":"...","folderId":"..."}— lee el cuerpo de un mensaje.
- El Worker ejecuta herramientas contra Zoho Mail API con el OAuth per-user del miembro (token ya en la sesión CF Access, s68), máximo 3 rondas.
- Guardarraíles del Oráculo:
- Rate-limit: 20 consultas/min (tabla
correo_chat_queries, que además es el dato real de la apuesta #9). - Timeout: 40 segundos + 2 intentos por cold start (footgun s88 — AI Studio puede tardar).
- Tamaño: pregunta ≤2000 chars, cuerpo ≤4000 chars (HTML→texto).
- Rate-limit: 20 consultas/min (tabla
- Responde
{answer, tool_rounds, duration_ms, model}. Cuerpos viajan al navegador, jamás a D1 (grill Q6).
Integración Zoho:
toolBuscar:/api/accounts/{id}/messages/searchcon restricciones semánticas (remitente, asunto, búsqueda libre).toolLeer:/api/accounts/{id}/folders/{id}/messages/{id}/content— parse HTML → texto limpio, acotado.- Ambas se autentican contra el OAuth personal del miembro (tokens en
_lib/zoho).
Tabla D1: correo_chat_queries
Telemetría del chat (apuesta #9 de WAGERS.md):
CREATE TABLE correo_chat_queries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner TEXT NOT NULL,
question TEXT NOT NULL,
tool_rounds INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
Propósito: medir adopción (≥3 días distintos hasta 31-08) y rendimiento (latencia, número de rondas de búsqueda).
Arquitectura F2 — Cola de Peticiones
Endpoints
GET /api/correo/peticiones?estado=...&limit=...
Lista las peticiones del miembro. Filtro opcional por estado (pendiente|atendida|descartada).
Identidad: CF Access (navegador) o Bearer del token personal (script de pasada).
POST /api/correo/peticiones
Crea una petición nueva. Solo desde navegador (CF Access).
{ "texto": "Respóndele a X que nos han pagado" }
PUT /api/correo/peticiones/:id
Atiende una petición (la pasada de /correo la llama):
{
"estado": "atendida|pendiente|descartada",
"respuesta": "el borrador que generé para ti...",
"compose_url": "https://mail.zoho.com/...#compose&..."
}
El compose_url es un deep-link a Zoho Compose con el cuerpo pre-rellenado (borrador).
DELETE /api/correo/peticiones/:id
Descarta una petición.
Tabla D1: correo_peticiones
CREATE TABLE correo_peticiones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner TEXT NOT NULL, -- 'Edu' | 'Dani' | 'Txell'
texto TEXT NOT NULL, -- la petición en palabras del miembro
estado TEXT NOT NULL DEFAULT 'pendiente', -- 'pendiente' | 'atendida' | 'descartada'
respuesta TEXT, -- qué hizo la secretaria (borrador incluido)
compose_url TEXT, -- deep-link a Zoho compose con el borrador, si aplica
created_at TEXT NOT NULL DEFAULT (datetime('now')),
atendida_at TEXT
);
Scoping Estricto
- Navegador (
/api/correo/peticionesdesde/correo): CF Access → identidad CF Access. - Pasada desatendida (
/correoejecutando la ronda): Bearer personal del miembro (MCP_TOKENSenv var, paresnombre:token). - No hay multi-tenancy horizontal: tokens de servicio no juegan. Cada petición es de un miembro específico, verificado contra
owner.
“Actúa” que no se pierde (23-09-2026)
Problema medido: la página pintaba solo el último informe y la pasada es horaria (8-22 h); ~85 % de las pasadas salen vacías (897 informes del 24-08 al 23-09: 232/254/251 vacíos de Dani/Edu/Txell), así que un aviso “Actúa” desaparecía de la vista una hora después, atendido o no. Caso real: el de Txell del 22-09 “pasar la facturación a Esferic Labs”.
Cambio: GET /api/correo/digest devuelve además pendientes — los “Actúa” de los informes de los últimos 7 días (DIAS_PENDIENTES, a criterio), agrupados por remitente+asunto normalizados, menos lo que la persona cerró. Cada aviso lleva los botones Hecho y Descartar (POST /api/correo/cerradas, tabla correo_cerradas, migración 0055). Cerrar tapa solo lo visto hasta ese momento: si una pasada posterior trae otra vez el mismo asunto (un correo nuevo), vuelve a salir. Lógica pura en functions/_lib/correo-pendientes.ts con tests en test/correo-pendientes.test.ts.
El chat ve lo mismo que la página: POST /api/correo/ask añade al último informe los “Actúa” sin cerrar y todo lo de las últimas 24 h, sin repetidos (avisosRecientes, tope MAX_AVISOS_CONTEXTO = 30). Antes, como casi todos los informes salen vacíos, Gemma no tenía de dónde contestar “explícame el aviso de Actúa”. Medido con los datos del 23-09: 1,9-3,8 mil caracteres más de contexto por persona.
Tono y muestreo del chat (23-09-2026): con temperature 1.0 4 de 4 respuestas de prueba a “veo un aviso de urgente ¿qué debería de hacer?” traían duplicados, palabras al azar o cortes, y todas empezaban por “Oye”/“Mira”. Pasa a 0.3 y el prompt pide: la respuesta primero sin muletillas, no repetir el informe que ya se ve, listas literales remitente — asunto, datos copiados tal cual, avisar de que algo puede estar ya hecho, y un remate directo opcional (“¡Hazlo hoy!”, que gustó a Edu).
Despliegue sin orden estricto: si la tabla aún no existe, pendientes llega null y la página cae al “Actúa” del último informe, como antes. La migración NO hay que aplicarla a mano: desde el 11-09-2026 el cron cf-pages-deploy de OPS aplica las migraciones D1 pendientes antes de construir (log: d1: aplicadas 0055_correo_cerradas.sql, 23-09 13:30). Desplegado y verificado en pantalla el 23-09-2026: el “Actúa” acumulado sale con sus botones y el primer “Hecho” real quedó guardado en correo_cerradas.
Comprobación previa de la pasada (task #338, 23-09-2026)
GET /api/correo/novedades?desde=<epoch_ms> (Bearer del miembro o CF Access) responde {ok, nuevos, peticiones}: cuántos correos llegaron después del marcador last-read (búsqueda Zoho fromDate del día anterior al marcador, todas las carpetas, filtrada por receivedTime) y cuántas peticiones siguen pendientes. Usa el OAuth del miembro en el workspace (el del chat), no el MCP de Claude, que exige la credencial guardada por Claude Code. correo-pass.ps1 la consulta antes de lanzar claude -p; si todo es 0, publica “sin novedades” y no arranca Claude. Cualquier fallo (ok:false, red, token caducado) = pasada completa, como siempre. Red de seguridad: a las 8, 13 y 18 la pasada es siempre completa.
Diseño Aplazado
- Envío directo de correo (toggle F3): diseñado, no construido. Las peticiones atendidas dejan borrador y deep-link; el miembro hace clic y envía manual desde Zoho.
- Persistencia del chat: conversación efímera por diseño (datos de contexto real del buzón viajan al navegador, no a D1).
Integración con F0
El chat F1 siempre usa el contexto principal del informe más reciente (correo_digests). Si no hay informe (primera pasada aún no corrió), lo dice claramente.
La pasada de /correo que atiende las peticiones F2 es la misma que genera el digest F0 — es una ronda completa sin intervención humana de los pasos 1-6 de claude-method.
Apuesta #9 (WAGERS.md)
Criterio: Para el 31-08-2026, ≥3 días distintos de uso del chat F1 Y ≥2 peticiones F2 atendidas (telemetría real en correo_chat_queries y correo_peticiones, nunca de memoria).
Desenlace:
- ✅ Cumplido → GO a seguir en F3+ (envío directo/toggle).
- ❌ No cumplido → F3+ se congela; F1/F2 quedan sin retirar.
Véase también
- [[entity—correo—endpoint—ask]]
- [[entity—correo—endpoint—peticiones]]
- [[entity—correo—table—chat-queries]]
- [[entity—correo—table—peticiones]]
- [[feature—workspace—oraculo-el]]
- [[concept—saas—multi-tenancy]]
- [[decision—20260403—multi-tenancy-rls]]
Referenciado desde
- Apartado Guías + aviso de bienvenida en el Dashboard
- Endpoint GET/POST /api/correo/peticiones — Cola de Peticiones (F2)
- Endpoint POST /api/correo/ask — Chat de la Secretaria (F1)
- Endpoint POST /api/correo/ask · Chat de la Secretaria (F1)
- Endpoint PUT/DELETE /api/correo/peticiones/:id — Actualización de Petición (F2)
- Endpoints /api/correo/peticiones · Cola de Encargos de Correo (F2)
- Tabla D1: correo_chat_queries · Telemetría del Chat F1
- Tabla D1: correo_peticiones · Cola de Encargos de Correo (F2)