♻️ refactor(server): flip auth/v1/eval-complete (backend) routes to thin stubs (batch 7)

- api/auth [...all] / check-user / resolve-username delegate to
  api-runtime (CheckUser/ResolveUsername response types re-exported for
  the signin page)
- api/v1 catch-all delegates to api-runtime/openapi
- agent-eval-run on-thread/on-trajectory-complete flip to their already
  ported api-runtime handlers
This commit is contained in:
Innei
2026-06-10 17:17:39 +08:00
parent 67d76ed7a4
commit 3cdc3c34b9
6 changed files with 12 additions and 370 deletions
+4 -34
View File
@@ -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;
+2 -62
View File
@@ -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';
@@ -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';
@@ -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;
@@ -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';
@@ -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';