- Schema: companyType, topics, salesScore, salesReason, offerPackage, approved, approvedAt, SearchHistory table - /api/search-history: GET (by mode) + POST (save query) - /api/ai-search: stadtwerke/industrie/custom prompts with history dedup - /api/enrich-leads: website scraping + GPT-4o-mini enrichment (fire-and-forget after each job) - /api/generate-email: personalized outreach via GPT-4o - Suche page: 3 mode tabs (Stadtwerke/Industrie/Freie Suche), Alle-Bundesländer queue button, AiSearchModal gets searchMode + history - Leadspeicher: Bewertung dots column, Paket badge column, Freigeben toggle button, email generator in SidePanel, approved-only export option - Leads API: approvedOnly + companyType filters, new fields in PATCH Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
222 lines
9.0 KiB
TypeScript
222 lines
9.0 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
|
||
const SYSTEM_PROMPT_DEFAULT = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschsprachigen Raum.
|
||
|
||
## Was passiert mit deiner Ausgabe
|
||
|
||
Die Suchanfragen die du erzeugst werden direkt an die Google Maps Places API übergeben. Das System sucht damit lokale Unternehmen (Name, Adresse, Telefon, Website) und findet anschließend automatisch die E-Mail-Adresse des Entscheidungsträgers (Inhaber, Geschäftsführer, CEO) über eine spezialisierte Datenbank.
|
||
|
||
Das bedeutet: Deine Queries müssen so formuliert sein, wie jemand einen Handwerker oder Dienstleister bei Google Maps suchen würde — kurz, präzise, branchenbezogen. Lange Sätze, Adjektive oder Marketing-Sprache funktionieren bei Maps-Suchen nicht.
|
||
|
||
## Deine Aufgabe
|
||
|
||
Analysiere die Beschreibung des Nutzers und erstelle 2–4 Google-Maps-optimierte Suchanfragen, die zusammen die gewünschte Zielgruppe möglichst vollständig abdecken.
|
||
|
||
## Feldmuster
|
||
|
||
- **query**: Die Branche oder Tätigkeit — so kurz wie möglich, auf Deutsch, wie ein Mensch bei Google Maps tippt. Keine Adjektive wie "klein" oder "professionell". Keine Firmennamen. Keine URLs.
|
||
- **region**: Bundesland, Stadt oder geografisches Gebiet. Wenn der Nutzer eine große Region nennt (z.B. "Deutschland" oder "Bayern"), splitte sinnvoll auf mehrere Städte oder Bundesländer auf um mehr Ergebnisse zu erzielen.
|
||
|
||
## Priorisierung
|
||
|
||
Wähle immer den geläufigsten, meistgesuchten Begriff für eine Branche — nicht Nischenbegriffe oder Synonyme. Beispiele:
|
||
- "Dachdecker" vor "Spengler" oder "Klempner"
|
||
- "Elektriker" vor "Elektroinstallateur"
|
||
- "Steuerberater" vor "Steuerkanzlei"
|
||
- "Solaranlage" vor "Photovoltaik Fachbetrieb"
|
||
|
||
Wenn der Nutzer mehrere Branchen nennt, priorisiere die volumenstärkste zuerst.
|
||
|
||
## Splitting-Strategie
|
||
|
||
Nutze mehrere Queries wenn:
|
||
- Die Region zu groß ist für eine Suche (Deutschland → München, Hamburg, Berlin, Köln)
|
||
- Der Nutzer explizit mehrere verschiedene Branchen nennt (dann je eine Query pro Branche)
|
||
- Der Nutzer breite Abdeckung möchte
|
||
|
||
Maximal 4 Queries. Keine Duplikate (gleiche query + gleiche region).
|
||
|
||
## Ausgabeformat
|
||
|
||
Antworte ausschließlich mit einem JSON-Array. Kein Markdown, kein erklärender Text, keine Kommentare — nur das reine JSON-Array.`;
|
||
|
||
const SYSTEM_PROMPT_STADTWERKE = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschen Energiesektor.
|
||
|
||
## Kontext
|
||
Das Tool sucht deutsche Stadtwerke und kommunale Energieversorger als potenzielle Kunden für ein Solarunternehmen (mein-solar.com). Stadtwerke betreiben lokale Energie-, Wasser- und Verkehrsinfrastruktur und sind starke Abnehmer für Photovoltaik-Großanlagen, Batteriespeicher und Ladeinfrastruktur.
|
||
|
||
## Was passiert mit deiner Ausgabe
|
||
Die Queries werden an die Google Maps Places API und Google SERP übergeben. Gesucht werden Stadtwerke-Websites, aus denen dann Entscheidungsträger-E-Mails extrahiert werden.
|
||
|
||
## Deine Aufgabe
|
||
Generiere 2–4 Suchanfragen die Stadtwerke, Gemeindewerke und kommunale Energieversorger in Deutschland finden. Decke verschiedene Bundesländer oder Regionen ab.
|
||
|
||
## Suchbegriffe die funktionieren
|
||
- "Stadtwerke" — findet kommunale Versorger
|
||
- "Gemeindewerk" — kleinere Gemeindebetriebe
|
||
- "Stadtwerk" — Variante
|
||
- "kommunaler Energieversorger" — für SERP
|
||
- Niemals: LinkedIn, XING, Verzeichnisse, Social Media
|
||
|
||
## Priorisierung
|
||
"Stadtwerke" schlägt immer "Gemeindewerk". Große Bundesländer (NRW, Bayern, BW) zuerst wenn keine Region angegeben.
|
||
|
||
## Bereits verwendete Queries (diese NICHT nochmal verwenden — generiere neue Regionen/Varianten):
|
||
[HISTORY_PLACEHOLDER]
|
||
|
||
## Beispiel
|
||
Eingabe: "Stadtwerke in Norddeutschland"
|
||
Ausgabe:
|
||
[
|
||
{ "query": "Stadtwerke", "region": "Hamburg" },
|
||
{ "query": "Stadtwerke", "region": "Schleswig-Holstein" },
|
||
{ "query": "Gemeindewerk", "region": "Niedersachsen" }
|
||
]
|
||
|
||
## Ausgabeformat
|
||
Nur reines JSON-Array, kein Markdown, keine Erklärungen.`;
|
||
|
||
const SYSTEM_PROMPT_INDUSTRIE = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschen Industriesektor.
|
||
|
||
## Kontext
|
||
Das Tool sucht deutsche Industriebetriebe als Kunden für ein Solarunternehmen (mein-solar.com), das PV-Großanlagen (5 kW bis 10+ MW), Batteriespeicher und Ladeinfrastruktur anbietet. Zielkunden sind Firmen mit hohem Energieverbrauch, großen Dachflächen oder Freiflächen, und Interesse an Kostensenkung durch Eigenerzeugung.
|
||
|
||
## Was passiert mit deiner Ausgabe
|
||
Die Queries werden an Google Maps und SERP übergeben um Unternehmenswebsites zu finden, aus denen Entscheidungsträger-E-Mails extrahiert werden.
|
||
|
||
## Deine Aufgabe
|
||
Generiere 2–4 Suchanfragen die energieintensive Industriebetriebe, Produktionsunternehmen, Logistiker oder Gewerbeparks finden — ideale Kandidaten für Solar-Großanlagen.
|
||
|
||
## Gut geeignete Branchen
|
||
Produktion/Fertigung, Logistik/Lager, Lebensmittelindustrie, Metallverarbeitung, Automobilzulieferer, Landwirtschaft (Agri-PV), Gewerbeparks, Einzelhandel (große Flächen)
|
||
|
||
## Suchbegriffe die funktionieren
|
||
Kurze branchenbezogene Begriffe die bei Google Maps echte Betriebe finden. Keine Adjektive.
|
||
|
||
## Priorisierung
|
||
Wähle immer den geläufigsten Begriff (z.B. "Logistikzentrum" vor "Lagerhaus"). Branchen mit hohem Energieverbrauch und Dachfläche zuerst.
|
||
|
||
## Bereits verwendete Queries (diese NICHT nochmal verwenden):
|
||
[HISTORY_PLACEHOLDER]
|
||
|
||
## Beispiel
|
||
Eingabe: "Industriebetriebe in Bayern mit großen Dachflächen"
|
||
Ausgabe:
|
||
[
|
||
{ "query": "Produktionsbetrieb", "region": "München" },
|
||
{ "query": "Logistikzentrum", "region": "Augsburg" },
|
||
{ "query": "Lebensmittelproduktion", "region": "Bayern" }
|
||
]
|
||
|
||
## Ausgabeformat
|
||
Nur reines JSON-Array, kein Markdown, keine Erklärungen.`;
|
||
|
||
function buildSystemPrompt(
|
||
searchMode: string,
|
||
history: Array<{ query: string; region: string }>
|
||
): string {
|
||
const historyText =
|
||
history.length > 0
|
||
? history
|
||
.slice(0, 20)
|
||
.map(h => `- "${h.query}" in "${h.region}"`)
|
||
.join("\n")
|
||
: "Keine bisherigen Suchen.";
|
||
|
||
if (searchMode === "stadtwerke") {
|
||
return SYSTEM_PROMPT_STADTWERKE.replace("[HISTORY_PLACEHOLDER]", historyText);
|
||
}
|
||
if (searchMode === "industrie") {
|
||
return SYSTEM_PROMPT_INDUSTRIE.replace("[HISTORY_PLACEHOLDER]", historyText);
|
||
}
|
||
return SYSTEM_PROMPT_DEFAULT;
|
||
}
|
||
|
||
export async function POST(req: NextRequest) {
|
||
try {
|
||
const body = await req.json() as {
|
||
description: string;
|
||
searchMode?: string;
|
||
history?: Array<{ query: string; region: string }>;
|
||
};
|
||
const { description, searchMode = "custom", history = [] } = body;
|
||
|
||
if (!description?.trim()) {
|
||
return NextResponse.json({ error: "Beschreibung fehlt" }, { status: 400 });
|
||
}
|
||
|
||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||
if (!apiKey) {
|
||
return NextResponse.json({ error: "OpenRouter API Key nicht konfiguriert" }, { status: 500 });
|
||
}
|
||
|
||
const systemPrompt = buildSystemPrompt(searchMode, history);
|
||
|
||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||
method: "POST",
|
||
headers: {
|
||
"Authorization": `Bearer ${apiKey}`,
|
||
"Content-Type": "application/json",
|
||
"HTTP-Referer": "https://mein-solar.com",
|
||
"X-Title": "MeinSolar Leads",
|
||
},
|
||
body: JSON.stringify({
|
||
model: "openai/gpt-4o-mini",
|
||
temperature: 0.4,
|
||
max_tokens: 512,
|
||
messages: [
|
||
{ role: "system", content: systemPrompt },
|
||
{ role: "user", content: description.trim() },
|
||
],
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.text();
|
||
console.error("[ai-search] OpenRouter error:", err);
|
||
return NextResponse.json({ error: "KI-Anfrage fehlgeschlagen" }, { status: 500 });
|
||
}
|
||
|
||
const data = await res.json() as {
|
||
choices: Array<{ message: { content: string } }>;
|
||
};
|
||
|
||
const raw = data.choices[0]?.message?.content?.trim() ?? "";
|
||
|
||
let queries: Array<{ query: string; region: string; count: number }>;
|
||
try {
|
||
queries = JSON.parse(raw);
|
||
} catch {
|
||
const match = raw.match(/\[[\s\S]*\]/);
|
||
if (!match) throw new Error("Kein JSON in Antwort");
|
||
queries = JSON.parse(match[0]);
|
||
}
|
||
|
||
queries = queries
|
||
.filter(q => typeof q.query === "string" && q.query.trim())
|
||
.slice(0, 4)
|
||
.map(q => ({
|
||
query: q.query.trim(),
|
||
region: (q.region ?? "").trim(),
|
||
count: 50,
|
||
}));
|
||
|
||
// Save to SearchHistory (fire and forget)
|
||
if (searchMode && searchMode !== "custom" && queries.length > 0) {
|
||
const baseUrl = req.url.replace(/\/api\/ai-search.*/, "");
|
||
for (const q of queries) {
|
||
fetch(`${baseUrl}/api/search-history`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ query: q.query, region: q.region, searchMode }),
|
||
}).catch(err => console.error("[ai-search] search-history save error:", err));
|
||
}
|
||
}
|
||
|
||
return NextResponse.json({ queries });
|
||
} catch (err) {
|
||
console.error("[ai-search] error:", err);
|
||
return NextResponse.json({ error: "Fehler bei der KI-Anfrage" }, { status: 500 });
|
||
}
|
||
}
|