refactor: 提取正则工具模块并清理废弃代码

前端:
- 新增 model-mapping-regex.ts 工具模块,统一正则验证和 LRU 缓存
- ModelMappingsTab/RoutingTab 重构为使用 computed 缓存匹配结果
- 移除多个组件中未使用的变量和函数
- 添加 HTMLImageElement/HTMLIFrameElement 到 ESLint 全局类型
- 修复 vitest 需要 --experimental-require-module 的问题

后端:
- 移除废弃的异步数据库支持 (get_async_db, AsyncSession)
- 将 async_utils 从 database/ 迁移到 utils/
- 改进 database.py 类型标注
- 修复 email 模块的 aiosmtplib 可选导入类型问题
This commit is contained in:
fawney19
2026-01-15 17:03:19 +08:00
parent bc16c0eec8
commit a223819dd7
25 changed files with 526 additions and 688 deletions

View File

@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest'
import {
MAX_MAPPING_LENGTH,
MAX_MODEL_NAME_LENGTH,
createLRURegexCache,
safeTestModelMappingPattern,
validateModelMappingPattern,
} from '@/features/models/utils/model-mapping-regex'
describe('model-mapping-regex', () => {
it('validateModelMappingPattern: rejects empty', () => {
expect(validateModelMappingPattern('').valid).toBe(false)
expect(validateModelMappingPattern(' ').valid).toBe(false)
})
it('validateModelMappingPattern: rejects too long', () => {
const tooLong = 'a'.repeat(MAX_MAPPING_LENGTH + 1)
const result = validateModelMappingPattern(tooLong)
expect(result.valid).toBe(false)
})
it('validateModelMappingPattern: rejects potentially dangerous patterns', () => {
const result = validateModelMappingPattern('(a+)+')
expect(result.valid).toBe(false)
})
it('validateModelMappingPattern: accepts basic patterns', () => {
expect(validateModelMappingPattern('claude-haiku-.*').valid).toBe(true)
expect(validateModelMappingPattern('gpt-4o').valid).toBe(true)
})
it('safeTestModelMappingPattern: matches case-insensitively and anchors', () => {
const cache = createLRURegexCache(10)
expect(safeTestModelMappingPattern('gpt-4o', 'GPT-4O', cache)).toBe(true)
expect(safeTestModelMappingPattern('gpt-4o', 'gpt-4o-mini', cache)).toBe(false)
})
it('safeTestModelMappingPattern: rejects overly long model names', () => {
const cache = createLRURegexCache(10)
const longName = 'a'.repeat(MAX_MODEL_NAME_LENGTH + 1)
expect(safeTestModelMappingPattern('a.*', longName, cache)).toBe(false)
})
})

View File

@@ -0,0 +1,150 @@
export const MAX_MAPPINGS_PER_MODEL = 50
export const MAX_MAPPING_LENGTH = 200
export const MAX_MODEL_NAME_LENGTH = 200
export interface ValidationResult {
valid: boolean
error?: string
}
// 危险的正则模式(可能导致 ReDoS
// 注意:后端使用 regex 库的 timeout 做强制保护;前端无法中断 JS 正则执行,只能做启发式拦截。
const DANGEROUS_REGEX_PATTERNS: RegExp[] = [
/\([^)]*[+*]\)[+*]/, // (x+)+, (x*)*, (x+)*, (x*)+
/\([^)]*\)\{[0-9]+,\}/, // (x){n,} 无上限
/\(\.\*\)\{[0-9]+,\}/, // (.*){n,} 贪婪量词 + 高重复
/\(\.\+\)\{[0-9]+,\}/, // (.+){n,} 贪婪量词 + 高重复
/\([^)]*\|[^)]*\)[+*]/, // (a|b)+ 选择分支 + 量词
/\(\.\*\)\+/, // (.*)+
/\(\.\+\)\+/, // (.+)+
/\([^)]*\*\)[+*]/, // 嵌套量词: (a*)+
/\(\\w\+\)\+/, // (\w+)+ - 检测字面量 \w
/\(\.\*\)\*/, // (.*)*
/\(.*\+.*\)\+/, // (a+b)+ 更通用的嵌套量词检测
/\[.*\]\{[0-9]+,\}\{/, // [x]{n,}{m,} 嵌套量词
/\.{2,}\*/, // ..* 连续通配
/\([^)]*\|[^)]*\)\*/, // (a|a)* 选择分支 + 星号
/\{[0-9]{2,},\}/, // {10,} 高重复次数无上限
/\(\[.*\]\+\)\+/, // ([x]+)+ 字符类嵌套量词
// 补充的危险模式
/\([^)]*[+*]\)\{[0-9]+,/, // (a+){n,} 量词后跟大括号量词
/\(\([^)]*[+*]\)[+*]\)/, // ((a+)+) 三层嵌套量词
/\(\?:[^)]*[+*]\)[+*]/, // (?:a+)+ 非捕获组嵌套量词
]
function isPotentiallyDangerousRegex(pattern: string): boolean {
return DANGEROUS_REGEX_PATTERNS.some(re => re.test(pattern))
}
export interface LRURegexCache {
get: (key: string) => RegExp | null | undefined
set: (key: string, value: RegExp | null) => void
clear: () => void
}
export function createLRURegexCache(maxSize: number): LRURegexCache {
const cache = new Map<string, RegExp | null>()
return {
get: (key: string) => {
if (!cache.has(key)) return undefined
const value = cache.get(key)!
cache.delete(key)
cache.set(key, value)
return value
},
set: (key: string, value: RegExp | null) => {
if (cache.has(key)) {
cache.delete(key)
} else if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value as string | undefined
if (firstKey !== undefined) {
cache.delete(firstKey)
}
}
cache.set(key, value)
},
clear: () => {
cache.clear()
},
}
}
export function validateModelMappingPattern(pattern: string): ValidationResult {
if (!pattern || !pattern.trim()) {
return { valid: false, error: '规则不能为空' }
}
if (pattern.length > MAX_MAPPING_LENGTH) {
return { valid: false, error: `规则过长 (最大 ${MAX_MAPPING_LENGTH} 字符)` }
}
if (isPotentiallyDangerousRegex(pattern)) {
return { valid: false, error: '规则包含潜在危险的正则构造' }
}
try {
new RegExp(`^${pattern}$`, 'i')
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
return { valid: false, error: `正则表达式语法错误: ${message}` }
}
return { valid: true }
}
export function getCompiledModelMappingRegex(
pattern: string,
cache: LRURegexCache,
): RegExp | null {
const normalized = pattern.trim()
if (!normalized) return null
if (normalized.length > MAX_MAPPING_LENGTH) {
return null
}
if (isPotentiallyDangerousRegex(normalized)) {
return null
}
let regex = cache.get(normalized)
if (regex === undefined) {
try {
regex = new RegExp(`^${normalized}$`, 'i')
cache.set(normalized, regex)
} catch {
cache.set(normalized, null)
return null
}
}
return regex
}
export function safeTestModelMappingPattern(
pattern: string,
modelName: string,
cache: LRURegexCache,
): boolean {
if (!pattern) return false
if (pattern.toLowerCase() === modelName.toLowerCase()) {
return true
}
if (pattern.length > MAX_MAPPING_LENGTH || modelName.length > MAX_MODEL_NAME_LENGTH) {
return false
}
const regex = getCompiledModelMappingRegex(pattern, cache)
if (regex === null) {
return false
}
try {
return regex.test(modelName)
} catch {
return false
}
}