mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'dev'
This commit is contained in:
@@ -481,6 +481,14 @@ export OPENAI_BASE_URL="http://127.0.0.1:3000/openai" # 根据实际填写你服
|
|||||||
export OPENAI_API_KEY="后台创建的API密钥" # 使用后台创建的API密钥
|
export OPENAI_API_KEY="后台创建的API密钥" # 使用后台创建的API密钥
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Codex 额外配置:**
|
||||||
|
|
||||||
|
需要在 `~/.codex/config.toml` 文件中添加以下配置来禁用响应存储:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
disable_response_storage = true
|
||||||
|
```
|
||||||
|
|
||||||
### 5. 第三方工具API接入
|
### 5. 第三方工具API接入
|
||||||
|
|
||||||
本服务支持多种API端点格式,方便接入不同的第三方工具(如Cherry Studio等):
|
本服务支持多种API端点格式,方便接入不同的第三方工具(如Cherry Studio等):
|
||||||
|
|||||||
@@ -30,7 +30,24 @@ const router = express.Router()
|
|||||||
router.get('/users', authenticateAdmin, async (req, res) => {
|
router.get('/users', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userService = require('../services/userService')
|
const userService = require('../services/userService')
|
||||||
const result = await userService.getAllUsers({ isActive: true, limit: 1000 }) // Get all active users
|
|
||||||
|
// Extract query parameters for filtering
|
||||||
|
const { role, isActive } = req.query
|
||||||
|
const options = { limit: 1000 }
|
||||||
|
|
||||||
|
// Apply role filter if provided
|
||||||
|
if (role) {
|
||||||
|
options.role = role
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply isActive filter if provided, otherwise default to active users only
|
||||||
|
if (isActive !== undefined) {
|
||||||
|
options.isActive = isActive === 'true'
|
||||||
|
} else {
|
||||||
|
options.isActive = true // Default to active users for backwards compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await userService.getAllUsers(options)
|
||||||
|
|
||||||
// Extract users array from the paginated result
|
// Extract users array from the paginated result
|
||||||
const allUsers = result.users || []
|
const allUsers = result.users || []
|
||||||
@@ -6204,4 +6221,54 @@ router.post('/migrate-api-keys-azure', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 📋 获取统一Claude Code User-Agent信息
|
||||||
|
router.get('/claude-code-version', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const CACHE_KEY = 'claude_code_user_agent:daily'
|
||||||
|
|
||||||
|
// 获取缓存的统一User-Agent
|
||||||
|
const unifiedUserAgent = await redis.client.get(CACHE_KEY)
|
||||||
|
const ttl = unifiedUserAgent ? await redis.client.ttl(CACHE_KEY) : 0
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
userAgent: unifiedUserAgent,
|
||||||
|
isActive: !!unifiedUserAgent,
|
||||||
|
ttlSeconds: ttl,
|
||||||
|
lastUpdated: unifiedUserAgent ? new Date().toISOString() : null
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Get unified Claude Code User-Agent error:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to get User-Agent information',
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 🗑️ 清除统一Claude Code User-Agent缓存
|
||||||
|
router.post('/claude-code-version/clear', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const CACHE_KEY = 'claude_code_user_agent:daily'
|
||||||
|
|
||||||
|
// 删除缓存的统一User-Agent
|
||||||
|
await redis.client.del(CACHE_KEY)
|
||||||
|
|
||||||
|
logger.info(`🗑️ Admin manually cleared unified Claude Code User-Agent cache`)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Unified User-Agent cache cleared successfully'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Clear unified User-Agent cache error:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to clear cache',
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|||||||
@@ -343,20 +343,22 @@ async function handleLoadCodeAssist(req, res) {
|
|||||||
|
|
||||||
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
||||||
|
|
||||||
// 根据账户配置决定项目ID:
|
// 智能处理项目ID:
|
||||||
// 1. 如果账户有项目ID -> 使用账户的项目ID(强制覆盖)
|
// 1. 如果账户配置了项目ID -> 使用账户的项目ID(覆盖请求中的)
|
||||||
// 2. 如果账户没有项目ID -> 传递 null(移除项目ID)
|
// 2. 如果账户没有项目ID -> 使用请求中的cloudaicompanionProject
|
||||||
let effectiveProjectId = null
|
// 3. 都没有 -> 传null
|
||||||
|
const effectiveProjectId = projectId || cloudaicompanionProject || null
|
||||||
|
|
||||||
if (projectId) {
|
logger.info('📋 loadCodeAssist项目ID处理逻辑', {
|
||||||
// 账户配置了项目ID,强制使用它
|
accountProjectId: projectId,
|
||||||
effectiveProjectId = projectId
|
requestProjectId: cloudaicompanionProject,
|
||||||
logger.info('Using account project ID for loadCodeAssist:', effectiveProjectId)
|
effectiveProjectId,
|
||||||
} else {
|
decision: projectId
|
||||||
// 账户没有配置项目ID,确保不传递项目ID
|
? '使用账户配置'
|
||||||
effectiveProjectId = null
|
: cloudaicompanionProject
|
||||||
logger.info('No project ID in account for loadCodeAssist, removing project parameter')
|
? '使用请求参数'
|
||||||
}
|
: '不使用项目ID'
|
||||||
|
})
|
||||||
|
|
||||||
const response = await geminiAccountService.loadCodeAssist(
|
const response = await geminiAccountService.loadCodeAssist(
|
||||||
client,
|
client,
|
||||||
@@ -413,20 +415,22 @@ async function handleOnboardUser(req, res) {
|
|||||||
|
|
||||||
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
||||||
|
|
||||||
// 根据账户配置决定项目ID:
|
// 智能处理项目ID:
|
||||||
// 1. 如果账户有项目ID -> 使用账户的项目ID(强制覆盖)
|
// 1. 如果账户配置了项目ID -> 使用账户的项目ID(覆盖请求中的)
|
||||||
// 2. 如果账户没有项目ID -> 传递 null(移除项目ID)
|
// 2. 如果账户没有项目ID -> 使用请求中的cloudaicompanionProject
|
||||||
let effectiveProjectId = null
|
// 3. 都没有 -> 传null
|
||||||
|
const effectiveProjectId = projectId || cloudaicompanionProject || null
|
||||||
|
|
||||||
if (projectId) {
|
logger.info('📋 onboardUser项目ID处理逻辑', {
|
||||||
// 账户配置了项目ID,强制使用它
|
accountProjectId: projectId,
|
||||||
effectiveProjectId = projectId
|
requestProjectId: cloudaicompanionProject,
|
||||||
logger.info('Using account project ID:', effectiveProjectId)
|
effectiveProjectId,
|
||||||
} else {
|
decision: projectId
|
||||||
// 账户没有配置项目ID,确保不传递项目ID(即使客户端传了也要移除)
|
? '使用账户配置'
|
||||||
effectiveProjectId = null
|
: cloudaicompanionProject
|
||||||
logger.info('No project ID in account, removing project parameter')
|
? '使用请求参数'
|
||||||
}
|
: '不使用项目ID'
|
||||||
|
})
|
||||||
|
|
||||||
// 如果提供了 tierId,直接调用 onboardUser
|
// 如果提供了 tierId,直接调用 onboardUser
|
||||||
if (tierId) {
|
if (tierId) {
|
||||||
@@ -593,11 +597,24 @@ async function handleGenerateContent(req, res) {
|
|||||||
|
|
||||||
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
||||||
|
|
||||||
|
// 智能处理项目ID:
|
||||||
|
// 1. 如果账户配置了项目ID -> 使用账户的项目ID(覆盖请求中的)
|
||||||
|
// 2. 如果账户没有项目ID -> 使用请求中的项目ID(如果有的话)
|
||||||
|
// 3. 都没有 -> 传null
|
||||||
|
const effectiveProjectId = account.projectId || project || null
|
||||||
|
|
||||||
|
logger.info('📋 项目ID处理逻辑', {
|
||||||
|
accountProjectId: account.projectId,
|
||||||
|
requestProjectId: project,
|
||||||
|
effectiveProjectId,
|
||||||
|
decision: account.projectId ? '使用账户配置' : project ? '使用请求参数' : '不使用项目ID'
|
||||||
|
})
|
||||||
|
|
||||||
const response = await geminiAccountService.generateContent(
|
const response = await geminiAccountService.generateContent(
|
||||||
client,
|
client,
|
||||||
{ model, request: actualRequestData },
|
{ model, request: actualRequestData },
|
||||||
user_prompt_id,
|
user_prompt_id,
|
||||||
account.projectId, // 始终使用账户配置的项目ID,忽略请求中的project
|
effectiveProjectId, // 使用智能决策的项目ID
|
||||||
req.apiKey?.id, // 使用 API Key ID 作为 session ID
|
req.apiKey?.id, // 使用 API Key ID 作为 session ID
|
||||||
proxyConfig // 传递代理配置
|
proxyConfig // 传递代理配置
|
||||||
)
|
)
|
||||||
@@ -729,11 +746,24 @@ async function handleStreamGenerateContent(req, res) {
|
|||||||
|
|
||||||
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
|
||||||
|
|
||||||
|
// 智能处理项目ID:
|
||||||
|
// 1. 如果账户配置了项目ID -> 使用账户的项目ID(覆盖请求中的)
|
||||||
|
// 2. 如果账户没有项目ID -> 使用请求中的项目ID(如果有的话)
|
||||||
|
// 3. 都没有 -> 传null
|
||||||
|
const effectiveProjectId = account.projectId || project || null
|
||||||
|
|
||||||
|
logger.info('📋 流式请求项目ID处理逻辑', {
|
||||||
|
accountProjectId: account.projectId,
|
||||||
|
requestProjectId: project,
|
||||||
|
effectiveProjectId,
|
||||||
|
decision: account.projectId ? '使用账户配置' : project ? '使用请求参数' : '不使用项目ID'
|
||||||
|
})
|
||||||
|
|
||||||
const streamResponse = await geminiAccountService.generateContentStream(
|
const streamResponse = await geminiAccountService.generateContentStream(
|
||||||
client,
|
client,
|
||||||
{ model, request: actualRequestData },
|
{ model, request: actualRequestData },
|
||||||
user_prompt_id,
|
user_prompt_id,
|
||||||
account.projectId, // 始终使用账户配置的项目ID,忽略请求中的project
|
effectiveProjectId, // 使用智能决策的项目ID
|
||||||
req.apiKey?.id, // 使用 API Key ID 作为 session ID
|
req.apiKey?.id, // 使用 API Key ID 作为 session ID
|
||||||
abortController.signal, // 传递中止信号
|
abortController.signal, // 传递中止信号
|
||||||
proxyConfig // 传递代理配置
|
proxyConfig // 传递代理配置
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ class ClaudeAccountService {
|
|||||||
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
subscriptionInfo = null, // 手动设置的订阅信息
|
subscriptionInfo = null, // 手动设置的订阅信息
|
||||||
autoStopOnWarning = false // 5小时使用量接近限制时自动停止调度
|
autoStopOnWarning = false, // 5小时使用量接近限制时自动停止调度
|
||||||
|
useUnifiedUserAgent = false // 是否使用统一Claude Code版本的User-Agent
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const accountId = uuidv4()
|
const accountId = uuidv4()
|
||||||
@@ -91,6 +92,7 @@ class ClaudeAccountService {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
schedulable: schedulable.toString(), // 是否可被调度
|
schedulable: schedulable.toString(), // 是否可被调度
|
||||||
autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度
|
autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度
|
||||||
|
useUnifiedUserAgent: useUnifiedUserAgent.toString(), // 是否使用统一Claude Code版本的User-Agent
|
||||||
// 优先使用手动设置的订阅信息,否则使用OAuth数据中的,否则默认为空
|
// 优先使用手动设置的订阅信息,否则使用OAuth数据中的,否则默认为空
|
||||||
subscriptionInfo: subscriptionInfo
|
subscriptionInfo: subscriptionInfo
|
||||||
? JSON.stringify(subscriptionInfo)
|
? JSON.stringify(subscriptionInfo)
|
||||||
@@ -122,6 +124,7 @@ class ClaudeAccountService {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
schedulable: schedulable.toString(), // 是否可被调度
|
schedulable: schedulable.toString(), // 是否可被调度
|
||||||
autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度
|
autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度
|
||||||
|
useUnifiedUserAgent: useUnifiedUserAgent.toString(), // 是否使用统一Claude Code版本的User-Agent
|
||||||
// 手动设置的订阅信息
|
// 手动设置的订阅信息
|
||||||
subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) : ''
|
subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) : ''
|
||||||
}
|
}
|
||||||
@@ -487,6 +490,8 @@ class ClaudeAccountService {
|
|||||||
schedulable: account.schedulable !== 'false', // 默认为true,兼容历史数据
|
schedulable: account.schedulable !== 'false', // 默认为true,兼容历史数据
|
||||||
// 添加自动停止调度设置
|
// 添加自动停止调度设置
|
||||||
autoStopOnWarning: account.autoStopOnWarning === 'true', // 默认为false
|
autoStopOnWarning: account.autoStopOnWarning === 'true', // 默认为false
|
||||||
|
// 添加统一User-Agent设置
|
||||||
|
useUnifiedUserAgent: account.useUnifiedUserAgent === 'true', // 默认为false
|
||||||
// 添加停止原因
|
// 添加停止原因
|
||||||
stoppedReason: account.stoppedReason || null
|
stoppedReason: account.stoppedReason || null
|
||||||
}
|
}
|
||||||
@@ -522,7 +527,8 @@ class ClaudeAccountService {
|
|||||||
'priority',
|
'priority',
|
||||||
'schedulable',
|
'schedulable',
|
||||||
'subscriptionInfo',
|
'subscriptionInfo',
|
||||||
'autoStopOnWarning'
|
'autoStopOnWarning',
|
||||||
|
'useUnifiedUserAgent'
|
||||||
]
|
]
|
||||||
const updatedData = { ...accountData }
|
const updatedData = { ...accountData }
|
||||||
|
|
||||||
@@ -1689,9 +1695,31 @@ class ClaudeAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚫 标记账户为未授权状态(401错误)
|
// 🚫 通用的账户错误标记方法
|
||||||
async markAccountUnauthorized(accountId, sessionHash = null) {
|
async markAccountError(accountId, errorType, sessionHash = null) {
|
||||||
|
const ERROR_CONFIG = {
|
||||||
|
unauthorized: {
|
||||||
|
status: 'unauthorized',
|
||||||
|
errorMessage: 'Account unauthorized (401 errors detected)',
|
||||||
|
timestampField: 'unauthorizedAt',
|
||||||
|
errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
||||||
|
logMessage: 'unauthorized'
|
||||||
|
},
|
||||||
|
blocked: {
|
||||||
|
status: 'blocked',
|
||||||
|
errorMessage: 'Account blocked (403 error detected - account may be suspended by Claude)',
|
||||||
|
timestampField: 'blockedAt',
|
||||||
|
errorCode: 'CLAUDE_OAUTH_BLOCKED',
|
||||||
|
logMessage: 'blocked'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const errorConfig = ERROR_CONFIG[errorType]
|
||||||
|
if (!errorConfig) {
|
||||||
|
throw new Error(`Unsupported error type: ${errorType}`)
|
||||||
|
}
|
||||||
|
|
||||||
const accountData = await redis.getClaudeAccount(accountId)
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
if (!accountData || Object.keys(accountData).length === 0) {
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
throw new Error('Account not found')
|
throw new Error('Account not found')
|
||||||
@@ -1699,10 +1727,10 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
// 更新账户状态
|
// 更新账户状态
|
||||||
const updatedAccountData = { ...accountData }
|
const updatedAccountData = { ...accountData }
|
||||||
updatedAccountData.status = 'unauthorized'
|
updatedAccountData.status = errorConfig.status
|
||||||
updatedAccountData.schedulable = 'false' // 设置为不可调度
|
updatedAccountData.schedulable = 'false' // 设置为不可调度
|
||||||
updatedAccountData.errorMessage = 'Account unauthorized (401 errors detected)'
|
updatedAccountData.errorMessage = errorConfig.errorMessage
|
||||||
updatedAccountData.unauthorizedAt = new Date().toISOString()
|
updatedAccountData[errorConfig.timestampField] = new Date().toISOString()
|
||||||
|
|
||||||
// 保存更新后的账户数据
|
// 保存更新后的账户数据
|
||||||
await redis.setClaudeAccount(accountId, updatedAccountData)
|
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||||
@@ -1714,7 +1742,7 @@ class ClaudeAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling`
|
`⚠️ Account ${accountData.name} (${accountId}) marked as ${errorConfig.logMessage} and disabled for scheduling`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 发送Webhook通知
|
// 发送Webhook通知
|
||||||
@@ -1724,9 +1752,10 @@ class ClaudeAccountService {
|
|||||||
accountId,
|
accountId,
|
||||||
accountName: accountData.name,
|
accountName: accountData.name,
|
||||||
platform: 'claude-oauth',
|
platform: 'claude-oauth',
|
||||||
status: 'unauthorized',
|
status: errorConfig.status,
|
||||||
errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
errorCode: errorConfig.errorCode,
|
||||||
reason: 'Account unauthorized (401 errors detected)'
|
reason: errorConfig.errorMessage,
|
||||||
|
timestamp: getISOStringWithTimezone(new Date())
|
||||||
})
|
})
|
||||||
} catch (webhookError) {
|
} catch (webhookError) {
|
||||||
logger.error('Failed to send webhook notification:', webhookError)
|
logger.error('Failed to send webhook notification:', webhookError)
|
||||||
@@ -1734,11 +1763,21 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
|
logger.error(`❌ Failed to mark account ${accountId} as ${errorType}:`, error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账户为未授权状态(401错误)
|
||||||
|
async markAccountUnauthorized(accountId, sessionHash = null) {
|
||||||
|
return this.markAccountError(accountId, 'unauthorized', sessionHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账户为被封锁状态(403错误)
|
||||||
|
async markAccountBlocked(accountId, sessionHash = null) {
|
||||||
|
return this.markAccountError(accountId, 'blocked', sessionHash)
|
||||||
|
}
|
||||||
|
|
||||||
// 🔄 重置账户所有异常状态
|
// 🔄 重置账户所有异常状态
|
||||||
async resetAccountStatus(accountId) {
|
async resetAccountStatus(accountId) {
|
||||||
try {
|
try {
|
||||||
@@ -1763,6 +1802,7 @@ class ClaudeAccountService {
|
|||||||
// 清除错误相关字段
|
// 清除错误相关字段
|
||||||
delete updatedAccountData.errorMessage
|
delete updatedAccountData.errorMessage
|
||||||
delete updatedAccountData.unauthorizedAt
|
delete updatedAccountData.unauthorizedAt
|
||||||
|
delete updatedAccountData.blockedAt
|
||||||
delete updatedAccountData.rateLimitedAt
|
delete updatedAccountData.rateLimitedAt
|
||||||
delete updatedAccountData.rateLimitStatus
|
delete updatedAccountData.rateLimitStatus
|
||||||
delete updatedAccountData.rateLimitEndAt
|
delete updatedAccountData.rateLimitEndAt
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class ClaudeCodeHeadersService {
|
|||||||
if (!userAgent) {
|
if (!userAgent) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const match = userAgent.match(/claude-cli\/(\d+\.\d+\.\d+)/)
|
const match = userAgent.match(/claude-cli\/([\d.]+(?:[a-zA-Z0-9-]*)?)/i)
|
||||||
return match ? match[1] : null
|
return match ? match[1] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class ClaudeCodeHeadersService {
|
|||||||
|
|
||||||
// 检查是否有 user-agent
|
// 检查是否有 user-agent
|
||||||
const userAgent = extractedHeaders['user-agent']
|
const userAgent = extractedHeaders['user-agent']
|
||||||
if (!userAgent || !userAgent.includes('claude-cli')) {
|
if (!userAgent || !/^claude-cli\/[\d.]+\s+\(/i.test(userAgent)) {
|
||||||
// 不是 Claude Code 的请求,不存储
|
// 不是 Claude Code 的请求,不存储
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const sessionHelper = require('../utils/sessionHelper')
|
|||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const claudeCodeHeadersService = require('./claudeCodeHeadersService')
|
const claudeCodeHeadersService = require('./claudeCodeHeadersService')
|
||||||
|
const redis = require('../models/redis')
|
||||||
|
|
||||||
class ClaudeRelayService {
|
class ClaudeRelayService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -23,7 +24,7 @@ class ClaudeRelayService {
|
|||||||
isRealClaudeCodeRequest(requestBody, clientHeaders) {
|
isRealClaudeCodeRequest(requestBody, clientHeaders) {
|
||||||
// 检查 user-agent 是否匹配 Claude Code 格式
|
// 检查 user-agent 是否匹配 Claude Code 格式
|
||||||
const userAgent = clientHeaders?.['user-agent'] || clientHeaders?.['User-Agent'] || ''
|
const userAgent = clientHeaders?.['user-agent'] || clientHeaders?.['User-Agent'] || ''
|
||||||
const isClaudeCodeUserAgent = /claude-cli\/\d+\.\d+\.\d+/.test(userAgent)
|
const isClaudeCodeUserAgent = /^claude-cli\/[\d.]+\s+\(/i.test(userAgent)
|
||||||
|
|
||||||
// 检查系统提示词是否包含 Claude Code 标识
|
// 检查系统提示词是否包含 Claude Code 标识
|
||||||
const hasClaudeCodeSystemPrompt = this._hasClaudeCodeSystemPrompt(requestBody)
|
const hasClaudeCodeSystemPrompt = this._hasClaudeCodeSystemPrompt(requestBody)
|
||||||
@@ -197,6 +198,13 @@ class ClaudeRelayService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 检查是否为403状态码(禁止访问)
|
||||||
|
else if (response.statusCode === 403) {
|
||||||
|
logger.error(
|
||||||
|
`🚫 Forbidden error (403) detected for account ${accountId}, marking as blocked`
|
||||||
|
)
|
||||||
|
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||||
|
}
|
||||||
// 检查是否为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}`)
|
||||||
@@ -610,6 +618,12 @@ class ClaudeRelayService {
|
|||||||
) {
|
) {
|
||||||
const url = new URL(this.claudeApiUrl)
|
const url = new URL(this.claudeApiUrl)
|
||||||
|
|
||||||
|
// 获取账户信息用于统一 User-Agent
|
||||||
|
const account = await claudeAccountService.getAccount(accountId)
|
||||||
|
|
||||||
|
// 获取统一的 User-Agent
|
||||||
|
const unifiedUA = await this.captureAndGetUnifiedUserAgent(clientHeaders, account)
|
||||||
|
|
||||||
// 获取过滤后的客户端 headers
|
// 获取过滤后的客户端 headers
|
||||||
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
||||||
|
|
||||||
@@ -656,11 +670,19 @@ class ClaudeRelayService {
|
|||||||
timeout: config.proxy.timeout
|
timeout: config.proxy.timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果客户端没有提供 User-Agent,使用默认值
|
// 使用统一 User-Agent 或客户端提供的,最后使用默认值
|
||||||
if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
|
if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
|
||||||
options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)'
|
const userAgent =
|
||||||
|
unifiedUA ||
|
||||||
|
clientHeaders?.['user-agent'] ||
|
||||||
|
clientHeaders?.['User-Agent'] ||
|
||||||
|
'claude-cli/1.0.102 (external, cli)'
|
||||||
|
options.headers['User-Agent'] = userAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`🔗 指纹是这个: ${options.headers['User-Agent']}`)
|
||||||
|
logger.info(`🔗 指纹是这个: ${options.headers['user-agent']}`)
|
||||||
|
|
||||||
// 使用自定义的 betaHeader 或默认值
|
// 使用自定义的 betaHeader 或默认值
|
||||||
const betaHeader =
|
const betaHeader =
|
||||||
requestOptions?.betaHeader !== undefined ? requestOptions.betaHeader : this.betaHeader
|
requestOptions?.betaHeader !== undefined ? requestOptions.betaHeader : this.betaHeader
|
||||||
@@ -868,6 +890,12 @@ class ClaudeRelayService {
|
|||||||
streamTransformer = null,
|
streamTransformer = null,
|
||||||
requestOptions = {}
|
requestOptions = {}
|
||||||
) {
|
) {
|
||||||
|
// 获取账户信息用于统一 User-Agent
|
||||||
|
const account = await claudeAccountService.getAccount(accountId)
|
||||||
|
|
||||||
|
// 获取统一的 User-Agent
|
||||||
|
const unifiedUA = await this.captureAndGetUnifiedUserAgent(clientHeaders, account)
|
||||||
|
|
||||||
// 获取过滤后的客户端 headers
|
// 获取过滤后的客户端 headers
|
||||||
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
||||||
|
|
||||||
@@ -908,9 +936,14 @@ class ClaudeRelayService {
|
|||||||
timeout: config.proxy.timeout
|
timeout: config.proxy.timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果客户端没有提供 User-Agent,使用默认值
|
// 使用统一 User-Agent 或客户端提供的,最后使用默认值
|
||||||
if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
|
if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
|
||||||
options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)'
|
const userAgent =
|
||||||
|
unifiedUA ||
|
||||||
|
clientHeaders?.['user-agent'] ||
|
||||||
|
clientHeaders?.['User-Agent'] ||
|
||||||
|
'claude-cli/1.0.102 (external, cli)'
|
||||||
|
options.headers['User-Agent'] = userAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用自定义的 betaHeader 或默认值
|
// 使用自定义的 betaHeader 或默认值
|
||||||
@@ -927,8 +960,32 @@ class ClaudeRelayService {
|
|||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
// 将错误处理逻辑封装在一个异步函数中
|
// 将错误处理逻辑封装在一个异步函数中
|
||||||
const handleErrorResponse = async () => {
|
const handleErrorResponse = async () => {
|
||||||
// 增加对5xx错误的处理
|
if (res.statusCode === 401) {
|
||||||
if (res.statusCode >= 500 && res.statusCode < 600) {
|
logger.warn(`🔐 [Stream] Unauthorized error (401) detected for account ${accountId}`)
|
||||||
|
|
||||||
|
await this.recordUnauthorizedError(accountId)
|
||||||
|
|
||||||
|
const errorCount = await this.getUnauthorizedErrorCount(accountId)
|
||||||
|
logger.info(
|
||||||
|
`🔐 [Stream] Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errorCount >= 1) {
|
||||||
|
logger.error(
|
||||||
|
`❌ [Stream] Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized`
|
||||||
|
)
|
||||||
|
await unifiedClaudeScheduler.markAccountUnauthorized(
|
||||||
|
accountId,
|
||||||
|
accountType,
|
||||||
|
sessionHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (res.statusCode === 403) {
|
||||||
|
logger.error(
|
||||||
|
`🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked`
|
||||||
|
)
|
||||||
|
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||||
|
} else if (res.statusCode >= 500 && res.statusCode < 600) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
||||||
)
|
)
|
||||||
@@ -1398,7 +1455,12 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
// 如果客户端没有提供 User-Agent,使用默认值
|
// 如果客户端没有提供 User-Agent,使用默认值
|
||||||
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) {
|
||||||
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)'
|
// 第三个方法不支持统一 User-Agent,使用简化逻辑
|
||||||
|
const userAgent =
|
||||||
|
clientHeaders?.['user-agent'] ||
|
||||||
|
clientHeaders?.['User-Agent'] ||
|
||||||
|
'claude-cli/1.0.102 (external, cli)'
|
||||||
|
options.headers['User-Agent'] = userAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用自定义的 betaHeader 或默认值
|
// 使用自定义的 betaHeader 或默认值
|
||||||
@@ -1535,7 +1597,6 @@ class ClaudeRelayService {
|
|||||||
async recordUnauthorizedError(accountId) {
|
async recordUnauthorizedError(accountId) {
|
||||||
try {
|
try {
|
||||||
const key = `claude_account:${accountId}:401_errors`
|
const key = `claude_account:${accountId}:401_errors`
|
||||||
const redis = require('../models/redis')
|
|
||||||
|
|
||||||
// 增加错误计数,设置5分钟过期时间
|
// 增加错误计数,设置5分钟过期时间
|
||||||
await redis.client.incr(key)
|
await redis.client.incr(key)
|
||||||
@@ -1551,7 +1612,6 @@ class ClaudeRelayService {
|
|||||||
async getUnauthorizedErrorCount(accountId) {
|
async getUnauthorizedErrorCount(accountId) {
|
||||||
try {
|
try {
|
||||||
const key = `claude_account:${accountId}:401_errors`
|
const key = `claude_account:${accountId}:401_errors`
|
||||||
const redis = require('../models/redis')
|
|
||||||
|
|
||||||
const count = await redis.client.get(key)
|
const count = await redis.client.get(key)
|
||||||
return parseInt(count) || 0
|
return parseInt(count) || 0
|
||||||
@@ -1565,7 +1625,6 @@ class ClaudeRelayService {
|
|||||||
async clearUnauthorizedErrors(accountId) {
|
async clearUnauthorizedErrors(accountId) {
|
||||||
try {
|
try {
|
||||||
const key = `claude_account:${accountId}:401_errors`
|
const key = `claude_account:${accountId}:401_errors`
|
||||||
const redis = require('../models/redis')
|
|
||||||
|
|
||||||
await redis.client.del(key)
|
await redis.client.del(key)
|
||||||
logger.info(`✅ Cleared 401 error count for account ${accountId}`)
|
logger.info(`✅ Cleared 401 error count for account ${accountId}`)
|
||||||
@@ -1574,6 +1633,103 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔧 动态捕获并获取统一的 User-Agent
|
||||||
|
async captureAndGetUnifiedUserAgent(clientHeaders, account) {
|
||||||
|
if (account.useUnifiedUserAgent !== 'true') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_KEY = 'claude_code_user_agent:daily'
|
||||||
|
const TTL = 90000 // 25小时
|
||||||
|
|
||||||
|
// ⚠️ 重要:这里通过正则表达式判断是否为 Claude Code 客户端
|
||||||
|
// 如果未来 Claude Code 的 User-Agent 格式发生变化,需要更新这个正则表达式
|
||||||
|
// 当前已知格式:claude-cli/1.0.102 (external, cli)
|
||||||
|
const CLAUDE_CODE_UA_PATTERN = /^claude-cli\/[\d.]+\s+\(/i
|
||||||
|
|
||||||
|
const clientUA = clientHeaders?.['user-agent'] || clientHeaders?.['User-Agent']
|
||||||
|
let cachedUA = await redis.client.get(CACHE_KEY)
|
||||||
|
|
||||||
|
if (clientUA && CLAUDE_CODE_UA_PATTERN.test(clientUA)) {
|
||||||
|
if (!cachedUA) {
|
||||||
|
// 没有缓存,直接存储
|
||||||
|
await redis.client.setex(CACHE_KEY, TTL, clientUA)
|
||||||
|
logger.info(`📱 Captured unified Claude Code User-Agent: ${clientUA}`)
|
||||||
|
cachedUA = clientUA
|
||||||
|
} else {
|
||||||
|
// 有缓存,比较版本号,保存更新的版本
|
||||||
|
const shouldUpdate = this.compareClaudeCodeVersions(clientUA, cachedUA)
|
||||||
|
if (shouldUpdate) {
|
||||||
|
await redis.client.setex(CACHE_KEY, TTL, clientUA)
|
||||||
|
logger.info(`🔄 Updated to newer Claude Code User-Agent: ${clientUA} (was: ${cachedUA})`)
|
||||||
|
cachedUA = clientUA
|
||||||
|
} else {
|
||||||
|
// 当前版本不比缓存版本新,仅刷新TTL
|
||||||
|
await redis.client.expire(CACHE_KEY, TTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedUA // 没有缓存返回 null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 比较Claude Code版本号,判断是否需要更新
|
||||||
|
// 返回 true 表示 newUA 版本更新,需要更新缓存
|
||||||
|
compareClaudeCodeVersions(newUA, cachedUA) {
|
||||||
|
try {
|
||||||
|
// 提取版本号:claude-cli/1.0.102 (external, cli) -> 1.0.102
|
||||||
|
// 支持多段版本号格式,如 1.0.102、2.1.0.beta1 等
|
||||||
|
const newVersionMatch = newUA.match(/claude-cli\/([\d.]+(?:[a-zA-Z0-9-]*)?)/i)
|
||||||
|
const cachedVersionMatch = cachedUA.match(/claude-cli\/([\d.]+(?:[a-zA-Z0-9-]*)?)/i)
|
||||||
|
|
||||||
|
if (!newVersionMatch || !cachedVersionMatch) {
|
||||||
|
// 无法解析版本号,优先使用新的
|
||||||
|
logger.warn(`⚠️ Unable to parse Claude Code versions: new=${newUA}, cached=${cachedUA}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersion = newVersionMatch[1]
|
||||||
|
const cachedVersion = cachedVersionMatch[1]
|
||||||
|
|
||||||
|
// 比较版本号 (semantic version)
|
||||||
|
const compareResult = this.compareSemanticVersions(newVersion, cachedVersion)
|
||||||
|
|
||||||
|
logger.debug(`🔍 Version comparison: ${newVersion} vs ${cachedVersion} = ${compareResult}`)
|
||||||
|
|
||||||
|
return compareResult > 0 // 新版本更大则返回 true
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`⚠️ Error comparing Claude Code versions, defaulting to update: ${error.message}`)
|
||||||
|
return true // 出错时优先使用新的
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔢 比较版本号
|
||||||
|
// 返回:1 表示 v1 > v2,-1 表示 v1 < v2,0 表示相等
|
||||||
|
compareSemanticVersions(version1, version2) {
|
||||||
|
// 将版本号字符串按"."分割成数字数组
|
||||||
|
const arr1 = version1.split('.')
|
||||||
|
const arr2 = version2.split('.')
|
||||||
|
|
||||||
|
// 获取两个版本号数组中的最大长度
|
||||||
|
const maxLength = Math.max(arr1.length, arr2.length)
|
||||||
|
|
||||||
|
// 循环遍历,逐段比较版本号
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
// 如果某个版本号的某一段不存在,则视为0
|
||||||
|
const num1 = parseInt(arr1[i] || 0, 10)
|
||||||
|
const num2 = parseInt(arr2[i] || 0, 10)
|
||||||
|
|
||||||
|
if (num1 > num2) {
|
||||||
|
return 1 // version1 大于 version2
|
||||||
|
}
|
||||||
|
if (num1 < num2) {
|
||||||
|
return -1 // version1 小于 version2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0 // 两个版本号相等
|
||||||
|
}
|
||||||
|
|
||||||
// 🎯 健康检查
|
// 🎯 健康检查
|
||||||
async healthCheck() {
|
async healthCheck() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1022,15 +1022,23 @@ async function loadCodeAssist(client, projectId = null, proxyConfig = null) {
|
|||||||
const clientMetadata = {
|
const clientMetadata = {
|
||||||
ideType: 'IDE_UNSPECIFIED',
|
ideType: 'IDE_UNSPECIFIED',
|
||||||
platform: 'PLATFORM_UNSPECIFIED',
|
platform: 'PLATFORM_UNSPECIFIED',
|
||||||
pluginType: 'GEMINI',
|
pluginType: 'GEMINI'
|
||||||
duetProject: projectId
|
}
|
||||||
|
|
||||||
|
// 只有当projectId存在时才添加duetProject
|
||||||
|
if (projectId) {
|
||||||
|
clientMetadata.duetProject = projectId
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
cloudaicompanionProject: projectId,
|
|
||||||
metadata: clientMetadata
|
metadata: clientMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有当projectId存在时才添加cloudaicompanionProject
|
||||||
|
if (projectId) {
|
||||||
|
request.cloudaicompanionProject = projectId
|
||||||
|
}
|
||||||
|
|
||||||
const axiosConfig = {
|
const axiosConfig = {
|
||||||
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:loadCodeAssist`,
|
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:loadCodeAssist`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1096,10 +1104,14 @@ async function onboardUser(client, tierId, projectId, clientMetadata, proxyConfi
|
|||||||
|
|
||||||
const onboardReq = {
|
const onboardReq = {
|
||||||
tierId,
|
tierId,
|
||||||
cloudaicompanionProject: projectId,
|
|
||||||
metadata: clientMetadata
|
metadata: clientMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有当projectId存在时才添加cloudaicompanionProject
|
||||||
|
if (projectId) {
|
||||||
|
onboardReq.cloudaicompanionProject = projectId
|
||||||
|
}
|
||||||
|
|
||||||
// 创建基础axios配置
|
// 创建基础axios配置
|
||||||
const baseAxiosConfig = {
|
const baseAxiosConfig = {
|
||||||
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:onboardUser`,
|
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:onboardUser`,
|
||||||
@@ -1278,7 +1290,6 @@ async function generateContent(
|
|||||||
// 按照 gemini-cli 的转换格式构造请求
|
// 按照 gemini-cli 的转换格式构造请求
|
||||||
const request = {
|
const request = {
|
||||||
model: requestData.model,
|
model: requestData.model,
|
||||||
project: projectId,
|
|
||||||
user_prompt_id: userPromptId,
|
user_prompt_id: userPromptId,
|
||||||
request: {
|
request: {
|
||||||
...requestData.request,
|
...requestData.request,
|
||||||
@@ -1286,6 +1297,11 @@ async function generateContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有当projectId存在时才添加project字段
|
||||||
|
if (projectId) {
|
||||||
|
request.project = projectId
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('🤖 generateContent API调用开始', {
|
logger.info('🤖 generateContent API调用开始', {
|
||||||
model: requestData.model,
|
model: requestData.model,
|
||||||
userPromptId,
|
userPromptId,
|
||||||
@@ -1340,7 +1356,6 @@ async function generateContentStream(
|
|||||||
// 按照 gemini-cli 的转换格式构造请求
|
// 按照 gemini-cli 的转换格式构造请求
|
||||||
const request = {
|
const request = {
|
||||||
model: requestData.model,
|
model: requestData.model,
|
||||||
project: projectId,
|
|
||||||
user_prompt_id: userPromptId,
|
user_prompt_id: userPromptId,
|
||||||
request: {
|
request: {
|
||||||
...requestData.request,
|
...requestData.request,
|
||||||
@@ -1348,6 +1363,11 @@ async function generateContentStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有当projectId存在时才添加project字段
|
||||||
|
if (projectId) {
|
||||||
|
request.project = projectId
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('🌊 streamGenerateContent API调用开始', {
|
logger.info('🌊 streamGenerateContent API调用开始', {
|
||||||
model: requestData.model,
|
model: requestData.model,
|
||||||
userPromptId,
|
userPromptId,
|
||||||
|
|||||||
@@ -636,6 +636,32 @@ class UnifiedClaudeScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账户为被封锁状态(403错误)
|
||||||
|
async markAccountBlocked(accountId, accountType, sessionHash = null) {
|
||||||
|
try {
|
||||||
|
// 只处理claude-official类型的账户,不处理claude-console和gemini
|
||||||
|
if (accountType === 'claude-official') {
|
||||||
|
await claudeAccountService.markAccountBlocked(accountId, sessionHash)
|
||||||
|
|
||||||
|
// 删除会话映射
|
||||||
|
if (sessionHash) {
|
||||||
|
await this._deleteSessionMapping(sessionHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`🚫 Account ${accountId} marked as blocked due to 403 error`)
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`ℹ️ Skipping blocked marking for non-Claude OAuth account: ${accountId} (${accountType})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to mark account as blocked: ${accountId} (${accountType})`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🚫 标记Claude Console账户为封锁状态(模型不支持)
|
// 🚫 标记Claude Console账户为封锁状态(模型不支持)
|
||||||
async blockConsoleAccount(accountId, reason) {
|
async blockConsoleAccount(accountId, reason) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const logger = require('./logger')
|
|||||||
class SessionHelper {
|
class SessionHelper {
|
||||||
/**
|
/**
|
||||||
* 生成会话哈希,用于sticky会话保持
|
* 生成会话哈希,用于sticky会话保持
|
||||||
* 基于Anthropic的prompt caching机制,优先使用cacheable内容
|
* 基于Anthropic的prompt caching机制,优先使用metadata中的session ID
|
||||||
* @param {Object} requestBody - 请求体
|
* @param {Object} requestBody - 请求体
|
||||||
* @returns {string|null} - 32字符的会话哈希,如果无法生成则返回null
|
* @returns {string|null} - 32字符的会话哈希,如果无法生成则返回null
|
||||||
*/
|
*/
|
||||||
@@ -13,11 +13,24 @@ class SessionHelper {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. 最高优先级:使用metadata中的session ID(直接使用,无需hash)
|
||||||
|
if (requestBody.metadata && requestBody.metadata.user_id) {
|
||||||
|
// 提取 session_xxx 部分
|
||||||
|
const userIdString = requestBody.metadata.user_id
|
||||||
|
const sessionMatch = userIdString.match(/session_([a-f0-9-]{36})/)
|
||||||
|
if (sessionMatch && sessionMatch[1]) {
|
||||||
|
const sessionId = sessionMatch[1]
|
||||||
|
// 直接返回session ID
|
||||||
|
logger.debug(`📋 Session ID extracted from metadata.user_id: ${sessionId}`)
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let cacheableContent = ''
|
let cacheableContent = ''
|
||||||
const system = requestBody.system || ''
|
const system = requestBody.system || ''
|
||||||
const messages = requestBody.messages || []
|
const messages = requestBody.messages || []
|
||||||
|
|
||||||
// 1. 优先提取带有cache_control: {"type": "ephemeral"}的内容
|
// 2. 提取带有cache_control: {"type": "ephemeral"}的内容
|
||||||
// 检查system中的cacheable内容
|
// 检查system中的cacheable内容
|
||||||
if (Array.isArray(system)) {
|
if (Array.isArray(system)) {
|
||||||
for (const part of system) {
|
for (const part of system) {
|
||||||
@@ -30,13 +43,13 @@ class SessionHelper {
|
|||||||
// 检查messages中的cacheable内容
|
// 检查messages中的cacheable内容
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const content = msg.content || ''
|
const content = msg.content || ''
|
||||||
|
let hasCacheControl = false
|
||||||
|
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
for (const part of content) {
|
for (const part of content) {
|
||||||
if (part && part.cache_control && part.cache_control.type === 'ephemeral') {
|
if (part && part.cache_control && part.cache_control.type === 'ephemeral') {
|
||||||
if (part.type === 'text') {
|
hasCacheControl = true
|
||||||
cacheableContent += part.text || ''
|
break
|
||||||
}
|
|
||||||
// 其他类型(如image)不参与hash计算
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
@@ -44,12 +57,31 @@ class SessionHelper {
|
|||||||
msg.cache_control &&
|
msg.cache_control &&
|
||||||
msg.cache_control.type === 'ephemeral'
|
msg.cache_control.type === 'ephemeral'
|
||||||
) {
|
) {
|
||||||
// 罕见情况,但需要检查
|
hasCacheControl = true
|
||||||
cacheableContent += content
|
}
|
||||||
|
|
||||||
|
if (hasCacheControl) {
|
||||||
|
for (const message of messages) {
|
||||||
|
let messageText = ''
|
||||||
|
if (typeof message.content === 'string') {
|
||||||
|
messageText = message.content
|
||||||
|
} else if (Array.isArray(message.content)) {
|
||||||
|
messageText = message.content
|
||||||
|
.filter((part) => part.type === 'text')
|
||||||
|
.map((part) => part.text || '')
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageText) {
|
||||||
|
cacheableContent += messageText
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 如果有cacheable内容,直接使用
|
// 3. 如果有cacheable内容,直接使用
|
||||||
if (cacheableContent) {
|
if (cacheableContent) {
|
||||||
const hash = crypto
|
const hash = crypto
|
||||||
.createHash('sha256')
|
.createHash('sha256')
|
||||||
@@ -60,7 +92,7 @@ class SessionHelper {
|
|||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback: 使用system内容
|
// 4. Fallback: 使用system内容
|
||||||
if (system) {
|
if (system) {
|
||||||
let systemText = ''
|
let systemText = ''
|
||||||
if (typeof system === 'string') {
|
if (typeof system === 'string') {
|
||||||
@@ -76,7 +108,7 @@ class SessionHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 最后fallback: 使用第一条消息内容
|
// 5. 最后fallback: 使用第一条消息内容
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
const firstMessage = messages[0]
|
const firstMessage = messages[0]
|
||||||
let firstMessageText = ''
|
let firstMessageText = ''
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class WebhookNotifier {
|
|||||||
const errorCodes = {
|
const errorCodes = {
|
||||||
'claude-oauth': {
|
'claude-oauth': {
|
||||||
unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
||||||
|
blocked: 'CLAUDE_OAUTH_BLOCKED',
|
||||||
error: 'CLAUDE_OAUTH_ERROR',
|
error: 'CLAUDE_OAUTH_ERROR',
|
||||||
disabled: 'CLAUDE_OAUTH_MANUALLY_DISABLED'
|
disabled: 'CLAUDE_OAUTH_MANUALLY_DISABLED'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -854,6 +854,51 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Claude User-Agent 版本配置 -->
|
||||||
|
<div v-if="form.platform === 'claude'" class="mt-4">
|
||||||
|
<label class="flex items-start">
|
||||||
|
<input
|
||||||
|
v-model="form.useUnifiedUserAgent"
|
||||||
|
class="mt-1 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div class="ml-3">
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
使用统一 Claude Code 版本
|
||||||
|
</span>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent,提高兼容性
|
||||||
|
</p>
|
||||||
|
<div v-if="unifiedUserAgent" class="mt-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-xs text-green-600 dark:text-green-400">
|
||||||
|
💡 当前统一版本:{{ unifiedUserAgent }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="ml-2 text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
:disabled="clearingCache"
|
||||||
|
type="button"
|
||||||
|
@click="clearUnifiedCache"
|
||||||
|
>
|
||||||
|
<i v-if="!clearingCache" class="fas fa-trash-alt mr-1"></i>
|
||||||
|
<div v-else class="loading-spinner mr-1"></div>
|
||||||
|
{{ clearingCache ? '清除中...' : '清除缓存' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-1">
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
⏳ 等待从 Claude Code 客户端捕获 User-Agent
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户,
|
||||||
|
或联系开发者检查 User-Agent 格式是否发生变化
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 所有平台的优先级设置 -->
|
<!-- 所有平台的优先级设置 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
@@ -1412,6 +1457,51 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Claude User-Agent 版本配置(编辑模式) -->
|
||||||
|
<div v-if="form.platform === 'claude'" class="mt-4">
|
||||||
|
<label class="flex items-start">
|
||||||
|
<input
|
||||||
|
v-model="form.useUnifiedUserAgent"
|
||||||
|
class="mt-1 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div class="ml-3">
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
使用统一 Claude Code 版本
|
||||||
|
</span>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
开启后将使用从真实 Claude Code 客户端捕获的统一 User-Agent,提高兼容性
|
||||||
|
</p>
|
||||||
|
<div v-if="unifiedUserAgent" class="mt-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-xs text-green-600 dark:text-green-400">
|
||||||
|
💡 当前统一版本:{{ unifiedUserAgent }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="ml-2 text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
:disabled="clearingCache"
|
||||||
|
type="button"
|
||||||
|
@click="clearUnifiedCache"
|
||||||
|
>
|
||||||
|
<i v-if="!clearingCache" class="fas fa-trash-alt mr-1"></i>
|
||||||
|
<div v-else class="loading-spinner mr-1"></div>
|
||||||
|
{{ clearingCache ? '清除中...' : '清除缓存' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-1">
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
⏳ 等待从 Claude Code 客户端捕获 User-Agent
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
💡 提示:如果长时间未能捕获,请确认有 Claude Code 客户端正在使用此账户,
|
||||||
|
或联系开发者检查 User-Agent 格式是否发生变化
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 所有平台的优先级设置(编辑模式) -->
|
<!-- 所有平台的优先级设置(编辑模式) -->
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
@@ -1904,7 +1994,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
import { useAccountsStore } from '@/stores/accounts'
|
import { useAccountsStore } from '@/stores/accounts'
|
||||||
@@ -1942,6 +2032,10 @@ const setupTokenAuthCode = ref('')
|
|||||||
const setupTokenCopied = ref(false)
|
const setupTokenCopied = ref(false)
|
||||||
const setupTokenSessionId = ref('')
|
const setupTokenSessionId = ref('')
|
||||||
|
|
||||||
|
// Claude Code 统一 User-Agent 信息
|
||||||
|
const unifiedUserAgent = ref('')
|
||||||
|
const clearingCache = ref(false)
|
||||||
|
|
||||||
// 初始化代理配置
|
// 初始化代理配置
|
||||||
const initProxyConfig = () => {
|
const initProxyConfig = () => {
|
||||||
if (props.account?.proxy && props.account.proxy.host && props.account.proxy.port) {
|
if (props.account?.proxy && props.account.proxy.host && props.account.proxy.port) {
|
||||||
@@ -1978,6 +2072,7 @@ const form = ref({
|
|||||||
accountType: props.account?.accountType || 'shared',
|
accountType: props.account?.accountType || 'shared',
|
||||||
subscriptionType: 'claude_max', // 默认为 Claude Max,兼容旧数据
|
subscriptionType: 'claude_max', // 默认为 Claude Max,兼容旧数据
|
||||||
autoStopOnWarning: props.account?.autoStopOnWarning || false, // 5小时限制自动停止调度
|
autoStopOnWarning: props.account?.autoStopOnWarning || false, // 5小时限制自动停止调度
|
||||||
|
useUnifiedUserAgent: props.account?.useUnifiedUserAgent || false, // 使用统一Claude Code版本
|
||||||
groupId: '',
|
groupId: '',
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
projectId: props.account?.projectId || '',
|
projectId: props.account?.projectId || '',
|
||||||
@@ -2255,6 +2350,7 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
data.claudeAiOauth = tokenInfo.claudeAiOauth || tokenInfo
|
data.claudeAiOauth = tokenInfo.claudeAiOauth || tokenInfo
|
||||||
data.priority = form.value.priority || 50
|
data.priority = form.value.priority || 50
|
||||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||||
|
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||||
// 添加订阅类型信息
|
// 添加订阅类型信息
|
||||||
data.subscriptionInfo = {
|
data.subscriptionInfo = {
|
||||||
accountType: form.value.subscriptionType || 'claude_max',
|
accountType: form.value.subscriptionType || 'claude_max',
|
||||||
@@ -2417,6 +2513,7 @@ const createAccount = async () => {
|
|||||||
}
|
}
|
||||||
data.priority = form.value.priority || 50
|
data.priority = form.value.priority || 50
|
||||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||||
|
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||||
// 添加订阅类型信息
|
// 添加订阅类型信息
|
||||||
data.subscriptionInfo = {
|
data.subscriptionInfo = {
|
||||||
accountType: form.value.subscriptionType || 'claude_max',
|
accountType: form.value.subscriptionType || 'claude_max',
|
||||||
@@ -2670,6 +2767,7 @@ const updateAccount = async () => {
|
|||||||
if (props.account.platform === 'claude') {
|
if (props.account.platform === 'claude') {
|
||||||
data.priority = form.value.priority || 50
|
data.priority = form.value.priority || 50
|
||||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||||
|
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||||
// 更新订阅类型信息
|
// 更新订阅类型信息
|
||||||
data.subscriptionInfo = {
|
data.subscriptionInfo = {
|
||||||
accountType: form.value.subscriptionType || 'claude_max',
|
accountType: form.value.subscriptionType || 'claude_max',
|
||||||
@@ -3071,6 +3169,7 @@ watch(
|
|||||||
accountType: newAccount.accountType || 'shared',
|
accountType: newAccount.accountType || 'shared',
|
||||||
subscriptionType: subscriptionType,
|
subscriptionType: subscriptionType,
|
||||||
autoStopOnWarning: newAccount.autoStopOnWarning || false,
|
autoStopOnWarning: newAccount.autoStopOnWarning || false,
|
||||||
|
useUnifiedUserAgent: newAccount.useUnifiedUserAgent || false,
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
projectId: newAccount.projectId || '',
|
projectId: newAccount.projectId || '',
|
||||||
@@ -3149,4 +3248,54 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 获取统一 User-Agent 信息
|
||||||
|
const fetchUnifiedUserAgent = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/admin/claude-code-version')
|
||||||
|
if (response.success && response.userAgent) {
|
||||||
|
unifiedUserAgent.value = response.userAgent
|
||||||
|
} else {
|
||||||
|
unifiedUserAgent.value = ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch unified User-Agent:', error)
|
||||||
|
unifiedUserAgent.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除统一 User-Agent 缓存
|
||||||
|
const clearUnifiedCache = async () => {
|
||||||
|
clearingCache.value = true
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post('/admin/claude-code-version/clear')
|
||||||
|
if (response.success) {
|
||||||
|
unifiedUserAgent.value = ''
|
||||||
|
showToast('统一User-Agent缓存已清除', 'success')
|
||||||
|
} else {
|
||||||
|
showToast('清除缓存失败', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear unified User-Agent cache:', error)
|
||||||
|
showToast('清除缓存失败:' + (error.message || '未知错误'), 'error')
|
||||||
|
} finally {
|
||||||
|
clearingCache.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时获取统一 User-Agent 信息
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取Claude Code统一User-Agent信息
|
||||||
|
fetchUnifiedUserAgent()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听平台变化,当切换到Claude平台时获取统一User-Agent信息
|
||||||
|
watch(
|
||||||
|
() => form.value.platform,
|
||||||
|
(newPlatform) => {
|
||||||
|
if (newPlatform === 'claude') {
|
||||||
|
fetchUnifiedUserAgent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -474,6 +474,20 @@
|
|||||||
<div class="whitespace-nowrap text-gray-300">echo $env:OPENAI_API_KEY</div>
|
<div class="whitespace-nowrap text-gray-300">echo $env:OPENAI_API_KEY</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
|
||||||
|
<h6 class="mb-2 font-medium text-yellow-800">Codex 额外配置</h6>
|
||||||
|
<p class="mb-3 text-sm text-yellow-700">
|
||||||
|
需要在
|
||||||
|
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
|
||||||
|
文件中添加以下配置来禁用响应存储:
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||||
|
>
|
||||||
|
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -985,6 +999,20 @@
|
|||||||
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
|
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
|
||||||
|
<h6 class="mb-2 font-medium text-yellow-800">Codex 额外配置</h6>
|
||||||
|
<p class="mb-3 text-sm text-yellow-700">
|
||||||
|
需要在
|
||||||
|
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
|
||||||
|
文件中添加以下配置来禁用响应存储:
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||||
|
>
|
||||||
|
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1487,6 +1515,20 @@
|
|||||||
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
|
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
|
||||||
|
<h6 class="mb-2 font-medium text-yellow-800">Codex 额外配置</h6>
|
||||||
|
<p class="mb-3 text-sm text-yellow-700">
|
||||||
|
需要在
|
||||||
|
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
|
||||||
|
文件中添加以下配置来禁用响应存储:
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||||
|
>
|
||||||
|
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -554,13 +554,17 @@ const formatDate = (dateString) => {
|
|||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
// Build params object, only including parameters with actual values
|
||||||
|
const params = {}
|
||||||
|
if (selectedRole.value && selectedRole.value.trim() !== '') {
|
||||||
|
params.role = selectedRole.value
|
||||||
|
}
|
||||||
|
if (selectedStatus.value !== '') {
|
||||||
|
params.isActive = selectedStatus.value
|
||||||
|
}
|
||||||
|
|
||||||
const [usersResponse, statsResponse] = await Promise.all([
|
const [usersResponse, statsResponse] = await Promise.all([
|
||||||
apiClient.get('/users', {
|
apiClient.get('/users', { params }),
|
||||||
params: {
|
|
||||||
role: selectedRole.value || undefined,
|
|
||||||
isActive: selectedStatus.value !== '' ? selectedStatus.value : undefined
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
apiClient.get('/users/stats/overview')
|
apiClient.get('/users/stats/overview')
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user