mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 完善 API Keys 批量删除功能并修复搜索跨选择问题
## 主要改进 ### 🔧 核心修复 - 修复搜索时勾选状态无法保存的问题 - 优化全选/取消全选逻辑,支持跨搜索结果保持选择状态 - 改进批量删除的用户体验 - 添加 Unicode 字符处理中间件,提升请求体解析稳定性 ### 🎯 具体变更 - **路由修复**: 解决批量删除路由匹配问题,调整路由顺序 - **API客户端**: 修复 DELETE 方法支持请求体数据传输 - **前端逻辑**: 分离筛选和搜索的监听器,搜索时保持已选中状态 - **全选优化**: 取消全选时只移除当前页选中项,保留其他页面选择 - **Unicode处理**: 添加无效 UTF-16 代理对清理和错误处理机制 - **配置管理**: 将 .mcp.json 添加到 .gitignore,避免本地配置被提交 ### 🚀 用户体验提升 - 支持跨搜索结果批量选择和删除 - 批量删除按钮显示选中数量 - 智能的全选状态管理 - 更好的 Unicode 字符处理容错性 ### 🧪 测试验证 - 验证搜索切换时选择状态保持 - 确认批量删除功能正常工作 - 检查 Redis 数据清理完整性 - 测试 Unicode 字符处理稳定性 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
52
src/app.js
52
src/app.js
@@ -141,10 +141,62 @@ class Application {
|
||||
if (buf && buf.length && !buf.toString(encoding || 'utf8').trim()) {
|
||||
throw new Error('Invalid JSON: empty body')
|
||||
}
|
||||
|
||||
// Unicode 字符清理 - 清理无效的 UTF-16 代理对
|
||||
if (buf && buf.length) {
|
||||
try {
|
||||
const str = buf.toString(encoding || 'utf8')
|
||||
// 移除无效的 UTF-16 代理对字符
|
||||
const cleanedStr = str.replace(
|
||||
/[\uDC00-\uDFFF](?![\uD800-\uDBFF])|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g,
|
||||
'\uFFFD'
|
||||
)
|
||||
|
||||
// 如果字符串被清理过,重新写入buffer
|
||||
if (cleanedStr !== str) {
|
||||
logger.warn('🧹 Cleaned invalid Unicode characters from request body')
|
||||
const cleanedBuf = Buffer.from(cleanedStr, encoding || 'utf8')
|
||||
// 将清理后的内容复制回原buffer
|
||||
cleanedBuf.copy(buf, 0)
|
||||
// 调整buffer长度
|
||||
buf._charsWritten = cleanedBuf.length
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
'⚠️ Unicode cleaning failed, proceeding with original buffer:',
|
||||
error.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
||||
|
||||
// 🧹 Unicode 错误处理中间件
|
||||
this.app.use((err, req, res, next) => {
|
||||
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
||||
// 检查是否是Unicode相关的JSON解析错误
|
||||
if (
|
||||
err.message.includes('surrogate') ||
|
||||
err.message.includes('UTF-16') ||
|
||||
err.message.includes('invalid character')
|
||||
) {
|
||||
logger.warn('🧹 Detected Unicode JSON parsing error, attempting recovery:', err.message)
|
||||
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message:
|
||||
'The request body contains invalid Unicode characters. Please ensure your text uses valid UTF-8 encoding and does not contain malformed surrogate pairs.'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
next(err)
|
||||
})
|
||||
|
||||
this.app.use(securityMiddleware)
|
||||
|
||||
// 🎯 信任代理
|
||||
|
||||
@@ -799,28 +799,20 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 删除API Key
|
||||
router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyId } = req.params
|
||||
|
||||
await apiKeyService.deleteApiKey(keyId)
|
||||
|
||||
logger.success(`🗑️ Admin deleted API key: ${keyId}`)
|
||||
return res.json({ success: true, message: 'API key deleted successfully' })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to delete API key:', error)
|
||||
return res.status(500).json({ error: 'Failed to delete API key', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 批量删除API Keys
|
||||
// 批量删除API Keys(必须在 :keyId 路由之前定义)
|
||||
router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyIds } = req.body
|
||||
|
||||
// 调试信息
|
||||
logger.info(`🐛 Batch delete request body: ${JSON.stringify(req.body)}`)
|
||||
logger.info(`🐛 keyIds type: ${typeof keyIds}, value: ${JSON.stringify(keyIds)}`)
|
||||
|
||||
// 参数验证
|
||||
if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) {
|
||||
logger.warn(
|
||||
`🚨 Invalid keyIds: ${JSON.stringify({ keyIds, type: typeof keyIds, isArray: Array.isArray(keyIds) })}`
|
||||
)
|
||||
return res.status(400).json({
|
||||
error: 'Invalid request',
|
||||
message: 'keyIds 必须是一个非空数组'
|
||||
@@ -843,7 +835,9 @@ router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`🗑️ Admin attempting batch delete of ${keyIds.length} API keys`)
|
||||
logger.info(
|
||||
`🗑️ Admin attempting batch delete of ${keyIds.length} API keys: ${JSON.stringify(keyIds)}`
|
||||
)
|
||||
|
||||
const results = {
|
||||
successCount: 0,
|
||||
@@ -855,8 +849,8 @@ router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
for (const keyId of keyIds) {
|
||||
try {
|
||||
// 检查API Key是否存在
|
||||
const apiKey = await apiKeyService.getApiKey(keyId)
|
||||
if (!apiKey) {
|
||||
const apiKey = await redis.getApiKey(keyId)
|
||||
if (!apiKey || Object.keys(apiKey).length === 0) {
|
||||
results.failedCount++
|
||||
results.errors.push({ keyId, error: 'API Key 不存在' })
|
||||
continue
|
||||
@@ -903,6 +897,21 @@ router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 删除单个API Key(必须在批量删除路由之后定义)
|
||||
router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyId } = req.params
|
||||
|
||||
await apiKeyService.deleteApiKey(keyId)
|
||||
|
||||
logger.success(`🗑️ Admin deleted API key: ${keyId}`)
|
||||
return res.json({ success: true, message: 'API key deleted successfully' })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to delete API key:', error)
|
||||
return res.status(500).json({ error: 'Failed to delete API key', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 👥 账户分组管理
|
||||
|
||||
// 创建账户分组
|
||||
|
||||
@@ -12,11 +12,52 @@ const sessionHelper = require('../utils/sessionHelper')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 🧹 Unicode 字符清理函数
|
||||
function cleanUnicodeString(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str
|
||||
}
|
||||
|
||||
// 移除无效的 UTF-16 代理对字符
|
||||
// 匹配无效的低代理字符 (0xDC00-0xDFFF) 没有对应的高代理字符
|
||||
// 匹配无效的高代理字符 (0xD800-0xDBFF) 没有对应的低代理字符
|
||||
return str.replace(
|
||||
/[\uDC00-\uDFFF](?![\uD800-\uDBFF])|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g,
|
||||
'\uFFFD'
|
||||
)
|
||||
}
|
||||
|
||||
// 🧹 递归清理对象中的 Unicode 字符
|
||||
function cleanUnicodeInObject(obj) {
|
||||
if (typeof obj === 'string') {
|
||||
return cleanUnicodeString(obj)
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => cleanUnicodeInObject(item))
|
||||
}
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
const cleaned = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
cleaned[cleanUnicodeString(key)] = cleanUnicodeInObject(value)
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// 🔧 共享的消息处理函数
|
||||
async function handleMessagesRequest(req, res) {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Unicode 字符清理 - 在输入验证之前清理请求体
|
||||
if (req.body) {
|
||||
req.body = cleanUnicodeInObject(req.body)
|
||||
}
|
||||
|
||||
// 严格的输入验证
|
||||
if (!req.body || typeof req.body !== 'object') {
|
||||
return res.status(400).json({
|
||||
|
||||
Reference in New Issue
Block a user