mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
♻️ 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:
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user