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; // ================================ // Tags API Schemas // ================================ export const TagUpdateSchema = z.object({ tagId: z.string().min(1, "Tag ID is required"), nameEn: z.string().min(1, "English name is required").max(100), }); export const MergeTargetSchema = z.union([ z.object({ id: z.string().min(1) }), z.object({ name: z.string().min(1).max(100), nameEn: z.string().min(1).max(100), }), ]); export const TagMergeSchema = z.object({ target: MergeTargetSchema, sourceTagIds: z.array(z.string().min(1)).min(1, "At least one source tag required"), }); export const TagMaintenanceRequestSchema = z.object({ apiKey: z.string().min(32, "Invalid API key format"), updates: z.array(TagUpdateSchema).default([]), merges: z.array(TagMergeSchema).default([]), }); // ================================ // Types // ================================ export type TagUpdate = z.infer; export type MergeTarget = z.infer; export type TagMerge = z.infer; export type TagMaintenanceRequest = z.infer;