answer-book-mcp
Version:
智能问答和决策辅助的 MCP (Model Context Protocol) 服务器
339 lines (294 loc) • 8.52 kB
JavaScript
/**
* 参数验证工具
* 提供输入验证、配置验证和数据清理功能
*/
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 = {
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'&': '&'
}
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
}
}