mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: claude账号新增保存claude的uuid
This commit is contained in:
@@ -2333,7 +2333,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
useUnifiedUserAgent,
|
||||
useUnifiedClientId,
|
||||
unifiedClientId,
|
||||
expiresAt
|
||||
expiresAt,
|
||||
extInfo
|
||||
} = req.body
|
||||
|
||||
if (!name) {
|
||||
@@ -2377,7 +2378,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
|
||||
useUnifiedClientId: useUnifiedClientId === true, // 默认为false
|
||||
unifiedClientId: unifiedClientId || '', // 统一的客户端标识
|
||||
expiresAt: expiresAt || null // 账户订阅到期时间
|
||||
expiresAt: expiresAt || null, // 账户订阅到期时间
|
||||
extInfo: extInfo || null
|
||||
})
|
||||
|
||||
// 如果是分组类型,将账户添加到分组
|
||||
|
||||
@@ -74,12 +74,14 @@ class ClaudeAccountService {
|
||||
useUnifiedUserAgent = false, // 是否使用统一Claude Code版本的User-Agent
|
||||
useUnifiedClientId = false, // 是否使用统一的客户端标识
|
||||
unifiedClientId = '', // 统一的客户端标识
|
||||
expiresAt = null // 账户订阅到期时间
|
||||
expiresAt = null, // 账户订阅到期时间
|
||||
extInfo = null // 额外扩展信息
|
||||
} = options
|
||||
|
||||
const accountId = uuidv4()
|
||||
|
||||
let accountData
|
||||
const normalizedExtInfo = this._normalizeExtInfo(extInfo, claudeAiOauth)
|
||||
|
||||
if (claudeAiOauth) {
|
||||
// 使用Claude标准格式的OAuth数据
|
||||
@@ -116,7 +118,9 @@ class ClaudeAccountService {
|
||||
? JSON.stringify(claudeAiOauth.subscriptionInfo)
|
||||
: '',
|
||||
// 账户订阅到期时间
|
||||
subscriptionExpiresAt: expiresAt || ''
|
||||
subscriptionExpiresAt: expiresAt || '',
|
||||
// 扩展信息
|
||||
extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : ''
|
||||
}
|
||||
} else {
|
||||
// 兼容旧格式
|
||||
@@ -146,7 +150,9 @@ class ClaudeAccountService {
|
||||
// 手动设置的订阅信息
|
||||
subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) : '',
|
||||
// 账户订阅到期时间
|
||||
subscriptionExpiresAt: expiresAt || ''
|
||||
subscriptionExpiresAt: expiresAt || '',
|
||||
// 扩展信息
|
||||
extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +199,8 @@ class ClaudeAccountService {
|
||||
autoStopOnWarning,
|
||||
useUnifiedUserAgent,
|
||||
useUnifiedClientId,
|
||||
unifiedClientId
|
||||
unifiedClientId,
|
||||
extInfo: normalizedExtInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +492,7 @@ class ClaudeAccountService {
|
||||
const scopes = account.scopes && account.scopes.trim() ? account.scopes.split(' ') : []
|
||||
const isOAuth = scopes.includes('user:profile') && scopes.includes('user:inference')
|
||||
const authType = isOAuth ? 'oauth' : 'setup-token'
|
||||
const parsedExtInfo = this._safeParseJson(account.extInfo)
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
@@ -548,7 +556,9 @@ class ClaudeAccountService {
|
||||
useUnifiedClientId: account.useUnifiedClientId === 'true', // 默认为false
|
||||
unifiedClientId: account.unifiedClientId || '', // 统一的客户端标识
|
||||
// 添加停止原因
|
||||
stoppedReason: account.stoppedReason || null
|
||||
stoppedReason: account.stoppedReason || null,
|
||||
// 扩展信息
|
||||
extInfo: parsedExtInfo
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -639,10 +649,12 @@ class ClaudeAccountService {
|
||||
'useUnifiedUserAgent',
|
||||
'useUnifiedClientId',
|
||||
'unifiedClientId',
|
||||
'subscriptionExpiresAt'
|
||||
'subscriptionExpiresAt',
|
||||
'extInfo'
|
||||
]
|
||||
const updatedData = { ...accountData }
|
||||
let shouldClearAutoStopFields = false
|
||||
let extInfoProvided = false
|
||||
|
||||
// 检查是否新增了 refresh token
|
||||
const oldRefreshToken = this._decryptSensitiveData(accountData.refreshToken)
|
||||
@@ -661,6 +673,10 @@ class ClaudeAccountService {
|
||||
} else if (field === 'subscriptionExpiresAt') {
|
||||
// 处理订阅到期时间,允许 null 值(永不过期)
|
||||
updatedData[field] = value ? value.toString() : ''
|
||||
} else if (field === 'extInfo') {
|
||||
const normalized = this._normalizeExtInfo(value, updates.claudeAiOauth)
|
||||
updatedData.extInfo = normalized ? JSON.stringify(normalized) : ''
|
||||
extInfoProvided = true
|
||||
} else if (field === 'claudeAiOauth') {
|
||||
// 更新 Claude AI OAuth 数据
|
||||
if (value) {
|
||||
@@ -672,6 +688,13 @@ class ClaudeAccountService {
|
||||
updatedData.status = 'active'
|
||||
updatedData.errorMessage = ''
|
||||
updatedData.lastRefreshAt = new Date().toISOString()
|
||||
|
||||
if (!extInfoProvided) {
|
||||
const normalized = this._normalizeExtInfo(value.extInfo, value)
|
||||
if (normalized) {
|
||||
updatedData.extInfo = JSON.stringify(normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updatedData[field] = value !== null && value !== undefined ? value.toString() : ''
|
||||
@@ -3040,6 +3063,93 @@ class ClaudeAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化扩展信息,提取组织与账户UUID
|
||||
* @param {object|string|null} extInfoSource - 原始扩展信息
|
||||
* @param {object|null} oauthPayload - OAuth 数据载荷
|
||||
* @returns {object|null} 规范化后的扩展信息
|
||||
*/
|
||||
_normalizeExtInfo(extInfoSource, oauthPayload) {
|
||||
let extInfo = null
|
||||
|
||||
if (extInfoSource) {
|
||||
if (typeof extInfoSource === 'string') {
|
||||
extInfo = this._safeParseJson(extInfoSource)
|
||||
} else if (typeof extInfoSource === 'object') {
|
||||
extInfo = { ...extInfoSource }
|
||||
}
|
||||
}
|
||||
|
||||
if (!extInfo && oauthPayload && typeof oauthPayload === 'object') {
|
||||
if (oauthPayload.extInfo) {
|
||||
if (typeof oauthPayload.extInfo === 'string') {
|
||||
extInfo = this._safeParseJson(oauthPayload.extInfo)
|
||||
} else if (typeof oauthPayload.extInfo === 'object') {
|
||||
extInfo = { ...oauthPayload.extInfo }
|
||||
}
|
||||
}
|
||||
|
||||
if (!extInfo) {
|
||||
const organization = oauthPayload.organization || null
|
||||
const account = oauthPayload.account || null
|
||||
|
||||
const normalized = {}
|
||||
const orgUuid =
|
||||
organization?.uuid ||
|
||||
organization?.id ||
|
||||
organization?.organization_uuid ||
|
||||
organization?.organization_id
|
||||
const accountUuid =
|
||||
account?.uuid || account?.id || account?.account_uuid || account?.account_id
|
||||
|
||||
if (orgUuid) {
|
||||
normalized.org_uuid = orgUuid
|
||||
}
|
||||
|
||||
if (accountUuid) {
|
||||
normalized.account_uuid = accountUuid
|
||||
}
|
||||
|
||||
extInfo = Object.keys(normalized).length > 0 ? normalized : null
|
||||
}
|
||||
}
|
||||
|
||||
if (!extInfo || typeof extInfo !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = {}
|
||||
|
||||
if (extInfo.org_uuid && typeof extInfo.org_uuid === 'string') {
|
||||
result.org_uuid = extInfo.org_uuid
|
||||
}
|
||||
|
||||
if (extInfo.account_uuid && typeof extInfo.account_uuid === 'string') {
|
||||
result.account_uuid = extInfo.account_uuid
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析 JSON 字符串
|
||||
* @param {string} value - 需要解析的字符串
|
||||
* @returns {object|null} 解析结果
|
||||
*/
|
||||
_safeParseJson(value) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return parsed && typeof parsed === 'object' ? parsed : null
|
||||
} catch (error) {
|
||||
logger.warn('⚠️ 解析扩展信息失败,已忽略:', error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async _removeAccountFields(accountId, fields = [], context = 'general_cleanup') {
|
||||
if (!Array.isArray(fields) || fields.length === 0) {
|
||||
return
|
||||
|
||||
@@ -219,13 +219,21 @@ async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, pro
|
||||
|
||||
const { data } = response
|
||||
|
||||
// 解析组织与账户信息
|
||||
const organizationInfo = data.organization || null
|
||||
const accountInfo = data.account || null
|
||||
const extInfo = extractExtInfo(data)
|
||||
|
||||
// 返回Claude格式的token数据,包含可能的套餐信息
|
||||
const result = {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000,
|
||||
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'],
|
||||
isMax: true
|
||||
isMax: true,
|
||||
organization: organizationInfo,
|
||||
account: accountInfo,
|
||||
extInfo
|
||||
}
|
||||
|
||||
// 如果响应中包含套餐信息,添加到返回结果中
|
||||
@@ -430,13 +438,21 @@ async function exchangeSetupTokenCode(authorizationCode, codeVerifier, state, pr
|
||||
|
||||
const { data } = response
|
||||
|
||||
// 解析组织与账户信息
|
||||
const organizationInfo = data.organization || null
|
||||
const accountInfo = data.account || null
|
||||
const extInfo = extractExtInfo(data)
|
||||
|
||||
// 返回Claude格式的token数据,包含可能的套餐信息
|
||||
const result = {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: '',
|
||||
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000,
|
||||
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'],
|
||||
isMax: true
|
||||
isMax: true,
|
||||
organization: organizationInfo,
|
||||
account: accountInfo,
|
||||
extInfo
|
||||
}
|
||||
|
||||
// 如果响应中包含套餐信息,添加到返回结果中
|
||||
@@ -513,11 +529,47 @@ function formatClaudeCredentials(tokenData) {
|
||||
refreshToken: tokenData.refreshToken,
|
||||
expiresAt: tokenData.expiresAt,
|
||||
scopes: tokenData.scopes,
|
||||
isMax: tokenData.isMax
|
||||
isMax: tokenData.isMax,
|
||||
organization: tokenData.organization || null,
|
||||
account: tokenData.account || null,
|
||||
extInfo: tokenData.extInfo || null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌响应中提取扩展信息
|
||||
* @param {object} data - 令牌响应
|
||||
* @returns {object|null} 包含组织与账户UUID的扩展信息
|
||||
*/
|
||||
function extractExtInfo(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const organization = data.organization || null
|
||||
const account = data.account || null
|
||||
|
||||
const ext = {}
|
||||
|
||||
const orgUuid =
|
||||
organization?.uuid ||
|
||||
organization?.id ||
|
||||
organization?.organization_uuid ||
|
||||
organization?.organization_id
|
||||
const accountUuid = account?.uuid || account?.id || account?.account_uuid || account?.account_id
|
||||
|
||||
if (orgUuid) {
|
||||
ext.org_uuid = orgUuid
|
||||
}
|
||||
|
||||
if (accountUuid) {
|
||||
ext.account_uuid = accountUuid
|
||||
}
|
||||
|
||||
return Object.keys(ext).length > 0 ? ext : null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OAUTH_CONFIG,
|
||||
generateOAuthParams,
|
||||
@@ -526,6 +578,7 @@ module.exports = {
|
||||
exchangeSetupTokenCode,
|
||||
parseCallbackUrl,
|
||||
formatClaudeCredentials,
|
||||
extractExtInfo,
|
||||
generateState,
|
||||
generateCodeVerifier,
|
||||
generateCodeChallenge,
|
||||
|
||||
@@ -4007,7 +4007,35 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
||||
|
||||
if (currentPlatform === 'claude') {
|
||||
// Claude使用claudeAiOauth字段
|
||||
data.claudeAiOauth = tokenInfo.claudeAiOauth || tokenInfo
|
||||
const claudeOauthPayload = tokenInfo.claudeAiOauth || tokenInfo
|
||||
data.claudeAiOauth = claudeOauthPayload
|
||||
if (claudeOauthPayload) {
|
||||
const extInfoPayload = {}
|
||||
const extSource = claudeOauthPayload.extInfo
|
||||
if (extSource && typeof extSource === 'object') {
|
||||
if (extSource.org_uuid) {
|
||||
extInfoPayload.org_uuid = extSource.org_uuid
|
||||
}
|
||||
if (extSource.account_uuid) {
|
||||
extInfoPayload.account_uuid = extSource.account_uuid
|
||||
}
|
||||
}
|
||||
|
||||
if (!extSource) {
|
||||
const orgUuid = claudeOauthPayload.organization?.uuid
|
||||
const accountUuid = claudeOauthPayload.account?.uuid
|
||||
if (orgUuid) {
|
||||
extInfoPayload.org_uuid = orgUuid
|
||||
}
|
||||
if (accountUuid) {
|
||||
extInfoPayload.account_uuid = accountUuid
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(extInfoPayload).length > 0) {
|
||||
data.extInfo = extInfoPayload
|
||||
}
|
||||
}
|
||||
data.priority = form.value.priority || 50
|
||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||
|
||||
Reference in New Issue
Block a user