生成记录改为数据库分页,每页 40 条,默认最近 7 天;可见预览限制为 4 路并发并使用 192px 缩略图,翻页取消旧请求,视频在详情中按需播放。保留精选、分类和原图操作,新增画布任务失败记录,统一时间显示。 模型管理拆分模型、上游和运行健康视图,提供搜索、筛选、分页和完整故障详情;列表使用摘要,编辑按需获取完整配置。后台按当前设置页加载,模型 GET 不再自动修改健康状态,98K 同步先显示变更预览。 验证:600 项现有辅助测试和 3 项新增回归测试通过,TypeScript 和本次 API/组件 lint 通过;浏览器验证请求峰值 4、翻页、搜索、失败重试、详情、模型筛选与 390px 窄屏。只读生产查询:当天页约 29ms,历史视频页 122ms,第 7800 页 739ms。两个并发索引已在生产建成,SQL 随代码保留。 已知检查状态:全仓 lint 存量 1004 项诊断使 check:quality 停在 lint 阶段;日志、品牌、CI、环境、凭据、元数据、测试与制品检查单独通过。
74 lines
3.3 KiB
TypeScript
74 lines
3.3 KiB
TypeScript
import { z } from "zod";
|
|
import { requireAdmin } from "@/lib/middleware/admin";
|
|
import { ChatSDKError } from "@/lib/errors";
|
|
import { db } from "@/lib/db";
|
|
import { aiModelHealthEvent } from "@/lib/db/schema";
|
|
import { and, desc, eq, sql } from "drizzle-orm";
|
|
import {
|
|
restoreAiModelHealth,
|
|
} from "@/lib/ai/runtime/model-health";
|
|
|
|
const restoreSchema = z.object({
|
|
modelId: z.string().uuid(),
|
|
});
|
|
|
|
export async function GET(request: Request) {
|
|
try {
|
|
await requireAdmin();
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const modelId = searchParams.get("modelId");
|
|
const providerId = searchParams.get("providerId");
|
|
const rawLimit = Number(searchParams.get("limit") || 20);
|
|
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(200, Math.floor(rawLimit))) : 20;
|
|
const rawPage = Number(searchParams.get("page") || 1);
|
|
const page = Number.isFinite(rawPage) ? Math.max(1, Math.floor(rawPage)) : 1;
|
|
const filters = [];
|
|
if (modelId) filters.push(eq(aiModelHealthEvent.modelId, modelId));
|
|
if (providerId) filters.push(eq(aiModelHealthEvent.providerId, providerId));
|
|
if (searchParams.get("eventType")) filters.push(eq(aiModelHealthEvent.eventType, searchParams.get("eventType")!));
|
|
const start = searchParams.get("startDate");
|
|
const end = searchParams.get("endDate");
|
|
if (start && /^\d{4}-\d{2}-\d{2}$/.test(start)) filters.push(sql`${aiModelHealthEvent.createdAt}>=${start}::date`);
|
|
if (end && /^\d{4}-\d{2}-\d{2}$/.test(end)) filters.push(sql`${aiModelHealthEvent.createdAt}<${end}::date+interval '1 day'`);
|
|
const events = await db.select({
|
|
id: aiModelHealthEvent.id, modelId: aiModelHealthEvent.modelId, modelDisplayName: aiModelHealthEvent.modelDisplayName,
|
|
modelRuntimeId: aiModelHealthEvent.modelRuntimeId, providerName: aiModelHealthEvent.providerName,
|
|
providerId: aiModelHealthEvent.providerId, eventType: aiModelHealthEvent.eventType,
|
|
errorMessage: aiModelHealthEvent.errorMessage, elapsedMs: aiModelHealthEvent.elapsedMs,
|
|
consecutiveFailures: aiModelHealthEvent.consecutiveFailures, autoDisabled: aiModelHealthEvent.autoDisabled,
|
|
counted: aiModelHealthEvent.counted, createdAt: aiModelHealthEvent.createdAt,
|
|
}).from(aiModelHealthEvent).where(and(...filters)).orderBy(desc(aiModelHealthEvent.createdAt), desc(aiModelHealthEvent.id))
|
|
.limit(limit + 1).offset((page - 1) * limit);
|
|
return Response.json({ events: events.slice(0, limit), hasMore: events.length > limit, page });
|
|
} catch (error) {
|
|
if (error instanceof ChatSDKError) {
|
|
return error.toResponse();
|
|
}
|
|
return new ChatSDKError("offline:admin").toResponse();
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
await requireAdmin();
|
|
|
|
const body = await request.json();
|
|
const data = restoreSchema.parse(body);
|
|
const models = await restoreAiModelHealth(data.modelId);
|
|
if (models.length === 0) {
|
|
return new ChatSDKError("bad_request:api", "Model not found").toResponse();
|
|
}
|
|
|
|
return Response.json({ model: models[0] });
|
|
} catch (error) {
|
|
if (error instanceof ChatSDKError) {
|
|
return error.toResponse();
|
|
}
|
|
if (error instanceof z.ZodError) {
|
|
return new ChatSDKError("bad_request:api", error.message).toResponse();
|
|
}
|
|
return new ChatSDKError("offline:admin").toResponse();
|
|
}
|
|
}
|