237 lines
8.0 KiB
TypeScript
237 lines
8.0 KiB
TypeScript
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<typeof ExternalLinkSchema>
|
||
export type Tag = z.infer<typeof TagSchema>
|
||
export type ProjectInput = z.infer<typeof ProjectInputSchema>
|
||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>
|
||
export type ProjectQuery = z.infer<typeof ProjectQuerySchema>
|
||
export type TaskStatus = z.infer<typeof TaskStatusEnum>
|
||
export type CreateDiscoveryTask = z.infer<typeof CreateDiscoveryTaskSchema>
|
||
export type UpdateDiscoveryTask = z.infer<typeof UpdateDiscoveryTaskSchema>
|
||
export type GetDiscoveryTasksQuery = z.infer<typeof GetDiscoveryTasksQuerySchema>
|
||
export type BatchResetTasks = z.infer<typeof BatchResetTasksSchema>
|
||
|
||
// ================================
|
||
// 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<typeof KeywordInputSchema>
|
||
export type BatchKeywordsRequest = z.infer<typeof BatchKeywordsRequestSchema>
|
||
export type Quarter = z.infer<typeof QuarterSchema>
|
||
export type VisualStyleRule = z.infer<typeof VisualStyleRuleSchema>
|
||
export type AIEventInput = z.infer<typeof AIEventInputSchema>
|
||
export type AIEventQuery = z.infer<typeof AIEventQuerySchema>
|
||
export type KeywordCloudResponse = z.infer<typeof KeywordCloudResponseSchema>
|