--- name: monitor_freelance_projects kind: pipeline lang: py domain: pipelines version: "1.0.0" purity: impure signature: "def monitor_freelance_projects(category: str = 'it-programming', language: str = 'es', query: str = '', pages: int = 1, include_upwork: bool = False, upwork_query: str = 'custom software', duckdb_path: str = '', xlsx_path: str = '', port: int = 9222, timeout_s: float = 25.0) -> dict" description: "Monitor de captacion de clientes freelance: scrapea proyectos nuevos de Workana (+ Upwork opcional) via CDP, los persiste en DuckDB con dedup por url, marca los de software a medida y exporta a Excel (hojas Nuevos y Todos)." tags: [market-intel, recon, launcher, pipelines, freelance, workana, upwork, duckdb, excel] uses_functions: - scrape_workana_projects_py_browser - scrape_upwork_projects_py_browser - duckdb_execute_py_infra - duckdb_upsert_py_infra - duckdb_query_readonly_py_infra - write_xlsx_sheets_py_infra uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [] tested: false tests: [] test_file_path: "" file_path: "python/functions/pipelines/monitor_freelance_projects.py" params: - name: category desc: "Categoria de Workana (segmento ?category= de la URL de listado). Default 'it-programming'." - name: language desc: "Idioma de los proyectos de Workana (?language=). Default 'es'." - name: query desc: "Query libre aplicada a ambas fuentes. En Workana va como extra_query; en Upwork sobrescribe upwork_query si no esta vacia." - name: pages desc: "Numero de paginas de listado a recorrer por fuente. Default 1." - name: include_upwork desc: "Si True, scrapea Upwork ademas de Workana. Default False (selectores Upwork sin validar en vivo + requiere login); si Upwork falla, el pipeline sigue solo con Workana." - name: upwork_query desc: "Query para Upwork cuando include_upwork. Default 'custom software'. El param 'query' lo sobrescribe si se pasa." - name: duckdb_path desc: "Ruta del archivo DuckDB de persistencia. Si vacia, usa ~/.fn_freelance/freelance.duckdb (crea el directorio)." - name: xlsx_path desc: "Ruta del .xlsx de salida. Si vacia, usa ~/.fn_freelance/freelance_projects.xlsx (crea el directorio). Se sobrescribe en cada corrida." - name: port desc: "Puerto de remote debugging del Chrome que usan los scrapers (CDP). Default 9222 (chromium-personal logueado). Usa 9333 para el Chrome aislado del browser_mcp." - name: timeout_s desc: "Timeout en segundos por pagina para los scrapers (navegacion + espera de cards). Default 25.0." output: "dict. En exito: {status:'ok', new_count:int (proyectos nuevos de esta corrida), total_in_db:int, new_projects:[...], xlsx_path:'', duckdb_path:'', sources:{workana:{count,status}, upwork:{count,status}|'skipped'}}. En error (sin lanzar): {status:'error', error:str, sources:{...}}." --- ## Ejemplo ```bash # Requiere un Chrome con remote debugging vivo en el puerto indicado. # Produccion (chromium-personal logueado, port 9222) con los paths por defecto: fn run monitor_freelance_projects # Probar contra el Chrome aislado del browser_mcp (port 9333) con paths efimeros: fn run monitor_freelance_projects --port 9333 \ --duckdb-path /tmp/freelance.duckdb --xlsx-path /tmp/freelance.xlsx ``` ```python import os, sys sys.path.insert(0, os.path.join("python", "functions")) from pipelines.monitor_freelance_projects import monitor_freelance_projects out = monitor_freelance_projects( category="it-programming", language="es", pages=1, port=9222, # chromium-personal logueado ) print(out["new_count"], "proyectos nuevos;", out["total_in_db"], "en la DB") print("Excel:", out["xlsx_path"]) ``` ## Cuando usarla Monitor de captacion de clientes: detecta proyectos freelance NUEVOS de Workana (programacion / software a medida) y los deja en DuckDB + Excel para revisar de un vistazo. Resalta los que pintan a "software a medida" (`is_custom_software`) sin filtrar el resto. Idempotente por `url`: re-correrlo no duplica ni pisa el `first_seen_at`. Agendable con dag_engine (step `function:`) para una foto diaria de oportunidades nuevas. ## Gotchas - **Requiere un Chrome con CDP vivo en `port`**: los scrapers (Workana/Upwork son SPAs) renderizan via Chrome DevTools Protocol. Sin remote debugging escuchando en ese puerto el pipeline devuelve `status:'error'` con el detalle. Produccion = 9222 (chromium-personal logueado); Chrome aislado = 9333 (browser_mcp). - **Upwork OFF por defecto**: sus selectores no estan validados en vivo (sin sesion Upwork). Con `include_upwork=True`, si Upwork devuelve `status:'error'` el pipeline loguea un WARN a stderr y sigue solo con Workana — nunca aborta por Upwork. - **El Excel se sobrescribe** por completo en cada corrida (`write_xlsx_sheets`). La fuente de verdad acumulativa es la DuckDB, no el .xlsx. - **`first_seen_at` lo posee la DB**: el upsert usa ownership selectivo (no esta en `update_cols`), asi que una re-corrida conserva la primera vez que se vio cada proyecto. `new_count` cuenta solo urls que no existian antes de esta corrida. - **Rate-limit / anti-bot**: scrapear muchas paginas seguidas puede disparar defensas de las plataformas. Mantener `pages` bajo y espaciar las corridas. - **Skills se guardan como `skills_json`** (TEXT con JSON) porque DuckDB no usa una columna lista aqui; en el Excel se re-expanden a una cadena separada por comas. ## Notas Pipeline impuro: compone seis funciones del registry sin reescribir su logica (2 scrapers CDP del dominio browser + 3 primitivas del grupo `duckdb` + el exporter `write_xlsx_sheets`). El flag `is_custom_software` se calcula con la constante `CUSTOM_SW_KEYWORDS` (keywords fuertes de desarrollo a medida) sobre title + snippet + skills, normalizados a minusculas y sin acentos. Validado end-to-end contra Workana real (CDP 9333) el 17/06/2026: - Golden: `new_count=9`, `total_in_db=9`, 4 proyectos `is_custom_software=True`, .xlsx con hojas "Nuevos" (9 filas + cabecera) y "Todos", DuckDB con 9 filas. - Edge dedup: 2a corrida identica -> `new_count=0`, `total_in_db` sigue en 9 (no duplica) y `first_seen_at` preservado (ownership del upsert por `url`).