mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-05-06 14:51:37 +00:00
Merge pull request #375 from Edric-Li/main
增强API KEYS 页面、增强粘性回话的续期、增强错误处理
This commit is contained in:
@@ -22,6 +22,12 @@ REDIS_PASSWORD=
|
|||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
REDIS_ENABLE_TLS=
|
REDIS_ENABLE_TLS=
|
||||||
|
|
||||||
|
# 🔗 会话管理配置
|
||||||
|
# 粘性会话TTL配置(小时),默认1小时
|
||||||
|
STICKY_SESSION_TTL_HOURS=1
|
||||||
|
# 续期阈值(分钟),默认0分钟(不续期)
|
||||||
|
STICKY_SESSION_RENEWAL_THRESHOLD_MINUTES=0
|
||||||
|
|
||||||
# 🎯 Claude API 配置
|
# 🎯 Claude API 配置
|
||||||
CLAUDE_API_URL=https://api.anthropic.com/v1/messages
|
CLAUDE_API_URL=https://api.anthropic.com/v1/messages
|
||||||
CLAUDE_API_VERSION=2023-06-01
|
CLAUDE_API_VERSION=2023-06-01
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ const config = {
|
|||||||
enableTLS: process.env.REDIS_ENABLE_TLS === 'true'
|
enableTLS: process.env.REDIS_ENABLE_TLS === 'true'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 🔗 会话管理配置
|
||||||
|
session: {
|
||||||
|
// 粘性会话TTL配置(小时),默认1小时
|
||||||
|
stickyTtlHours: parseFloat(process.env.STICKY_SESSION_TTL_HOURS) || 1,
|
||||||
|
// 续期阈值(分钟),默认0分钟(不续期)
|
||||||
|
renewalThresholdMinutes: parseInt(process.env.STICKY_SESSION_RENEWAL_THRESHOLD_MINUTES) || 0
|
||||||
|
},
|
||||||
|
|
||||||
// 🎯 Claude API配置
|
// 🎯 Claude API配置
|
||||||
claude: {
|
claude: {
|
||||||
apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
|
apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
|
||||||
|
|||||||
@@ -1356,9 +1356,12 @@ class RedisClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔗 会话sticky映射管理
|
// 🔗 会话sticky映射管理
|
||||||
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
|
async setSessionAccountMapping(sessionHash, accountId, ttl = null) {
|
||||||
|
const appConfig = require('../../config/config')
|
||||||
|
// 从配置读取TTL(小时),转换为秒,默认1小时
|
||||||
|
const defaultTTL = ttl !== null ? ttl : (appConfig.session?.stickyTtlHours || 1) * 60 * 60
|
||||||
const key = `sticky_session:${sessionHash}`
|
const key = `sticky_session:${sessionHash}`
|
||||||
await this.client.set(key, accountId, 'EX', ttl)
|
await this.client.set(key, accountId, 'EX', defaultTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSessionAccountMapping(sessionHash) {
|
async getSessionAccountMapping(sessionHash) {
|
||||||
@@ -1366,6 +1369,57 @@ class RedisClient {
|
|||||||
return await this.client.get(key)
|
return await this.client.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚀 智能会话TTL续期:剩余时间少于阈值时自动续期
|
||||||
|
async extendSessionAccountMappingTTL(sessionHash) {
|
||||||
|
const appConfig = require('../../config/config')
|
||||||
|
const key = `sticky_session:${sessionHash}`
|
||||||
|
|
||||||
|
// 📊 从配置获取参数
|
||||||
|
const ttlHours = appConfig.session?.stickyTtlHours || 1 // 小时,默认1小时
|
||||||
|
const thresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0 // 分钟,默认0(不续期)
|
||||||
|
|
||||||
|
// 如果阈值为0,不执行续期
|
||||||
|
if (thresholdMinutes === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullTTL = ttlHours * 60 * 60 // 转换为秒
|
||||||
|
const renewalThreshold = thresholdMinutes * 60 // 转换为秒
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前剩余TTL(秒)
|
||||||
|
const remainingTTL = await this.client.ttl(key)
|
||||||
|
|
||||||
|
// 键不存在或已过期
|
||||||
|
if (remainingTTL === -2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键存在但没有TTL(永不过期,不需要处理)
|
||||||
|
if (remainingTTL === -1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 智能续期策略:仅在剩余时间少于阈值时才续期
|
||||||
|
if (remainingTTL < renewalThreshold) {
|
||||||
|
await this.client.expire(key, fullTTL)
|
||||||
|
logger.debug(
|
||||||
|
`🔄 Renewed sticky session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}min, renewed to ${ttlHours}h)`
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剩余时间充足,无需续期
|
||||||
|
logger.debug(
|
||||||
|
`✅ Sticky session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}min)`
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to extend session TTL:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async deleteSessionAccountMapping(sessionHash) {
|
async deleteSessionAccountMapping(sessionHash) {
|
||||||
const key = `sticky_session:${sessionHash}`
|
const key = `sticky_session:${sessionHash}`
|
||||||
return await this.client.del(key)
|
return await this.client.del(key)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) =>
|
|||||||
// 获取所有API Keys
|
// 获取所有API Keys
|
||||||
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { timeRange = 'all' } = req.query // all, 7days, monthly
|
const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom
|
||||||
const apiKeys = await apiKeyService.getAllApiKeys()
|
const apiKeys = await apiKeyService.getAllApiKeys()
|
||||||
|
|
||||||
// 获取用户服务来补充owner信息
|
// 获取用户服务来补充owner信息
|
||||||
@@ -132,7 +132,32 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const searchPatterns = []
|
const searchPatterns = []
|
||||||
|
|
||||||
if (timeRange === 'today') {
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
|
// 自定义日期范围
|
||||||
|
const redisClient = require('../models/redis')
|
||||||
|
const start = new Date(startDate)
|
||||||
|
const end = new Date(endDate)
|
||||||
|
|
||||||
|
// 确保日期范围有效
|
||||||
|
if (start > end) {
|
||||||
|
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制最大范围为365天
|
||||||
|
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
if (daysDiff > 365) {
|
||||||
|
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成日期范围内每天的搜索模式
|
||||||
|
const currentDate = new Date(start)
|
||||||
|
while (currentDate <= end) {
|
||||||
|
const tzDate = redisClient.getDateInTimezone(currentDate)
|
||||||
|
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||||
|
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
|
}
|
||||||
|
} else if (timeRange === 'today') {
|
||||||
// 今日 - 使用时区日期
|
// 今日 - 使用时区日期
|
||||||
const redisClient = require('../models/redis')
|
const redisClient = require('../models/redis')
|
||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
@@ -233,7 +258,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost)
|
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 7天或本月:重新计算统计数据
|
// 7天、本月或自定义日期范围:重新计算统计数据
|
||||||
const tempUsage = {
|
const tempUsage = {
|
||||||
requests: 0,
|
requests: 0,
|
||||||
tokens: 0,
|
tokens: 0,
|
||||||
@@ -274,12 +299,28 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
||||||
|
|
||||||
const modelKeys =
|
let modelKeys = []
|
||||||
timeRange === 'today'
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
// 自定义日期范围:获取范围内所有日期的模型统计
|
||||||
: timeRange === '7days'
|
const start = new Date(startDate)
|
||||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
const end = new Date(endDate)
|
||||||
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
const currentDate = new Date(start)
|
||||||
|
|
||||||
|
while (currentDate <= end) {
|
||||||
|
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
|
||||||
|
const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}`
|
||||||
|
const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`)
|
||||||
|
modelKeys = modelKeys.concat(dayKeys)
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
modelKeys =
|
||||||
|
timeRange === 'today'
|
||||||
|
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
||||||
|
: timeRange === '7days'
|
||||||
|
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
||||||
|
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
||||||
|
}
|
||||||
|
|
||||||
const modelStatsMap = new Map()
|
const modelStatsMap = new Map()
|
||||||
|
|
||||||
@@ -295,8 +336,8 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (timeRange === 'today') {
|
} else if (timeRange === 'today' || timeRange === 'custom') {
|
||||||
// today选项已经在查询时过滤了,不需要额外处理
|
// today和custom选项已经在查询时过滤了,不需要额外处理
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelMatch = key.match(
|
const modelMatch = key.match(
|
||||||
@@ -619,7 +660,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
expirationMode
|
expirationMode,
|
||||||
|
icon
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.success(`🔑 Admin created new API key: ${name}`)
|
logger.success(`🔑 Admin created new API key: ${name}`)
|
||||||
@@ -655,7 +697,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
expirationMode
|
expirationMode,
|
||||||
|
icon
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
// 输入验证
|
// 输入验证
|
||||||
@@ -701,7 +744,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
expirationMode
|
expirationMode,
|
||||||
|
icon
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保留原始 API Key 供返回
|
// 保留原始 API Key 供返回
|
||||||
@@ -3878,10 +3922,10 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制最大范围为31天
|
// 限制最大范围为365天
|
||||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
if (daysDiff > 31) {
|
if (daysDiff > 365) {
|
||||||
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成日期范围内所有日期的搜索模式
|
// 生成日期范围内所有日期的搜索模式
|
||||||
@@ -4342,10 +4386,10 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
|||||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制最大范围为31天
|
// 限制最大范围为365天
|
||||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
if (daysDiff > 31) {
|
if (daysDiff > 365) {
|
||||||
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成日期范围内所有日期的搜索模式
|
// 生成日期范围内所有日期的搜索模式
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class ApiKeyService {
|
|||||||
weeklyOpusCostLimit = 0,
|
weeklyOpusCostLimit = 0,
|
||||||
tags = [],
|
tags = [],
|
||||||
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
||||||
expirationMode = 'fixed' // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活)
|
expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活)
|
||||||
|
icon = '' // 新增:图标(base64编码)
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 生成简单的API Key (64字符十六进制)
|
// 生成简单的API Key (64字符十六进制)
|
||||||
@@ -78,7 +79,8 @@ class ApiKeyService {
|
|||||||
expiresAt: expirationMode === 'fixed' ? expiresAt || '' : '', // 固定模式才设置过期时间
|
expiresAt: expirationMode === 'fixed' ? expiresAt || '' : '', // 固定模式才设置过期时间
|
||||||
createdBy: options.createdBy || 'admin',
|
createdBy: options.createdBy || 'admin',
|
||||||
userId: options.userId || '',
|
userId: options.userId || '',
|
||||||
userUsername: options.userUsername || ''
|
userUsername: options.userUsername || '',
|
||||||
|
icon: icon || '' // 新增:图标(base64编码)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存API Key数据并建立哈希映射
|
// 保存API Key数据并建立哈希映射
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ const crypto = require('crypto')
|
|||||||
const ProxyHelper = require('../utils/proxyHelper')
|
const ProxyHelper = require('../utils/proxyHelper')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const logger = require('../utils/logger')
|
|
||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
|
const logger = require('../utils/logger')
|
||||||
const { maskToken } = require('../utils/tokenMask')
|
const { maskToken } = require('../utils/tokenMask')
|
||||||
const {
|
const {
|
||||||
logRefreshStart,
|
logRefreshStart,
|
||||||
@@ -707,6 +707,8 @@ class ClaudeAccountService {
|
|||||||
// 验证映射的账户是否仍然可用
|
// 验证映射的账户是否仍然可用
|
||||||
const mappedAccount = activeAccounts.find((acc) => acc.id === mappedAccountId)
|
const mappedAccount = activeAccounts.find((acc) => acc.id === mappedAccountId)
|
||||||
if (mappedAccount) {
|
if (mappedAccount) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -733,7 +735,9 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
// 如果有会话哈希,建立新的映射
|
// 如果有会话哈希,建立新的映射
|
||||||
if (sessionHash) {
|
if (sessionHash) {
|
||||||
await redis.setSessionAccountMapping(sessionHash, selectedAccountId, 3600) // 1小时过期
|
// 从配置获取TTL(小时),转换为秒
|
||||||
|
const ttlSeconds = (config.session?.stickyTtlHours || 1) * 60 * 60
|
||||||
|
await redis.setSessionAccountMapping(sessionHash, selectedAccountId, ttlSeconds)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Created new sticky session mapping: ${sortedAccounts[0].name} (${selectedAccountId}) for session ${sessionHash}`
|
`🎯 Created new sticky session mapping: ${sortedAccounts[0].name} (${selectedAccountId}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -827,6 +831,8 @@ class ClaudeAccountService {
|
|||||||
)
|
)
|
||||||
await redis.deleteSessionAccountMapping(sessionHash)
|
await redis.deleteSessionAccountMapping(sessionHash)
|
||||||
} else {
|
} else {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session shared account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}`
|
`🎯 Using sticky session shared account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -885,7 +891,9 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
// 如果有会话哈希,建立新的映射
|
// 如果有会话哈希,建立新的映射
|
||||||
if (sessionHash) {
|
if (sessionHash) {
|
||||||
await redis.setSessionAccountMapping(sessionHash, selectedAccountId, 3600) // 1小时过期
|
// 从配置获取TTL(小时),转换为秒
|
||||||
|
const ttlSeconds = (config.session?.stickyTtlHours || 1) * 60 * 60
|
||||||
|
await redis.setSessionAccountMapping(sessionHash, selectedAccountId, ttlSeconds)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Created new sticky session mapping for shared account: ${candidateAccounts[0].name} (${selectedAccountId}) for session ${sessionHash}`
|
`🎯 Created new sticky session mapping for shared account: ${candidateAccounts[0].name} (${selectedAccountId}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -211,19 +211,7 @@ class ClaudeRelayService {
|
|||||||
// 检查是否为5xx状态码
|
// 检查是否为5xx状态码
|
||||||
else if (response.statusCode >= 500 && response.statusCode < 600) {
|
else if (response.statusCode >= 500 && response.statusCode < 600) {
|
||||||
logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`)
|
logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`)
|
||||||
// 记录5xx错误
|
await this._handleServerError(accountId, response.statusCode, sessionHash)
|
||||||
await claudeAccountService.recordServerError(accountId, response.statusCode)
|
|
||||||
// 检查是否需要标记为临时错误状态(连续3次500)
|
|
||||||
const errorCount = await claudeAccountService.getServerErrorCount(accountId)
|
|
||||||
logger.info(
|
|
||||||
`🔥 Account ${accountId} has ${errorCount} consecutive 5xx errors in the last 5 minutes`
|
|
||||||
)
|
|
||||||
if (errorCount > 10) {
|
|
||||||
logger.error(
|
|
||||||
`❌ Account ${accountId} exceeded 5xx error threshold (${errorCount} errors), marking as temp_error`
|
|
||||||
)
|
|
||||||
await claudeAccountService.markAccountTempError(accountId, sessionHash)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 检查是否为429状态码
|
// 检查是否为429状态码
|
||||||
else if (response.statusCode === 429) {
|
else if (response.statusCode === 429) {
|
||||||
@@ -764,7 +752,7 @@ class ClaudeRelayService {
|
|||||||
onRequest(req)
|
onRequest(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on('error', async (error) => {
|
||||||
console.error(': ❌ ', error)
|
console.error(': ❌ ', error)
|
||||||
logger.error('❌ Claude API request error:', error.message, {
|
logger.error('❌ Claude API request error:', error.message, {
|
||||||
code: error.code,
|
code: error.code,
|
||||||
@@ -784,14 +772,19 @@ class ClaudeRelayService {
|
|||||||
errorMessage = 'Connection refused by Claude API server'
|
errorMessage = 'Connection refused by Claude API server'
|
||||||
} else if (error.code === 'ETIMEDOUT') {
|
} else if (error.code === 'ETIMEDOUT') {
|
||||||
errorMessage = 'Connection timed out to Claude API server'
|
errorMessage = 'Connection timed out to Claude API server'
|
||||||
|
|
||||||
|
await this._handleServerError(accountId, 504, null, 'Network')
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(new Error(errorMessage))
|
reject(new Error(errorMessage))
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('timeout', () => {
|
req.on('timeout', async () => {
|
||||||
req.destroy()
|
req.destroy()
|
||||||
logger.error('❌ Claude API request timeout')
|
logger.error('❌ Claude API request timeout')
|
||||||
|
|
||||||
|
await this._handleServerError(accountId, 504, null, 'Request')
|
||||||
|
|
||||||
reject(new Error('Request timeout'))
|
reject(new Error('Request timeout'))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1013,19 +1006,7 @@ class ClaudeRelayService {
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
||||||
)
|
)
|
||||||
// 记录5xx错误
|
await this._handleServerError(accountId, res.statusCode, sessionHash, '[Stream]')
|
||||||
await claudeAccountService.recordServerError(accountId, res.statusCode)
|
|
||||||
// 检查是否需要标记为临时错误状态(连续3次500)
|
|
||||||
const errorCount = await claudeAccountService.getServerErrorCount(accountId)
|
|
||||||
logger.info(
|
|
||||||
`🔥 [Stream] Account ${accountId} has ${errorCount} consecutive 5xx errors in the last 5 minutes`
|
|
||||||
)
|
|
||||||
if (errorCount > 10) {
|
|
||||||
logger.error(
|
|
||||||
`❌ [Stream] Account ${accountId} exceeded 5xx error threshold (${errorCount} errors), marking as temp_error`
|
|
||||||
)
|
|
||||||
await claudeAccountService.markAccountTempError(accountId, sessionHash)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1361,7 +1342,7 @@ class ClaudeRelayService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on('error', async (error) => {
|
||||||
logger.error('❌ Claude stream request error:', error.message, {
|
logger.error('❌ Claude stream request error:', error.message, {
|
||||||
code: error.code,
|
code: error.code,
|
||||||
errno: error.errno,
|
errno: error.errno,
|
||||||
@@ -1408,9 +1389,10 @@ class ClaudeRelayService {
|
|||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('timeout', () => {
|
req.on('timeout', async () => {
|
||||||
req.destroy()
|
req.destroy()
|
||||||
logger.error('❌ Claude stream request timeout')
|
logger.error('❌ Claude stream request timeout')
|
||||||
|
|
||||||
if (!responseStream.headersSent) {
|
if (!responseStream.headersSent) {
|
||||||
responseStream.writeHead(504, {
|
responseStream.writeHead(504, {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@@ -1510,7 +1492,7 @@ class ClaudeRelayService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on('error', async (error) => {
|
||||||
logger.error('❌ Claude stream request error:', error.message, {
|
logger.error('❌ Claude stream request error:', error.message, {
|
||||||
code: error.code,
|
code: error.code,
|
||||||
errno: error.errno,
|
errno: error.errno,
|
||||||
@@ -1557,9 +1539,10 @@ class ClaudeRelayService {
|
|||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
req.on('timeout', () => {
|
req.on('timeout', async () => {
|
||||||
req.destroy()
|
req.destroy()
|
||||||
logger.error('❌ Claude stream request timeout')
|
logger.error('❌ Claude stream request timeout')
|
||||||
|
|
||||||
if (!responseStream.headersSent) {
|
if (!responseStream.headersSent) {
|
||||||
responseStream.writeHead(504, {
|
responseStream.writeHead(504, {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@@ -1596,6 +1579,33 @@ class ClaudeRelayService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🛠️ 统一的错误处理方法
|
||||||
|
async _handleServerError(accountId, statusCode, sessionHash = null, context = '') {
|
||||||
|
try {
|
||||||
|
await claudeAccountService.recordServerError(accountId, statusCode)
|
||||||
|
const errorCount = await claudeAccountService.getServerErrorCount(accountId)
|
||||||
|
|
||||||
|
// 根据错误类型设置不同的阈值和日志前缀
|
||||||
|
const isTimeout = statusCode === 504
|
||||||
|
const threshold = 3 // 统一使用3次阈值
|
||||||
|
const prefix = context ? `${context} ` : ''
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`⏱️ ${prefix}${isTimeout ? 'Timeout' : 'Server'} error for account ${accountId}, error count: ${errorCount}/${threshold}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errorCount > threshold) {
|
||||||
|
const errorTypeLabel = isTimeout ? 'timeout' : '5xx'
|
||||||
|
logger.error(
|
||||||
|
`❌ ${prefix}Account ${accountId} exceeded ${errorTypeLabel} error threshold (${errorCount} errors), marking as temp_error`
|
||||||
|
)
|
||||||
|
await claudeAccountService.markAccountTempError(accountId, sessionHash)
|
||||||
|
}
|
||||||
|
} catch (handlingError) {
|
||||||
|
logger.error(`❌ Failed to handle ${context} server error:`, handlingError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔄 重试逻辑
|
// 🔄 重试逻辑
|
||||||
async _retryRequest(requestFunc, maxRetries = 3) {
|
async _retryRequest(requestFunc, maxRetries = 3) {
|
||||||
let lastError
|
let lastError
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ class UnifiedClaudeScheduler {
|
|||||||
requestedModel
|
requestedModel
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -789,6 +791,8 @@ class UnifiedClaudeScheduler {
|
|||||||
requestedModel
|
requestedModel
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ class UnifiedGeminiScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -382,6 +384,8 @@ class UnifiedGeminiScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ class UnifiedOpenAIScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -406,6 +408,8 @@ class UnifiedOpenAIScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||||
|
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})`
|
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})`
|
||||||
)
|
)
|
||||||
|
|||||||
202
web/admin-spa/package-lock.json
generated
202
web/admin-spa/package-lock.json
generated
@@ -15,7 +15,9 @@
|
|||||||
"element-plus": "^2.4.4",
|
"element-plus": "^2.4.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
@@ -1366,6 +1368,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -1643,6 +1654,19 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -1710,6 +1734,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -1766,6 +1799,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2304,6 +2349,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/exit-on-epipe": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
||||||
@@ -2379,6 +2433,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz",
|
||||||
|
"integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -2497,6 +2557,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
@@ -3804,6 +3873,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/printj": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"printj": "bin/printj.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -4083,6 +4164,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -5126,6 +5219,24 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -5244,6 +5355,95 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.2.0",
|
||||||
|
"cfb": "^1.1.4",
|
||||||
|
"codepage": "~1.14.0",
|
||||||
|
"commander": "~2.17.1",
|
||||||
|
"crc-32": "~1.2.0",
|
||||||
|
"exit-on-epipe": "~1.0.1",
|
||||||
|
"fflate": "^0.3.8",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/adler-32": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"exit-on-epipe": "~1.0.1",
|
||||||
|
"printj": "~1.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"adler32": "bin/adler32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/codepage": {
|
||||||
|
"version": "1.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz",
|
||||||
|
"integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "~2.14.1",
|
||||||
|
"exit-on-epipe": "~1.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"codepage": "bin/codepage.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/codepage/node_modules/commander": {
|
||||||
|
"version": "2.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
||||||
|
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/commander": {
|
||||||
|
"version": "2.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
|
||||||
|
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
"element-plus": "^2.4.4",
|
"element-plus": "^2.4.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
|
|||||||
@@ -110,19 +110,21 @@
|
|||||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
|
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
|
||||||
>名称 <span class="text-red-500">*</span></label
|
>名称 <span class="text-red-500">*</span></label
|
||||||
>
|
>
|
||||||
<input
|
<div>
|
||||||
v-model="form.name"
|
<input
|
||||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
v-model="form.name"
|
||||||
:class="{ 'border-red-500': errors.name }"
|
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
:placeholder="
|
:class="{ 'border-red-500': errors.name }"
|
||||||
form.createType === 'batch'
|
:placeholder="
|
||||||
? '输入基础名称(将自动添加序号)'
|
form.createType === 'batch'
|
||||||
: '为您的 API Key 取一个名称'
|
? '输入基础名称(将自动添加序号)'
|
||||||
"
|
: '为您的 API Key 取一个名称'
|
||||||
required
|
"
|
||||||
type="text"
|
required
|
||||||
@input="errors.name = ''"
|
type="text"
|
||||||
/>
|
@input="errors.name = ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
||||||
{{ errors.name }}
|
{{ errors.name }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -32,14 +32,16 @@
|
|||||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||||
>名称</label
|
>名称</label
|
||||||
>
|
>
|
||||||
<input
|
<div>
|
||||||
v-model="form.name"
|
<input
|
||||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
v-model="form.name"
|
||||||
maxlength="100"
|
class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
placeholder="请输入API Key名称"
|
maxlength="100"
|
||||||
required
|
placeholder="请输入API Key名称"
|
||||||
type="text"
|
required
|
||||||
/>
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
||||||
用于识别此 API Key 的用途
|
用于识别此 API Key 的用途
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<!-- 模态框 -->
|
<!-- 模态框 -->
|
||||||
<div
|
<div
|
||||||
class="modal-content relative mx-auto flex max-h-[90vh] w-[95%] max-w-2xl flex-col p-4 sm:w-full sm:max-w-3xl sm:p-6 md:p-8"
|
class="modal-content relative mx-auto flex max-h-[90vh] w-[95%] max-w-5xl flex-col p-4 sm:w-full sm:p-6 md:p-8"
|
||||||
>
|
>
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user