diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index 8069b4e..039f036 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -1478,4 +1478,152 @@ export async function refreshIFlowTokens(refreshToken) { scope: tokenData.scope, apiKey: userInfo.apiKey }; +} + +/** + * Kiro Token 刷新常量 + */ +const KIRO_REFRESH_CONSTANTS = { + REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', + CONTENT_TYPE_JSON: 'application/json', + AUTH_METHOD_SOCIAL: 'social', + DEFAULT_PROVIDER: 'Google', + REQUEST_TIMEOUT: 30000, + DEFAULT_REGION: 'us-east-1' +}; + +/** + * 通过 refreshToken 获取 accessToken + * @param {string} refreshToken - Kiro 的 refresh token + * @param {string} region - AWS 区域 (默认: us-east-1) + * @returns {Promise} 包含 accessToken 等信息的对象 + */ +async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) { + const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_URL.replace('{{region}}', region); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), KIRO_REFRESH_CONSTANTS.REQUEST_TIMEOUT); + + try { + const response = await fetch(refreshUrl, { + method: 'POST', + headers: { + 'Content-Type': KIRO_REFRESH_CONSTANTS.CONTENT_TYPE_JSON + }, + body: JSON.stringify({ refreshToken }), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const data = await response.json(); + + if (!data.accessToken) { + throw new Error('Invalid refresh response: Missing accessToken'); + } + + const expiresIn = data.expiresIn || 3600; + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + + return { + accessToken: data.accessToken, + refreshToken: data.refreshToken || refreshToken, + profileArn: data.profileArn || '', + expiresAt: expiresAt, + authMethod: KIRO_REFRESH_CONSTANTS.AUTH_METHOD_SOCIAL, + provider: KIRO_REFRESH_CONSTANTS.DEFAULT_PROVIDER, + region: region + }; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error('Request timeout'); + } + throw error; + } +} + +/** + * 批量导入 Kiro refreshToken 并生成凭据文件 + * @param {string[]} refreshTokens - refreshToken 数组 + * @param {string} region - AWS 区域 (默认: us-east-1) + * @returns {Promise} 批量处理结果 + */ +export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) { + const results = { + total: refreshTokens.length, + success: 0, + failed: 0, + details: [] + }; + + for (let i = 0; i < refreshTokens.length; i++) { + const refreshToken = refreshTokens[i].trim(); + + if (!refreshToken) { + results.details.push({ + index: i + 1, + success: false, + error: 'Empty token' + }); + results.failed++; + continue; + } + + try { + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); + + const tokenData = await refreshKiroToken(refreshToken, region); + + // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json + const timestamp = Date.now(); + const folderName = `${timestamp}_kiro-auth-token`; + const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); + await fs.promises.mkdir(targetDir, { recursive: true }); + + const credPath = path.join(targetDir, `${folderName}.json`); + await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2)); + + const relativePath = path.relative(process.cwd(), credPath); + + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); + + results.details.push({ + index: i + 1, + success: true, + path: relativePath, + expiresAt: tokenData.expiresAt + }); + results.success++; + + } catch (error) { + console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); + + results.details.push({ + index: i + 1, + success: false, + error: error.message + }); + results.failed++; + } + } + + // 如果有成功的,广播事件并自动关联 + if (results.success > 0) { + broadcastEvent('oauth_batch_success', { + provider: 'claude-kiro-oauth', + count: results.success, + timestamp: new Date().toISOString() + }); + + // 自动关联新生成的凭据到 Pools + await autoLinkProviderConfigs(CONFIG); + } + + return results; } \ No newline at end of file diff --git a/src/ui-manager.js b/src/ui-manager.js index 660d1aa..fe42515 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -56,7 +56,7 @@ import { getAllProviderModels, getProviderModels } from './provider-models.js'; import { CONFIG } from './config-manager.js'; import { serviceInstances, getServiceAdapter } from './adapter.js'; import { initApiService } from './service-manager.js'; -import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth, handleIFlowOAuth } from './oauth-handlers.js'; +import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth, handleIFlowOAuth, batchImportKiroRefreshTokens } from './oauth-handlers.js'; import { generateUUID, normalizePath, @@ -2126,6 +2126,46 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return true; } + // Batch import Kiro refresh tokens + // 批量导入 Kiro refreshToken + if (method === 'POST' && pathParam === '/api/kiro/batch-import-tokens') { + try { + const body = await getRequestBody(req); + const { refreshTokens, region } = body; + + if (!refreshTokens || !Array.isArray(refreshTokens) || refreshTokens.length === 0) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: 'refreshTokens array is required and must not be empty' + })); + return true; + } + + console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens...`); + + const result = await batchImportKiroRefreshTokens(refreshTokens, region || 'us-east-1'); + + console.log(`[Kiro Batch Import] Completed: ${result.success} success, ${result.failed} failed`); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + ...result + })); + return true; + + } catch (error) { + console.error('[Kiro Batch Import] Error:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: error.message + })); + return true; + } + } + return false; } diff --git a/static/app/i18n.js b/static/app/i18n.js index 4b760f5..f36bf78 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -132,6 +132,19 @@ const translations = { 'oauth.kiro.step2': '使用您的 {method} 账号登录', 'oauth.kiro.step3': '授权完成后页面会自动关闭', 'oauth.kiro.step4': '刷新本页面查看凭据文件', + 'oauth.kiro.batchImport': '批量导入 refreshToken', + 'oauth.kiro.batchImportDesc': '批量导入已有的 refreshToken 生成凭据文件', + 'oauth.kiro.batchImportInstructions': '请输入 refreshToken,每行一个。系统将自动刷新并生成凭据文件。', + 'oauth.kiro.refreshTokensLabel': 'RefreshToken 列表', + 'oauth.kiro.refreshTokensPlaceholder': '每行输入一个 refreshToken\n例如:\naorAxxxxxxxx\naorAyyyyyyyy\naorAzzzzzzzz', + 'oauth.kiro.tokenCount': '待导入数量:', + 'oauth.kiro.importing': '正在导入中,请稍候...', + 'oauth.kiro.startImport': '开始导入', + 'oauth.kiro.noTokens': '请输入至少一个 refreshToken', + 'oauth.kiro.importSuccess': '导入成功!共 {count} 个凭据已生成', + 'oauth.kiro.importAllFailed': '导入失败!共 {count} 个 token 刷新失败', + 'oauth.kiro.importPartial': '部分成功:{success} 个成功,{failed} 个失败', + 'oauth.kiro.importError': '导入出错', 'oauth.iflow.step1': '点击下方按钮在浏览器中打开 iFlow 授权页面', 'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权', 'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key', @@ -560,6 +573,19 @@ const translations = { 'oauth.kiro.step2': 'Log in with your {method} account', 'oauth.kiro.step3': 'The page will close automatically after authorization', 'oauth.kiro.step4': 'Refresh this page to view the credentials file', + 'oauth.kiro.batchImport': 'Batch Import refreshToken', + 'oauth.kiro.batchImportDesc': 'Batch import existing refreshTokens to generate credential files', + 'oauth.kiro.batchImportInstructions': 'Enter refreshTokens, one per line. The system will automatically refresh and generate credential files.', + 'oauth.kiro.refreshTokensLabel': 'RefreshToken List', + 'oauth.kiro.refreshTokensPlaceholder': 'Enter one refreshToken per line\nExample:\naorAxxxxxxxx\naorAyyyyyyyy\naorAzzzzzzzz', + 'oauth.kiro.tokenCount': 'Tokens to import:', + 'oauth.kiro.importing': 'Importing, please wait...', + 'oauth.kiro.startImport': 'Start Import', + 'oauth.kiro.noTokens': 'Please enter at least one refreshToken', + 'oauth.kiro.importSuccess': 'Import successful! {count} credentials generated', + 'oauth.kiro.importAllFailed': 'Import failed! {count} tokens failed to refresh', + 'oauth.kiro.importPartial': 'Partial success: {success} succeeded, {failed} failed', + 'oauth.kiro.importError': 'Import error', 'oauth.iflow.step1': 'Click the button below to open the iFlow authorization page', 'oauth.iflow.step2': 'Log in with your iFlow account and authorize', 'oauth.iflow.step3': 'After authorization, the system will automatically fetch the API Key', diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index 1c3b5c9..eff5d9d 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -489,6 +489,13 @@ function showKiroAuthMethodSelector(providerType) {
${t('oauth.kiro.awsBuilderDesc')}
+