UNPKG

answer-book-mcp

Version:

智能问答和决策辅助的 MCP (Model Context Protocol) 服务器

339 lines (294 loc) 8.52 kB
/** * 参数验证工具 * 提供输入验证、配置验证和数据清理功能 */ import Joi from 'joi' import { logger } from './logger.js' /** * 配置文件验证模式 */ const configSchema = Joi.object({ server: Joi.object({ name: Joi.string().required(), version: Joi.string().required(), description: Joi.string().required() }).required(), features: Joi.object({ smart_matching: Joi.boolean().default(true), history_tracking: Joi.boolean().default(true), multilingual: Joi.boolean().default(true), personalization: Joi.boolean().default(false) }).required(), data: Joi.object({ answers_path: Joi.string().required(), history_path: Joi.string().required(), max_history_records: Joi.number().integer().min(100).max(10000).default(1000) }).required(), matching: Joi.object({ min_confidence: Joi.number().min(0).max(1).default(0.3), fallback_to_random: Joi.boolean().default(true), keyword_weight: Joi.number().min(0).max(1).default(0.7), category_weight: Joi.number().min(0).max(1).default(0.3) }).required(), logging: Joi.object({ level: Joi.string().valid('error', 'warn', 'info', 'debug').default('info'), file: Joi.string().default('./logs/server.log'), max_files: Joi.number().integer().min(1).max(10).default(5), max_size: Joi.string().default('10m') }).optional() }) /** * 问题参数验证模式 */ const askQuestionSchema = Joi.object({ question: Joi.string().min(1).max(1000).required(), category: Joi.string().valid('life', 'work', 'love', 'decision', 'general').optional(), language: Joi.string().valid('zh', 'en').default('zh'), save_history: Joi.boolean().default(true), mood_preference: Joi.string().valid('positive', 'neutral', 'encouraging').default('positive') }) /** * 建议参数验证模式 */ const getAdviceSchema = Joi.object({ situation: Joi.string().min(10).max(2000).required(), options: Joi.array().items(Joi.string().max(200)).max(10).optional(), priority: Joi.string().valid('practical', 'emotional', 'balanced', 'risk_averse', 'innovative').default('balanced'), timeline: Joi.string().valid('urgent', 'normal', 'flexible').optional() }) /** * 随机答案参数验证模式 */ const randomAnswerSchema = Joi.object({ category: Joi.string().valid('life', 'work', 'love', 'decision', 'general').optional(), mood: Joi.string().valid('positive', 'neutral', 'encouraging').default('positive'), language: Joi.string().valid('zh', 'en').default('zh') }) /** * 历史查询参数验证模式 */ const getHistorySchema = Joi.object({ limit: Joi.number().integer().min(1).max(100).default(10), category: Joi.string().valid('life', 'work', 'love', 'decision', 'general').optional(), date_from: Joi.string().isoDate().optional(), date_to: Joi.string().isoDate().optional(), search: Joi.string().max(100).optional() }) /** * 保存问题参数验证模式 */ const saveQuestionSchema = Joi.object({ question: Joi.string().min(1).max(1000).required(), answer: Joi.string().min(1).max(2000).required(), category: Joi.string().required(), confidence: Joi.number().min(0).max(1).required(), source: Joi.string().valid('smart', 'random', 'fallback').required(), user_feedback: Joi.string().valid('helpful', 'not_helpful', 'irrelevant').optional() }) /** * 答案数据验证模式 */ const answerSchema = Joi.object({ id: Joi.string().required(), text: Joi.string().min(1).max(2000).required(), text_en: Joi.string().max(2000).optional(), category: Joi.string().required(), tags: Joi.array().items(Joi.string()).required(), weight: Joi.number().min(0).max(1).required(), mood: Joi.string().valid('positive', 'neutral', 'encouraging').required(), usage_count: Joi.number().integer().min(0).default(0), success_rate: Joi.number().min(0).max(1).default(0.5), created_at: Joi.string().isoDate().required(), updated_at: Joi.string().isoDate().required() }) /** * 验证配置文件 */ export function validateConfig (config) { const { error, value } = configSchema.validate(config, { allowUnknown: false, stripUnknown: true }) if (error) { logger.error('配置验证失败', { error: error.details }) throw new Error(`配置验证失败: ${error.message}`) } return value } /** * 验证问题参数 */ export function validateAskQuestion (params) { return validateParams(askQuestionSchema, params, 'ask_question') } /** * 验证建议参数 */ export function validateGetAdvice (params) { return validateParams(getAdviceSchema, params, 'get_advice') } /** * 验证随机答案参数 */ export function validateRandomAnswer (params) { return validateParams(randomAnswerSchema, params, 'random_answer') } /** * 验证历史查询参数 */ export function validateGetHistory (params) { return validateParams(getHistorySchema, params, 'get_history') } /** * 验证保存问题参数 */ export function validateSaveQuestion (params) { return validateParams(saveQuestionSchema, params, 'save_question') } /** * 验证答案数据 */ export function validateAnswer (answer) { return validateParams(answerSchema, answer, 'answer') } /** * 通用参数验证函数 */ function validateParams (schema, params, context) { const { error, value } = schema.validate(params, { allowUnknown: false, stripUnknown: true, convert: true }) if (error) { logger.warn(`参数验证失败: ${context}`, { error: error.details, params }) throw new Error(`参数验证失败: ${error.message}`) } return value } /** * 清理和过滤用户输入 */ export function sanitizeInput (input) { if (typeof input !== 'string') { return input } return input .trim() .replace(/[<>"'&]/g, (match) => { const entities = { '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '&': '&amp;' } return entities[match] }) .substring(0, 1000) // 长度限制 } /** * 验证文本内容安全性 */ export function validateTextSafety (text) { if (typeof text !== 'string') { return false } // 检查恶意模式 const maliciousPatterns = [ /<script[^>]*>.*?<\/script>/gi, /javascript:/gi, /on\w+\s*=/gi, /<iframe[^>]*>.*?<\/iframe>/gi, /('|(\-\-)|(;)|(\||\|)|(\*|\*))/i, /(exec(\s|\+)+(s|x)p\w+)/i, /union.*select/i, /insert.*into/i, /delete.*from/i, /update.*set/i ] return !maliciousPatterns.some(pattern => pattern.test(text)) } /** * 验证日期范围 */ export function validateDateRange (dateFrom, dateTo) { if (!dateFrom || !dateTo) { return true } const from = new Date(dateFrom) const to = new Date(dateTo) if (isNaN(from.getTime()) || isNaN(to.getTime())) { throw new Error('无效的日期格式') } if (from > to) { throw new Error('开始日期不能晚于结束日期') } const maxRange = 365 * 24 * 60 * 60 * 1000 // 1年 if (to - from > maxRange) { throw new Error('日期范围不能超过1年') } return true } /** * 验证分类有效性 */ export function validateCategory (category) { const validCategories = ['life', 'work', 'love', 'decision', 'general'] return validCategories.includes(category) } /** * 验证语言代码 */ export function validateLanguage (language) { const validLanguages = ['zh', 'en'] return validLanguages.includes(language) } /** * 批量验证答案数据 */ export function validateAnswers (answers) { if (!Array.isArray(answers)) { throw new Error('答案数据必须是数组') } const validatedAnswers = [] const errors = [] for (let i = 0; i < answers.length; i++) { try { const validatedAnswer = validateAnswer(answers[i]) validatedAnswers.push(validatedAnswer) } catch (error) { errors.push({ index: i, error: error.message, data: answers[i] }) } } if (errors.length > 0) { logger.warn('部分答案数据验证失败', { errors }) } return { valid: validatedAnswers, errors } } /** * 创建验证中间件 */ export function createValidator (schema) { return (params) => { const { error, value } = schema.validate(params, { allowUnknown: false, stripUnknown: true, convert: true }) if (error) { throw new Error(`参数验证失败: ${error.message}`) } return value } }