mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 23:12:38 +08:00
refactor(validators): migrate validation logic to Zod for agents and sessions
This commit is contained in:
parent
77df6fd58e
commit
5048d6987d
@ -1,24 +1,12 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { validationResult } from 'express-validator'
|
||||
|
||||
import { agentService } from '../../../../services/agents'
|
||||
import { loggerService } from '../../../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMiddleware')
|
||||
|
||||
// Error handler for validation
|
||||
export const handleValidationErrors = (req: Request, res: Response, next: any): void => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
details: errors.array()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// Since Zod validators handle their own errors, this is now a pass-through
|
||||
export const handleValidationErrors = (_req: Request, _res: Response, next: any): void => {
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +1,15 @@
|
||||
import { body, param } from 'express-validator'
|
||||
import { AgentIdParamSchema, CreateAgentRequestSchema, UpdateAgentRequestSchema } from '@types'
|
||||
|
||||
export const validateAgent = [
|
||||
body('name').notEmpty().withMessage('Name is required'),
|
||||
body('model').notEmpty().withMessage('Model is required'),
|
||||
body('description').optional().isString(),
|
||||
body('avatar').optional().isString(),
|
||||
body('instructions').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateAgentUpdate = [
|
||||
body('name').optional().notEmpty().withMessage('Name cannot be empty'),
|
||||
body('model').optional().notEmpty().withMessage('Model cannot be empty'),
|
||||
body('description').optional().isString(),
|
||||
body('avatar').optional().isString(),
|
||||
body('instructions').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
export const validateAgent = createZodValidator({
|
||||
body: CreateAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')]
|
||||
export const validateAgentUpdate = createZodValidator({
|
||||
body: UpdateAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentId = createZodValidator({
|
||||
params: AgentIdParamSchema
|
||||
})
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { query } from 'express-validator'
|
||||
import { PaginationQuerySchema } from '@types'
|
||||
|
||||
export const validatePagination = [
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'),
|
||||
query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative'),
|
||||
query('status')
|
||||
.optional()
|
||||
.isIn(['idle', 'running', 'completed', 'failed', 'stopped'])
|
||||
.withMessage('Invalid status filter')
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validatePagination = createZodValidator({
|
||||
query: PaginationQuerySchema
|
||||
})
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { body } from 'express-validator'
|
||||
import { CreateSessionMessageRequestSchema } from '@types'
|
||||
|
||||
export const validateSessionMessage = [
|
||||
body('content').notEmpty().isString().withMessage('Content must be a valid string')
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSessionMessage = createZodValidator({
|
||||
body: CreateSessionMessageRequestSchema
|
||||
})
|
||||
|
||||
@ -1,47 +1,15 @@
|
||||
import { body, param } from 'express-validator'
|
||||
import { CreateSessionRequestSchema, SessionIdParamSchema, UpdateSessionRequestSchema } from '@types'
|
||||
|
||||
export const validateSession = [
|
||||
body('name').optional().isString(),
|
||||
body('sub_agent_ids').optional().isArray(),
|
||||
body('user_goal').optional().isString(),
|
||||
body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']),
|
||||
body('external_session_id').optional().isString(),
|
||||
body('model').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSessionUpdate = [
|
||||
body('name').optional().isString(),
|
||||
body('main_agent_id').optional().notEmpty().withMessage('Main agent ID cannot be empty'),
|
||||
body('sub_agent_ids').optional().isArray(),
|
||||
body('user_goal').optional().isString(),
|
||||
body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']),
|
||||
body('external_session_id').optional().isString(),
|
||||
body('model').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
export const validateSession = createZodValidator({
|
||||
body: CreateSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateStatusUpdate = [
|
||||
body('status')
|
||||
.notEmpty()
|
||||
.isIn(['idle', 'running', 'completed', 'failed', 'stopped'])
|
||||
.withMessage('Valid status is required')
|
||||
]
|
||||
export const validateSessionUpdate = createZodValidator({
|
||||
body: UpdateSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionId = [param('sessionId').notEmpty().withMessage('Session ID is required')]
|
||||
export const validateSessionId = createZodValidator({
|
||||
params: SessionIdParamSchema
|
||||
})
|
||||
|
||||
68
src/main/apiServer/routes/agents/validators/zodValidator.ts
Normal file
68
src/main/apiServer/routes/agents/validators/zodValidator.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { NextFunction,Request, Response } from 'express'
|
||||
import { ZodError, ZodType } from 'zod'
|
||||
|
||||
export interface ValidationRequest extends Request {
|
||||
validatedBody?: any
|
||||
validatedParams?: any
|
||||
validatedQuery?: any
|
||||
}
|
||||
|
||||
export interface ZodValidationConfig {
|
||||
body?: ZodType
|
||||
params?: ZodType
|
||||
query?: ZodType
|
||||
}
|
||||
|
||||
export const createZodValidator = (config: ZodValidationConfig) => {
|
||||
return (req: ValidationRequest, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
if (config.body && req.body) {
|
||||
req.validatedBody = config.body.parse(req.body)
|
||||
}
|
||||
|
||||
if (config.params && req.params) {
|
||||
req.validatedParams = config.params.parse(req.params)
|
||||
}
|
||||
|
||||
if (config.query && req.query) {
|
||||
req.validatedQuery = config.query.parse(req.query)
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const validationErrors = error.issues.map((err) => ({
|
||||
type: 'field',
|
||||
value: err.input,
|
||||
msg: err.message,
|
||||
path: err.path.map(p => String(p)).join('.'),
|
||||
location: getLocationFromPath(err.path, config)
|
||||
}))
|
||||
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
details: validationErrors
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Internal validation error',
|
||||
type: 'internal_error',
|
||||
code: 'validation_processing_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocationFromPath(path: (string | number | symbol)[], config: ZodValidationConfig): string {
|
||||
if (config.body && path.length > 0) return 'body'
|
||||
if (config.params && path.length > 0) return 'params'
|
||||
if (config.query && path.length > 0) return 'query'
|
||||
return 'unknown'
|
||||
}
|
||||
@ -5,11 +5,21 @@
|
||||
import { ModelMessage, modelMessageSchema, TextStreamPart, UIMessageChunk } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
// ------------------ Core enums and helper types ------------------
|
||||
export const PermissionModeSchema = z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan'])
|
||||
export type PermissionMode = z.infer<typeof PermissionModeSchema>
|
||||
|
||||
export type SessionMessageRole = ModelMessage['role']
|
||||
|
||||
const sessionMessageRoles = ['assistant', 'user', 'system', 'tool'] as const satisfies readonly [
|
||||
SessionMessageRole,
|
||||
...SessionMessageRole[]
|
||||
]
|
||||
|
||||
export const SessionMessageRoleSchema = z.enum(sessionMessageRoles)
|
||||
|
||||
export type SessionMessageType = TextStreamPart<Record<string, any>>['type']
|
||||
|
||||
export const AgentTypeSchema = z.enum(['claude-code'])
|
||||
export type AgentType = z.infer<typeof AgentTypeSchema>
|
||||
|
||||
@ -17,8 +27,7 @@ export const isAgentType = (type: unknown): type is AgentType => {
|
||||
return AgentTypeSchema.safeParse(type).success
|
||||
}
|
||||
|
||||
export type SessionMessageType = TextStreamPart<Record<string, any>>['type']
|
||||
|
||||
// ------------------ Tool metadata ------------------
|
||||
export const ToolSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
@ -28,6 +37,7 @@ export const ToolSchema = z.object({
|
||||
|
||||
export type Tool = z.infer<typeof ToolSchema>
|
||||
|
||||
// ------------------ Agent configuration & base schema ------------------
|
||||
export const AgentConfigurationSchema = z
|
||||
.object({
|
||||
permission_mode: PermissionModeSchema.default('default'), // Permission mode, default to 'default'
|
||||
@ -62,6 +72,8 @@ export const AgentBaseSchema = z.object({
|
||||
|
||||
export type AgentBase = z.infer<typeof AgentBaseSchema>
|
||||
|
||||
// ------------------ Persistence entities ------------------
|
||||
|
||||
// Agent entity representing an autonomous agent configuration
|
||||
export const AgentEntitySchema = AgentBaseSchema.extend({
|
||||
id: z.string(),
|
||||
@ -95,7 +107,7 @@ export const AgentSessionMessageEntitySchema = z.object({
|
||||
id: z.number(), // Auto-increment primary key
|
||||
session_id: z.string(), // Reference to session
|
||||
// manual defined. may not synced with ai sdk definition
|
||||
role: z.enum(['assistant', 'user', 'system', 'tool']), // 'assistant' | 'user' | 'system' | 'tool'
|
||||
role: SessionMessageRoleSchema, // Enforce roles supported by modelMessageSchema
|
||||
content: modelMessageSchema,
|
||||
agent_session_id: z.string(), // agent session id, use to resume agent session
|
||||
metadata: z.record(z.string(), z.any()).optional(), // Additional metadata (optional)
|
||||
@ -105,6 +117,8 @@ export const AgentSessionMessageEntitySchema = z.object({
|
||||
|
||||
export type AgentSessionMessageEntity = z.infer<typeof AgentSessionMessageEntitySchema>
|
||||
|
||||
// ------------------ Session message payload ------------------
|
||||
|
||||
// Structured content for session messages that preserves both AI SDK and raw data
|
||||
export interface SessionMessageContent {
|
||||
chunk: UIMessageChunk[] // UI-friendly AI SDK chunks for rendering
|
||||
@ -119,10 +133,11 @@ export interface SessionMessageContent {
|
||||
// - mcps: Optional array of MCP (Model Control Protocol) tool IDs
|
||||
// - allowed_tools: Optional array of permitted tool IDs
|
||||
// - configuration: Optional agent settings (temperature, top_p, etc.)
|
||||
export interface BaseAgentForm {
|
||||
// ------------------ Form models ------------------
|
||||
export type BaseAgentForm = {
|
||||
id?: string
|
||||
type: AgentType
|
||||
// These fileds should be editable by user.
|
||||
// These fields should be editable by user.
|
||||
name: string
|
||||
description?: string
|
||||
instructions?: string
|
||||
@ -130,30 +145,22 @@ export interface BaseAgentForm {
|
||||
accessible_paths: string[]
|
||||
}
|
||||
|
||||
export interface AddAgentForm extends BaseAgentForm {
|
||||
id?: never
|
||||
}
|
||||
export type AddAgentForm = Omit<BaseAgentForm, 'id'> & { id?: never }
|
||||
|
||||
export interface UpdateAgentForm extends Partial<BaseAgentForm> {
|
||||
export type UpdateAgentForm = Partial<Omit<BaseAgentForm, 'type'>> & {
|
||||
id: string
|
||||
type?: never
|
||||
}
|
||||
|
||||
export type AgentForm = AddAgentForm | UpdateAgentForm
|
||||
|
||||
export interface BaseSessionForm extends AgentBase {}
|
||||
export type BaseSessionForm = AgentBase
|
||||
|
||||
export interface CreateSessionForm extends BaseSessionForm {
|
||||
id?: never
|
||||
}
|
||||
export type CreateSessionForm = BaseSessionForm & { id?: never }
|
||||
|
||||
export interface UpdateSessionForm extends Partial<BaseSessionForm> {
|
||||
id: string
|
||||
}
|
||||
export type UpdateSessionForm = Partial<BaseSessionForm> & { id: string }
|
||||
|
||||
// ------------------------
|
||||
// API Data Transfer Object
|
||||
// ------------------------
|
||||
// ------------------ API data transfer objects ------------------
|
||||
export interface CreateAgentRequest extends AgentBase {
|
||||
type: AgentType
|
||||
}
|
||||
@ -220,3 +227,45 @@ export const AgentServerErrorSchema = z.object({
|
||||
})
|
||||
|
||||
export type AgentServerError = z.infer<typeof AgentServerErrorSchema>
|
||||
|
||||
// ------------------ API validation schemas ------------------
|
||||
|
||||
// Parameter validation schemas
|
||||
export const AgentIdParamSchema = z.object({
|
||||
agentId: z.string().min(1, 'Agent ID is required')
|
||||
})
|
||||
|
||||
export const SessionIdParamSchema = z.object({
|
||||
sessionId: z.string().min(1, 'Session ID is required')
|
||||
})
|
||||
|
||||
// Query validation schemas
|
||||
export const PaginationQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
||||
offset: z.coerce.number().int().min(0).optional().default(0),
|
||||
status: z.enum(['idle', 'running', 'completed', 'failed', 'stopped']).optional()
|
||||
})
|
||||
|
||||
// Request body validation schemas derived from shared bases
|
||||
const agentCreatableSchema = AgentBaseSchema.extend({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
model: z.string().min(1, 'Model is required')
|
||||
})
|
||||
|
||||
export const CreateAgentRequestSchema = agentCreatableSchema.extend({
|
||||
type: AgentTypeSchema
|
||||
})
|
||||
|
||||
export const UpdateAgentRequestSchema = AgentBaseSchema.partial()
|
||||
|
||||
const sessionCreatableSchema = AgentBaseSchema.extend({
|
||||
model: z.string().min(1, 'Model is required')
|
||||
})
|
||||
|
||||
export const CreateSessionRequestSchema = sessionCreatableSchema
|
||||
|
||||
export const UpdateSessionRequestSchema = sessionCreatableSchema.partial()
|
||||
|
||||
export const CreateSessionMessageRequestSchema = z.object({
|
||||
content: z.string().min(1, 'Content must be a valid string')
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user