mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
76 lines
3.8 KiB
TypeScript
76 lines
3.8 KiB
TypeScript
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" } })
|
||
}
|