import { z } from 'zod' // ================================ // Enums // ================================ export const ProjectStatusEnum = z.enum(['ACTIVE', 'ARCHIVED']) export const LinkTypeEnum = z.enum(['WEBSITE', 'GITHUB', 'HUGGINGFACE', 'PAPER']) // ================================ // Base Schemas // ================================ export const ExternalLinkSchema = z.object({ type: LinkTypeEnum, url: z.string() .min(1, 'URL is required') .max(2000, 'URL is too long') .refine((url) => { try { const parsed = new URL(url) return ['http:', 'https:'].includes(parsed.protocol) } catch { return false } }, 'URL must use http or https protocol'), title: z.string().max(200).optional() }) export const TagSchema = z.object({ name: z.string().min(1).max(50), nameEn: z.string().max(50).optional() }) // ================================ // Project Schemas // ================================ export const ProjectBaseSchema = z.object({ name: z.string().min(1).max(200), nameEn: z.string().max(200).optional(), description: z.string().min(10).max(500), descriptionEn: z.string().max(500).optional(), content: z.string().max(10000).optional(), contentEn: z.string().max(10000).optional(), status: ProjectStatusEnum.default('ACTIVE'), source: z.string().max(100).optional() }) export const ProjectInputSchema = ProjectBaseSchema.extend({ tags: z.array(TagSchema).min(1, 'At least one tag is required').max(10), links: z.array(ExternalLinkSchema).min(1, 'At least one link is required').max(10) }) // ================================ // Webhook Schemas // ================================ export const WebhookAuthSchema = z.object({ apiKey: z.string().min(32, 'Invalid API key format') }) export const WebhookPayloadSchema = WebhookAuthSchema.extend({ projects: z.array(ProjectInputSchema).min(1).max(100) }) // ================================ // Discovery Task Schemas // ================================ export const TaskStatusEnum = z.enum(['PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED']) export const CreateDiscoveryTaskSchema = WebhookAuthSchema.extend({ tasks: z.array(z.object({ sourceUrl: z.string().url().max(2000), sourceType: z.string().max(50).default('manual'), })).min(1) }) export const UpdateDiscoveryTaskSchema = z.object({ apiKey: z.string().min(32), status: TaskStatusEnum, explorationData: z.record(z.unknown()).optional(), explorationSummary: z.string().max(1000).optional(), errorMessage: z.string().max(2000).optional(), }) export const GetDiscoveryTasksQuerySchema = z.object({ status: TaskStatusEnum.optional(), limit: z.coerce.number().int().positive().max(100).default(10), offset: z.coerce.number().int().nonnegative().default(0), }) /** * 批量重置任务 Schema * 支持两种模式:按 ID 列表重置 或 按状态筛选重置 */ export const BatchResetTasksSchema = WebhookAuthSchema.extend({ // 模式1: 指定任务 ID 列表 taskIds: z.array(z.string()).optional(), // 模式2: 按状态筛选(不传则默认重置 IN_PROGRESS 和 FAILED) statuses: z.array(TaskStatusEnum).optional(), }).refine( (data) => data.taskIds || data.statuses, { message: '必须提供 taskIds 或 statuses 之一' } ) /** * 检查任务去重 Schema * 用于在创建任务前检查 URL 是否已存在 */ export const CheckTaskDuplicatesSchema = WebhookAuthSchema.extend({ urls: z.array(z.string().url().max(2000)).min(1).max(100), sourceType: z.string().max(50).optional(), }) // ================================ // Query Schemas // ================================ export const ProjectQuerySchema = z.object({ search: z.string().min(2).max(100).optional(), tags: z.array(z.string()).optional(), status: ProjectStatusEnum.optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20) }) // ================================ // Types // ================================ export type ExternalLink = z.infer export type Tag = z.infer export type ProjectInput = z.infer export type WebhookPayload = z.infer export type ProjectQuery = z.infer export type TaskStatus = z.infer export type CreateDiscoveryTask = z.infer export type UpdateDiscoveryTask = z.infer export type GetDiscoveryTasksQuery = z.infer export type BatchResetTasks = z.infer // ================================ // Keyword Cloud Schemas // ================================ // 视觉配置 Schema const VisualConfigSchema = z.object({ color: z.enum(['primary', 'secondary', 'accent', 'gray']), size: z.enum(['text-5xl', 'text-4xl', 'text-3xl', 'text-2xl', 'text-xl', 'text-lg', 'text-base']), border: z.enum(['border-4', 'border-2']), rotation: z.string().regex(/^-?rotate-\d+$/).nullable().optional(), }) // 关键词输入 Schema export const KeywordInputSchema = z.object({ word: z.string().min(1).max(100), trendScore: z.number().int().min(0).max(100), description: z.string().min(10).max(500), descriptionEn: z.string().max(500).optional(), detailPoints: z.array(z.string().min(5).max(100)).min(1).max(5), detailPointsEn: z.array(z.string().max(100)).max(5).optional(), visualConfig: VisualConfigSchema, }) // 批量写入关键词请求 Schema export const BatchKeywordsRequestSchema = z.object({ quarter: z.string().regex(/^\d{4}-Q[1-4]$/, "格式应为 YYYY-QN"), keywords: z.array(KeywordInputSchema).min(1).max(50), }) // 季度 Schema export const QuarterSchema = z.object({ quarter: z.string().regex(/^\d{4}-Q[1-4]$/), title: z.string().min(1).max(200), titleEn: z.string().max(200).optional(), subtitle: z.string().max(500).optional(), subtitleEn: z.string().max(500).optional(), displayOrder: z.number().int().min(0).default(0), isActive: z.boolean().default(true), }) // 视觉规则 Schema export const VisualStyleRuleSchema = z.object({ name: z.string().min(1).max(100), minScore: z.number().int().min(0).max(100), maxScore: z.number().int().min(0).max(100), color: z.enum(['primary', 'secondary', 'accent', 'gray']), size: z.enum(['text-5xl', 'text-4xl', 'text-3xl', 'text-2xl', 'text-xl', 'text-lg', 'text-base']), border: z.enum(['border-4', 'border-2']), rotation: z.string().regex(/^-?rotate-\d+$/).nullable().optional(), priority: z.number().int().min(0).default(0), enabled: z.boolean().default(true), }).refine(data => data.minScore < data.maxScore, { message: "minScore 必须小于 maxScore", }) // API 响应 Schema export const KeywordCloudResponseSchema = z.object({ success: z.boolean(), data: z.any().optional(), error: z.string().optional(), }) // ================================ // AI Timeline Schemas // ================================ export const AIEventInputSchema = z.object({ title: z.string().min(1).max(200), titleEn: z.string().max(200).optional(), eventDate: z.string().datetime(), description: z.string().min(10).max(500), descriptionEn: z.string().max(500).optional(), imageUrl: z.string().url(), sourceUrl: z.string().url().optional(), }) export const AIEventQuerySchema = z.object({ year: z.string().regex(/^\d{4}$/).optional(), limit: z.string().regex(/^\d+$/).transform(Number).optional(), offset: z.string().regex(/^\d+$/).transform(Number).optional(), }) // ================================ // Types // ================================ export type KeywordInput = z.infer export type BatchKeywordsRequest = z.infer export type Quarter = z.infer export type VisualStyleRule = z.infer export type AIEventInput = z.infer export type AIEventQuery = z.infer export type KeywordCloudResponse = z.infer