since3-portal/modules/templates/ai/server.ts
2026-10-05 08:23:02 +02:00

76 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only"
import Anthropic from "@anthropic-ai/sdk"
import { getProfile } from "@/lib/supabase/dal"
import { createAdminClient } from "@/lib/supabase/admin"
import { createClient } from "@/lib/supabase/server"
import { resolveContent } from "../lib/content"
import { getTemplate } from "../lib/registry"
import type { AiSettings, TemplateContent, TemplateDefinition } from "../lib/types"
import type { AiAccess } from "./schemas"
export const AI_MODEL = "claude-opus-5-5"
// Lehnt das Modell eine Anfrage aus Sicherheitsgründen ab, übernimmt serverseitig automatisch ein Ersatzmodell
export const FALLBACK = { betas: ["server-side-fallback-2026-07-01"], fallbacks: "default" as const }
// KI-Anfragen pro geteiltem Link und Tag (Kostenbremse für öffentliche Demos)
const DAILY_LIMIT = Number(process.env.AI_DAILY_LIMIT ?? 150)
let client: Anthropic | null = null
export function getAnthropic() {
client ??= new Anthropic()
return client
}
type AiFeature = keyof Pick<AiSettings, "chat" | "phone" | "assistant" | "content">
export type AiContext = { def: TemplateDefinition; content: TemplateContent }
type AccessResult = AiContext | { error: string; status: number }
// Admins testen über die Varianten-ID (ohne Limit), Kunden über den Freigabe-Link (nur freigeschaltete Demos, mit Tageslimit)
export async function resolveAiAccess(access: AiAccess, feature: AiFeature): Promise<AccessResult> {
if ("variantId" in access) {
const profile = await getProfile()
if (profile?.role !== "admin") return { error: "Keine Berechtigung.", status: 403 }
const supabase = await createClient()
const { data } = await supabase.from("template_variants").select("template_key, content").eq("id", access.variantId).maybeSingle()
const def = data ? getTemplate(data.template_key) : undefined
if (!data || !def) return { error: "Variante nicht gefunden.", status: 404 }
return { def, content: resolveContent(def, data.content) }
}
const admin = createAdminClient()
const { data } = await admin
.from("template_variants")
.select("id, template_key, content, share_expires_at")
.eq("share_token", access.token)
.maybeSingle()
const def = data ? getTemplate(data.template_key) : undefined
const expired = data?.share_expires_at ? new Date(data.share_expires_at).getTime() <= Date.now() : false
if (!data || !def || expired) return { error: "Dieser Link ist nicht mehr gültig.", status: 404 }
const content = resolveContent(def, data.content)
if (!content.ai[feature]) return { error: "Diese KI-Demo ist für den Link nicht freigeschaltet.", status: 403 }
const { data: allowed } = await admin.rpc("consume_ai_quota", { p_variant_id: data.id, p_limit: DAILY_LIMIT })
if (allowed !== true) return { error: "Das Tageslimit für diese Demo ist erreicht. Bitte morgen erneut testen.", status: 429 }
return { def, content }
}
export function aiErrorMessage(error: unknown) {
if (error instanceof Anthropic.AuthenticationError || error instanceof Anthropic.PermissionDeniedError) {
return "Die KI ist noch nicht eingerichtet – bitte den API-Schlüssel prüfen."
}
if (error instanceof Anthropic.RateLimitError) return "Gerade sind sehr viele Anfragen unterwegs. Bitte gleich noch einmal versuchen."
if (error instanceof Anthropic.APIError) return "Die KI ist gerade nicht erreichbar. Bitte später erneut versuchen."
if (error instanceof Anthropic.AnthropicError) return "Die KI ist noch nicht eingerichtet: ANTHROPIC_API_KEY in der .env eintragen."
return "Etwas ist schiefgelaufen. Bitte erneut versuchen."
}
export function ndjsonResponse(body: ReadableStream<Uint8Array>) {
return new Response(body, { headers: { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-store" } })
}