Shop-Service-Check und Shop-Vorschau in der Kaltmail
Kostenloser Check unter /shop-check: öffentliche Shop-Seiten lesen, Lücken zeigen, 3 Beispielantworten mit Quelle, Test mit schon befülltem Wissen. Dieselbe Technik als Vorschau auf der Landing für Outreach-Leads.
Der Shop-Service-Check ist der Einstieg vor der Registrierung (Backlog I-040) und das Ziel der Anzeigen: Ein Shop-Betreiber gibt unter /shop-check seine Shop-Adresse ein und sieht nach rund 30 Sekunden
- das erkannte Shopsystem,
- was Kunden bei ihm finden (Versand & Lieferzeit, Rücksendung, Zahlungsarten, Kontakt & Erreichbarkeit, FAQ, jeweils mit Quelle),
- wo Antworten fehlen („Keine Angaben zum Versand nach Österreich oder in die Schweiz“, „Rücksendefrist nicht eindeutig“, „Keine Kontaktzeiten gefunden“ …),
- 3 Beispielantworten auf typische Kundenfragen, nur aus seinen Seiten, mit Link zur Quelle,
- den CTA „14 Tage kostenlos testen – Ihr Shop-Wissen ist schon drin“ →
/register?branche=shop-kundenservice&src=shop-check&check=<id>.
Formulierung immer „Hinweise für Kundenfragen“, nie Rechtsprüfung.
Die Shop-Vorschau in der Kaltmail (I-038) nutzt denselben Kern: Vor dem Versand einer Outreach-Mail läuft der Check für die Website des Leads, die System-Landing mit ?o=<outreach_id> zeigt dann den Kasten „So würde
Ablauf
| Schritt | Wo | Was |
|---|---|---|
| Formular | ShopCheckController::index (GET/POST /shop-check) |
Honeypot website, signierter Formular-Zeitstempel (≥ 2 s), URL-Prüfung inkl. EgressGuard (nur öffentliche https-Adressen), gleicher Shop innerhalb 24 h → vorhandenes Ergebnis, Deckel (siehe unten) → Zeile in shop_checks, Weiterleitung auf /shop-check/<id> |
| Start | POST /shop-check/<id>/run (von webroot/js/shop-check.js) |
Einmal (atomarer Claim queued → running), Session-Lock frei, läuft weiter, wenn der Tab zugeht |
| Fortschritt | GET /shop-check/<id>/status |
alle 1,5 s: system → pages → answers |
| 1. System | _shared-Tool detectShopSystem |
Startseite, max. 1 Weiterleitung, über apis.shop_homepage (egress_guard: true) |
| 2. Seiten | ShopPageImporter::collect über _shared-Tool shopFetchPage |
apis.shop_site nur im Speicher, egress_guard: true (auch im getrackten Kontext meco: https:443, IP gepinnt, keine Weiterleitungen). Nicht gelesen: Impressum, Über uns, Konto, Warenkorb, Kasse, Datenschutz. Versteckter Text fliegt raus (Prompt-Injection). Zeitbudget 45 s |
| 3. Analyse | ShopCheckAnalyzer |
lokal, deterministisch (Regeln, kein LLM): Themen + Lücken, nur so weit die Seiten wirklich gelesen wurden (siehe unten) |
| 3. Antworten | ShopCheckService::answers |
ein LLM-Aufruf, Profil haiku, max_tokens 900, Kontext ≤ 14.000 Zeichen, strikter Prompt (nur aus den nummerierten Auszügen, Auszüge = Daten), Quelle = Auszugsnummer → Seiten-URL; fremde Links werden entfernt, erfundene Quellen verworfen |
| Ohne JavaScript | bin/cake shop_checks (cronjobs.sh, alle 5 min) |
führt liegengebliebene Checks aus, markiert hängende (> 5 min) als fehlgeschlagen, löscht nach 30 Tagen |
Seitenauswahl und ehrliche Lücken (Prüflauf 29.09.)
Anlass: Der Marketing-Agent hat senf.de, braeckers.com, climatenuts.de, rau-gmbh.de und cayin.com geprüft. Nachgebaut als Fixtures in tests/fixtures/shop_check/, Tests in ShopCheckRealCasesTest.
- Seitenauswahl (
ShopPageImporter::discoverLinks): Jeder Link bekommt Punkte. 4 gibt es für ein Stichwort im letzten Pfad-Segment (/service-hilfe/versand-zahlung/), 3 für eins im Linktext, 2 für eins weiter vorn im Pfad. Schwache Wörter (hilfe, service, fragen) zählen nur als FAQ und nur, wenn es keinen besseren Treffer gibt. Links in Kopf, Fuß oder Navigation bekommen +1. Gelesen wird in der Reihenfolge Versand, Widerruf, Zahlung, FAQ, Kontakt, AGB, je Art höchstens 1–2 Seiten, insgesamt bis zu 12. Mit „Rechtliches“ im Pfad wird nichts ausgeschlossen, gesperrt sind nur Impressum, Datenschutz, Konto, Warenkorb und Kasse. - Große Startseiten:
shopFetchPageliefert bis zu 2 MB (vorher 600 KB, bei senf.de fehlten dadurch die Footer-Links). - Sitemap-Rückfall: Verlinkt die Startseite weniger als 4 Seitenarten, liest der Check
/sitemap.xml, auch einen Sitemap-Index und.xml.gz-Teile (höchstens 2, Seiten-Sitemaps zuerst). Es gelten nur Adressen desselben Hosts. - Weiterleitungen auf demselben Host (z. B.
/?v=<hash>bei cayin.com) werden einmal verfolgt.shopFetchPagegibt dafür bei 30x denlocation-Header zurück. - Ehrliche Lücken: Zu jeder Seitenart hält
coveragefest, ob sie gelesen wurde (read), leer war (empty) oder nicht erreichbar (unreachable); fehlt der Eintrag, war sie nirgends verlinkt. Lücken wie „Keine Lieferzeit“ gibt es nur für gelesene Seiten. „Nicht gefunden“ steht nur, wenn die Art nirgends verlinkt war, andere Infoseiten aber gelesen wurden. Nicht erreichbare Seiten heißen „nicht geprüft“. Widerruf ohne gelesene Seite heißt immer „nicht geprüft“, nie „fehlt“. - Nichts lesbar (Shop nicht erreichbar oder keine Infoseite lesbar): eigener Fehlerzustand
ShopCheckService::UNREADABLE(„Wir konnten Ihren Shop gerade nicht lesen …“), ohne Themen und ohne Lücken. - Bot-Abwehr (Prod 29.09.: erster Ads-Klick, Shop hinter Cloudflare, 403 „Just a moment…“):
detectShopSystembzw.ShopPageImporter::looksBlockederkennen HTTP 403,cf-mitigated: challenge, Challenge-Seiten („Just a moment“, „Attention Required“,cf-chl,challenge-platform) und Cloudflare/Captcha bei 429/503. Dann genau ein zweiter Versuch mit Browser-Kopfzeilen (ShopCheckService::BROWSER_HEADERS: Chrome-User-Agent, Accept, Accept-Language; ehrliche Kennung inX-TaskMonkey-Check), auch für die Unterseiten. Challenges werden nie gelöst. Klappt es nicht, Statusblocked(ShopCheckService::BLOCKED, kein „später versuchen“),result.blocked_by=cloudflare/firewall. Die Seite zeigt ein E-Mail-Feld („Wir prüfen Ihren Shop manuell …“),POST /shop-check/<id>/contactspeichertcontact_email+contact_requested_ateinmal pro Check und schickt eine Plattform-Mail anOnboarding.alert.recipients.email(Shop, Schutz, E-Mail als Reply-To, Herkunft nur nach Einwilligung wie beim Signup, Link zum Check). Die manuelle Prüfung macht Marcus. - Antworten passen zur Regel: Jede Frage bekommt nur Auszüge ihrer Seitenarten (
QUESTION_KINDS), und als Quelle zählen nur diese. Danach vergleichtcontradictions()die Antworten mit der Regel-Erkennung. Beispiel: Die Regel hat Versand ins Ausland gefunden, die Antwort sagt aber „steht nichts“. Oder die Antwort sagt „Rechnung ja“, obwohl die gelesene Zahlungsseite keine Rechnung nennt. Umgekehrt widerspricht „Rechnung nicht angeboten“, wenn die Regel Rechnung gefunden hat. Dann folgt ein zweiter Aufruf mit Hinweis; widerspricht die Antwort danach immer noch, wird sie weggelassen. - Verneinung zählt nicht als Treffer: Zahlarten (und der Rechnungs-Abgleich der Antworten) nutzen
ShopCheckAnalyzer::affirms(). Ein Treffer mit Verneinung im selben Satzteil („Kauf auf Rechnung wird derzeit nicht angeboten“, „keinen Kauf auf Rechnung“) ist kein Beleg; Satzende, Gedankenstrich und „aber/sondern/jedoch“ begrenzen den Satzteil. „Rechnung“ allein zählt nur in einer Aufzählung mit anderen Zahlarten („PayPal, Kreditkarte, Rechnung“). - Zeiträume werden vereinheitlicht: „2 tagen“ → „2 Tage“, „1-3 werktagen“ → „1–3 Werktage“, „binnen vierzehn Tagen“ → „14 Tage“.
- Vorschau (I-038) nur mit mindestens 2 gelesenen Infoseiten und mindestens 2 belegten Antworten.
Übernahme beim Signup
/register?…&check=<id> legt die ID als SignupIntent-Quelle shop_check ab (nur aus der Query, 24 Hex). Sie steht danach auch in tenants.signup_source.attribution.shop_check (Auswertung Checks → Signups). Beim Anlegen eines Shop-Kundenservice-Workspaces (branche = shop-service, Aliase shop-kundenservice …) übernimmt ShopCheckService::adopt:
- die gelesenen Abschnitte als Wissen (
knowledge_entries,model = shop_pages, dasselbe wie „Wissen aus Shop-Seiten einlesen“; der Schritt gilt als erledigt), - Shopsystem (WooCommerce, Gambio, Shopware 6) und Shop-Adresse als Vorschlag in
tenant_settings shop_setup.state.prefill./manage/shop-setupbelegt Schritt 1 damit vor (ShopSetupService::withPrefill); gespeichert wird erst mit dem Formular, also mit Prüfung und AV-Vertrag.
Andere Branchen übernehmen nichts. Die Register-Seite nennt den übernommenen Shop.
Vorschau in der Kaltmail (I-038)
- Kampagne:
lead_campaigns.<id>.shop_preview = ['enabled' => true](heutetaskmonkey_wismo, Kampagne pausiert). sendOutreachruft vor dem VersandbuildShopPreview(url, outreach_id)(meco, Core-HandlerApp\Tooling\ShopPreviewTools, nur meco lautCoreHandlerRegistry). Gleicher Shop innerhalb 24 h: Kopie des vorhandenen Ergebnisses. Fehler blockieren den Versand nie; der Mailtext bleibt, wie er freigegeben wurde.- Landings unter
/shop-kundenservice/(Hauptseite + System-Seiten) laden/js/shop-preview.js. Mit?o=<outreach_id>holt esGET /shop-check/preview/<o>. Angezeigt wird nur, was nutzbar ist: Check fertig, System unterstützt, mind. 2 Antworten mit Quelle. Sonst bleibt die Landing unverändert. - Der CTA im Kasten führt auf
/register?branche=shop-kundenservice&src=shop-preview&o=<o>&check=<id>, also mit Übernahme. ?o=hängtsendOutreach/runFollowupSequenceseit 29.09. auch an mehrstufige Pfade (/shop-kundenservice/woocommerce/).
Kosten, Missbrauch, Daten
Alles in der Plattform-Config config/platform.php → Platform.shop_check (nie Tenant-Config):
| Schlüssel | Default | Bedeutung |
|---|---|---|
enabled |
true |
false → /shop-check liefert 404 |
limits |
3 pro Besucher/Tag, 2 pro Minute, 150 gesamt/Tag | PublicChatBudget::consumeLimits, Topf _shop-check, Besucher = HMAC der IP (keine IP gespeichert), Dateien nach 2 Tagen gelöscht |
tenant |
meco |
ToolRunner-Kontext (Tools, LLM-Profil, Logs in task_executions, Chat-ID shopcheck_<id>) |
llm_profile, max_tokens, max_context_chars |
haiku, 900, 14.000 |
ein LLM-Aufruf pro Check, ca. 1 Cent |
reuse_hours |
24 | gleicher Host → vorhandenes Ergebnis, nur mit aktueller ShopCheckService::ENGINE_VERSION |
retention_days |
30 | danach löscht bin/cake shop_checks die Zeile |
Tabelle shop_checks: ID (24 Hex, 96 bit Zufall, die einzige „Berechtigung“ für die Ergebnis-URL), Quelle (check/outreach), Outreach-ID, Shop-Adresse, System, engine_version (Version der Auswertung; ändert sich die Auswertung fachlich, ENGINE_VERSION hochzählen, sonst gehen bis zu 24 h alte Ergebnisse an Seite und Kaltmail), Status/Fortschritt, Ergebnis-JSON, gelesene Abschnitte (für die Übernahme), adopted_at. Keine IP, keine Namen (Impressum wird nicht gelesen). E-Mail nur bei blocked, wenn der Besucher sie selbst für die manuelle Prüfung einträgt (contact_email, gleiche Löschfrist).
Captcha: Das Projekt hat keins. Heute Honeypot + Formular-Zeitstempel + Deckel. Kommt Turnstile/hCaptcha dazu, gehört die Prüfung in ShopCheckController::index vor consumeBudget.
Vertrauensmodell
Gilt für die Plattform (getrackter Kontext meco). Eingabe ist nur eine URL, kein Code. Jede Verbindung zum Shop läuft über den deklarativen api-Zweig des ToolRunners mit erzwungenem EgressGuard (egress_guard: true), die frühe Formular-Prüfung nutzt denselben Guard. Das LLM bekommt nur sichtbaren Seitentext, seine Antwort wird geprüft (nur 3 Fragen, Quelle aus den Auszügen, keine fremden Links, gekürzt) und auf der Seite escaped ausgegeben.
Tests
tests/TestCase/Service/Shop/ShopCheckServiceTest.php (gefaktes HTTP + LLM: Ablauf, EgressGuard-Pinning, kein Impressum, versteckter Text, Lücken, Quellen, Übernahme, Wiederverwendung, Vorschau, Wartung), tests/TestCase/Service/Shop/ShopCheckBlockedTest.php (Cloudflare-403, Browser-Retry, blocked, Lead-Mail), tests/TestCase/Controller/ShopCheckControllerTest.php (Formular-Schutz, Deckel, Ablauf bis Ergebnis, Vorschau-JSON, E-Mail-Feld bei blocked), tests/TestCase/Service/Onboarding/SignupShopCheckTest.php (Signup), tests/TestCase/Tooling/ShopPreviewToolsTest.php (buildShopPreview).