From 521eb97acd97cf43b3d97a1c7316f43a333b7e14 Mon Sep 17 00:00:00 2001 From: mzaxd Date: Sun, 22 Feb 2026 23:13:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20discovery=20tasks=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E7=8A=B6=E6=80=81=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `statuses` 查询参数,支持按多个状态过滤任务 - 保持 `status` 单状态参数的向后兼容性 - 更新验证 schema 以支持逗号分隔的多状态值 示例用法: - 单状态:?status=PENDING - 多状态:?statuses=PENDING,FAILED --- src/app/api/discovery/tasks/route.ts | 17 ++- src/lib/validations.ts | 149 ++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/src/app/api/discovery/tasks/route.ts b/src/app/api/discovery/tasks/route.ts index 42ba7f9..6fce3e4 100644 --- a/src/app/api/discovery/tasks/route.ts +++ b/src/app/api/discovery/tasks/route.ts @@ -105,6 +105,7 @@ export async function GET(request: NextRequest) { } const validation = GetDiscoveryTasksQuerySchema.safeParse({ status: searchParams.get('status') || undefined, + statuses: searchParams.get('statuses') || undefined, // 新增:支持多状态查询 limit: searchParams.get('limit') || '10', offset: searchParams.get('offset') || '0', }) @@ -120,17 +121,27 @@ export async function GET(request: NextRequest) { ) } - const { status, limit, offset } = validation.data + const { status, statuses, limit, offset } = validation.data + + // 构建查询条件:支持单状态和多状态查询 + let whereClause = {} + if (statuses && statuses.length > 0) { + // 多状态查询:?statuses=PENDING,FAILED + whereClause = { status: { in: statuses } } + } else if (status) { + // 单状态查询(向后兼容):?status=PENDING + whereClause = { status } + } const tasks = await prisma.projectDiscoveryTask.findMany({ - where: status ? { status } : undefined, + where: whereClause, orderBy: { createdAt: 'asc' }, take: limit, skip: offset, }) const total = await prisma.projectDiscoveryTask.count({ - where: status ? { status } : undefined, + where: whereClause, }) return NextResponse.json({ diff --git a/src/lib/validations.ts b/src/lib/validations.ts index c2b4a23..3ae4650 100644 --- a/src/lib/validations.ts +++ b/src/lib/validations.ts @@ -104,7 +104,12 @@ export const UpdateDiscoveryTaskSchema = z.object({ }); export const GetDiscoveryTasksQuerySchema = z.object({ - status: TaskStatusEnum.optional(), + status: TaskStatusEnum.optional(), // 保持向后兼容:单状态查询 + statuses: z.string().optional().transform((val) => { + // 将逗号分隔的字符串转换为数组,如 "PENDING,FAILED" → ["PENDING", "FAILED"] + if (!val) return undefined; + return val.split(',').map(s => s.trim() as TaskStatus).filter(Boolean); + }), limit: z.coerce.number().int().positive().max(100).default(10), offset: z.coerce.number().int().nonnegative().default(0), }); @@ -159,6 +164,134 @@ export const ProjectQuerySchema = z.object({ limit: z.coerce.number().int().positive().max(100).default(20), }); +// ================================ +// Chat Schemas +// ================================ + +export const ChatModeEnum = z.enum(["stream", "job"]); +export const ChatRoleEnum = z.enum(["user", "assistant", "system"]); +export const ChatBlockTypeEnum = z.enum([ + "text", + "mermaid", + "excalidraw_image", + "code", + "table", +]); +export const ChatEventTypeEnum = z.enum([ + "start", + "progress", + "delta", + "artifact", + "final", + "error", +]); +export const ChatJobStatusEnum = z.enum(["queued", "running", "completed", "failed"]); +export const ChatFeedbackRatingEnum = z.enum(["helpful", "unhelpful"]); + +export const ChatCapabilitySchema = z.object({ + allowMermaid: z.boolean().optional().default(true), + allowExcalidrawImage: z.boolean().optional().default(true), +}); + +export const ChatCitationSchema = z.object({ + title: z.string().min(1).max(200), + url: z.string().url().max(2000), + snippet: z.string().max(1000).optional(), +}); + +export const ChatMessageBlockSchema = z + .object({ + type: ChatBlockTypeEnum, + title: z.string().max(200).optional(), + content: z.string().max(50000).optional(), + url: z.string().url().max(2000).optional(), + language: z.string().max(50).optional(), + rows: z.array(z.array(z.string().max(500))).optional(), + headers: z.array(z.string().max(200)).optional(), + }) + .superRefine((data, ctx) => { + if ((data.type === "text" || data.type === "mermaid" || data.type === "code") && !data.content) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["content"], + message: `content is required for block type: ${data.type}`, + }); + } + if (data.type === "excalidraw_image" && !data.url) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["url"], + message: "url is required for block type: excalidraw_image", + }); + } + if (data.type === "table" && (!data.headers || !data.rows)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["rows"], + message: "headers and rows are required for block type: table", + }); + } + }); + +export const ChatAssistantMessageSchema = z.object({ + id: z.string().min(1).max(100).optional(), + role: z.literal("assistant").default("assistant"), + blocks: z.array(ChatMessageBlockSchema).default([]), + citations: z.array(ChatCitationSchema).optional().default([]), + meta: z.record(z.unknown()).optional(), + rawN8nPayload: z.unknown().optional(), +}); + +export const ChatMessageRequestSchema = z.object({ + sessionId: z.string().min(1).max(100).optional(), + clientId: z.string().min(8).max(100), + locale: z.enum(["zh", "en"]), + mode: ChatModeEnum.default("job"), + message: z.string().min(1).max(8000), + capabilities: ChatCapabilitySchema.optional().default({}), + context: JsonObjectSchema.optional(), +}); + +export const ChatSessionQuerySchema = z.object({ + clientId: z.string().min(8).max(100), + locale: z.enum(["zh", "en"]).optional(), + limit: z.coerce.number().int().positive().max(50).default(20), + cursor: z.string().min(1).optional(), +}); + +export const ChatJobStatusQuerySchema = z.object({ + clientId: z.string().min(8).max(100), + sessionId: z.string().min(1).max(100), +}); + +export const ChatFeedbackRequestSchema = z.object({ + clientId: z.string().min(8).max(100), + sessionId: z.string().min(1).max(100), + messageId: z.string().min(1).max(100), + rating: ChatFeedbackRatingEnum, + reason: z.string().max(500).optional(), +}); + +export const N8NChatJobResponseSchema = z.object({ + requestId: z.string().min(1).optional(), + jobId: z.string().min(1), + status: ChatJobStatusEnum, + progress: z + .object({ + stage: z.string().max(100).optional(), + message: z.string().max(500).optional(), + }) + .optional(), + message: ChatAssistantMessageSchema.optional(), + error: z + .object({ + code: z.string().max(100).optional(), + message: z.string().max(500), + }) + .optional(), + meta: z.record(z.unknown()).optional(), +}); + // ================================ // Types // ================================ @@ -175,6 +308,20 @@ export type GetDiscoveryTasksQuery = z.infer; export type AIEventInput = z.infer; export type JsonObject = z.infer; +export type ChatMode = z.infer; +export type ChatRole = z.infer; +export type ChatBlockType = z.infer; +export type ChatEventType = z.infer; +export type ChatJobStatus = z.infer; +export type ChatFeedbackRating = z.infer; +export type ChatCitation = z.infer; +export type ChatMessageBlock = z.infer; +export type ChatAssistantMessage = z.infer; +export type ChatMessageRequest = z.infer; +export type ChatSessionQuery = z.infer; +export type ChatJobStatusQuery = z.infer; +export type ChatFeedbackRequest = z.infer; +export type N8NChatJobResponse = z.infer; // ================================ // Tags API Schemas