mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 21:17:30 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a636a36f6 | ||
|
|
b61e1062bf | ||
|
|
1b18a1226d | ||
|
|
0b2372abab | ||
|
|
8aca1f9dd1 | ||
|
|
b63f2f78fc | ||
|
|
c971d239ff | ||
|
|
01d6e30e82 | ||
|
|
5fd78b6411 | ||
|
|
9ad5c85c2c |
6357
pnpm-lock.yaml
generated
Normal file
6357
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -449,9 +449,8 @@ async function handleMessages(req, res) {
|
||||
|
||||
// 添加代理配置
|
||||
if (proxyConfig) {
|
||||
const proxyHelper = new ProxyHelper()
|
||||
axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpsAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -732,9 +731,8 @@ async function handleModels(req, res) {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
if (proxyConfig) {
|
||||
const proxyHelper = new ProxyHelper()
|
||||
axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpsAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
}
|
||||
const response = await axios(axiosConfig)
|
||||
models = (response.data.models || []).map((m) => ({
|
||||
@@ -1234,9 +1232,8 @@ async function handleCountTokens(req, res) {
|
||||
}
|
||||
|
||||
if (proxyConfig) {
|
||||
const proxyHelper = new ProxyHelper()
|
||||
axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpsAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1963,9 +1960,8 @@ async function handleStandardGenerateContent(req, res) {
|
||||
}
|
||||
|
||||
if (proxyConfig) {
|
||||
const proxyHelper = new ProxyHelper()
|
||||
axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpsAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2246,9 +2242,8 @@ async function handleStandardStreamGenerateContent(req, res) {
|
||||
}
|
||||
|
||||
if (proxyConfig) {
|
||||
const proxyHelper = new ProxyHelper()
|
||||
axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpsAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
axiosConfig.httpAgent = ProxyHelper.createProxyAgent(proxyConfig)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -131,7 +131,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
groupId,
|
||||
dailyQuota,
|
||||
quotaResetTime,
|
||||
maxConcurrentTasks
|
||||
maxConcurrentTasks,
|
||||
disableAutoProtection
|
||||
} = req.body
|
||||
|
||||
if (!name || !apiUrl || !apiKey) {
|
||||
@@ -151,6 +152,10 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 校验上游错误自动防护开关
|
||||
const normalizedDisableAutoProtection =
|
||||
disableAutoProtection === true || disableAutoProtection === 'true'
|
||||
|
||||
// 验证accountType的有效性
|
||||
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
|
||||
return res
|
||||
@@ -180,7 +185,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
maxConcurrentTasks:
|
||||
maxConcurrentTasks !== undefined && maxConcurrentTasks !== null
|
||||
? Number(maxConcurrentTasks)
|
||||
: 0
|
||||
: 0,
|
||||
disableAutoProtection: normalizedDisableAutoProtection
|
||||
})
|
||||
|
||||
// 如果是分组类型,将账户添加到分组(CCR 归属 Claude 平台分组)
|
||||
@@ -250,6 +256,13 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
|
||||
// 规范化上游错误自动防护开关
|
||||
if (mappedUpdates.disableAutoProtection !== undefined) {
|
||||
mappedUpdates.disableAutoProtection =
|
||||
mappedUpdates.disableAutoProtection === true ||
|
||||
mappedUpdates.disableAutoProtection === 'true'
|
||||
}
|
||||
|
||||
// 处理分组的变更
|
||||
if (mappedUpdates.accountType !== undefined) {
|
||||
// 如果之前是分组类型,需要从所有分组中移除
|
||||
|
||||
@@ -67,7 +67,8 @@ class ClaudeConsoleAccountService {
|
||||
schedulable = true, // 是否可被调度
|
||||
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
||||
quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
|
||||
maxConcurrentTasks = 0 // 最大并发任务数,0表示无限制
|
||||
maxConcurrentTasks = 0, // 最大并发任务数,0表示无限制
|
||||
disableAutoProtection = false // 是否关闭自动防护(429/401/400/529 不自动禁用)
|
||||
} = options
|
||||
|
||||
// 验证必填字段
|
||||
@@ -115,7 +116,8 @@ class ClaudeConsoleAccountService {
|
||||
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
||||
quotaResetTime, // 额度重置时间
|
||||
quotaStoppedAt: '', // 因额度停用的时间
|
||||
maxConcurrentTasks: maxConcurrentTasks.toString() // 最大并发任务数,0表示无限制
|
||||
maxConcurrentTasks: maxConcurrentTasks.toString(), // 最大并发任务数,0表示无限制
|
||||
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护
|
||||
}
|
||||
|
||||
const client = redis.getClientSafe()
|
||||
@@ -153,6 +155,7 @@ class ClaudeConsoleAccountService {
|
||||
quotaResetTime,
|
||||
quotaStoppedAt: null,
|
||||
maxConcurrentTasks, // 新增:返回并发限制配置
|
||||
disableAutoProtection, // 新增:返回自动防护开关
|
||||
activeTaskCount: 0 // 新增:新建账户当前并发数为0
|
||||
}
|
||||
}
|
||||
@@ -213,7 +216,8 @@ class ClaudeConsoleAccountService {
|
||||
|
||||
// 并发控制相关
|
||||
maxConcurrentTasks: parseInt(accountData.maxConcurrentTasks) || 0,
|
||||
activeTaskCount
|
||||
activeTaskCount,
|
||||
disableAutoProtection: accountData.disableAutoProtection === 'true'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -259,6 +263,7 @@ class ClaudeConsoleAccountService {
|
||||
}
|
||||
accountData.isActive = accountData.isActive === 'true'
|
||||
accountData.schedulable = accountData.schedulable !== 'false' // 默认为true
|
||||
accountData.disableAutoProtection = accountData.disableAutoProtection === 'true'
|
||||
|
||||
if (accountData.proxy) {
|
||||
accountData.proxy = JSON.parse(accountData.proxy)
|
||||
@@ -367,6 +372,9 @@ class ClaudeConsoleAccountService {
|
||||
if (updates.maxConcurrentTasks !== undefined) {
|
||||
updatedData.maxConcurrentTasks = updates.maxConcurrentTasks.toString()
|
||||
}
|
||||
if (updates.disableAutoProtection !== undefined) {
|
||||
updatedData.disableAutoProtection = updates.disableAutoProtection.toString()
|
||||
}
|
||||
|
||||
// ✅ 直接保存 subscriptionExpiresAt(如果提供)
|
||||
// Claude Console 没有 token 刷新逻辑,不会覆盖此字段
|
||||
|
||||
@@ -37,6 +37,8 @@ class ClaudeConsoleRelayService {
|
||||
throw new Error('Claude Console Claude account not found')
|
||||
}
|
||||
|
||||
const autoProtectionDisabled = account.disableAutoProtection === true
|
||||
|
||||
logger.info(
|
||||
`📤 Processing Claude Console API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${account.name} (${accountId}), request: ${requestId}`
|
||||
)
|
||||
@@ -248,27 +250,41 @@ class ClaudeConsoleRelayService {
|
||||
|
||||
// 检查错误状态并相应处理
|
||||
if (response.status === 401) {
|
||||
logger.warn(`🚫 Unauthorized error detected for Claude Console account ${accountId}`)
|
||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||
logger.warn(
|
||||
`🚫 Unauthorized error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||
}
|
||||
} else if (accountDisabledError) {
|
||||
logger.error(
|
||||
`🚫 Account disabled error (400) detected for Claude Console account ${accountId}, marking as blocked`
|
||||
`🚫 Account disabled error (400) detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 传入完整的错误详情到 webhook
|
||||
const errorDetails =
|
||||
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
|
||||
await claudeConsoleAccountService.markConsoleAccountBlocked(accountId, errorDetails)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markConsoleAccountBlocked(accountId, errorDetails)
|
||||
}
|
||||
} else if (response.status === 429) {
|
||||
logger.warn(`🚫 Rate limit detected for Claude Console account ${accountId}`)
|
||||
logger.warn(
|
||||
`🚫 Rate limit detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 收到429先检查是否因为超过了手动配置的每日额度
|
||||
await claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||
})
|
||||
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
}
|
||||
} else if (response.status === 529) {
|
||||
logger.warn(`🚫 Overload error detected for Claude Console account ${accountId}`)
|
||||
await claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||
logger.warn(
|
||||
`🚫 Overload error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||
}
|
||||
} else if (response.status === 200 || response.status === 201) {
|
||||
// 如果请求成功,检查并移除错误状态
|
||||
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(accountId)
|
||||
@@ -597,6 +613,7 @@ class ClaudeConsoleRelayService {
|
||||
})
|
||||
|
||||
response.data.on('end', async () => {
|
||||
const autoProtectionDisabled = account.disableAutoProtection === true
|
||||
// 记录原始错误消息到日志(方便调试,包含供应商信息)
|
||||
logger.error(
|
||||
`📝 [Stream] Upstream error response from ${account?.name || accountId}: ${errorDataForCheck.substring(0, 500)}`
|
||||
@@ -609,24 +626,41 @@ class ClaudeConsoleRelayService {
|
||||
)
|
||||
|
||||
if (response.status === 401) {
|
||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||
logger.warn(
|
||||
`🚫 [Stream] Unauthorized error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||
}
|
||||
} else if (accountDisabledError) {
|
||||
logger.error(
|
||||
`🚫 [Stream] Account disabled error (400) detected for Claude Console account ${accountId}, marking as blocked`
|
||||
`🚫 [Stream] Account disabled error (400) detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 传入完整的错误详情到 webhook
|
||||
await claudeConsoleAccountService.markConsoleAccountBlocked(
|
||||
accountId,
|
||||
errorDataForCheck
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markConsoleAccountBlocked(
|
||||
accountId,
|
||||
errorDataForCheck
|
||||
)
|
||||
}
|
||||
} else if (response.status === 429) {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
logger.warn(
|
||||
`🚫 [Stream] Rate limit detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 检查是否因为超过每日额度
|
||||
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||
})
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
}
|
||||
} else if (response.status === 529) {
|
||||
await claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||
logger.warn(
|
||||
`🚫 [Stream] Overload error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
|
||||
@@ -3,6 +3,7 @@ const zlib = require('zlib')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const ProxyHelper = require('../utils/proxyHelper')
|
||||
const { filterForClaude } = require('../utils/headerFilter')
|
||||
const claudeAccountService = require('./claudeAccountService')
|
||||
const unifiedClaudeScheduler = require('./unifiedClaudeScheduler')
|
||||
const sessionHelper = require('../utils/sessionHelper')
|
||||
@@ -877,62 +878,9 @@ class ClaudeRelayService {
|
||||
|
||||
// 🔧 过滤客户端请求头
|
||||
_filterClientHeaders(clientHeaders) {
|
||||
// 需要移除的敏感 headers
|
||||
const sensitiveHeaders = [
|
||||
'content-type',
|
||||
'user-agent',
|
||||
'x-api-key',
|
||||
'authorization',
|
||||
'x-authorization',
|
||||
'host',
|
||||
'content-length',
|
||||
'connection',
|
||||
'proxy-authorization',
|
||||
'content-encoding',
|
||||
'transfer-encoding'
|
||||
]
|
||||
|
||||
// 🆕 需要移除的浏览器相关 headers(避免CORS问题)
|
||||
const browserHeaders = [
|
||||
'origin',
|
||||
'referer',
|
||||
'sec-fetch-mode',
|
||||
'sec-fetch-site',
|
||||
'sec-fetch-dest',
|
||||
'sec-ch-ua',
|
||||
'sec-ch-ua-mobile',
|
||||
'sec-ch-ua-platform',
|
||||
'accept-language',
|
||||
'accept-encoding',
|
||||
'accept',
|
||||
'cache-control',
|
||||
'pragma',
|
||||
'anthropic-dangerous-direct-browser-access' // 这个头可能触发CORS检查
|
||||
]
|
||||
|
||||
// 应该保留的 headers(用于会话一致性和追踪)
|
||||
const allowedHeaders = [
|
||||
'x-request-id',
|
||||
'anthropic-version', // 保留API版本
|
||||
'anthropic-beta' // 保留beta功能
|
||||
]
|
||||
|
||||
const filteredHeaders = {}
|
||||
|
||||
// 转发客户端的非敏感 headers
|
||||
Object.keys(clientHeaders || {}).forEach((key) => {
|
||||
const lowerKey = key.toLowerCase()
|
||||
// 如果在允许列表中,直接保留
|
||||
if (allowedHeaders.includes(lowerKey)) {
|
||||
filteredHeaders[key] = clientHeaders[key]
|
||||
}
|
||||
// 如果不在敏感列表和浏览器列表中,也保留
|
||||
else if (!sensitiveHeaders.includes(lowerKey) && !browserHeaders.includes(lowerKey)) {
|
||||
filteredHeaders[key] = clientHeaders[key]
|
||||
}
|
||||
})
|
||||
|
||||
return filteredHeaders
|
||||
// 使用统一的 headerFilter 工具类 - 移除 CDN、浏览器和代理相关 headers
|
||||
// 同时伪装成正常的直接客户端请求,避免触发上游 API 的安全检查
|
||||
return filterForClaude(clientHeaders)
|
||||
}
|
||||
|
||||
_applyRequestIdentityTransform(body, headers, context = {}) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const axios = require('axios')
|
||||
const ProxyHelper = require('../utils/proxyHelper')
|
||||
const logger = require('../utils/logger')
|
||||
const { filterForOpenAI } = require('../utils/headerFilter')
|
||||
const openaiResponsesAccountService = require('./openaiResponsesAccountService')
|
||||
const apiKeyService = require('./apiKeyService')
|
||||
const unifiedOpenAIScheduler = require('./unifiedOpenAIScheduler')
|
||||
@@ -73,9 +74,9 @@ class OpenAIResponsesRelayService {
|
||||
const targetUrl = `${fullAccount.baseApi}${req.path}`
|
||||
logger.info(`🎯 Forwarding to: ${targetUrl}`)
|
||||
|
||||
// 构建请求头
|
||||
// 构建请求头 - 使用统一的 headerFilter 移除 CDN headers
|
||||
const headers = {
|
||||
...this._filterRequestHeaders(req.headers),
|
||||
...filterForOpenAI(req.headers),
|
||||
Authorization: `Bearer ${fullAccount.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -810,29 +811,10 @@ class OpenAIResponsesRelayService {
|
||||
return { resetsInSeconds, errorData }
|
||||
}
|
||||
|
||||
// 过滤请求头
|
||||
// 过滤请求头 - 已迁移到 headerFilter 工具类
|
||||
// 此方法保留用于向后兼容,实际使用 filterForOpenAI()
|
||||
_filterRequestHeaders(headers) {
|
||||
const filtered = {}
|
||||
const skipHeaders = [
|
||||
'host',
|
||||
'content-length',
|
||||
'authorization',
|
||||
'x-api-key',
|
||||
'x-cr-api-key',
|
||||
'connection',
|
||||
'upgrade',
|
||||
'sec-websocket-key',
|
||||
'sec-websocket-version',
|
||||
'sec-websocket-extensions'
|
||||
]
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (!skipHeaders.includes(key.toLowerCase())) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
return filterForOpenAI(headers)
|
||||
}
|
||||
|
||||
// 估算费用(简化版本,实际应该根据不同的定价模型)
|
||||
|
||||
133
src/utils/headerFilter.js
Normal file
133
src/utils/headerFilter.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 统一的 CDN Headers 过滤列表
|
||||
*
|
||||
* 用于各服务在原有过滤逻辑基础上,额外移除 Cloudflare CDN 和代理相关的 headers
|
||||
* 避免触发上游 API(如 88code)的安全检查
|
||||
*/
|
||||
|
||||
// Cloudflare CDN headers(橙色云代理模式会添加这些)
|
||||
const cdnHeaders = [
|
||||
'x-real-ip',
|
||||
'x-forwarded-for',
|
||||
'x-forwarded-proto',
|
||||
'x-forwarded-host',
|
||||
'x-forwarded-port',
|
||||
'x-accel-buffering',
|
||||
'cf-ray',
|
||||
'cf-connecting-ip',
|
||||
'cf-ipcountry',
|
||||
'cf-visitor',
|
||||
'cf-request-id',
|
||||
'cdn-loop',
|
||||
'true-client-ip'
|
||||
]
|
||||
|
||||
/**
|
||||
* 为 OpenAI/Responses API 过滤 headers
|
||||
* 在原有 skipHeaders 基础上添加 CDN headers
|
||||
*/
|
||||
function filterForOpenAI(headers) {
|
||||
const skipHeaders = [
|
||||
'host',
|
||||
'content-length',
|
||||
'authorization',
|
||||
'x-api-key',
|
||||
'x-cr-api-key',
|
||||
'connection',
|
||||
'upgrade',
|
||||
'sec-websocket-key',
|
||||
'sec-websocket-version',
|
||||
'sec-websocket-extensions',
|
||||
...cdnHeaders // 添加 CDN headers
|
||||
]
|
||||
|
||||
const filtered = {}
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (!skipHeaders.includes(key.toLowerCase())) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Claude/Anthropic API 过滤 headers
|
||||
* 在原有逻辑基础上添加 CDN headers 到敏感列表
|
||||
*/
|
||||
function filterForClaude(headers) {
|
||||
const sensitiveHeaders = [
|
||||
'content-type',
|
||||
'user-agent',
|
||||
'x-api-key',
|
||||
'authorization',
|
||||
'x-authorization',
|
||||
'host',
|
||||
'content-length',
|
||||
'connection',
|
||||
'proxy-authorization',
|
||||
'content-encoding',
|
||||
'transfer-encoding',
|
||||
...cdnHeaders // 添加 CDN headers
|
||||
]
|
||||
|
||||
const browserHeaders = [
|
||||
'origin',
|
||||
'referer',
|
||||
'sec-fetch-mode',
|
||||
'sec-fetch-site',
|
||||
'sec-fetch-dest',
|
||||
'sec-ch-ua',
|
||||
'sec-ch-ua-mobile',
|
||||
'sec-ch-ua-platform',
|
||||
'accept-language',
|
||||
'accept-encoding',
|
||||
'accept',
|
||||
'cache-control',
|
||||
'pragma',
|
||||
'anthropic-dangerous-direct-browser-access'
|
||||
]
|
||||
|
||||
const allowedHeaders = ['x-request-id', 'anthropic-version', 'anthropic-beta']
|
||||
|
||||
const filtered = {}
|
||||
Object.keys(headers || {}).forEach((key) => {
|
||||
const lowerKey = key.toLowerCase()
|
||||
if (allowedHeaders.includes(lowerKey)) {
|
||||
filtered[key] = headers[key]
|
||||
} else if (!sensitiveHeaders.includes(lowerKey) && !browserHeaders.includes(lowerKey)) {
|
||||
filtered[key] = headers[key]
|
||||
}
|
||||
})
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Gemini API 过滤 headers(如果需要转发客户端 headers 时使用)
|
||||
* 目前 Gemini 服务不转发客户端 headers,仅提供此方法备用
|
||||
*/
|
||||
function filterForGemini(headers) {
|
||||
const skipHeaders = [
|
||||
'host',
|
||||
'content-length',
|
||||
'authorization',
|
||||
'x-api-key',
|
||||
'connection',
|
||||
...cdnHeaders // 添加 CDN headers
|
||||
]
|
||||
|
||||
const filtered = {}
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (!skipHeaders.includes(key.toLowerCase())) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cdnHeaders,
|
||||
filterForOpenAI,
|
||||
filterForClaude,
|
||||
filterForGemini
|
||||
}
|
||||
17
web/admin-spa/package-lock.json
generated
17
web/admin-spa/package-lock.json
generated
@@ -1157,6 +1157,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -1351,6 +1352,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1587,6 +1589,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
@@ -3060,13 +3063,15 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -3618,6 +3623,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3764,6 +3770,7 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -3789,7 +3796,7 @@
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -4028,6 +4035,7 @@
|
||||
"integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -4525,6 +4533,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4915,6 +4924,7 @@
|
||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -5115,6 +5125,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz",
|
||||
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.18",
|
||||
"@vue/compiler-sfc": "3.5.18",
|
||||
|
||||
@@ -1451,6 +1451,26 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上游错误处理 -->
|
||||
<div v-if="form.platform === 'claude-console'">
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>上游错误处理</label
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.disableAutoProtection"
|
||||
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
上游错误不自动暂停调度
|
||||
</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
勾选后遇到 401/400/429/529 等上游错误仅记录日志并透传,不自动禁用或限流
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI-Responses 特定字段 -->
|
||||
@@ -3070,6 +3090,26 @@
|
||||
<p class="mt-1 text-xs text-gray-500">账号被限流后暂停调度的时间(分钟)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上游错误处理(编辑模式)-->
|
||||
<div v-if="form.platform === 'claude-console'">
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
上游错误处理
|
||||
</label>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.disableAutoProtection"
|
||||
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
上游错误不自动暂停调度
|
||||
</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
勾选后遇到 401/400/429/529 等上游错误仅记录日志并透传,不自动禁用或限流
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI-Responses 特定字段(编辑模式)-->
|
||||
@@ -3912,6 +3952,7 @@ const form = ref({
|
||||
})(),
|
||||
userAgent: props.account?.userAgent || '',
|
||||
enableRateLimit: props.account ? props.account.rateLimitDuration > 0 : true,
|
||||
disableAutoProtection: props.account?.disableAutoProtection === true,
|
||||
// 额度管理字段
|
||||
dailyQuota: props.account?.dailyQuota || 0,
|
||||
dailyUsage: props.account?.dailyUsage || 0,
|
||||
@@ -5015,6 +5056,10 @@ const createAccount = async () => {
|
||||
data.userAgent = form.value.userAgent || null
|
||||
// 如果不启用限流,传递 0 表示不限流
|
||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||
// 上游错误处理(仅 Claude Console)
|
||||
if (form.value.platform === 'claude-console') {
|
||||
data.disableAutoProtection = !!form.value.disableAutoProtection
|
||||
}
|
||||
// 额度管理字段
|
||||
data.dailyQuota = form.value.dailyQuota || 0
|
||||
data.quotaResetTime = form.value.quotaResetTime || '00:00'
|
||||
@@ -5343,6 +5388,8 @@ const updateAccount = async () => {
|
||||
data.userAgent = form.value.userAgent || null
|
||||
// 如果不启用限流,传递 0 表示不限流
|
||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||
// 上游错误处理
|
||||
data.disableAutoProtection = !!form.value.disableAutoProtection
|
||||
// 额度管理字段
|
||||
data.dailyQuota = form.value.dailyQuota || 0
|
||||
data.quotaResetTime = form.value.quotaResetTime || '00:00'
|
||||
@@ -5964,7 +6011,9 @@ watch(
|
||||
dailyUsage: newAccount.dailyUsage || 0,
|
||||
quotaResetTime: newAccount.quotaResetTime || '00:00',
|
||||
// 并发控制字段
|
||||
maxConcurrentTasks: newAccount.maxConcurrentTasks || 0
|
||||
maxConcurrentTasks: newAccount.maxConcurrentTasks || 0,
|
||||
// 上游错误处理
|
||||
disableAutoProtection: newAccount.disableAutoProtection === true
|
||||
}
|
||||
|
||||
// 如果是Claude Console账户,加载实时使用情况
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-indigo-500 to-blue-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||
></div>
|
||||
<CustomDropdown
|
||||
v-model="accountSortBy"
|
||||
icon="fa-sort-amount-down"
|
||||
v-model="accountsSortBy"
|
||||
:icon="accountsSortOrder === 'asc' ? 'fa-sort-amount-up' : 'fa-sort-amount-down'"
|
||||
icon-color="text-indigo-500"
|
||||
:options="sortOptions"
|
||||
placeholder="选择排序"
|
||||
@change="sortAccounts()"
|
||||
@change="handleDropdownSort"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1873,8 +1873,7 @@ const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCanc
|
||||
// 数据状态
|
||||
const accounts = ref([])
|
||||
const accountsLoading = ref(false)
|
||||
const accountSortBy = ref('name')
|
||||
const accountsSortBy = ref('')
|
||||
const accountsSortBy = ref('name')
|
||||
const accountsSortOrder = ref('asc')
|
||||
const apiKeys = ref([]) // 保留用于其他功能(如删除账户时显示绑定信息)
|
||||
const bindingCounts = ref({}) // 轻量级绑定计数,用于显示"绑定: X 个API Key"
|
||||
@@ -2735,7 +2734,10 @@ const loadClaudeUsage = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 排序账户
|
||||
// 记录上一次的排序字段,用于判断下拉选择是否是同一字段被再次选择
|
||||
let lastDropdownSortField = 'name'
|
||||
|
||||
// 排序账户(表头点击使用)
|
||||
const sortAccounts = (field) => {
|
||||
if (field) {
|
||||
if (accountsSortBy.value === field) {
|
||||
@@ -2744,9 +2746,23 @@ const sortAccounts = (field) => {
|
||||
accountsSortBy.value = field
|
||||
accountsSortOrder.value = 'asc'
|
||||
}
|
||||
// 同步下拉选择器的状态记录
|
||||
lastDropdownSortField = field
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉选择器排序处理(支持再次选择同一选项时切换排序方向)
|
||||
const handleDropdownSort = (field) => {
|
||||
if (field === lastDropdownSortField) {
|
||||
// 选择同一字段,切换排序方向
|
||||
accountsSortOrder.value = accountsSortOrder.value === 'asc' ? 'desc' : 'asc'
|
||||
} else {
|
||||
// 选择不同字段,重置为升序
|
||||
accountsSortOrder.value = 'asc'
|
||||
}
|
||||
lastDropdownSortField = field
|
||||
}
|
||||
|
||||
// 格式化数字(与原版保持一致)
|
||||
const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '0'
|
||||
@@ -3993,20 +4009,20 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 监听排序选择变化
|
||||
watch(accountSortBy, (newVal) => {
|
||||
const fieldMap = {
|
||||
name: 'name',
|
||||
dailyTokens: 'dailyTokens',
|
||||
dailyRequests: 'dailyRequests',
|
||||
totalTokens: 'totalTokens',
|
||||
lastUsed: 'lastUsed'
|
||||
}
|
||||
|
||||
if (fieldMap[newVal]) {
|
||||
sortAccounts(fieldMap[newVal])
|
||||
}
|
||||
})
|
||||
// 监听排序选择变化 - 已重构为 handleDropdownSort,此处注释保留原逻辑参考
|
||||
// watch(accountSortBy, (newVal) => {
|
||||
// const fieldMap = {
|
||||
// name: 'name',
|
||||
// dailyTokens: 'dailyTokens',
|
||||
// dailyRequests: 'dailyRequests',
|
||||
// totalTokens: 'totalTokens',
|
||||
// lastUsed: 'lastUsed'
|
||||
// }
|
||||
//
|
||||
// if (fieldMap[newVal]) {
|
||||
// sortAccounts(fieldMap[newVal])
|
||||
// }
|
||||
// })
|
||||
|
||||
watch(currentPage, () => {
|
||||
updateSelectAllState()
|
||||
|
||||
Reference in New Issue
Block a user