⚙️ Worker & Pipelines

Vier Pipelines, jeweils mehrere Worker als Stufen. Jede Zeile ist Selbstauskunft — der Worker meldet bei jedem Lauf selbst, was er an welcher Search getan hat. „Backlog" = der eigene Eingangsstapel dieses Workers (nicht die Gesamt-Seeds).

🎯 Lead-Gewinnung

Firma finden → filtern → crawlen → Kontakt & Job-Signal

Stufe Worker Quelle Status Letzter Lauf 24h Läufe erzeugt 24h Backlog Searches 24h
Discovery (Tavily) Firmen-Suche (Tavily) · city_search Tavily ⏳ wartet
Discovery (Tavily) Jobseiten-Suche (Tavily) · jobsearch Tavily ⏳ wartet
Filter Seed-Filter (raw→legit) · seed_filter
LLM-Precheck — rohe URLs aus seed_urls_raw als legit Seeds übernehmen oder verwerfen.
🔴 still 212
Crawl & Anreichern Crawl & Validate · crawl_validate
Crawlt legit Seeds, extrahiert Kontakte und validiert die Firmenseite.
Firmen-Website 🔴 still 2.594
Crawl & Anreichern Adress-Parser · address_parse
Zerlegt Freitext-Anschriften (seed_urls.company_address) deterministisch in street/plz/ort/land der Kontakte.
🔴 still
Job-Signal Stellen-/Signal-Radar · jobscan
Crawlt Firmen-Homepage + Karriereseite, extrahiert offene Stellen und Opportunity-Signale (Jobs/Digitalisierung/Ausschreibung/Beratung) je Firma.
Firmen-Website ⚠️ hängt? 27s vor 127 0 10.983
2
Nutzen Pitch-Scoring · pitch_score 💤 dormant

💼 Externe Jobs & Ausschreibungen

Direkt aus Behörden-/EU-APIs — kein Crawl nötig

Stufe Worker Quelle Status Letzter Lauf 24h Läufe erzeugt 24h Backlog Searches 24h
Discovery (API) Stellen-Scanner · jobportals Bundesagentur-API ⏳ wartet
Discovery (API) Ausschreibungen · tenders TED-API ⏳ wartet

🌍 Wealth-Atlas

Validierte Seeds öffentlich schalten

Stufe Worker Quelle Status Letzter Lauf 24h Läufe erzeugt 24h Backlog Searches 24h
Publish & Wartung Atlas-Auto-Promotion · atlas_promote
Promotet validierte Atlas-Seeds automatisch in den oeffentlichen Wealth-Atlas (wealth_offices).
🔴 still
Publish & Wartung Atlas Geo-Wartung · atlas_revert_geo 💤 dormant

📧 Kampagnen-Versand (separat)

Alter Kampagnen-Mailer — NICHT der Pitch-Outreach (der läuft im delta-Container)

Stufe Worker Quelle Status Letzter Lauf 24h Läufe erzeugt 24h Backlog Searches 24h
Sender Kampagnen-Versand · campaign_send
Verteilt Kampagnen-Mails ueber das eingestellte Zeitfenster.
🔴 still 110

Status: 🟢 produziert · ⚪ idle (nichts zu tun) · 🟠 idle trotz Backlog (erschöpft/blockiert) · 🔵 läuft · ⏳ wartet auf Cron · 💤 dormant (nur manuell) · 🔴 still trotz Cron · ⚠️ Fehler. ⏸ orange Searches = pausiert, werden aber noch bearbeitet (verschwendetes Budget).

🔗 SSOT — Crawl-Ergebnis-Kontrakt

Beide Crawler-Lanes schreiben über EINE Funktion ingest_seed_result() (in lib/seed_result.php) in die DB: der DeepSeek-Worker ruft sie direkt, Eva sendet das JSON an den HTTP-Endpoint. So ist der A/B-Vergleich fair — gleicher Schreibpfad, gleiche Validierung.

