mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 支持sessionKey完成oauth授权
This commit is contained in:
@@ -255,6 +255,108 @@ router.post('/claude-accounts/exchange-setup-token-code', authenticateAdmin, asy
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Cookie自动授权端点 (基于sessionKey自动完成OAuth流程)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// 普通OAuth的Cookie自动授权
|
||||||
|
router.post('/claude-accounts/oauth-with-cookie', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { sessionKey, proxy } = req.body
|
||||||
|
|
||||||
|
// 验证sessionKey参数
|
||||||
|
if (!sessionKey || typeof sessionKey !== 'string' || sessionKey.trim().length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'sessionKey不能为空',
|
||||||
|
message: '请提供有效的sessionKey值'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedSessionKey = sessionKey.trim()
|
||||||
|
|
||||||
|
logger.info('🍪 Starting Cookie-based OAuth authorization', {
|
||||||
|
sessionKeyLength: trimmedSessionKey.length,
|
||||||
|
sessionKeyPrefix: trimmedSessionKey.substring(0, 10) + '...',
|
||||||
|
hasProxy: !!proxy
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行Cookie自动授权流程
|
||||||
|
const result = await oauthHelper.oauthWithCookie(trimmedSessionKey, proxy, false)
|
||||||
|
|
||||||
|
logger.success('🎉 Cookie-based OAuth authorization completed successfully')
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
claudeAiOauth: result.claudeAiOauth,
|
||||||
|
organizationUuid: result.organizationUuid,
|
||||||
|
capabilities: result.capabilities
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Cookie-based OAuth authorization failed:', {
|
||||||
|
error: error.message,
|
||||||
|
sessionKeyLength: req.body.sessionKey ? req.body.sessionKey.length : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Cookie授权失败',
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup Token的Cookie自动授权
|
||||||
|
router.post('/claude-accounts/setup-token-with-cookie', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { sessionKey, proxy } = req.body
|
||||||
|
|
||||||
|
// 验证sessionKey参数
|
||||||
|
if (!sessionKey || typeof sessionKey !== 'string' || sessionKey.trim().length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'sessionKey不能为空',
|
||||||
|
message: '请提供有效的sessionKey值'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedSessionKey = sessionKey.trim()
|
||||||
|
|
||||||
|
logger.info('🍪 Starting Cookie-based Setup Token authorization', {
|
||||||
|
sessionKeyLength: trimmedSessionKey.length,
|
||||||
|
sessionKeyPrefix: trimmedSessionKey.substring(0, 10) + '...',
|
||||||
|
hasProxy: !!proxy
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行Cookie自动授权流程(Setup Token模式)
|
||||||
|
const result = await oauthHelper.oauthWithCookie(trimmedSessionKey, proxy, true)
|
||||||
|
|
||||||
|
logger.success('🎉 Cookie-based Setup Token authorization completed successfully')
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
claudeAiOauth: result.claudeAiOauth,
|
||||||
|
organizationUuid: result.organizationUuid,
|
||||||
|
capabilities: result.capabilities
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Cookie-based Setup Token authorization failed:', {
|
||||||
|
error: error.message,
|
||||||
|
sessionKeyLength: req.body.sessionKey ? req.body.sessionKey.length : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Cookie授权失败',
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 获取所有Claude账户
|
// 获取所有Claude账户
|
||||||
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ const OAUTH_CONFIG = {
|
|||||||
SCOPES_SETUP: 'user:inference' // Setup Token 只需要推理权限
|
SCOPES_SETUP: 'user:inference' // Setup Token 只需要推理权限
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cookie自动授权配置常量
|
||||||
|
const COOKIE_OAUTH_CONFIG = {
|
||||||
|
CLAUDE_AI_URL: 'https://claude.ai',
|
||||||
|
ORGANIZATIONS_URL: 'https://claude.ai/api/organizations',
|
||||||
|
AUTHORIZE_URL_TEMPLATE: 'https://claude.ai/v1/oauth/{organization_uuid}/authorize'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成随机的 state 参数
|
* 生成随机的 state 参数
|
||||||
* @returns {string} 随机生成的 state (base64url编码)
|
* @returns {string} 随机生成的 state (base64url编码)
|
||||||
@@ -570,8 +577,299 @@ function extractExtInfo(data) {
|
|||||||
return Object.keys(ext).length > 0 ? ext : null
|
return Object.keys(ext).length > 0 ? ext : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Cookie自动授权相关方法 (基于Clove项目实现)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建带Cookie的请求头
|
||||||
|
* @param {string} sessionKey - sessionKey值
|
||||||
|
* @returns {object} 请求头对象
|
||||||
|
*/
|
||||||
|
function buildCookieHeaders(sessionKey) {
|
||||||
|
return {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Cookie: `sessionKey=${sessionKey}`,
|
||||||
|
Origin: COOKIE_OAUTH_CONFIG.CLAUDE_AI_URL,
|
||||||
|
Referer: `${COOKIE_OAUTH_CONFIG.CLAUDE_AI_URL}/new`,
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用Cookie获取组织UUID和能力列表
|
||||||
|
* @param {string} sessionKey - sessionKey值
|
||||||
|
* @param {object|null} proxyConfig - 代理配置(可选)
|
||||||
|
* @returns {Promise<{organizationUuid: string, capabilities: string[]}>}
|
||||||
|
*/
|
||||||
|
async function getOrganizationInfo(sessionKey, proxyConfig = null) {
|
||||||
|
const headers = buildCookieHeaders(sessionKey)
|
||||||
|
const agent = createProxyAgent(proxyConfig)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (agent) {
|
||||||
|
logger.info(`🌐 Using proxy for organization info: ${ProxyHelper.maskProxyInfo(proxyConfig)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('🔄 Fetching organization info with Cookie', {
|
||||||
|
url: COOKIE_OAUTH_CONFIG.ORGANIZATIONS_URL,
|
||||||
|
hasProxy: !!proxyConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
const axiosConfig = {
|
||||||
|
headers,
|
||||||
|
timeout: 30000,
|
||||||
|
maxRedirects: 0 // 禁止自动重定向,以便检测Cloudflare拦截(302)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent) {
|
||||||
|
axiosConfig.httpAgent = agent
|
||||||
|
axiosConfig.httpsAgent = agent
|
||||||
|
axiosConfig.proxy = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(COOKIE_OAUTH_CONFIG.ORGANIZATIONS_URL, axiosConfig)
|
||||||
|
|
||||||
|
if (!response.data || !Array.isArray(response.data)) {
|
||||||
|
throw new Error('获取组织信息失败:响应格式无效')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到具有chat能力且能力最多的组织
|
||||||
|
let bestOrg = null
|
||||||
|
let maxCapabilities = []
|
||||||
|
|
||||||
|
for (const org of response.data) {
|
||||||
|
const capabilities = org.capabilities || []
|
||||||
|
|
||||||
|
// 必须有chat能力
|
||||||
|
if (!capabilities.includes('chat')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择能力最多的组织
|
||||||
|
if (capabilities.length > maxCapabilities.length) {
|
||||||
|
bestOrg = org
|
||||||
|
maxCapabilities = capabilities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bestOrg || !bestOrg.uuid) {
|
||||||
|
throw new Error('未找到具有chat能力的组织')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success('✅ Found organization', {
|
||||||
|
uuid: bestOrg.uuid,
|
||||||
|
capabilities: maxCapabilities
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizationUuid: bestOrg.uuid,
|
||||||
|
capabilities: maxCapabilities
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
const { status } = error.response
|
||||||
|
|
||||||
|
if (status === 403 || status === 401) {
|
||||||
|
throw new Error('Cookie授权失败:无效的sessionKey或已过期')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 302) {
|
||||||
|
throw new Error('请求被Cloudflare拦截,请稍后重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`获取组织信息失败:HTTP ${status}`)
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error('获取组织信息失败:网络错误或超时')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用Cookie自动获取授权code
|
||||||
|
* @param {string} sessionKey - sessionKey值
|
||||||
|
* @param {string} organizationUuid - 组织UUID
|
||||||
|
* @param {string} scope - 授权scope
|
||||||
|
* @param {object|null} proxyConfig - 代理配置(可选)
|
||||||
|
* @returns {Promise<{authorizationCode: string, codeVerifier: string, state: string}>}
|
||||||
|
*/
|
||||||
|
async function authorizeWithCookie(sessionKey, organizationUuid, scope, proxyConfig = null) {
|
||||||
|
// 生成PKCE参数
|
||||||
|
const codeVerifier = generateCodeVerifier()
|
||||||
|
const codeChallenge = generateCodeChallenge(codeVerifier)
|
||||||
|
const state = generateState()
|
||||||
|
|
||||||
|
// 构建授权URL
|
||||||
|
const authorizeUrl = COOKIE_OAUTH_CONFIG.AUTHORIZE_URL_TEMPLATE.replace(
|
||||||
|
'{organization_uuid}',
|
||||||
|
organizationUuid
|
||||||
|
)
|
||||||
|
|
||||||
|
// 构建请求payload
|
||||||
|
const payload = {
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: OAUTH_CONFIG.CLIENT_ID,
|
||||||
|
organization_uuid: organizationUuid,
|
||||||
|
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
|
||||||
|
scope,
|
||||||
|
state,
|
||||||
|
code_challenge: codeChallenge,
|
||||||
|
code_challenge_method: 'S256'
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...buildCookieHeaders(sessionKey),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = createProxyAgent(proxyConfig)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (agent) {
|
||||||
|
logger.info(
|
||||||
|
`🌐 Using proxy for Cookie authorization: ${ProxyHelper.maskProxyInfo(proxyConfig)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('🔄 Requesting authorization with Cookie', {
|
||||||
|
url: authorizeUrl,
|
||||||
|
scope,
|
||||||
|
hasProxy: !!proxyConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
const axiosConfig = {
|
||||||
|
headers,
|
||||||
|
timeout: 30000,
|
||||||
|
maxRedirects: 0 // 禁止自动重定向,以便检测Cloudflare拦截(302)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent) {
|
||||||
|
axiosConfig.httpAgent = agent
|
||||||
|
axiosConfig.httpsAgent = agent
|
||||||
|
axiosConfig.proxy = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(authorizeUrl, payload, axiosConfig)
|
||||||
|
|
||||||
|
// 从响应中获取redirect_uri
|
||||||
|
const redirectUri = response.data?.redirect_uri
|
||||||
|
|
||||||
|
if (!redirectUri) {
|
||||||
|
throw new Error('授权响应中未找到redirect_uri')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('📎 Got redirect URI', { redirectUri: `${redirectUri.substring(0, 80)}...` })
|
||||||
|
|
||||||
|
// 解析redirect_uri获取authorization code
|
||||||
|
const url = new URL(redirectUri)
|
||||||
|
const authorizationCode = url.searchParams.get('code')
|
||||||
|
const responseState = url.searchParams.get('state')
|
||||||
|
|
||||||
|
if (!authorizationCode) {
|
||||||
|
throw new Error('redirect_uri中未找到授权码')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的授权码(包含state,如果有的话)
|
||||||
|
const fullCode = responseState ? `${authorizationCode}#${responseState}` : authorizationCode
|
||||||
|
|
||||||
|
logger.success('✅ Got authorization code via Cookie', {
|
||||||
|
codeLength: authorizationCode.length,
|
||||||
|
codePrefix: `${authorizationCode.substring(0, 10)}...`
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
authorizationCode: fullCode,
|
||||||
|
codeVerifier,
|
||||||
|
state
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
const { status } = error.response
|
||||||
|
|
||||||
|
if (status === 403 || status === 401) {
|
||||||
|
throw new Error('Cookie授权失败:无效的sessionKey或已过期')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 302) {
|
||||||
|
throw new Error('请求被Cloudflare拦截,请稍后重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorData = error.response.data
|
||||||
|
let errorMessage = `HTTP ${status}`
|
||||||
|
|
||||||
|
if (errorData) {
|
||||||
|
if (typeof errorData === 'string') {
|
||||||
|
errorMessage += `: ${errorData}`
|
||||||
|
} else if (errorData.error) {
|
||||||
|
errorMessage += `: ${errorData.error}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`授权请求失败:${errorMessage}`)
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error('授权请求失败:网络错误或超时')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整的Cookie自动授权流程
|
||||||
|
* @param {string} sessionKey - sessionKey值
|
||||||
|
* @param {object|null} proxyConfig - 代理配置(可选)
|
||||||
|
* @param {boolean} isSetupToken - 是否为Setup Token模式
|
||||||
|
* @returns {Promise<{claudeAiOauth: object, organizationUuid: string, capabilities: string[]}>}
|
||||||
|
*/
|
||||||
|
async function oauthWithCookie(sessionKey, proxyConfig = null, isSetupToken = false) {
|
||||||
|
logger.info('🍪 Starting Cookie-based OAuth flow', {
|
||||||
|
isSetupToken,
|
||||||
|
hasProxy: !!proxyConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
// 步骤1:获取组织信息
|
||||||
|
logger.debug('Step 1/3: Fetching organization info...')
|
||||||
|
const { organizationUuid, capabilities } = await getOrganizationInfo(sessionKey, proxyConfig)
|
||||||
|
|
||||||
|
// 步骤2:确定scope并获取授权code
|
||||||
|
const scope = isSetupToken ? OAUTH_CONFIG.SCOPES_SETUP : 'user:profile user:inference'
|
||||||
|
|
||||||
|
logger.debug('Step 2/3: Getting authorization code...', { scope })
|
||||||
|
const { authorizationCode, codeVerifier, state } = await authorizeWithCookie(
|
||||||
|
sessionKey,
|
||||||
|
organizationUuid,
|
||||||
|
scope,
|
||||||
|
proxyConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// 步骤3:交换token
|
||||||
|
logger.debug('Step 3/3: Exchanging token...')
|
||||||
|
const tokenData = isSetupToken
|
||||||
|
? await exchangeSetupTokenCode(authorizationCode, codeVerifier, state, proxyConfig)
|
||||||
|
: await exchangeCodeForTokens(authorizationCode, codeVerifier, state, proxyConfig)
|
||||||
|
|
||||||
|
logger.success('✅ Cookie-based OAuth flow completed', {
|
||||||
|
isSetupToken,
|
||||||
|
organizationUuid,
|
||||||
|
hasAccessToken: !!tokenData.accessToken,
|
||||||
|
hasRefreshToken: !!tokenData.refreshToken
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
claudeAiOauth: tokenData,
|
||||||
|
organizationUuid,
|
||||||
|
capabilities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
OAUTH_CONFIG,
|
OAUTH_CONFIG,
|
||||||
|
COOKIE_OAUTH_CONFIG,
|
||||||
generateOAuthParams,
|
generateOAuthParams,
|
||||||
generateSetupTokenParams,
|
generateSetupTokenParams,
|
||||||
exchangeCodeForTokens,
|
exchangeCodeForTokens,
|
||||||
@@ -584,5 +882,10 @@ module.exports = {
|
|||||||
generateCodeChallenge,
|
generateCodeChallenge,
|
||||||
generateAuthUrl,
|
generateAuthUrl,
|
||||||
generateSetupTokenAuthUrl,
|
generateSetupTokenAuthUrl,
|
||||||
createProxyAgent
|
createProxyAgent,
|
||||||
|
// Cookie自动授权相关方法
|
||||||
|
buildCookieHeaders,
|
||||||
|
getOrganizationInfo,
|
||||||
|
authorizeWithCookie,
|
||||||
|
oauthWithCookie
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2029,6 +2029,7 @@
|
|||||||
<!-- 步骤2: OAuth授权 -->
|
<!-- 步骤2: OAuth授权 -->
|
||||||
<OAuthFlow
|
<OAuthFlow
|
||||||
v-if="oauthStep === 2 && form.addType === 'oauth'"
|
v-if="oauthStep === 2 && form.addType === 'oauth'"
|
||||||
|
ref="oauthFlowRef"
|
||||||
:platform="form.platform"
|
:platform="form.platform"
|
||||||
:proxy="form.proxy"
|
:proxy="form.proxy"
|
||||||
@back="oauthStep = 1"
|
@back="oauthStep = 1"
|
||||||
@@ -2052,11 +2053,45 @@
|
|||||||
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">
|
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">
|
||||||
Claude Setup Token 授权
|
Claude Setup Token 授权
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
<!-- 授权方式选择 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="mb-3 text-sm font-medium text-blue-800 dark:text-blue-300">
|
||||||
|
选择授权方式:
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="authMethod"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500"
|
||||||
|
type="radio"
|
||||||
|
value="manual"
|
||||||
|
@change="onAuthMethodChange"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-blue-800 dark:text-blue-300">
|
||||||
|
<i class="fas fa-link mr-1" />手动授权
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="authMethod"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500"
|
||||||
|
type="radio"
|
||||||
|
value="cookie"
|
||||||
|
@change="onAuthMethodChange"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-blue-800 dark:text-blue-300">
|
||||||
|
<i class="fas fa-cookie mr-1" />Cookie 自动授权
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动授权流程 -->
|
||||||
|
<div v-if="authMethod === 'manual'" class="space-y-4">
|
||||||
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
||||||
请按照以下步骤通过 Setup Token 完成 Claude 账户的授权:
|
请按照以下步骤通过 Setup Token 完成 Claude 账户的授权:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- 步骤1: 生成授权链接 -->
|
<!-- 步骤1: 生成授权链接 -->
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||||
@@ -2182,6 +2217,113 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Cookie自动授权流程 -->
|
||||||
|
<div v-if="authMethod === 'cookie'" class="space-y-4">
|
||||||
|
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
||||||
|
使用 sessionKey 自动完成授权,无需手动打开链接。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-cookie text-blue-500" />sessionKey
|
||||||
|
<span
|
||||||
|
v-if="parsedSessionKeyCount > 1"
|
||||||
|
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
|
||||||
|
>
|
||||||
|
{{ parsedSessionKeyCount }} 个
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="sessionKey"
|
||||||
|
class="form-input w-full resize-y border-gray-300 font-mono text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
|
:class="{ 'border-red-500': cookieAuthError }"
|
||||||
|
placeholder="每行一个 sessionKey,例如: sk-ant-sid01-xxxxx... sk-ant-sid01-yyyyy..."
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="parsedSessionKeyCount > 1"
|
||||||
|
class="mt-1 text-xs text-blue-600 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-info-circle mr-1" />
|
||||||
|
将批量创建 {{ parsedSessionKeyCount }} 个账户
|
||||||
|
</p>
|
||||||
|
<p v-if="cookieAuthError" class="mt-1 text-xs text-red-500">
|
||||||
|
{{ cookieAuthError }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 帮助说明 -->
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex items-center text-xs text-blue-600 hover:text-blue-700"
|
||||||
|
type="button"
|
||||||
|
@click="showSessionKeyHelp = !showSessionKeyHelp"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
showSessionKeyHelp
|
||||||
|
? 'fas fa-chevron-down mr-1'
|
||||||
|
: 'fas fa-chevron-right mr-1'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
如何获取 sessionKey?
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="showSessionKeyHelp"
|
||||||
|
class="mt-3 rounded border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<ol class="space-y-2 text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
<li>1. 在浏览器中登录 <strong>claude.ai</strong></li>
|
||||||
|
<li>2. 按 <strong>F12</strong> 打开开发者工具</li>
|
||||||
|
<li>3. 切换到 <strong>"Application"</strong> (应用) 标签页</li>
|
||||||
|
<li>
|
||||||
|
4. 在左侧选择 <strong>"Cookies"</strong> →
|
||||||
|
<strong>"https://claude.ai"</strong>
|
||||||
|
</li>
|
||||||
|
<li>5. 找到键为 <strong>"sessionKey"</strong> 的那一行</li>
|
||||||
|
<li>6. 复制其 <strong>"Value"</strong> (值) 列的内容</li>
|
||||||
|
</ol>
|
||||||
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-info-circle mr-1" />
|
||||||
|
sessionKey 通常以 "sk-ant-" 开头
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 开始授权按钮 -->
|
||||||
|
<button
|
||||||
|
class="btn btn-primary w-full px-4 py-3"
|
||||||
|
:disabled="cookieAuthLoading || !sessionKey.trim()"
|
||||||
|
type="button"
|
||||||
|
@click="handleCookieAuth"
|
||||||
|
>
|
||||||
|
<div v-if="cookieAuthLoading" class="loading-spinner mr-2" />
|
||||||
|
<i v-else class="fas fa-magic mr-2" />
|
||||||
|
<template v-if="cookieAuthLoading && batchProgress.total > 1">
|
||||||
|
正在授权 {{ batchProgress.current }}/{{ batchProgress.total }}...
|
||||||
|
</template>
|
||||||
|
<template v-else-if="cookieAuthLoading"> 授权中... </template>
|
||||||
|
<template v-else> 开始自动授权 </template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded border border-yellow-300 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900/30"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-yellow-800 dark:text-yellow-300">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1" />
|
||||||
|
<strong>提示:</strong>如果您设置了代理,Cookie授权也会使用相同的代理配置。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2196,6 +2338,7 @@
|
|||||||
上一步
|
上一步
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="authMethod === 'manual'"
|
||||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||||
:disabled="!canExchangeSetupToken || setupTokenExchanging"
|
:disabled="!canExchangeSetupToken || setupTokenExchanging"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -3538,6 +3681,9 @@ const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCanc
|
|||||||
const isEdit = computed(() => !!props.account)
|
const isEdit = computed(() => !!props.account)
|
||||||
const show = ref(true)
|
const show = ref(true)
|
||||||
|
|
||||||
|
// OAuthFlow 组件引用
|
||||||
|
const oauthFlowRef = ref(null)
|
||||||
|
|
||||||
// OAuth步骤
|
// OAuth步骤
|
||||||
const oauthStep = ref(1)
|
const oauthStep = ref(1)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -3551,6 +3697,22 @@ const setupTokenAuthCode = ref('')
|
|||||||
const setupTokenCopied = ref(false)
|
const setupTokenCopied = ref(false)
|
||||||
const setupTokenSessionId = ref('')
|
const setupTokenSessionId = ref('')
|
||||||
|
|
||||||
|
// Cookie自动授权相关状态
|
||||||
|
const authMethod = ref('manual') // 'manual' | 'cookie'
|
||||||
|
const sessionKey = ref('')
|
||||||
|
const cookieAuthLoading = ref(false)
|
||||||
|
const cookieAuthError = ref('')
|
||||||
|
const showSessionKeyHelp = ref(false)
|
||||||
|
const batchProgress = ref({ current: 0, total: 0 }) // 批量进度
|
||||||
|
|
||||||
|
// 解析后的 sessionKey 数量
|
||||||
|
const parsedSessionKeyCount = computed(() => {
|
||||||
|
return sessionKey.value
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0).length
|
||||||
|
})
|
||||||
|
|
||||||
// Claude Code 统一 User-Agent 信息
|
// Claude Code 统一 User-Agent 信息
|
||||||
const unifiedUserAgent = ref('')
|
const unifiedUserAgent = ref('')
|
||||||
const clearingCache = ref(false)
|
const clearingCache = ref(false)
|
||||||
@@ -4193,10 +4355,198 @@ const exchangeSetupTokenCode = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理OAuth成功
|
// =============================================================================
|
||||||
const handleOAuthSuccess = async (tokenInfo) => {
|
// Cookie自动授权相关方法
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Cookie自动授权(支持批量)
|
||||||
|
const handleCookieAuth = async () => {
|
||||||
|
// 解析多行输入
|
||||||
|
const sessionKeys = sessionKey.value
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0)
|
||||||
|
|
||||||
|
if (sessionKeys.length === 0) {
|
||||||
|
cookieAuthError.value = '请输入至少一个 sessionKey'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieAuthLoading.value = true
|
||||||
|
cookieAuthError.value = ''
|
||||||
|
batchProgress.value = { current: 0, total: sessionKeys.length }
|
||||||
|
|
||||||
|
const isSetupToken = form.value.addType === 'setup-token'
|
||||||
|
const proxyPayload = buildProxyPayload(form.value.proxy)
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (let i = 0; i < sessionKeys.length; i++) {
|
||||||
|
batchProgress.value.current = i + 1
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
sessionKey: sessionKeys[i],
|
||||||
|
...(proxyPayload && { proxy: proxyPayload })
|
||||||
|
}
|
||||||
|
|
||||||
|
let result
|
||||||
|
if (isSetupToken) {
|
||||||
|
result = await accountsStore.oauthSetupTokenWithCookie(payload)
|
||||||
|
} else {
|
||||||
|
result = await accountsStore.oauthWithCookie(payload)
|
||||||
|
}
|
||||||
|
results.push(result)
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({
|
||||||
|
index: i + 1,
|
||||||
|
key: sessionKeys[i].substring(0, 20) + '...',
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batchProgress.value = { current: 0, total: 0 }
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
try {
|
||||||
|
// 成功后处理OAuth数据(传递数组)
|
||||||
|
// cookieAuthLoading 保持 true,直到账号创建完成
|
||||||
|
await handleOAuthSuccess(results)
|
||||||
|
} finally {
|
||||||
|
cookieAuthLoading.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cookieAuthLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0 && results.length === 0) {
|
||||||
|
cookieAuthError.value = '全部授权失败,请检查 sessionKey 是否有效'
|
||||||
|
} else if (errors.length > 0) {
|
||||||
|
cookieAuthError.value = `${errors.length} 个授权失败`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置Cookie授权状态
|
||||||
|
const resetCookieAuth = () => {
|
||||||
|
sessionKey.value = ''
|
||||||
|
cookieAuthError.value = ''
|
||||||
|
showSessionKeyHelp.value = false
|
||||||
|
batchProgress.value = { current: 0, total: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换授权方式时重置状态
|
||||||
|
const onAuthMethodChange = () => {
|
||||||
|
// 切换到手动模式时清除Cookie相关状态
|
||||||
|
if (authMethod.value === 'manual') {
|
||||||
|
resetCookieAuth()
|
||||||
|
} else {
|
||||||
|
// 切换到Cookie模式时清除手动授权状态
|
||||||
|
setupTokenAuthUrl.value = ''
|
||||||
|
setupTokenAuthCode.value = ''
|
||||||
|
setupTokenSessionId.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 Claude 账户数据(辅助函数)
|
||||||
|
const buildClaudeAccountData = (tokenInfo, accountName, clientId) => {
|
||||||
|
const proxyPayload = buildProxyPayload(form.value.proxy)
|
||||||
|
const claudeOauthPayload = tokenInfo.claudeAiOauth || tokenInfo
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: accountName,
|
||||||
|
description: form.value.description,
|
||||||
|
accountType: form.value.accountType,
|
||||||
|
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
|
||||||
|
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
|
||||||
|
expiresAt: form.value.expiresAt || undefined,
|
||||||
|
proxy: proxyPayload,
|
||||||
|
claudeAiOauth: claudeOauthPayload,
|
||||||
|
priority: form.value.priority || 50,
|
||||||
|
autoStopOnWarning: form.value.autoStopOnWarning || false,
|
||||||
|
useUnifiedUserAgent: form.value.useUnifiedUserAgent || false,
|
||||||
|
useUnifiedClientId: form.value.useUnifiedClientId || false,
|
||||||
|
unifiedClientId: clientId,
|
||||||
|
subscriptionInfo: {
|
||||||
|
accountType: form.value.subscriptionType || 'claude_max',
|
||||||
|
hasClaudeMax: form.value.subscriptionType === 'claude_max',
|
||||||
|
hasClaudePro: form.value.subscriptionType === 'claude_pro',
|
||||||
|
manuallySet: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 extInfo
|
||||||
|
if (claudeOauthPayload) {
|
||||||
|
const extInfoPayload = {}
|
||||||
|
const extSource = claudeOauthPayload.extInfo
|
||||||
|
if (extSource?.org_uuid) extInfoPayload.org_uuid = extSource.org_uuid
|
||||||
|
if (extSource?.account_uuid) extInfoPayload.account_uuid = extSource.account_uuid
|
||||||
|
|
||||||
|
if (!extSource) {
|
||||||
|
if (claudeOauthPayload.organization?.uuid) {
|
||||||
|
extInfoPayload.org_uuid = claudeOauthPayload.organization.uuid
|
||||||
|
}
|
||||||
|
if (claudeOauthPayload.account?.uuid) {
|
||||||
|
extInfoPayload.account_uuid = claudeOauthPayload.account.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(extInfoPayload).length > 0) {
|
||||||
|
data.extInfo = extInfoPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理OAuth成功(支持批量)
|
||||||
|
const handleOAuthSuccess = async (tokenInfoOrList) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
const currentPlatform = form.value.platform
|
||||||
|
|
||||||
|
// Claude 平台支持批量创建
|
||||||
|
if (currentPlatform === 'claude' && Array.isArray(tokenInfoOrList)) {
|
||||||
|
const tokenInfoList = tokenInfoOrList
|
||||||
|
const isBatch = tokenInfoList.length > 1
|
||||||
|
const baseName = form.value.name
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (let i = 0; i < tokenInfoList.length; i++) {
|
||||||
|
const tokenInfo = tokenInfoList[i]
|
||||||
|
// 批量时自动命名
|
||||||
|
const accountName = isBatch ? `${baseName}_${i + 1}` : baseName
|
||||||
|
// 如果启用统一客户端标识,为每个账户生成独立 ID
|
||||||
|
const clientId = form.value.useUnifiedClientId ? generateClientId() : ''
|
||||||
|
const data = buildClaudeAccountData(tokenInfo, accountName, clientId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await accountsStore.createClaudeAccount(data)
|
||||||
|
results.push(result)
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({ name: accountName, error: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理结果
|
||||||
|
if (results.length > 0) {
|
||||||
|
const msg = isBatch
|
||||||
|
? `成功创建 ${results.length}/${tokenInfoList.length} 个账户`
|
||||||
|
: '账户创建成功'
|
||||||
|
showToast(msg, 'success')
|
||||||
|
emit('success', results[0]) // 兼容单个创建的返回
|
||||||
|
}
|
||||||
|
if (errors.length > 0) {
|
||||||
|
showToast(`${errors.length} 个账户创建失败`, 'error')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个 tokenInfo 或其他平台的处理(保持原有逻辑)
|
||||||
|
const tokenInfo = Array.isArray(tokenInfoOrList) ? tokenInfoOrList[0] : tokenInfoOrList
|
||||||
|
|
||||||
// OAuth模式也需要确保生成客户端ID
|
// OAuth模式也需要确保生成客户端ID
|
||||||
if (
|
if (
|
||||||
form.value.platform === 'claude' &&
|
form.value.platform === 'claude' &&
|
||||||
@@ -4218,8 +4568,6 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
proxy: proxyPayload
|
proxy: proxyPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPlatform = form.value.platform
|
|
||||||
|
|
||||||
if (currentPlatform === 'claude') {
|
if (currentPlatform === 'claude') {
|
||||||
// Claude使用claudeAiOauth字段
|
// Claude使用claudeAiOauth字段
|
||||||
const claudeOauthPayload = tokenInfo.claudeAiOauth || tokenInfo
|
const claudeOauthPayload = tokenInfo.claudeAiOauth || tokenInfo
|
||||||
@@ -4380,6 +4728,8 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
// 错误已通过 toast 显示给用户
|
// 错误已通过 toast 显示给用户
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
// 重置 OAuthFlow 组件的加载状态(如果是通过 OAuth 模式调用)
|
||||||
|
oauthFlowRef.value?.resetCookieAuth()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,146 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">Claude 账户授权</h4>
|
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">Claude 账户授权</h4>
|
||||||
|
|
||||||
|
<!-- 授权方式选择 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block text-sm font-medium text-blue-800 dark:text-blue-300">
|
||||||
|
选择授权方式
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="authMethod"
|
||||||
|
class="text-blue-600 focus:ring-blue-500"
|
||||||
|
name="claude-auth-method"
|
||||||
|
type="radio"
|
||||||
|
value="manual"
|
||||||
|
@change="onAuthMethodChange"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-blue-900 dark:text-blue-200">手动授权</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="authMethod"
|
||||||
|
class="text-blue-600 focus:ring-blue-500"
|
||||||
|
name="claude-auth-method"
|
||||||
|
type="radio"
|
||||||
|
value="cookie"
|
||||||
|
@change="onAuthMethodChange"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-blue-900 dark:text-blue-200">Cookie自动授权</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cookie自动授权表单 -->
|
||||||
|
<div v-if="authMethod === 'cookie'" class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||||
|
>
|
||||||
|
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
使用 claude.ai 的 sessionKey 自动完成 OAuth 授权流程,无需手动打开浏览器。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- sessionKey输入 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label
|
||||||
|
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-cookie text-blue-500" />
|
||||||
|
sessionKey
|
||||||
|
<span
|
||||||
|
v-if="parsedSessionKeyCount > 1"
|
||||||
|
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
|
||||||
|
>
|
||||||
|
{{ parsedSessionKeyCount }} 个
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="text-blue-500 hover:text-blue-600"
|
||||||
|
type="button"
|
||||||
|
@click="showSessionKeyHelp = !showSessionKeyHelp"
|
||||||
|
>
|
||||||
|
<i class="fas fa-question-circle" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="sessionKey"
|
||||||
|
class="form-input w-full resize-y font-mono text-sm"
|
||||||
|
placeholder="每行一个 sessionKey,例如: sk-ant-sid01-xxxxx... sk-ant-sid01-yyyyy..."
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="parsedSessionKeyCount > 1"
|
||||||
|
class="mt-1 text-xs text-blue-600 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-info-circle mr-1" />
|
||||||
|
将批量创建 {{ parsedSessionKeyCount }} 个账户
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 帮助说明 -->
|
||||||
|
<div
|
||||||
|
v-if="showSessionKeyHelp"
|
||||||
|
class="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-900/30"
|
||||||
|
>
|
||||||
|
<h5 class="mb-2 font-semibold text-amber-800 dark:text-amber-200">
|
||||||
|
<i class="fas fa-lightbulb mr-1" />如何获取 sessionKey
|
||||||
|
</h5>
|
||||||
|
<ol
|
||||||
|
class="list-inside list-decimal space-y-1 text-xs text-amber-700 dark:text-amber-300"
|
||||||
|
>
|
||||||
|
<li>在浏览器中登录 <strong>claude.ai</strong></li>
|
||||||
|
<li>
|
||||||
|
按
|
||||||
|
<kbd class="rounded bg-gray-200 px-1 dark:bg-gray-700">F12</kbd>
|
||||||
|
打开开发者工具
|
||||||
|
</li>
|
||||||
|
<li>切换到 <strong>Application</strong>(应用)标签页</li>
|
||||||
|
<li>
|
||||||
|
在左侧找到 <strong>Cookies</strong> → <strong>https://claude.ai</strong>
|
||||||
|
</li>
|
||||||
|
<li>找到键为 <strong>sessionKey</strong> 的那一行</li>
|
||||||
|
<li>复制其 <strong>Value</strong>(值)列的内容</li>
|
||||||
|
</ol>
|
||||||
|
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
<i class="fas fa-info-circle mr-1" />
|
||||||
|
sessionKey 通常以
|
||||||
|
<code class="rounded bg-gray-200 px-1 dark:bg-gray-700">sk-ant-sid01-</code>
|
||||||
|
开头
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<div
|
||||||
|
v-if="cookieAuthError"
|
||||||
|
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-red-600 dark:text-red-400">
|
||||||
|
<i class="fas fa-exclamation-circle mr-1" />
|
||||||
|
{{ cookieAuthError }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 授权按钮 -->
|
||||||
|
<button
|
||||||
|
class="btn btn-primary w-full px-4 py-3 text-base font-semibold"
|
||||||
|
:disabled="cookieAuthLoading || !sessionKey.trim()"
|
||||||
|
type="button"
|
||||||
|
@click="handleCookieAuth"
|
||||||
|
>
|
||||||
|
<div v-if="cookieAuthLoading" class="loading-spinner mr-2" />
|
||||||
|
<i v-else class="fas fa-magic mr-2" />
|
||||||
|
<template v-if="cookieAuthLoading && batchProgress.total > 1">
|
||||||
|
正在授权 {{ batchProgress.current }}/{{ batchProgress.total }}...
|
||||||
|
</template>
|
||||||
|
<template v-else-if="cookieAuthLoading"> 正在授权... </template>
|
||||||
|
<template v-else> 开始自动授权 </template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动授权流程 -->
|
||||||
|
<div v-else>
|
||||||
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
||||||
请按照以下步骤完成 Claude 账户的授权:
|
请按照以下步骤完成 Claude 账户的授权:
|
||||||
</p>
|
</p>
|
||||||
@@ -144,6 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Gemini OAuth流程 -->
|
<!-- Gemini OAuth流程 -->
|
||||||
<div v-else-if="platform === 'gemini'">
|
<div v-else-if="platform === 'gemini'">
|
||||||
@@ -636,7 +777,9 @@
|
|||||||
>
|
>
|
||||||
上一步
|
上一步
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Cookie自动授权模式不显示此按钮(Claude平台) -->
|
||||||
<button
|
<button
|
||||||
|
v-if="!(platform === 'claude' && authMethod === 'cookie')"
|
||||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||||
:disabled="!canExchange || exchanging"
|
:disabled="!canExchange || exchanging"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -682,6 +825,22 @@ const verificationUriComplete = ref('')
|
|||||||
const remainingSeconds = ref(0)
|
const remainingSeconds = ref(0)
|
||||||
let countdownTimer = null
|
let countdownTimer = null
|
||||||
|
|
||||||
|
// Cookie自动授权相关状态
|
||||||
|
const authMethod = ref('manual') // 'manual' | 'cookie'
|
||||||
|
const sessionKey = ref('')
|
||||||
|
const cookieAuthLoading = ref(false)
|
||||||
|
const cookieAuthError = ref('')
|
||||||
|
const showSessionKeyHelp = ref(false)
|
||||||
|
const batchProgress = ref({ current: 0, total: 0 }) // 批量进度
|
||||||
|
|
||||||
|
// 解析后的 sessionKey 数量
|
||||||
|
const parsedSessionKeyCount = computed(() => {
|
||||||
|
return sessionKey.value
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0).length
|
||||||
|
})
|
||||||
|
|
||||||
// 计算是否可以交换code
|
// 计算是否可以交换code
|
||||||
const canExchange = computed(() => {
|
const canExchange = computed(() => {
|
||||||
if (props.platform === 'droid') {
|
if (props.platform === 'droid') {
|
||||||
@@ -984,4 +1143,93 @@ const exchangeCode = async () => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopCountdown()
|
stopCountdown()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cookie自动授权处理(支持批量)
|
||||||
|
const handleCookieAuth = async () => {
|
||||||
|
// 解析多行输入
|
||||||
|
const sessionKeys = sessionKey.value
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0)
|
||||||
|
|
||||||
|
if (sessionKeys.length === 0) {
|
||||||
|
cookieAuthError.value = '请输入至少一个 sessionKey'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieAuthLoading.value = true
|
||||||
|
cookieAuthError.value = ''
|
||||||
|
batchProgress.value = { current: 0, total: sessionKeys.length }
|
||||||
|
|
||||||
|
// 构建代理配置
|
||||||
|
const proxyConfig = props.proxy?.enabled
|
||||||
|
? {
|
||||||
|
type: props.proxy.type,
|
||||||
|
host: props.proxy.host,
|
||||||
|
port: parseInt(props.proxy.port),
|
||||||
|
username: props.proxy.username || null,
|
||||||
|
password: props.proxy.password || null
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (let i = 0; i < sessionKeys.length; i++) {
|
||||||
|
batchProgress.value.current = i + 1
|
||||||
|
try {
|
||||||
|
const result = await accountsStore.oauthWithCookie({
|
||||||
|
sessionKey: sessionKeys[i],
|
||||||
|
proxy: proxyConfig
|
||||||
|
})
|
||||||
|
results.push(result)
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({
|
||||||
|
index: i + 1,
|
||||||
|
key: sessionKeys[i].substring(0, 20) + '...',
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batchProgress.value = { current: 0, total: 0 }
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
// emit 后父组件会调用 handleOAuthSuccess 创建账号
|
||||||
|
// cookieAuthLoading 保持 true,成功后表单会关闭,失败时父组件会处理
|
||||||
|
emit('success', results) // 返回数组(单个时也是数组)
|
||||||
|
// 注意:不在这里设置 cookieAuthLoading = false
|
||||||
|
// 父组件创建账号完成后表单会关闭/重置
|
||||||
|
} else {
|
||||||
|
// 全部授权失败时才恢复按钮状态
|
||||||
|
cookieAuthLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0 && results.length === 0) {
|
||||||
|
cookieAuthError.value = '全部授权失败,请检查 sessionKey 是否有效'
|
||||||
|
} else if (errors.length > 0) {
|
||||||
|
cookieAuthError.value = `${errors.length} 个授权失败`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置Cookie授权状态
|
||||||
|
const resetCookieAuth = () => {
|
||||||
|
sessionKey.value = ''
|
||||||
|
cookieAuthError.value = ''
|
||||||
|
cookieAuthLoading.value = false
|
||||||
|
batchProgress.value = { current: 0, total: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换授权方式时重置状态
|
||||||
|
const onAuthMethodChange = () => {
|
||||||
|
resetCookieAuth()
|
||||||
|
authUrl.value = ''
|
||||||
|
authCode.value = ''
|
||||||
|
sessionId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法供父组件调用
|
||||||
|
defineExpose({
|
||||||
|
resetCookieAuth
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -750,6 +750,39 @@ export const useAccountsStore = defineStore('accounts', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cookie自动授权 - 普通OAuth
|
||||||
|
const oauthWithCookie = async (payload) => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post('/admin/claude-accounts/oauth-with-cookie', payload)
|
||||||
|
if (response.success) {
|
||||||
|
return response.data
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Cookie授权失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie自动授权 - Setup Token
|
||||||
|
const oauthSetupTokenWithCookie = async (payload) => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post(
|
||||||
|
'/admin/claude-accounts/setup-token-with-cookie',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
if (response.success) {
|
||||||
|
return response.data
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Cookie授权失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成Gemini OAuth URL
|
// 生成Gemini OAuth URL
|
||||||
const generateGeminiAuthUrl = async (proxyConfig) => {
|
const generateGeminiAuthUrl = async (proxyConfig) => {
|
||||||
try {
|
try {
|
||||||
@@ -914,6 +947,8 @@ export const useAccountsStore = defineStore('accounts', () => {
|
|||||||
exchangeClaudeCode,
|
exchangeClaudeCode,
|
||||||
generateClaudeSetupTokenUrl,
|
generateClaudeSetupTokenUrl,
|
||||||
exchangeClaudeSetupTokenCode,
|
exchangeClaudeSetupTokenCode,
|
||||||
|
oauthWithCookie,
|
||||||
|
oauthSetupTokenWithCookie,
|
||||||
generateGeminiAuthUrl,
|
generateGeminiAuthUrl,
|
||||||
exchangeGeminiCode,
|
exchangeGeminiCode,
|
||||||
generateOpenAIAuthUrl,
|
generateOpenAIAuthUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user