feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
@@ -0,0 +1,118 @@
---
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:'<abs>', duckdb_path:'<abs>', 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`).