Claim-Endpoint (Eva)POST /webresearch/seed_claim.php — atomar claimen + sofort auf is_craweled=1 stellen (kein SELECT-Pattern mehr!)
Ingest-Endpoint (Eva)POST /webresearch/seed_result_ingest.php (Body: JSON + Feld token)
Funktion (für Worker)ingest_seed_result($db, $payload) in lib/seed_result.php
Token-Quellelib/ingest_config.php (nicht im Web sichtbar; Eva liest die Datei)
Batchmehrere Ergebnisse auf einmal: {"token":"…","batch":[ {…}, {…} ]}
Schema
{
    "seed_id": "int — PFLICHT, ID aus WEBRESEARCH_seed_urls",
    "crawler": "'worker' | 'bot' — wer das Ergebnis erzeugt hat",
    "status": "'done' (Daten gefunden) | 'give_up' (nichts brauchbar)",
    "reason": "string — give_up-Grund oder done-Notiz",
    "company": {
        "name": "string — Firmenname",
        "summary": "string — STECKBRIEF (-> legit_note): Taetigkeit, Spezialisierungen, Team/Mitarbeiter, Lage/Standort-Besonderheit, USP/Auffaelliges. So konkret wie moeglich, damit GPT daraus eine personalisierte Mail bauen kann. Mehrere Saetze erlaubt.",
        "address": "string — Postadresse (Strasse Nr, PLZ Ort), aus Impressum/Kontakt. Leer wenn nicht gefunden.",
        "language": "'de' | 'en' | 'es' | 'other'"
    },
    "contacts": [
        {
            "person_name": "string oder leer",
            "role": "z.B. 'Geschaeftsfuehrer' / 'Einkauf' / 'Allgemein'",
            "email": "string oder leer",
            "phone": "string oder leer",
            "found_on_url": "auf welcher URL gefunden",
            "rank": "int 1..N — 1 = BESTER Ansprechpartner fuer einen Software-Pitch (Einkauf/Entscheider), 2 = naechster, ... (du entscheidest die Reihenfolge)"
        }
    ],
    "pages_visited": [
        "array von URLs — optional, fuer Audit"
    ]
}
Beispiel-Payload
{
    "seed_id": 2720,
    "crawler": "bot",
    "status": "done",
    "reason": "Impressum + Team-Seite ausgewertet",
    "company": {
        "name": "Junkersdorf Hausverwaltung GmbH",
        "summary": "Hausverwaltung in Koeln, ~1.700 Wohneinheiten, WEG- und Mietverwaltung.",
        "address": "Aachener Str. 1253, 50858 Koeln",
        "language": "de"
    },
    "contacts": [
        {
            "person_name": "Kenny Roteweit",
            "role": "Geschaeftsfuehrer",
            "email": "info@junkersdorf-koeln.de",
            "phone": "0221 6802598",
            "found_on_url": "https://www.junkersdorf-hausverwaltung.de/impressum",
            "rank": 1
        },
        {
            "person_name": "",
            "role": "Buchhaltung",
            "email": "buchhaltung@junkersdorf-koeln.de",
            "phone": "",
            "found_on_url": "https://www.junkersdorf-hausverwaltung.de/kontakt",
            "rank": 2
        }
    ],
    "pages_visited": [
        "https://www.junkersdorf-hausverwaltung.de/",
        "https://www.junkersdorf-hausverwaltung.de/impressum"
    ]
}
🤖 Bot-Lane — so ruft Eva ihre Aufträge ab

Eva wacht alle paar Minuten auf, ruft den Claim-Endpoint auf, bekommt N Seeds IHRER Lane (crawler='bot') zurück — atomar geclaimt und sofort auf is_craweled=1 umgestellt. Der Worker (DeepSeek) macht es identisch mit crawler='worker'. Nur AKTIVE Searches — pausierte/abgeschlossene/archivierte werden nicht ausgegeben.

POST /webresearch/seed_claim.php
Content-Type: application/json

{
  "token":      "<ingest_token aus lib/ingest_config.php>",
  "crawler":    "bot",            // Worker nutzt "worker"
  "count":      3,                // 1..10 Seeds in einem Claim
  "worker_id":  "eva-1",          // Audit
  "orphan_min": 15                // hängende Claims nach N Minuten freigeben
}

→ Response: { ok, claimed, orphans_released, claim_token,
              seeds: [ {id, url, project_id, question, ...}, ... ] }

Pro Seed crawlen → Ergebnis-JSON (oben) bauen → an POST /webresearch/seed_result_ingest.php senden. Der Endpoint markiert is_craweled=2 und schreibt Profile + Kontakte. Eva muss NICHT selbst per SQL schreiben.

⚠️ Veraltetes Anti-Pattern: SELECT direkt aus WEBRESEARCH_seed_urls + manuell is_craweled=1 setzen. Das war race-anfällig (doppel-Claims, hängende Seeds bei Crash). Nicht mehr verwenden — immer den Claim-Endpoint oben. Insbesondere: nicht gegen eine lokale SQLite-Kopie arbeiten, die enthält nur einen alten Snapshot ohne crawler-Spalte.

Volle Anweisung in ~/.openclaw/workspace-eva/WEBRESEARCH_PIPELINE.md.