Files
agent-park/src/lib/validations.ts
T

242 lines
7.7 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(),
domains: z.array(z.string()).optional(),
productForms: 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>;
// ================================
// 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),
});
const ExistingMergeTargetSchema = z.object({
id: z.string().min(1, "Target tag ID is required"),
name: z.string().min(1).max(100).optional(),
nameEn: z.string().min(1).max(100).optional(),
});
const NewMergeTargetSchema = z.object({
name: z.string().min(1, "Target name is required").max(100),
nameEn: z.string().min(1, "Target English name is required").max(100),
});
export const MergeTargetSchema = z.union([ExistingMergeTargetSchema, NewMergeTargetSchema]);
export const TagMergeSchema = z.object({
target: MergeTargetSchema,
sourceTagIds: z.array(z.string().min(1)).min(1, "At least one source tag required"),
}).superRefine((data, ctx) => {
const seenSourceIds = new Set<string>();
data.sourceTagIds.forEach((sourceTagId, index) => {
if (seenSourceIds.has(sourceTagId)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["sourceTagIds", index],
message: `Duplicate source tag ID: ${sourceTagId}`,
});
return;
}
seenSourceIds.add(sourceTagId);
});
if ("id" in data.target && seenSourceIds.has(data.target.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["sourceTagIds"],
message: "Self merge is not allowed: target tag cannot be in sourceTagIds",
});
}
});
export const TagMaintenanceRequestSchema = z.object({
apiKey: z.string().min(32, "Invalid API key format"),
updates: z.array(TagUpdateSchema).default([]),
merges: z.array(TagMergeSchema).default([]),
}).superRefine((data, ctx) => {
const seenUpdateTagIds = new Set<string>();
const seenMergeSourceTagIds = new Set<string>();
data.updates.forEach((update, index) => {
if (seenUpdateTagIds.has(update.tagId)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["updates", index, "tagId"],
message: `Duplicate update tag ID: ${update.tagId}`,
});
return;
}
seenUpdateTagIds.add(update.tagId);
});
data.merges.forEach((merge, mergeIndex) => {
merge.sourceTagIds.forEach((sourceTagId, sourceIndex) => {
if (seenMergeSourceTagIds.has(sourceTagId)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["merges", mergeIndex, "sourceTagIds", sourceIndex],
message: `Source tag ID appears in multiple merges: ${sourceTagId}`,
});
return;
}
seenMergeSourceTagIds.add(sourceTagId);
});
});
});
// ================================
// 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>;