120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable";
|
|
import { runCreditRpc } from "@/lib/consultation-billing";
|
|
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
|
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
const idSchema = z.string().uuid();
|
|
const reservedLeaseMs = 15 * 60 * 1000;
|
|
// ponytail: Staging MVP ceiling—without a queue/worker, status lazily refunds reservations after this lease.
|
|
function reservationLeaseExpired(updatedAt: unknown, now = Date.now()) {
|
|
if (typeof updatedAt !== "string") return false;
|
|
const timestamp = Date.parse(updatedAt);
|
|
return Number.isFinite(timestamp) && now - timestamp >= reservedLeaseMs;
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
let supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>;
|
|
try {
|
|
supabase = await createServerSupabaseClient();
|
|
} catch (error) {
|
|
return jsonForSupabaseSetupFailure(error, "GET /api/consult/status");
|
|
}
|
|
|
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
if (authError || !user) {
|
|
return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
|
}
|
|
|
|
const params = new URL(request.url).searchParams;
|
|
const sessionIdValue = params.get("sessionId");
|
|
const requestIdValue = params.get("requestId");
|
|
const activeLookup = sessionIdValue === null && requestIdValue === null;
|
|
if (!activeLookup && (sessionIdValue === null || requestIdValue === null)) {
|
|
return NextResponse.json({ error: "咨询请求格式不正确" }, { status: 400 });
|
|
}
|
|
|
|
const sessionId = sessionIdValue === null ? null : idSchema.safeParse(sessionIdValue);
|
|
const requestId = requestIdValue === null ? null : idSchema.safeParse(requestIdValue);
|
|
if ((sessionId && !sessionId.success) || (requestId && !requestId.success)) {
|
|
return NextResponse.json({ error: "咨询请求格式不正确" }, { status: 400 });
|
|
}
|
|
|
|
let accounting: ReturnType<typeof createAdminSupabaseClient>;
|
|
try {
|
|
accounting = createAdminSupabaseClient();
|
|
} catch (error) {
|
|
return jsonForSupabaseSetupFailure(error, "GET /api/consult/status");
|
|
}
|
|
|
|
let query = accounting
|
|
.from("consultation_requests")
|
|
.select("request_id,session_id,status,response_message,updated_at")
|
|
.eq("user_id", user.id);
|
|
|
|
if (activeLookup) {
|
|
query = query.eq("status", "reserved").order("created_at", { ascending: false }).limit(1);
|
|
} else {
|
|
query = query
|
|
.eq("session_id", sessionId!.data)
|
|
.eq("request_id", requestId!.data);
|
|
}
|
|
|
|
const { data, error } = await query.maybeSingle();
|
|
if (error) {
|
|
return NextResponse.json({ error: "暂时无法读取咨询状态" }, { status: 503 });
|
|
}
|
|
if (!data || typeof data.session_id !== "string") {
|
|
return NextResponse.json({ error: "咨询请求不存在" }, { status: 404 });
|
|
}
|
|
|
|
let statusData = data;
|
|
if (statusData.status === "reserved" && reservationLeaseExpired(statusData.updated_at)) {
|
|
let cancellation;
|
|
try {
|
|
cancellation = await runCreditRpc(
|
|
accounting,
|
|
"cancel_consultation_credit",
|
|
user.id,
|
|
statusData.request_id,
|
|
);
|
|
} catch {
|
|
return NextResponse.json({ error: "暂时无法回收超时咨询" }, { status: 503 });
|
|
}
|
|
if (!cancellation.success && cancellation.error_code !== "request_completed") {
|
|
return NextResponse.json({ error: "暂时无法回收超时咨询" }, { status: 503 });
|
|
}
|
|
|
|
const { data: settledData, error: settledError } = await accounting
|
|
.from("consultation_requests")
|
|
.select("request_id,session_id,status,response_message,updated_at")
|
|
.eq("user_id", user.id)
|
|
.eq("session_id", statusData.session_id)
|
|
.eq("request_id", statusData.request_id)
|
|
.maybeSingle();
|
|
if (settledError || !settledData || typeof settledData.session_id !== "string") {
|
|
return NextResponse.json({ error: "暂时无法读取咨询状态" }, { status: 503 });
|
|
}
|
|
statusData = settledData;
|
|
}
|
|
|
|
const { data: sessionRow } = await supabase
|
|
.from("chat_sessions")
|
|
.select("title")
|
|
.eq("id", statusData.session_id)
|
|
.eq("user_id", user.id)
|
|
.maybeSingle();
|
|
|
|
return NextResponse.json({
|
|
requestId: statusData.request_id,
|
|
sessionId: statusData.session_id,
|
|
status: statusData.status,
|
|
responseMessage: statusData.response_message,
|
|
updatedAt: statusData.updated_at,
|
|
...(typeof sessionRow?.title === "string" ? { title: sessionRow.title } : {}),
|
|
});
|
|
}
|