Automatiza la bΓΊsqueda de empleo: scraping multi-portal, dashboard con CRM de candidaturas y cartas de presentaciΓ³n generadas con IA en tiempo real.
π Demo en vivo β automatico-five.vercel.app
Buscando trabajo como desarrollador notΓ© que pasaba mΓ‘s tiempo filtrando portales que preparando candidaturas. Lo automaticΓ©: un script que raspa Tecnoempleo e InfoJobs, filtra por mi stack exacto, detecta solo las ofertas nuevas y me da un dashboard donde gestionar el estado de cada aplicaciΓ³n y generar una carta personalizada con IA, directamente en el navegador.
El reto tΓ©cnico interesante fue hacer que el mismo cΓ³digo funcionase en local (Ollama, fs) y en producciΓ³n serverless (Groq, Vercel Blob) sin duplicar lΓ³gica.
- Scraping multi-portal: Tecnoempleo (HTML + Cheerio) e InfoJobs (API oficial OAuth 2.0 / JSON embebido como fallback), con pausas entre peticiones y deduplicaciΓ³n por URL.
- DetecciΓ³n de novedades: compara contra
seen_jobs.json; las nuevas siempre aparecen primero y nunca quedan fuera por el tope de resultados. - Filtrado inteligente: por palabra completa (
javano descartajavascript), ciudad, modalidad y nivel de experiencia β ordena de menor a mayor seniority. - Dashboard web: historial de ejecuciones por fecha, bΓΊsqueda con logs en directo vΓa SSE, CRM con filtros en tiempo real. SPA vanilla JS, sin framework, sin build step.
- Cartas con IA en streaming: tokens en tiempo real (SSE). Si ya existe, se sirve al instante desde cachΓ©.
- Doble proveedor de LLM: Ollama en local (gratis, sin API key) y Groq en producciΓ³n, intercambiables sin cambiar el cΓ³digo de negocio.
- Resumen por email (opcional, nodemailer/SMTP).
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Scrapers β
β tecnoempleo.js β Cheerio (HTML) β
β infojobsApi.js β OAuth 2.0 / JSON embebido β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β ofertas crudas
ββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ
β Pipeline de filtrado β
β Β· palabra completa Β· ciudad Β· duplicados β
β Β· detecciΓ³n de nuevas (seen_jobs.json) β
β Β· orden por seniority β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β resumen.json + resumen.md
βββββββββββββββΌβββββββββββββββββββ
β Storage adapter β
β local β fs | prod β Blob β
βββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ
β Express / Vercel Serverless Function β
β REST API + SSE (bΓΊsqueda en directo, streaming) β
β Auth: timing-safe token + rate-limit por IP β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β bajo demanda
ββββββββββΌβββββββββ
β LLM adapter β
β Ollama (local) β
β Groq (cloud) β
βββββββββββββββββββ
src/ai/llm.js expone generar() y generarStream(). Internamente detecta el entorno: si existe GROQ_API_KEY parsea el stream SSE estilo OpenAI (data: {...} / data: [DONE]); si no, parsea el NDJSON de Ollama ({ response, done }). Ambos corren modelos Llama, asΓ el mismo prompt funciona igual en los dos entornos. El servidor nunca sabe quΓ© proveedor hay debajo.
Las cartas se generan con SSE: el cliente ve tokens en tiempo real. Si el usuario cierra la conexiΓ³n (botΓ³n "Detener"), req.on('close') aborta la peticiΓ³n HTTP al LLM antes de que termine, liberando recursos sin esperar:
const controller = new AbortController();
req.on('close', () => controller.abort());
await llm.generarStream(prompt, (token) => send({ token }), controller.signal);Para evitar que un atacante mida tiempos de respuesta y deduzca caracteres del token de administraciΓ³n:
function tokenCorrecto(enviado, token) {
const a = Buffer.from(String(enviado || ''));
const b = Buffer.from(token);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Tras 5 intentos fallidos en 15 minutos la IP recibe 429. El contador se limpia en el primer acierto. El mapa tiene tope de 1000 IPs para evitar OOM en serverless.
src/utils/store.js abstrae fs (local) y @vercel/blob (producciΓ³n) con la misma API: leerJSON, escribirJSON, leerTexto, escribirTexto, listar. El servidor no sabe en quΓ© entorno corre.
java aparece en javascript, javaspring, javafx⦠El filtro usa \b para excluir solo si es palabra completa:
const regex = new RegExp(`\\b${keyword}\\b`, 'i');| Capa | TecnologΓa |
|---|---|
| Runtime | Node.js 18+ (CommonJS) |
| Framework | Express 5 |
| Scraping | axios + Cheerio |
| IA | Ollama (local) / Groq API (cloud) β Llama 3 |
| Persistencia | fs (local) / Vercel Blob (prod) |
| Frontend | Vanilla JS + CSS β sin framework, sin build step |
| Deploy | Vercel (funciΓ³n serverless ΓΊnica) |
| Tests | Playwright (smoke test + screenshot) |
Requisitos: Node.js 18+ y Ollama con un modelo instalado (ollama pull llama3) si quieres generar cartas en local.
git clone https://github.com/Mariomin23/automatico.git
cd automatico
npm install
cp .env.example .env # rellena tus credencialesnpm start # scraping CLI β genera el resumen del dΓa
npm run serve # dashboard en http://localhost:3000
npm run dev # scraping con hot-reload
npm run serve:dev # dashboard con hot-reloadCada ejecuciΓ³n crea output/YYYY-MM-DD/:
| Archivo | Contenido |
|---|---|
resumen.md |
Resumen legible |
resumen.json |
Datos que consume el dashboard |
{slug}_carta.md |
Cartas generadas bajo demanda |
Variables en .env (ver .env.example):
| Variable | DescripciΓ³n |
|---|---|
OLLAMA_MODEL |
Modelo Ollama en local (default: llama3) |
OLLAMA_BASE_URL |
URL de Ollama (default: http://localhost:11434) |
GROQ_API_KEY |
Si existe, activa Groq en lugar de Ollama |
GROQ_MODEL |
Modelo Groq (default: llama-3.1-8b-instant) |
INFOJOBS_CLIENT_ID / SECRET |
API oficial InfoJobs |
MAX_OFFERS_PER_RUN |
Tope de ofertas (las nuevas nunca quedan fuera) |
ADMIN_TOKEN |
Protege endpoints de escritura en producciΓ³n |
BLOB_READ_WRITE_TOKEN |
Vercel Blob (solo producciΓ³n) |
EMAIL_* |
SMTP para envΓo opcional por email |
Tu perfil (stack, keywords, ciudad, palabras excluidas) va en src/config/profile.js β alimenta tanto los filtros de scraping como los prompts de las cartas.
La app Express completa corre como una ΓΊnica funciΓ³n serverless (api/index.js). En producciΓ³n el cΓ³digo detecta el entorno y activa Vercel Blob y Groq automΓ‘ticamente.
vercel deploy --prodβββ src/
β βββ scraper/ # tecnoempleo.js Β· infojobs.js Β· infojobsApi.js
β βββ ai/ # llm.js (adaptador) Β· letterWriter.js Β· prompts.js Β· scorer.js
β βββ utils/ # store.js Β· storage.js Β· output.js Β· mailer.js
β βββ config/ # profile.js β perfil y criterios de bΓΊsqueda
β βββ index.js # entry point CLI
β βββ server.js # dashboard Express + API REST
βββ public/ # SPA vanilla JS (sin framework, sin build)
βββ api/index.js # entry point serverless para Vercel
βββ data/
β βββ seen_jobs.json # URLs ya procesadas
β βββ applications_status.json # CRM β estado por slug
βββ output/YYYY-MM-DD/ # resΓΊmenes y cartas por fecha
MIT Β· Mario Minuesa Β· mario.minuesa@gmail.com
