Erster Produktionslauf schlug mit HTTP 500 fehl, lokal lief derselbe Code grün. Ursache: Vercel kompiliert jede Datei einzeln statt sie zu bündeln, und weil das Projekt `"type": "module"` ist, verlangt Nodes ESM-Lader explizite Dateiendungen in relativen Importen. Vite löst im Entwicklungsbetrieb grosszügiger auf und verdeckt das. Das ist genau die Klasse Fehler, die kein Typecheck und kein Test findet — nur ein echter Aufruf gegen die deployte Funktion. Geprüft gegen Produktion: HTTP 200, 20 Einträge aus 3/3 Quellen in 24 Sekunden, Beurteilung läuft, Originalquellen verlinkt, keine Laufzeitfehler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
/**
|
|
* Livia Research-PoC — der eine serverseitige Endpoint (§20).
|
|
*
|
|
* POST /api/livia/research/refresh
|
|
*
|
|
* Ruft die drei Quellen ab, normalisiert, beurteilt einmal per LLM und gibt
|
|
* die relevanten Leads zurück. Kein Scheduler, keine Queue, keine Datenbank —
|
|
* der Lauf beginnt und endet mit dieser Anfrage.
|
|
*
|
|
* Läuft als serverlose Funktion auf derselben Vercel-Instanz, die schon die
|
|
* Anwendung ausliefert. Das ist der Grund, warum für den PoC gar keine neue
|
|
* Infrastruktur nötig war: der Hoster kann bereits Serverfunktionen, es hat
|
|
* bisher nur niemand eine gebraucht.
|
|
*/
|
|
|
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
import {
|
|
SOURCES,
|
|
fetchZhkResearch,
|
|
fetchGreaterZurichResearch,
|
|
fetchZefixResearch,
|
|
} from '../../_lib/researchSources.js'
|
|
import type { ResearchItem } from '../../_lib/researchSources.js'
|
|
import { analyzeResearchItems } from '../../_lib/researchAnalysis.js'
|
|
import type { ResearchRefreshResult, ResearchSourceResult } from '../../../src/domain/researchLead.js'
|
|
|
|
interface Quelle {
|
|
id: string
|
|
name: string
|
|
url: string
|
|
laden: () => Promise<ResearchItem[]>
|
|
}
|
|
|
|
const QUELLEN: Quelle[] = [
|
|
{ ...SOURCES.ZHK, laden: fetchZhkResearch },
|
|
{ ...SOURCES.GZA, laden: fetchGreaterZurichResearch },
|
|
{ ...SOURCES.ZEFIX, laden: fetchZefixResearch },
|
|
]
|
|
|
|
export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
// Ein Live-Lauf soll nie aus einem Zwischenspeicher beantwortet werden.
|
|
res.setHeader('Cache-Control', 'no-store')
|
|
|
|
if (req.method !== 'POST') {
|
|
res.statusCode = 405
|
|
res.end(JSON.stringify({ error: 'Nur POST' }))
|
|
return
|
|
}
|
|
|
|
// Alle drei parallel: eine langsame Quelle soll die beiden anderen nicht aufhalten.
|
|
const ergebnisse = await Promise.all(
|
|
QUELLEN.map(async (q): Promise<{ meta: ResearchSourceResult; items: ResearchItem[] }> => {
|
|
try {
|
|
const items = await q.laden()
|
|
return {
|
|
meta: { id: q.id, name: q.name, url: q.url, ok: true, itemCount: items.length },
|
|
items,
|
|
}
|
|
} catch (err) {
|
|
// Eine kaputte Quelle darf die Demo nicht zerstören (§21).
|
|
return {
|
|
meta: {
|
|
id: q.id, name: q.name, url: q.url, ok: false, itemCount: 0,
|
|
error: err instanceof Error ? err.message : 'Unbekannter Fehler',
|
|
},
|
|
items: [],
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
|
|
const sources = ergebnisse.map(e => e.meta)
|
|
const items = ergebnisse.flatMap(e => e.items)
|
|
|
|
const basis: ResearchRefreshResult = {
|
|
sources,
|
|
analyzed: items.length,
|
|
discarded: 0,
|
|
watchlist: 0,
|
|
leads: [],
|
|
refreshedAt: new Date().toISOString(),
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
res.statusCode = 200
|
|
res.end(JSON.stringify({
|
|
...basis,
|
|
analysisError: 'Keine der drei Quellen lieferte Einträge.',
|
|
}))
|
|
return
|
|
}
|
|
|
|
try {
|
|
const { leads, discarded, watchlist } = await analyzeResearchItems(items)
|
|
res.statusCode = 200
|
|
res.end(JSON.stringify({ ...basis, leads, discarded, watchlist }))
|
|
} catch (err) {
|
|
// Gelesen wurde trotzdem — das sagen wir offen, statt eine leere Liste als
|
|
// «nichts gefunden» auszugeben.
|
|
res.statusCode = 200
|
|
res.end(JSON.stringify({
|
|
...basis,
|
|
analysisError: err instanceof Error ? err.message : 'Die Beurteilung schlug fehl.',
|
|
}))
|
|
}
|
|
}
|