更新完善页面
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
const crypto = require('crypto')
|
||||
|
||||
const KEYWORD_MAX = 200
|
||||
const URL_MAX = 2048
|
||||
const BASE64_MAX = 5 * 1024 * 1024
|
||||
|
||||
const BANNED_PATTERNS = [
|
||||
/ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?)/i,
|
||||
/system\s*(prompt|message|role)/i,
|
||||
/disregard/i,
|
||||
/forget\s+(everything|all)/i,
|
||||
/act\s+as\s+(a\s+)?(developer|admin|root)/i,
|
||||
/reveal\s+(your|the)\s+(prompt|system|secret|key)/i,
|
||||
/api[_-]?key/i,
|
||||
/password|secret/i
|
||||
]
|
||||
|
||||
function sanitizeKeyword(input) {
|
||||
if (input === undefined || input === null) return ''
|
||||
const text = String(input).trim()
|
||||
if (text.length === 0) return ''
|
||||
if (text.length > KEYWORD_MAX) {
|
||||
return { error: `关键词不能超过 ${KEYWORD_MAX} 个字符` }
|
||||
}
|
||||
for (const re of BANNED_PATTERNS) {
|
||||
if (re.test(text)) return { error: '关键词包含非法内容' }
|
||||
}
|
||||
return { value: text }
|
||||
}
|
||||
|
||||
function sanitizeImageUrl(input) {
|
||||
if (!input) return { value: '' }
|
||||
const text = String(input).trim()
|
||||
if (text.length === 0) return { value: '' }
|
||||
if (text.length > URL_MAX) return { error: '图片 URL 过长' }
|
||||
if (/^https?:\/\//i.test(text)) {
|
||||
let parsed
|
||||
try {
|
||||
parsed = new URL(text)
|
||||
} catch (e) {
|
||||
return { error: '图片 URL 格式不合法' }
|
||||
}
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return { error: '图片 URL 协议必须是 http/https' }
|
||||
}
|
||||
const host = parsed.hostname
|
||||
if (!host || host.length === 0) return { error: '图片 URL 缺少主机' }
|
||||
if (host.includes('\u0000') || /[\s<>"'`\\]/.test(host)) {
|
||||
return { error: '图片 URL 主机名非法' }
|
||||
}
|
||||
const labels = host.split('.')
|
||||
for (const label of labels) {
|
||||
if (label.length === 0 || label.length > 63) return { error: '图片 URL 主机名非法' }
|
||||
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label)) {
|
||||
return { error: '图片 URL 主机名非法' }
|
||||
}
|
||||
}
|
||||
if (text.includes('..') || text.includes('\u0000')) return { error: '图片 URL 格式不合法' }
|
||||
} else if (!text.startsWith('/uploads/')) {
|
||||
return { error: '图片 URL 必须是 https/http 链接或 /uploads/ 相对路径' }
|
||||
}
|
||||
return { value: text }
|
||||
}
|
||||
|
||||
function sanitizeImageBase64(input) {
|
||||
if (!input) return { error: '缺少图片数据' }
|
||||
const text = String(input)
|
||||
if (text.length > BASE64_MAX) return { error: `图片不能超过 ${(BASE64_MAX / 1024 / 1024).toFixed(1)}MB` }
|
||||
if (!/^[A-Za-z0-9+/=\r\n]+$/.test(text)) return { error: '图片 base64 格式不合法' }
|
||||
return { value: text }
|
||||
}
|
||||
|
||||
function makeCacheKey(prefix, payload) {
|
||||
const json = JSON.stringify(payload, Object.keys(payload).sort())
|
||||
return prefix + ':' + crypto.createHash('sha1').update(json).digest('hex')
|
||||
}
|
||||
|
||||
class LRU {
|
||||
constructor(max = 100, ttlMs = 5 * 60 * 1000) {
|
||||
this.max = max
|
||||
this.ttlMs = ttlMs
|
||||
this.map = new Map()
|
||||
}
|
||||
get(key) {
|
||||
const item = this.map.get(key)
|
||||
if (!item) return undefined
|
||||
if (Date.now() > item.expire) {
|
||||
this.map.delete(key)
|
||||
return undefined
|
||||
}
|
||||
this.map.delete(key)
|
||||
this.map.set(key, item)
|
||||
return item.value
|
||||
}
|
||||
set(key, value) {
|
||||
if (this.map.has(key)) this.map.delete(key)
|
||||
this.map.set(key, { value, expire: Date.now() + this.ttlMs })
|
||||
if (this.map.size > this.max) {
|
||||
const first = this.map.keys().next().value
|
||||
this.map.delete(first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TokenBucket {
|
||||
constructor(capacity, refillPerSec) {
|
||||
this.capacity = capacity
|
||||
this.tokens = capacity
|
||||
this.refillPerSec = refillPerSec
|
||||
this.lastRefill = Date.now()
|
||||
this.queue = []
|
||||
}
|
||||
take() {
|
||||
this._refill()
|
||||
if (this.tokens >= 1) {
|
||||
this.tokens -= 1
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
_refill() {
|
||||
const now = Date.now()
|
||||
const elapsed = (now - this.lastRefill) / 1000
|
||||
if (elapsed > 0) {
|
||||
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSec)
|
||||
this.lastRefill = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
KEYWORD_MAX,
|
||||
URL_MAX,
|
||||
BASE64_MAX,
|
||||
sanitizeKeyword,
|
||||
sanitizeImageUrl,
|
||||
sanitizeImageBase64,
|
||||
makeCacheKey,
|
||||
LRU,
|
||||
TokenBucket
|
||||
}
|
||||
Reference in New Issue
Block a user