mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: 新增 OpenAI-Responses 账户管理功能和独立自动停止标记机制
## 功能新增 - 实现 OpenAI-Responses 账户服务(openaiResponsesAccountService.js) - 支持使用账户内置 API Key 进行请求转发 - 实现每日额度管理和重置机制 - 支持代理配置和优先级设置 - 实现 OpenAI-Responses 中继服务(openaiResponsesRelayService.js) - 处理请求转发和响应流处理 - 自动记录使用统计信息 - 支持流式和非流式响应 - 新增管理界面的 OpenAI-Responses 账户管理功能 - 完整的 CRUD 操作支持 - 实时额度监控和状态管理 - 支持手动重置限流和每日额度 ## 架构改进 - 引入独立的自动停止标记机制,区分不同原因的自动停止 - rateLimitAutoStopped: 限流自动停止 - fiveHourAutoStopped: 5小时限制自动停止 - tempErrorAutoStopped: 临时错误自动停止 - quotaAutoStopped: 额度耗尽自动停止 - 修复手动修改调度状态时自动恢复的问题 - 统一清理逻辑,防止状态冲突 ## 其他优化 - getAccountUsageStats 支持不同账户类型参数 - 统一调度器支持 OpenAI-Responses 账户类型 - WebHook 通知增强,支持新账户类型的事件 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ const claudeConsoleAccountService = require('../services/claudeConsoleAccountSer
|
||||
const bedrockAccountService = require('../services/bedrockAccountService')
|
||||
const geminiAccountService = require('../services/geminiAccountService')
|
||||
const openaiAccountService = require('../services/openaiAccountService')
|
||||
const openaiResponsesAccountService = require('../services/openaiResponsesAccountService')
|
||||
const azureOpenaiAccountService = require('../services/azureOpenaiAccountService')
|
||||
const accountGroupService = require('../services/accountGroupService')
|
||||
const redis = require('../models/redis')
|
||||
@@ -1946,7 +1947,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
// 获取会话窗口使用统计(仅对有活跃窗口的账户)
|
||||
@@ -2381,7 +2382,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
return {
|
||||
@@ -2784,7 +2785,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
return {
|
||||
@@ -3234,7 +3235,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
return {
|
||||
@@ -5762,7 +5763,7 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||
return {
|
||||
...account,
|
||||
usage: {
|
||||
@@ -6309,7 +6310,7 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
return {
|
||||
...account,
|
||||
@@ -6709,4 +6710,334 @@ router.post('/claude-code-version/clear', authenticateAdmin, async (req, res) =>
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== OpenAI-Responses 账户管理 API ====================
|
||||
|
||||
// 获取所有 OpenAI-Responses 账户
|
||||
router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { platform, groupId } = req.query
|
||||
let accounts = await openaiResponsesAccountService.getAllAccounts(true)
|
||||
|
||||
// 根据查询参数进行筛选
|
||||
if (platform && platform !== 'openai-responses') {
|
||||
accounts = []
|
||||
}
|
||||
|
||||
// 根据分组ID筛选
|
||||
if (groupId) {
|
||||
const group = await accountGroupService.getGroup(groupId)
|
||||
if (group && group.platform === 'openai' && group.memberIds && group.memberIds.length > 0) {
|
||||
accounts = accounts.filter((account) => group.memberIds.includes(account.id))
|
||||
} else {
|
||||
accounts = []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理额度信息、使用统计和绑定的 API Key 数量
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
// 检查是否需要重置额度
|
||||
const today = redis.getDateStringInTimezone()
|
||||
if (account.lastResetDate !== today) {
|
||||
// 今天还没重置过,需要重置
|
||||
await openaiResponsesAccountService.updateAccount(account.id, {
|
||||
dailyUsage: '0',
|
||||
lastResetDate: today,
|
||||
quotaStoppedAt: ''
|
||||
})
|
||||
account.dailyUsage = '0'
|
||||
account.lastResetDate = today
|
||||
account.quotaStoppedAt = ''
|
||||
}
|
||||
|
||||
// 检查并清除过期的限流状态
|
||||
await openaiResponsesAccountService.checkAndClearRateLimit(account.id)
|
||||
|
||||
// 获取使用统计信息
|
||||
let usageStats
|
||||
try {
|
||||
usageStats = await redis.getAccountUsageStats(account.id, 'openai-responses')
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`Failed to get usage stats for OpenAI-Responses account ${account.id}:`,
|
||||
error
|
||||
)
|
||||
usageStats = {
|
||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||
total: { requests: 0, tokens: 0, allTokens: 0 },
|
||||
monthly: { requests: 0, tokens: 0, allTokens: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// 计算绑定的API Key数量(支持 responses: 前缀)
|
||||
const allKeys = await redis.getAllApiKeys()
|
||||
let boundCount = 0
|
||||
|
||||
for (const key of allKeys) {
|
||||
// 检查是否绑定了该账户(包括 responses: 前缀)
|
||||
if (
|
||||
key.openaiAccountId === account.id ||
|
||||
key.openaiAccountId === `responses:${account.id}`
|
||||
) {
|
||||
boundCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 调试日志:检查绑定计数
|
||||
if (boundCount > 0) {
|
||||
logger.info(`OpenAI-Responses account ${account.id} has ${boundCount} bound API keys`)
|
||||
}
|
||||
|
||||
return {
|
||||
...account,
|
||||
boundApiKeysCount: boundCount,
|
||||
usage: {
|
||||
daily: usageStats.daily,
|
||||
total: usageStats.total,
|
||||
monthly: usageStats.monthly
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process OpenAI-Responses account ${account.id}:`, error)
|
||||
return {
|
||||
...account,
|
||||
boundApiKeysCount: 0,
|
||||
usage: {
|
||||
daily: { requests: 0, tokens: 0, allTokens: 0 },
|
||||
total: { requests: 0, tokens: 0, allTokens: 0 },
|
||||
monthly: { requests: 0, tokens: 0, allTokens: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
res.json({ success: true, data: accountsWithStats })
|
||||
} catch (error) {
|
||||
logger.error('Failed to get OpenAI-Responses accounts:', error)
|
||||
res.status(500).json({ success: false, message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 创建 OpenAI-Responses 账户
|
||||
router.post('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const account = await openaiResponsesAccountService.createAccount(req.body)
|
||||
res.json({ success: true, account })
|
||||
} catch (error) {
|
||||
logger.error('Failed to create OpenAI-Responses account:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新 OpenAI-Responses 账户
|
||||
router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const updates = req.body
|
||||
|
||||
// 验证priority的有效性(1-100)
|
||||
if (updates.priority !== undefined) {
|
||||
const priority = parseInt(updates.priority)
|
||||
if (isNaN(priority) || priority < 1 || priority > 100) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Priority must be a number between 1 and 100'
|
||||
})
|
||||
}
|
||||
updates.priority = priority.toString()
|
||||
}
|
||||
|
||||
const result = await openaiResponsesAccountService.updateAccount(id, updates)
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json(result)
|
||||
}
|
||||
|
||||
res.json({ success: true, ...result })
|
||||
} catch (error) {
|
||||
logger.error('Failed to update OpenAI-Responses account:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除 OpenAI-Responses 账户
|
||||
router.delete('/openai-responses-accounts/:id', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
const account = await openaiResponsesAccountService.getAccount(id)
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Account not found'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查是否在分组中
|
||||
const groups = await accountGroupService.getAllGroups()
|
||||
for (const group of groups) {
|
||||
if (group.platform === 'openai' && group.memberIds && group.memberIds.includes(id)) {
|
||||
await accountGroupService.removeMemberFromGroup(group.id, id)
|
||||
logger.info(`Removed OpenAI-Responses account ${id} from group ${group.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await openaiResponsesAccountService.deleteAccount(id)
|
||||
res.json({ success: true, ...result })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete OpenAI-Responses account:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 切换 OpenAI-Responses 账户调度状态
|
||||
router.put(
|
||||
'/openai-responses-accounts/:id/toggle-schedulable',
|
||||
authenticateAdmin,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
const result = await openaiResponsesAccountService.toggleSchedulable(id)
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json(result)
|
||||
}
|
||||
|
||||
// 仅在停止调度时发送通知
|
||||
if (!result.schedulable) {
|
||||
await webhookNotifier.sendAccountEvent('account.status_changed', {
|
||||
accountId: id,
|
||||
platform: 'openai-responses',
|
||||
schedulable: result.schedulable,
|
||||
changedBy: 'admin',
|
||||
action: 'stopped_scheduling'
|
||||
})
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
logger.error('Failed to toggle OpenAI-Responses account schedulable status:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 切换 OpenAI-Responses 账户激活状态
|
||||
router.put('/openai-responses-accounts/:id/toggle', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
const account = await openaiResponsesAccountService.getAccount(id)
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Account not found'
|
||||
})
|
||||
}
|
||||
|
||||
const newActiveStatus = account.isActive === 'true' ? 'false' : 'true'
|
||||
await openaiResponsesAccountService.updateAccount(id, {
|
||||
isActive: newActiveStatus
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
isActive: newActiveStatus === 'true'
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to toggle OpenAI-Responses account status:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 重置 OpenAI-Responses 账户限流状态
|
||||
router.post(
|
||||
'/openai-responses-accounts/:id/reset-rate-limit',
|
||||
authenticateAdmin,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
await openaiResponsesAccountService.updateAccount(id, {
|
||||
rateLimitedAt: '',
|
||||
rateLimitStatus: '',
|
||||
status: 'active',
|
||||
errorMessage: ''
|
||||
})
|
||||
|
||||
logger.info(`🔄 Admin manually reset rate limit for OpenAI-Responses account ${id}`)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Rate limit reset successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to reset OpenAI-Responses account rate limit:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 重置 OpenAI-Responses 账户状态(清除所有异常状态)
|
||||
router.post('/openai-responses-accounts/:id/reset-status', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
const result = await openaiResponsesAccountService.resetAccountStatus(id)
|
||||
|
||||
logger.success(`✅ Admin reset status for OpenAI-Responses account: ${id}`)
|
||||
return res.json({ success: true, data: result })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to reset OpenAI-Responses account status:', error)
|
||||
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 手动重置 OpenAI-Responses 账户的每日使用量
|
||||
router.post('/openai-responses-accounts/:id/reset-usage', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
await openaiResponsesAccountService.updateAccount(id, {
|
||||
dailyUsage: '0',
|
||||
lastResetDate: redis.getDateStringInTimezone(),
|
||||
quotaStoppedAt: ''
|
||||
})
|
||||
|
||||
logger.success(`✅ Admin manually reset daily usage for OpenAI-Responses account ${id}`)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Daily usage reset successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to reset OpenAI-Responses account usage:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -6,6 +6,8 @@ const config = require('../../config/config')
|
||||
const { authenticateApiKey } = require('../middleware/auth')
|
||||
const unifiedOpenAIScheduler = require('../services/unifiedOpenAIScheduler')
|
||||
const openaiAccountService = require('../services/openaiAccountService')
|
||||
const openaiResponsesAccountService = require('../services/openaiResponsesAccountService')
|
||||
const openaiResponsesRelayService = require('../services/openaiResponsesRelayService')
|
||||
const apiKeyService = require('../services/apiKeyService')
|
||||
const crypto = require('crypto')
|
||||
const ProxyHelper = require('../utils/proxyHelper')
|
||||
@@ -34,51 +36,81 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel =
|
||||
throw new Error('No available OpenAI account found')
|
||||
}
|
||||
|
||||
// 获取账户详情
|
||||
let account = await openaiAccountService.getAccount(result.accountId)
|
||||
if (!account || !account.accessToken) {
|
||||
throw new Error(`OpenAI account ${result.accountId} has no valid accessToken`)
|
||||
}
|
||||
// 根据账户类型获取账户详情
|
||||
let account,
|
||||
accessToken,
|
||||
proxy = null
|
||||
|
||||
// 检查 token 是否过期并自动刷新(双重保护)
|
||||
if (openaiAccountService.isTokenExpired(account)) {
|
||||
if (account.refreshToken) {
|
||||
logger.info(`🔄 Token expired, auto-refreshing for account ${account.name} (fallback)`)
|
||||
if (result.accountType === 'openai-responses') {
|
||||
// 处理 OpenAI-Responses 账户
|
||||
account = await openaiResponsesAccountService.getAccount(result.accountId)
|
||||
if (!account || !account.apiKey) {
|
||||
throw new Error(`OpenAI-Responses account ${result.accountId} has no valid apiKey`)
|
||||
}
|
||||
|
||||
// OpenAI-Responses 账户不需要 accessToken,直接返回账户信息
|
||||
accessToken = null // OpenAI-Responses 使用账户内的 apiKey
|
||||
|
||||
// 解析代理配置
|
||||
if (account.proxy) {
|
||||
try {
|
||||
await openaiAccountService.refreshAccountToken(result.accountId)
|
||||
// 重新获取更新后的账户
|
||||
account = await openaiAccountService.getAccount(result.accountId)
|
||||
logger.info(`✅ Token refreshed successfully in route handler`)
|
||||
} catch (refreshError) {
|
||||
logger.error(`Failed to refresh token for ${account.name}:`, refreshError)
|
||||
throw new Error(`Token expired and refresh failed: ${refreshError.message}`)
|
||||
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse proxy configuration:', e)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Token expired and no refresh token available for account ${account.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 解密 accessToken(account.accessToken 是加密的)
|
||||
const accessToken = openaiAccountService.decrypt(account.accessToken)
|
||||
if (!accessToken) {
|
||||
throw new Error('Failed to decrypt OpenAI accessToken')
|
||||
}
|
||||
|
||||
// 解析代理配置
|
||||
let proxy = null
|
||||
if (account.proxy) {
|
||||
try {
|
||||
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse proxy configuration:', e)
|
||||
logger.info(`Selected OpenAI-Responses account: ${account.name} (${result.accountId})`)
|
||||
} else {
|
||||
// 处理普通 OpenAI 账户
|
||||
account = await openaiAccountService.getAccount(result.accountId)
|
||||
if (!account || !account.accessToken) {
|
||||
throw new Error(`OpenAI account ${result.accountId} has no valid accessToken`)
|
||||
}
|
||||
|
||||
// 检查 token 是否过期并自动刷新(双重保护)
|
||||
if (openaiAccountService.isTokenExpired(account)) {
|
||||
if (account.refreshToken) {
|
||||
logger.info(`🔄 Token expired, auto-refreshing for account ${account.name} (fallback)`)
|
||||
try {
|
||||
await openaiAccountService.refreshAccountToken(result.accountId)
|
||||
// 重新获取更新后的账户
|
||||
account = await openaiAccountService.getAccount(result.accountId)
|
||||
logger.info(`✅ Token refreshed successfully in route handler`)
|
||||
} catch (refreshError) {
|
||||
logger.error(`Failed to refresh token for ${account.name}:`, refreshError)
|
||||
throw new Error(`Token expired and refresh failed: ${refreshError.message}`)
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Token expired and no refresh token available for account ${account.name}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 解密 accessToken(account.accessToken 是加密的)
|
||||
accessToken = openaiAccountService.decrypt(account.accessToken)
|
||||
if (!accessToken) {
|
||||
throw new Error('Failed to decrypt OpenAI accessToken')
|
||||
}
|
||||
|
||||
// 解析代理配置
|
||||
if (account.proxy) {
|
||||
try {
|
||||
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse proxy configuration:', e)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Selected OpenAI account: ${account.name} (${result.accountId})`)
|
||||
}
|
||||
|
||||
logger.info(`Selected OpenAI account: ${account.name} (${result.accountId})`)
|
||||
return {
|
||||
accessToken,
|
||||
accountId: result.accountId,
|
||||
accountName: account.name,
|
||||
accountType: result.accountType,
|
||||
proxy,
|
||||
account
|
||||
}
|
||||
@@ -151,9 +183,16 @@ const handleResponses = async (req, res) => {
|
||||
accessToken,
|
||||
accountId,
|
||||
accountName: _accountName,
|
||||
accountType,
|
||||
proxy,
|
||||
account
|
||||
} = await getOpenAIAuthToken(apiKeyData, sessionId, requestedModel)
|
||||
|
||||
// 如果是 OpenAI-Responses 账户,使用专门的中继服务处理
|
||||
if (accountType === 'openai-responses') {
|
||||
logger.info(`🔀 Using OpenAI-Responses relay service for account: ${account.name}`)
|
||||
return await openaiResponsesRelayService.handleRequest(req, res, account, apiKeyData)
|
||||
}
|
||||
// 基于白名单构造上游所需的请求头,确保键为小写且值受控
|
||||
const incoming = req.headers || {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user