Volver a la wiki

Menú Correo del Workspace · F0/F1/F2 — Chat y Cola de Peticiones

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.

Arquitectura F1 — Chat

Endpoint: POST /api/correo/ask

Identidad: solo navegador (CF Access). Los chats son de personas, no de scripts.

Flujo:

  1. Cliente envía {question, history?} (pregunta + historial previo de la sesión, máx 12 mensajes).
  2. Worker carga el contexto principal: el informe más reciente (correo_digests) del miembro — resumen, ruido filtrado, ítems.
  3. 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.
  4. 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.
  5. 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).
  6. Responde {answer, tool_rounds, duration_ms, model}. Cuerpos viajan al navegador, jamás a D1 (grill Q6).

Integración 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

“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

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:

Véase también

Subir