Files
agent-park/src/lib/validations.ts
T
mzaxd fc46804405 feat: 新增 Tag Janitor API 用于 n8n 自动化标签合并
- 新增 GET /api/tags 接口返回标签列表及项目计数
- 新增 POST /api/tags/maintenance 接口支持批量 nameEn 补全和标签合并
- 合并逻辑包含 ProjectTag 去重处理(避免复合主键冲突)
- 成功后触发 ISR revalidatePath 刷新项目列表页
- 新增 Zod 校验 schemas(TagMaintenanceRequestSchema 等)
- 包含 n8n workflow JSON 模板及文档
2026-02-01 22:51:45 +08:00

299 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>;
// ================================
// 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<typeof TagUpdateSchema>;
export type MergeTarget = z.infer<typeof MergeTargetSchema>;
export type TagMerge = z.infer<typeof TagMergeSchema>;
export type TagMaintenanceRequest = z.infer<typeof TagMaintenanceRequestSchema>;