⚙️ Worker & Pipeline

Die Pipeline besteht aus 4 Phasen. Phase 0 holt URLs, Phase 1 filtert auf echte Firmen, Phase 2 extrahiert Profile + Kontakte, Phase 3 bewertet pro Pitch. 🟢 = aktiv im Cron · 🟡 = altes Skript (deaktiviert oder Fallback)

🌱 Phase 0 — Seed-Harvesting (Tavily)

Sucht neue Firmen-URLs ueber Tavily-API und legt sie in WEBRESEARCH_seed_urls_raw ab.

worker_city_search.php 🟢 aktiv (PHP, neu) PHP
Iteriert Top-100 DE-Staedte × Projekte mit search_template. Dedup pro (Projekt, Stadt) in WEBRESEARCH_search_runs.
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_city_search.php · 9,7 KB · geaendert 2026-05-20 12:12
Cron30 6-21 * * * (16×/Tag, 06-21h)
Aufrufdocker exec bots-admin php /var/www/html/webresearch/worker_city_search.php
Tavily-Last~240 Calls/Tag (50% Budget)
Log/var/log/openclaw/webresearch_city_search.log
Letzte Aktivität (DB)22 min vor (2026-06-05 00:00:20)
Output 2406 Runs · 23608 In raw eingefuegt
worker_seed_urls_raw_ingest.py 🟡 alt (manuell oder von Eva) Python
Aelterer Tavily-Wrapper, akzeptiert beliebige Query. Eva-Persona kann ihn manuell triggern.
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_seed_urls_raw_ingest.py · 3,5 KB · geaendert 2026-04-27 12:09
Cron— (kein Cron, nur on-demand)
Aufrufpython worker_seed_urls_raw_ingest.py <project_id> "<query>"
Tavily-Laston-demand
Letzte Aktivität (DB)4 h vor (2026-06-04 20:00:21)
Output 23612 Raws insgesamt

🔬 Phase 1 — Seed-Filter (LLM-Vetting raw → legit)

Prueft jede raw-URL: echte Firma oder Directory/SERP/Wiki/Spam? DeepSeek-Verdict.

worker_seed_filter.php 🟢 aktiv (PHP, neu) PHP + DeepSeek
HTTP-Check + DeepSeek-JSON-Verdict (legit/directory/aggregator/wiki/spam/dead). Akzeptierte raws kommen als WEBRESEARCH_seed_urls (is_legit=1).
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_seed_filter.php · 7,7 KB · geaendert 2026-05-20 16:58
Cron*/15 * * * * (alle 15min)
Aufrufdocker exec bots-admin php /var/www/html/webresearch/worker_seed_filter.php --batch=100
Tavily-Last— (nur DeepSeek, ~$0.0002/raw)
Log/var/log/openclaw/webresearch_seed_filter.log
Letzte Aktivität (DB)4 h vor (2026-06-04 20:15:42)
Output 12875 akzeptiert · 10663 abgewiesen · 0 pending
worker_seed_urls_raw_precheck.py 🟡 alt (heuristisch, kein LLM) Python
Aelterer Heuristik-Precheck (Dedup, Reachability) — wird durch seed_filter.php abgeloest.
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_seed_urls_raw_precheck.py · 10,0 KB · geaendert 2026-04-28 16:00
Cron— (deaktiviert)
Aufrufpython worker_seed_urls_raw_precheck.py <project_id>
Tavily-Last
Letzte Aktivität (DB)

🕵️ Phase 2 — Seed-Profile (Multi-Page LLM-Agent)

Pro legit-Seed: DeepSeek-Agent-Loop mit Tools (fetch_url, save_company_profile, save_contact). Findet Impressum/Kontakt/Team und extrahiert Profil + Personen.

worker_seed_profile.php 🟢 aktiv (PHP, agentisch, neu) PHP + DeepSeek (Tool-Use)
Agent-Loop max 5 Tool-Calls. Audit pro Seed in WEBRESEARCH_profile_runs. Mehrere Worker parallel: --worker_id=2,3,... mit eigenen Lock-Files.
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_seed_profile.php · 21,4 KB · geaendert 2026-05-21 16:47
Cron*/3 * * * * (alle 3min, batch=5)
Aufrufdocker exec bots-admin php /var/www/html/webresearch/worker_seed_profile.php --batch=5 --worker_id=1
Tavily-Last— (nur DeepSeek, ~$0.003/seed)
Log/var/log/openclaw/webresearch_seed_profile.log
Letzte Aktivität (DB)4 h vor (2026-06-04 20:19:10)
Output 7184 Runs gesamt · 5378 done · 89 give_up · 270 error · 11721 Kontakte gesp.
worker_contact_extract.php 🟡 alt (1 LLM-Call auf Homepage) PHP + DeepSeek
Aelterer One-Shot-Extractor. Findet nur Kontakte auf der Startseite. Bleibt als Fallback drin.
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_contact_extract.php · 22,7 KB · geaendert 2026-05-19 00:02
Cron— (durch seed_profile abgeloest, kein Cron)
Aufrufdocker exec bots-admin php /var/www/html/webresearch/worker_contact_extract.php
Tavily-Last
Letzte Aktivität (DB)52 min vor (2026-06-04 23:30:53)
Output 25150 Kontakte gesamt

🎯 Phase 3 — Pitch-Scoring (KI-Match)

Bewertet pro Pitch wie gut jeder Kontakt zur Zielfunktion passt (0–100). Triggered on-demand vom Dashboard.

worker_pitch_score.php 🟢 manuell (Button im Contacts-View) PHP + DeepSeek
Schreibt in COMMUNICATION_EVENTS_pitch_contact_scores. Per-Pitch eine Zeile pro Kontakt: score, role_bucket, reason.
Pfad/srv/openclaw/sites/admin_bots/webresearch/worker_pitch_score.php · 6,9 KB · geaendert 2026-05-19 12:30
Cron— (Button "⚡ Scoring starten" oder CLI)
Aufrufdocker exec bots-admin php /var/www/html/webresearch/worker_pitch_score.php --pitch_id=N --project_id=N
Tavily-Last— (nur DeepSeek, ~$0.0003/Kontakt)
Letzte Aktivität (DB)16 d vor (2026-05-19 12:35:39)
Output 5 Scores insgesamt

🕒 Cron-Aufstellung (Tavily-Budget)

WorkerFrequenzCalls/TagCalls/Monat
wealth_atlas_harvester (phi)0 * * * * (stuendlich)~240~7.200 (48 %)
worker_city_search (webresearch)30 6-21 * * * (16×/Tag)~240~7.200 (48 %)
Total Tavily-Plan: 15.000/Monat~14.400 (96 %)

🔗 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 Projekte — 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.