From 90920cb1c99389c9694822c4208741bf1001c8337d8fef9809f9b725dc03505a Mon Sep 17 00:00:00 2001 From: tlg Date: Fri, 12 Jun 2026 13:24:51 +0200 Subject: [PATCH] =?UTF-8?q?Skill=20projekt-anlegen=20erweitern:=20Rahmenbe?= =?UTF-8?q?dingungs-Bewertung,=20K=C3=A4ufer/Firma/Kontakt-Extraktion,=20C?= =?UTF-8?q?RM-Ablauf=20Firma=E2=86=92Kontakt=E2=86=92VC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/projekt-anlegen/SKILL.md | 132 +++++++++++++++++++----- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/.claude/skills/projekt-anlegen/SKILL.md b/.claude/skills/projekt-anlegen/SKILL.md index df5edbd..1d7ef3b 100644 --- a/.claude/skills/projekt-anlegen/SKILL.md +++ b/.claude/skills/projekt-anlegen/SKILL.md @@ -13,20 +13,21 @@ description: >- # Projekt anlegen: Projektangebote bewerten und ins EspoCRM eintragen -Pipeline: Ausschreibung beschaffen → Anforderungen extrahieren → einordnen → gegen Lebenslauf bewerten → Matches berechnen → **Review im Chat (Pflicht)** → als Opportunity ins EspoCRM schreiben (per Shell) → verifizieren. +Pipeline: Ausschreibung beschaffen → Anforderungen + Käufer/Firma/Kontakt extrahieren → einordnen → gegen Lebenslauf bewerten → Rahmenbedingungen gegen Vorgaben bewerten → Matches berechnen → **Review im Chat (Pflicht)** → Firma → Kontakt → Verkaufschance ins EspoCRM schreiben (per Shell) → verifizieren. ## Voraussetzungen und feste Pfade | Zweck | Pfad | |---|---| | Lebenslauf (Markdown) | `vorgaben/Lebenslauf_Dr-Ing_Thomas_Langer.md` | +| Rahmenbedingungen (Misc-Bewertung) | `vorgaben/rahmenbedingungen.md` | | EspoCRM-Zugangsdaten | `.secrets/espocrm-api.md` | -Beide Dateien mit dem Read-Tool lesen. Ohne Lebenslauf keine Bewertung, ohne Zugangsdaten kein CRM-Eintrag. +Alle drei mit dem Read-Tool lesen. Ohne Lebenslauf keine fachliche Bewertung, ohne Rahmenbedingungen keine automatische Misc-Bewertung, ohne Zugangsdaten kein CRM-Eintrag. Die Zugangsdaten-Datei enthält Base-URL, Auth-Header-Schema, API-Key sowie verifizierte Entitäts- und Feldnamen. **Der API-Key darf niemals im Chat angezeigt, in Dateien geschrieben oder in Commits/Doku übernommen werden** — er wird nur zur Laufzeit in eine Shell-Variable gelesen und direkt in `curl`-Aufrufe eingesetzt. -## Schritt 1: Ausschreibung beschaffen +## Schritt 1: Ausschreibung beschaffen und Eckdaten extrahieren Die Projektausschreibung kommt auf zwei Wegen: - **URL:** Nennt der Nutzer eine URL, hole den Seitentext mit dem **WebFetch-Tool**. @@ -39,6 +40,11 @@ Daraus entnehmen: - **Projekt-URL**: die tatsächliche URL der Seite, falls vorhanden. - **Beschreibung und Aufgaben**: nur Kontext für die Bewertung, keine Tabellenzeilen. - **Anforderungen**: jede Anforderung einzeln, im Originalwortlaut (behutsam kürzen ist erlaubt, Bedeutung nie verändern). Rahmenbedingungen (Start, Einsatzort/Remote-Anteil, Auslastung, Laufzeit) zählen als Anforderungen der Kategorie Misc. +- **Käufer-Typ**: Agentur (Wiederverkäufer) ODER Endkunde (Direktauftrag). + - Agentur-Signale: bekannte Personaldienstleister/Vermittler (Hays, GULP/Randstad, SThree/Computer Futures, Aristo …), Formulierungen wie „im Auftrag unseres Kunden", „für unseren Kunden", Vermittler-Kontext des Portals. + - Nicht eindeutig bestimmbar → im Review nachfragen. +- **Firmenname**: Name der Agentur bzw. des Endkunden. +- **Ansprechperson**: vollständiger Name, falls genannt. ## Schritt 2: Einordnung (Kat.) @@ -58,7 +64,41 @@ Lebenslauf vollständig lesen und jede Anforderung nüchtern bewerten: - **❌** wenn der Lebenslauf nichts Belastbares hergibt. Streng bleiben — eine geschönte Bewertung macht die Match-Zahlen wertlos. Kalibrierung aus der Praxis: Proof-of-Concept-Erfahrung deckt „produktiven Betrieb" nicht ab; „mehrjährig" wörtlich nehmen; ein ALM-Produktname (z. B. „Azure DevOps Server") belegt keine Cloud-Plattform-Erfahrung. - **❔** nur bei echter Teilevidenz, wenn die Entscheidung von Wissen abhängt, das nur Thomas hat — solche Fälle klärt das Review. - „wie z. B."-Aufzählungen: gleichwertige Alternativen zählen als Abdeckung (Beispiel: Ollama/llama.cpp/Transformers decken „LLM-Inference-Stacks wie z. B. vLLM, TGI, Triton" ab). -- Rahmenbedingungen (Misc): Ortsbezug kann der Lebenslauf beantworten (Büro in Unterhaching bei München). Verfügbarkeit, Auslastung und Laufzeit kann er nicht beantworten → ❔ vorschlagen, Thomas setzt im Review ✅ oder ❌. +- Rahmenbedingungen (Misc) werden NICHT hier, sondern in Schritt 3b automatisch gegen `vorgaben/rahmenbedingungen.md` bewertet. + +## Schritt 3b: Rahmenbedingungen automatisch bewerten (Misc) + +`vorgaben/rahmenbedingungen.md` lesen und jede Misc-Anforderung danach bewerten: + +- **Verfügbar ab / Projektstart:** Start ≤ heute + 8 Wochen (56 Tage) → ✅; > 8 Wochen → ❌; nicht genannt → ❔. („heute" = aktuelles Datum.) +- **Auslastung:** 75–100 % → ✅; < 75 % → ❔; nicht genannt → ❔. +- **Laufzeit:** jede → ✅. +- **Einsatzort/Remote:** 100 % Remote → ✅. Bei < 100 % Remote (Hybrid/Onsite) Distanz des Onsite-Orts zu Sauerlach bestimmen (siehe unten): ≤ 50 km → ✅; 50–60 km → ❔; > 60 km → ❌. Einsatzort/Remote-Anteil unklar oder nicht genannt → ❌. + +**Distanzbestimmung (Geokodierung, Pflicht bei Onsite/Hybrid):** Onsite-Ort per Nominatim auflösen und Luftlinie zu Sauerlach (47,9721 N / 11,6528 O) per Haversine rechnen. Bei Geokodierungs-Fehler/Mehrdeutigkeit (Ausgabe `UNBEKANNT`) → ❔. Nominatim-Policy: max. 1 Request/s. + +```bash +python3 - "" <<'PY' +import sys, json, urllib.request, urllib.parse +from math import radians, sin, cos, asin, sqrt +ort = sys.argv[1] +UA = 'bewerb-projekt-anlegen/1.0 (Thomas.Langer@destengs.com)' +def geo(q): + url = 'https://nominatim.openstreetmap.org/search?' + urllib.parse.urlencode( + {'q': q, 'format': 'json', 'countrycodes': 'de', 'limit': 1}) + d = json.load(urllib.request.urlopen( + urllib.request.Request(url, headers={'User-Agent': UA}), timeout=20)) + return (float(d[0]['lat']), float(d[0]['lon'])) if d else None +S = (47.9721357, 11.6528398) +g = geo(ort) +if not g: + print('UNBEKANNT') +else: + dlat = radians(g[0]-S[0]); dlon = radians(g[1]-S[1]) + h = sin(dlat/2)**2 + cos(radians(S[0]))*cos(radians(g[0]))*sin(dlon/2)**2 + print(f'{2*6371*asin(sqrt(h)):.1f} km') +PY +``` ## Schritt 4: Match-Berechnung @@ -86,28 +126,77 @@ Must-have-Match: 50 % · Nice-to-have-Match: 33 % - `Kat.` ∈ {Must, Nice, Misc}; Bewertungsspalte ∈ {✅, ❌, ❔}. - Beide Match-Zahlen in einer Zeile, getrennt durch ` · `. -## Schritt 6: Review im Chat — immer vor dem CRM-Eintrag +## Schritt 6: Review im Chat — immer vor jedem CRM-Schreibvorgang Niemals ohne explizite Freigabe ins CRM schreiben. Im Chat zeigen: 1. Vorgesehener **Name** der Opportunity (= Projektname) und **Projektlink**. -2. Den wörtlichen Beschreibungs-Markdown in einem Codeblock (damit das Format sichtbar ist). -3. Kurze Begründungen zu allen ❌- und ❔-Bewertungen außerhalb der Tabelle — hier stecken die Ermessensentscheidungen, die Thomas korrigieren können muss. +2. **Käufer-Typ** (Agentur/Direktauftrag) mit Kurzbegründung. +3. **Firma:** Name und `type` (Reseller bei Agentur, Customer bei Direktauftrag); „neu anlegen" oder „bestehende nutzen: ". Bei ähnlichen, nicht identischen Treffern die Kandidatenliste zeigen und entscheiden lassen. +4. **Kontakt:** Name; „neu anlegen" oder „bestehend nutzen: ". +5. **Verknüpfungs-Zuordnung** der Verkaufschance (account vs. cAccount1, Kontakt). +6. Den wörtlichen Beschreibungs-Markdown in einem Codeblock. +7. Kurze Begründungen zu allen ❌- und ❔-Bewertungen (Must/Nice/Misc) außerhalb der Tabelle. -Korrekturen kommen als „Nr. X → ✅/❌/❔". Danach Matches neu berechnen und die geänderten Werte nennen. Erst nach Freigabe weiter zu Schritt 7. +Korrekturen kommen als „Nr. X → ✅/❌/❔" oder als Korrektur zu Firma/Kontakt/Typ. Danach betroffene Matches neu berechnen und geänderte Werte nennen. Erst nach Freigabe weiter zu Schritt 7. -## Schritt 7: CRM-Eintrag per Shell (curl) +## Schritt 7: CRM-Eintrag per Shell — Reihenfolge Firma → Kontakt → Verkaufschance -Diese Umgebung erreicht die CRM-Domain direkt — der API-Zugriff läuft per `curl` aus der Shell (kein Browser nötig). - -**Key und Base-URL sicher laden** (Werte landen in Variablen, werden nicht ausgegeben): +Diese Umgebung erreicht die CRM-Domain direkt; der Zugriff läuft per `curl`. Key/Base sicher laden (Werte nicht ausgeben): ```bash KEY=$(grep -oE '[0-9a-f]{32}' .secrets/espocrm-api.md | head -1) BASE=$(grep -oE 'https://[^ `]+/api/v1' .secrets/espocrm-api.md | head -1) ``` -**Duplikat-Prüfung** — existiert der Name schon, „ (2)", „ (3)", … anhängen: +### 7.1 Firma (Account) +Dedup-Suche mit einem Kern-Token des Namens (z. B. „Aristo"): + +```bash +curl -s -G "$BASE/Account" -H "X-Api-Key: $KEY" \ + --data-urlencode 'where[0][type]=contains' \ + --data-urlencode 'where[0][attribute]=name' \ + --data-urlencode 'where[0][value]=' \ + --data-urlencode 'select=name,type' --data-urlencode 'maxSize=50' +``` + +Treffer gemäß Review-Entscheidung behandeln. Bestehende Firma → deren `id` als ACCOUNT_ID merken. Sonst neu anlegen — Payload mit dem **Write-Tool** nach `/tmp/account.json` (`type`: `Reseller` bei Agentur, `Customer` bei Direktauftrag): + +```json +{"name": "", "type": ""} +``` + +```bash +curl -s -i -X POST "$BASE/Account" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/account.json +``` + +`id` aus der Antwort als ACCOUNT_ID merken. + +### 7.2 Kontakt (Contact) +Dedup-Suche per Nachname: + +```bash +curl -s -G "$BASE/Contact" -H "X-Api-Key: $KEY" \ + --data-urlencode 'where[0][type]=contains' \ + --data-urlencode 'where[0][attribute]=name' \ + --data-urlencode 'where[0][value]=' \ + --data-urlencode 'select=name,accountName' --data-urlencode 'maxSize=50' +``` + +Bestehenden Kontakt → dessen `id` als CONTACT_ID merken. Sonst neu anlegen — Payload nach `/tmp/contact.json`; die **Verknüpfung zur Firma über `accountId` ist beim Neu-Anlegen zwingend**: + +```json +{"firstName": "", "lastName": "", "accountId": ""} +``` + +```bash +curl -s -i -X POST "$BASE/Contact" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/contact.json +``` + +`id` aus der Antwort als CONTACT_ID merken. Namensaufteilung: letztes Token = `lastName`, der Rest = `firstName`; ungewöhnliche Fälle im Review klären. + +### 7.3 Verkaufschance (Opportunity) +Duplikat-Prüfung des Namens (bei Treffer Suffix „ (2)", „ (3)", … anhängen): ```bash curl -s -G "$BASE/Opportunity" -H "X-Api-Key: $KEY" \ @@ -117,22 +206,19 @@ curl -s -G "$BASE/Opportunity" -H "X-Api-Key: $KEY" \ --data-urlencode 'select=name' --data-urlencode 'maxSize=100' ``` -Aus der zurückgegebenen `list` die vorhandenen Namen prüfen und den eindeutigen Namen bestimmen. - -**Anlegen:** Die Beschreibung enthält Markdown mit Zeilenumbrüchen und `|`. Um Shell-Escaping zu vermeiden, den Payload mit dem **Write-Tool** als JSON-Datei nach `/tmp/opp.json` schreiben: +Payload nach `/tmp/opp.json`. Verknüpfungen je Käufer-Typ: +- **Agentur:** `cAccount1Id` = ACCOUNT_ID („Über Agentur"); **kein** `accountId`. +- **Direktauftrag:** `accountId` = ACCOUNT_ID; **kein** `cAccount1Id`. ```json -{"name": "", "description": "", "cProjektlink": ""} +{"name": "", "description": "", "cProjektlink": "", "contactsIds": [""], "cAccount1Id": ""} ``` -Dann posten (`-i` zeigt die Header inkl. `X-Status-Reason` bei Fehlern): - ```bash -curl -s -i -X POST "$BASE/Opportunity" -H "X-Api-Key: $KEY" \ - -H 'Content-Type: application/json' --data @/tmp/opp.json +curl -s -i -X POST "$BASE/Opportunity" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/opp.json ``` -Der minimale Payload (`name`, `description`, `cProjektlink`) reicht — Stage und Wahrscheinlichkeit setzt das CRM selbst. Bei HTTP 400 steht die Ursache im `X-Status-Reason`-Header. Nachträgliche Korrekturen: `PUT "$BASE/Opportunity/"` mit den geänderten Feldern. Danach `/tmp/opp.json` löschen. +Minimaler Payload genügt (Stage/Wahrscheinlichkeit setzt das CRM). Bei HTTP 400 steht die Ursache im `X-Status-Reason`-Header. Nachträgliche Korrekturen: `PUT "$BASE/Opportunity/"`. Danach `/tmp/account.json /tmp/contact.json /tmp/opp.json` löschen. ## Schritt 8: Verifizieren und berichten @@ -142,4 +228,4 @@ Nach dem POST den Datensatz gegenlesen: curl -s -G "$BASE/Opportunity/" -H "X-Api-Key: $KEY" ``` -Prüfen: stimmen Name und Projektlink, hat die Beschreibung die Match-Zeile und alle Tabellenzeilen, sind ✅/❌/❔ intakt. Dann im Chat melden: gewählter Name (inkl. evtl. Duplikat-Suffix), beide Match-Zahlen und der Direktlink `https://crm.creature-go.com/#Opportunity/view/`. +Prüfen: stimmen Name und Projektlink, hat die Beschreibung die Match-Zeile und alle Tabellenzeilen, sind ✅/❌/❔ intakt; sind die Verknüpfungen korrekt — Agentur: `cAccount1Name` gesetzt und `accountName` leer; Direktauftrag: `accountName` gesetzt; `contactsIds`/`contactId` enthält den Kontakt. Dann im Chat melden: gewählter Name (inkl. evtl. Duplikat-Suffix), beide Match-Zahlen, Firma und Kontakt (jeweils neu/bestehend) und der Direktlink `https://crm.creature-go.com/#Opportunity/view/`.