Skip to content

Mariomin23/automatico

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

25 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

job-hunter-ai

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

Dashboard β€” historial de bΓΊsquedas, CRM de candidaturas y generaciΓ³n de cartas con streaming

Node.js Express Cheerio LLM Vercel Blob Vanilla JS MIT


Por quΓ© lo construΓ­

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.


QuΓ© hace

  • 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 (java no descarta javascript), 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).

Arquitectura

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  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)  β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Decisiones tΓ©cnicas relevantes

Adaptador de LLM: mismo contrato, dos proveedores

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.

Streaming real con AbortController

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);

ComparaciΓ³n de tokens en tiempo constante

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);
}

Anti-fuerza-bruta por IP en memoria

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.

Capa de persistencia intercambiable

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.

Filtrado de palabras completas

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');

Stack

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)

Empezar en local

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 credenciales
npm 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-reload

Cada 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

ConfiguraciΓ³n

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.


Despliegue en Vercel

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

Estructura

β”œβ”€β”€ 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

Licencia

MIT Β· Mario Minuesa Β· mario.minuesa@gmail.com

About

πŸ€– Job Hunter AI β€” scrapes Tecnoempleo & InfoJobs, filters by your profile, tracks applications in a web CRM and writes AI cover letters (Ollama/Groq). Node.js + Express.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages