diff --git a/src/app/(backend)/api/auth/[...all]/route.ts b/src/app/(backend)/api/auth/[...all]/route.ts index 28642011ac..fd684b4b92 100644 --- a/src/app/(backend)/api/auth/[...all]/route.ts +++ b/src/app/(backend)/api/auth/[...all]/route.ts @@ -1,36 +1,6 @@ -import { toNextJsHandler } from 'better-auth/next-js'; -import type { NextRequest } from 'next/server'; +import { betterAuthAPIHandler } from '~server/api-runtime/betterAuth'; -import { auth } from '@/auth'; +const handler = (req: Request) => betterAuthAPIHandler(req); -const jsonContentTypeRegex = /^application\/(?:[a-z0-9.+-]*\+)?json/i; - -const handler = toNextJsHandler(auth); - -const malformedJsonResponse = () => - Response.json({ code: 'INVALID_JSON', message: 'Malformed JSON request body' }, { status: 400 }); - -/** - * better-call currently treats Request.json() SyntaxError as a server error. - * Validate JSON bodies at the route boundary so malformed client payloads stay 400s. - */ -const validateJsonBody = async (request: Request) => { - const contentType = request.headers.get('content-type') || ''; - if (!request.body || !jsonContentTypeRegex.test(contentType)) return; - - try { - await request.clone().json(); - } catch (error) { - if (error instanceof SyntaxError) return malformedJsonResponse(); - throw error; - } -}; - -export const GET = handler.GET; - -export const POST = async (request: NextRequest) => { - const invalidJsonResponse = await validateJsonBody(request); - if (invalidJsonResponse) return invalidJsonResponse; - - return handler.POST(request); -}; +export const GET = handler; +export const POST = handler; diff --git a/src/app/(backend)/api/auth/check-user/route.ts b/src/app/(backend)/api/auth/check-user/route.ts index 95f69bcd27..8a10899fe1 100644 --- a/src/app/(backend)/api/auth/check-user/route.ts +++ b/src/app/(backend)/api/auth/check-user/route.ts @@ -1,62 +1,2 @@ -import { and, eq } from 'drizzle-orm'; -import { type NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; - -import { account } from '@/database/schemas/betterAuth'; -import { users } from '@/database/schemas/user'; -import { serverDB } from '@/database/server'; - -export interface CheckUserResponseData { - exists: boolean; - hasPassword?: boolean; -} - -/** - * Check if a user exists by email - * @param req - POST request with { email: string } - * @returns { exists: boolean, emailVerified?: boolean } - */ -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - const { email } = body; - - if (!email || typeof email !== 'string') { - return NextResponse.json({ error: 'Email is required', exists: false }, { status: 400 }); - } - - // Query database for user with this email - const [user] = await serverDB - .select({ - emailVerified: users.emailVerified, - id: users.id, - }) - .from(users) - .where(eq(users.email, email.toLowerCase().trim())) - .limit(1); - - if (!user) { - return NextResponse.json({ exists: false }); - } - - const accounts = await serverDB - .select({ - password: account.password, - providerId: account.providerId, - }) - .from(account) - .where(and(eq(account.userId, user.id))); - const hasPassword = accounts.some( - (a) => - a.providerId === 'credential' && typeof a.password === 'string' && a.password.length > 0, - ); - - return NextResponse.json({ - exists: true, - hasPassword, - } satisfies CheckUserResponseData); - } catch (error) { - console.error('Error checking user existence:', error); - return NextResponse.json({ error: 'Internal server error', exists: false }, { status: 500 }); - } -} +export type { CheckUserResponseData } from '~server/api-runtime/auth'; +export { checkUserAPIHandler as POST } from '~server/api-runtime/auth'; diff --git a/src/app/(backend)/api/auth/resolve-username/route.ts b/src/app/(backend)/api/auth/resolve-username/route.ts index 6b42f59e3d..d26c3dff0a 100644 --- a/src/app/(backend)/api/auth/resolve-username/route.ts +++ b/src/app/(backend)/api/auth/resolve-username/route.ts @@ -1,51 +1,2 @@ -import { eq } from 'drizzle-orm'; -import { type NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; - -import { users } from '@/database/schemas/user'; -import { serverDB } from '@/database/server'; - -export interface ResolveUsernameResponseData { - email?: string | null; - exists: boolean; -} - -/** - * Resolve a username to the associated email address. - * @param req - POST request with { username: string } - * @returns { exists: boolean, email?: string | null } - */ -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - const { username } = body; - - if (!username || typeof username !== 'string') { - return NextResponse.json({ error: 'Username is required', exists: false }, { status: 400 }); - } - - const normalizedUsername = username.trim(); - - if (!normalizedUsername) { - return NextResponse.json({ error: 'Username is required', exists: false }, { status: 400 }); - } - - const [user] = await serverDB - .select({ email: users.email }) - .from(users) - .where(eq(users.username, normalizedUsername)) - .limit(1); - - if (!user || !user.email) { - return NextResponse.json({ exists: false } satisfies ResolveUsernameResponseData); - } - - return NextResponse.json({ - email: user.email, - exists: true, - } satisfies ResolveUsernameResponseData); - } catch (error) { - console.error('Error resolving username to email:', error); - return NextResponse.json({ error: 'Internal server error', exists: false }, { status: 500 }); - } -} +export type { ResolveUsernameResponseData } from '~server/api-runtime/auth'; +export { resolveUsernameAPIHandler as POST } from '~server/api-runtime/auth'; diff --git a/src/app/(backend)/api/v1/[[...route]]/route.ts b/src/app/(backend)/api/v1/[[...route]]/route.ts index 20694db214..b22f9d03ca 100644 --- a/src/app/(backend)/api/v1/[[...route]]/route.ts +++ b/src/app/(backend)/api/v1/[[...route]]/route.ts @@ -1,8 +1,7 @@ -import lobeOpenApi from '@lobechat/openapi'; +import { openAPIHandler } from '~server/api-runtime/openapi'; -const handler = (request: Request) => lobeOpenApi.fetch(request); +const handler = (req: Request) => openAPIHandler(req); -// Export all required HTTP method handlers export const GET = handler; export const POST = handler; export const PUT = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts index f62e7436a5..fda12f33a7 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts @@ -1,111 +1 @@ -import debug from 'debug'; -import { NextResponse } from 'next/server'; - -import { AgentEvalRunModel } from '@/database/models/agentEval'; -import { getServerDB } from '@/database/server'; -import { AgentEvalRunService } from '~server/services/agentEvalRun'; -import { AgentEvalRunWorkflow, type OnThreadCompletePayload } from '~server/workflows/agentEvalRun'; -import { resolveAgentEvalRunWorkspace } from '~server/workflows/agentEvalRun/utils'; - -const log = debug('lobe-server:workflows:on-thread-complete'); - -/** - * On-thread-complete webhook handler (for pass@k). - * - * Receives a POST from the AgentRuntimeService completion webhook after a - * thread-level agent operation finishes. Evaluates the thread independently, - * writes result to thread.metadata, then checks if all K threads for the - * topic are done. If so, aggregates into RunTopic and checks run completion. - * - * This is a plain Next.js route handler (NOT an Upstash workflow / serve()). - */ -export async function POST(req: Request) { - try { - const body = (await req.json()) as OnThreadCompletePayload; - const { - runId, - testCaseId, - threadId, - topicId, - userId, - operationId: _operationId, - reason, - status, - cost, - duration, - errorMessage, - llmCalls, - steps, - toolCalls, - totalTokens, - } = body; - - if (!runId || !testCaseId || !threadId || !topicId || !userId) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); - } - - log( - 'Received: runId=%s testCaseId=%s threadId=%s status=%s cost=%s duration=%s', - runId, - testCaseId, - threadId, - status, - cost, - duration, - ); - - const db = await getServerDB(); - const wsId = await resolveAgentEvalRunWorkspace(db, runId); - - // Check if run was aborted — skip processing to avoid overwriting abort state - const runModel = new AgentEvalRunModel(db, userId, wsId); - const run = await runModel.findById(runId); - if (run?.status === 'aborted') { - log('Run aborted, skipping: runId=%s testCaseId=%s threadId=%s', runId, testCaseId, threadId); - return NextResponse.json({ cancelled: true }); - } - - const service = new AgentEvalRunService(db, userId, wsId); - - const { allThreadsDone, allRunDone } = await service.recordThreadCompletion({ - runId, - status, - telemetry: { - completionReason: reason, - cost, - duration, - errorMessage, - llmCalls, - steps, - toolCalls, - totalTokens, - }, - testCaseId, - threadId, - topicId, - }); - - log( - 'Thread completion: threadId=%s allThreadsDone=%s allRunDone=%s', - threadId, - allThreadsDone, - allRunDone, - ); - - if (allRunDone) { - console.info( - '[on-thread-complete] All test cases done for run %s, triggering finalize', - runId, - ); - await AgentEvalRunWorkflow.triggerFinalizeRun({ runId, userId }); - } - - return NextResponse.json({ allRunDone, allThreadsDone, success: true }); - } catch (error) { - console.error('[on-thread-complete] Error:', error); - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Internal error' }, - { status: 500 }, - ); - } -} +export { agentEvalRunOnThreadCompleteAPIHandler as POST } from '~server/api-runtime/agentEvalRunWorkflow'; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts index 8f2f4f2614..8d86fd303f 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts @@ -1,109 +1 @@ -import debug from 'debug'; -import { NextResponse } from 'next/server'; - -import { AgentEvalRunModel } from '@/database/models/agentEval'; -import { getServerDB } from '@/database/server'; -import { AgentEvalRunService } from '~server/services/agentEvalRun'; -import { - AgentEvalRunWorkflow, - type OnTrajectoryCompletePayload, -} from '~server/workflows/agentEvalRun'; -import { resolveAgentEvalRunWorkspace } from '~server/workflows/agentEvalRun/utils'; - -const log = debug('lobe-server:workflows:on-trajectory-complete'); - -/** - * On-trajectory-complete webhook handler - * - * Receives a POST from the AgentRuntimeService completion webhook after an - * agent operation finishes (success or error). Checks whether all test cases - * for the run are done and, if so, triggers the finalize-run workflow. - * - * This is a plain Next.js route handler (NOT an Upstash workflow / serve()). - */ -export async function POST(req: Request) { - try { - const body = (await req.json()) as OnTrajectoryCompletePayload; - const { - runId, - testCaseId, - userId, - operationId, - reason, - status, - cost, - duration, - errorDetail, - errorMessage, - llmCalls, - steps, - toolCalls, - totalTokens, - } = body; - - if (!runId || !testCaseId || !userId) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); - } - - log( - 'Received: runId=%s testCaseId=%s operationId=%s reason=%s status=%s cost=%s duration=%s steps=%s totalTokens=%s', - runId, - testCaseId, - operationId, - reason, - status, - cost, - duration, - steps, - totalTokens, - ); - - const db = await getServerDB(); - const wsId = await resolveAgentEvalRunWorkspace(db, runId); - - // Check if run was aborted — skip processing to avoid overwriting abort state - const runModel = new AgentEvalRunModel(db, userId, wsId); - const run = await runModel.findById(runId); - if (run?.status === 'aborted') { - log('Run aborted, skipping: runId=%s testCaseId=%s', runId, testCaseId); - return NextResponse.json({ cancelled: true }); - } - - const service = new AgentEvalRunService(db, userId, wsId); - - const { allDone, completedCount } = await service.recordTrajectoryCompletion({ - runId, - status, - telemetry: { - completionReason: reason, - cost, - duration, - errorDetail, - errorMessage, - llmCalls, - steps, - toolCalls, - totalTokens, - }, - testCaseId, - }); - - log('Completion check: %d completed, allDone=%s', completedCount, allDone); - - if (allDone) { - console.info( - '[on-trajectory-complete] All test cases done for run %s, triggering finalize', - runId, - ); - await AgentEvalRunWorkflow.triggerFinalizeRun({ runId, userId }); - } - - return NextResponse.json({ success: true }); - } catch (error) { - console.error('[on-trajectory-complete] Error:', error); - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Internal error' }, - { status: 500 }, - ); - } -} +export { agentEvalRunOnTrajectoryCompleteAPIHandler as POST } from '~server/api-runtime/agentEvalRunWorkflow';