SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.1 KB · 41 lines typescript
Raw Blame History
1import { z } from 'zod';2import { NextResponse } from 'next/server';3import { researchTurn, listSessions } from '@/lib/ai/research';4import { clientIpHash, consumeQuota, currentUserId, getAnonId } from '@/lib/ai/request';56export const runtime = 'nodejs';7export const maxDuration = 300;89const Body = z.object({ sessionId: z.string().regex(/^rs_[a-z0-9]+$/).nullable().optional(), message: z.string().min(1).max(4000) });1011/** POST → Server-Sent Events stream of ResearchEvent JSON lines. */12export async function POST(req: Request) {13  const parsed = Body.safeParse(await req.json().catch(() => null));14  if (!parsed.success) return NextResponse.json({ error: 'Invalid request' }, { status: 400 });15  const [userId, ipHash, anonId] = await Promise.all([currentUserId(), clientIpHash(), getAnonId()]);16  const quota = await consumeQuota('research', { userId, ipHash });17  if (!quota.ok) return NextResponse.json({ error: `Daily research limit reached (${quota.limit}). Sign in for a higher limit.` }, { status: 429 });1819  const encoder = new TextEncoder();20  const stream = new ReadableStream<Uint8Array>({21    async start(controller) {22      const send = (e: unknown) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(e)}\n\n`));23      try {24        for await (const ev of researchTurn({ sessionId: parsed.data.sessionId ?? null, anonId, userId, message: parsed.data.message, signal: req.signal })) send(ev);25      } catch (err) {26        send({ type: 'error', message: err instanceof Error ? err.message : String(err) });27      } finally {28        controller.enqueue(encoder.encode('data: [DONE]\n\n'));29        controller.close();30      }31    },32  });33  return new Response(stream, { headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-accel-buffering': 'no' } });34}3536export async function GET() {37  const [userId, anonId] = await Promise.all([currentUserId(), getAnonId(false)]);38  const sessions = await listSessions({ anonId, userId });39  return NextResponse.json({ sessions: sessions.map((s) => ({ id: s.id, title: s.title, messageCount: s.messageCount, updatedAt: s.updatedAt })) });40}41