feat(scanner): Funde mit Akquise und Opportunities abgleichen

Neu gefundene Projekte mit Treffer werden einmalig per gpt-5.6-luna mit den
Akquise-Eintraegen und Opportunities der letzten N Tage (Vorgabe 14)
verglichen. Passt ein Eintrag, zeigt der Scanner "bereits beworben!" samt
Begruendung. Schalter und Zeitraum (Tage/Wochen) in Einstellungen -> Scanner,
Neupruefung von Hand im Detail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-17 17:12:17 +02:00
co-authored by Claude Opus 5
parent 87ca52a34b
commit af3ca33743
15 changed files with 549 additions and 10 deletions
+71 -1
View File
@@ -40,12 +40,81 @@ sequenceDiagram
P->>M: E-Mail an Agent oder Standardempfaenger
end
end
opt Abgleich aktiv, Projekt hat Treffer, noch nicht geprüft
P->>P: Akquise und Opportunities der letzten N Tage laden
P->>AI: gpt-5.6-luna: Projekt vs. CRM-Einträge
AI-->>P: { matches: [{ type, id, reason }] }
P->>DB: already_applied, applied_refs, applied_check_at
end
```
Zwei Sparmassnahmen stecken bewusst **vor** dem OpenAI-Aufruf: der vorhandene
Treffer (Unique-Key `agent_id, project_id`) und die Ausschlussbegriffe. Ein
Projekt, das „Arbeitnehmerüberlassung" enthält, kostet damit keinen Cent.
## Abgleich „bereits beworben!"
Dieselbe Ausschreibung kommt oft mehrfach: über zwei Börsen, über mehrere
Vermittler, mit anderem Titel und anderem Link. Damit niemand ein Projekt ein
zweites Mal anbietet, gleicht `services/appliedCheck.js` jeden Fund mit der
**Akquise-Liste** (`crm_acquisitions`) und den **Opportunities**
(`crm_opportunities`) ab. Steht er dort schon, trägt der Treffer im Dashboard,
unter *Unpassend* und im Detail die Markierung **bereits beworben!**; das Detail
nennt die passenden Einträge samt Begründung.
```mermaid
flowchart TD
A["Projekt im Lauf verarbeitet"] --> B{"applied_check_active?"}
B -->|nein| X["nichts tun"]
B -->|ja| C{"schon geprüft?<br/>applied_check_at"}
C -->|ja| X
C -->|nein| D{"mindestens ein Treffer?"}
D -->|nein| X
D -->|ja| E["Kandidaten laden:<br/>Akquise found_at, Opportunities created_at<br/>der letzten N Tage, ohne Papierkorb"]
E --> F{"Kandidaten?"}
F -->|nein| N["nicht beworben"]
F -->|ja| G{"gleicher Link?"}
G -->|ja| Y["bereits beworben"]
G -->|nein| H["gpt-5.6-luna, reasoning_effort low"]
H -->|Treffer mit bekannter Id| Y
H -->|keine| N
N --> S["am Projekt speichern"]
Y --> S
```
| Einstellung | Vorgabe | Bedeutung |
| --- | --- | --- |
| `applied_check_active` | `true` | Schalter in *Einstellungen → Scanner* |
| `applied_check_days` | `14` | Zeitraum zurück, in Tagen (Oberfläche: Tage oder Wochen), höchstens 365 |
Entscheidungen dahinter:
- **Eigenes Modell.** Die Bewertung CV gegen Projekt bleibt bei `gpt-4o-mini`;
der Abgleich nutzt `gpt-5.6-luna` (Konstante `APPLIED_CHECK_MODEL`). Er muss
umformulierte Ausschreibungen als dieselbe erkennen und ähnliche Rollen bei
anderem Kunden auseinanderhalten - das ist ein Urteil, kein Textvergleich.
Der Schlüssel ist derselbe `openai_api_key`.
- **Einmal je Projekt, nur mit Treffer.** Die Börsen liefern bei jedem Lauf
dieselben Projekte erneut; `applied_check_at` verhindert, dass sie jedes Mal
bezahlt werden. Projekte ohne Treffer zeigt die Oberfläche nie an.
- **Gleicher Link braucht keine KI.** Verglichen wird ohne Protokoll, `www`,
Query und Schrägstrich am Ende.
- **Nur Ids, die wir geschickt haben.** Nennt das Modell eine unbekannte Id,
zählt sie nicht.
- **Fehler setzen aus, nicht ab.** Scheitert der Aufruf (Schlüssel, Quote,
Netz), bleibt der Abgleich für den Rest des Laufs aus und das Projekt
ungeprüft - der nächste Lauf versucht es wieder. Die Bewertung läuft davon
unberührt weiter.
- **Momentaufnahme.** Geprüft wird beim Fund. Wird die Bewerbung erst danach
in der Akquise erfasst, holt *Auf Bewerbung prüfen* im Detail den Abgleich
von Hand nach (`POST /projects/:id/applied-check`, auch bei abgeschaltetem
Automatismus).
Die drei Spalten an `projects` legt `initScannerDatabase()` per
`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` an - `sync()` ohne `alter` tut das in
bestehenden Tabellen nicht.
## Wann läuft er
```mermaid
@@ -215,7 +284,7 @@ Zertifikatswarnung. Der DNS-Eintrag muss beim Anbieter gelöscht werden.
| Unpassend | `ScannerRejected.tsx` | Treffer mit Status `rejected` |
| Suchagenten | `ScannerAgents.tsx` | Beraterprofile, CV-Upload |
| Börsen-Konten | `ScannerPortals.tsx` | Verbindungsstatus, Zugangsdaten, Cookie-Import |
| Einstellungen | `settings/ScannerSettings.tsx` | Intervall, Schwellenwerte, OpenAI, Ton, E-Mail |
| Einstellungen | `settings/ScannerSettings.tsx` | Intervall, Schwellenwerte, Abgleich mit Akquise/Opportunities, OpenAI, Ton, E-Mail |
**Wichtig zum Ton:** Er hängt an der Dashboard-Komponente. Wer auf einen anderen
Menüpunkt wechselt, hört nichts - die Komponente wird ausgebaut und die
@@ -240,6 +309,7 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`:
| POST | `/portals/test-connection/:id` | Mini-Scrape bzw. Session-Prüfung |
| GET | `/matches` | Filter: `status`, `agentId`, `minPercentage`, `limit` |
| PUT/DELETE | `/matches/:id` | Status setzen, löschen |
| POST | `/projects/:id/applied-check` | Abgleich „bereits beworben!" von Hand wiederholen |
| GET/POST | `/settings` | Geheimnisse maskiert |
| POST | `/scan` | Lauf starten, `409` wenn schon einer läuft |
| GET | `/status` | läuft ein Lauf, Scheduler an, Datenbankname |