diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index 039f036..e0428f1 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -1548,13 +1548,72 @@ async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DE } } +/** + * 检查 Kiro 凭据是否已存在(基于 refreshToken + provider 组合) + * @param {string} refreshToken - 要检查的 refreshToken + * @param {string} provider - 提供商名称 (默认: 'claude-kiro-oauth') + * @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果 + */ +export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'claude-kiro-oauth') { + const kiroDir = path.join(process.cwd(), 'configs', 'kiro'); + + try { + // 检查 configs/kiro 目录是否存在 + if (!fs.existsSync(kiroDir)) { + return { isDuplicate: false }; + } + + // 递归扫描所有 JSON 文件 + const scanDirectory = async (dirPath) => { + const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + const result = await scanDirectory(fullPath); + if (result.isDuplicate) { + return result; + } + } else if (entry.isFile() && entry.name.endsWith('.json')) { + try { + const content = await fs.promises.readFile(fullPath, 'utf8'); + const credentials = JSON.parse(content); + + // 检查 refreshToken 是否匹配 + if (credentials.refreshToken && credentials.refreshToken === refreshToken) { + const relativePath = path.relative(process.cwd(), fullPath); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`); + return { + isDuplicate: true, + existingPath: relativePath + }; + } + } catch (parseError) { + // 忽略解析错误的文件 + } + } + } + + return { isDuplicate: false }; + }; + + return await scanDirectory(kiroDir); + + } catch (error) { + console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message); + return { isDuplicate: false }; + } +} + /** * 批量导入 Kiro refreshToken 并生成凭据文件 * @param {string[]} refreshTokens - refreshToken 数组 * @param {string} region - AWS 区域 (默认: us-east-1) + * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) * @returns {Promise} 批量处理结果 */ -export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) { +export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, skipDuplicateCheck = false) { const results = { total: refreshTokens.length, success: 0, @@ -1575,6 +1634,21 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_ continue; } + // 检查重复 + if (!skipDuplicateCheck) { + const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken); + if (duplicateCheck.isDuplicate) { + results.details.push({ + index: i + 1, + success: false, + error: 'duplicate', + existingPath: duplicateCheck.existingPath + }); + results.failed++; + continue; + } + } + try { console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); @@ -1626,4 +1700,268 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_ } return results; -} \ No newline at end of file +} + +/** + * 批量导入 Kiro refreshToken 并生成凭据文件(流式版本,支持实时进度回调) + * @param {string[]} refreshTokens - refreshToken 数组 + * @param {string} region - AWS 区域 (默认: us-east-1) + * @param {Function} onProgress - 进度回调函数,每处理完一个 token 调用 + * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) + * @returns {Promise} 批量处理结果 + */ +export async function batchImportKiroRefreshTokensStream(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, onProgress = null, skipDuplicateCheck = false) { + const results = { + total: refreshTokens.length, + success: 0, + failed: 0, + details: [] + }; + + for (let i = 0; i < refreshTokens.length; i++) { + const refreshToken = refreshTokens[i].trim(); + const progressData = { + index: i + 1, + total: refreshTokens.length, + current: null + }; + + if (!refreshToken) { + progressData.current = { + index: i + 1, + success: false, + error: 'Empty token' + }; + results.details.push(progressData.current); + results.failed++; + + // 发送进度更新 + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: results.failed + }); + } + continue; + } + + // 检查重复 + if (!skipDuplicateCheck) { + const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken); + if (duplicateCheck.isDuplicate) { + progressData.current = { + index: i + 1, + success: false, + error: 'duplicate', + existingPath: duplicateCheck.existingPath + }; + results.details.push(progressData.current); + results.failed++; + + // 发送进度更新 + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: 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}`); + + progressData.current = { + index: i + 1, + success: true, + path: relativePath, + expiresAt: tokenData.expiresAt + }; + results.details.push(progressData.current); + results.success++; + + } catch (error) { + console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); + + progressData.current = { + index: i + 1, + success: false, + error: error.message + }; + results.details.push(progressData.current); + results.failed++; + } + + // 发送进度更新 + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: 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; +} + +/** + * 导入 AWS SSO 凭据用于 Kiro (Builder ID 模式) + * 从用户上传的 AWS SSO cache 文件中导入凭据 + * @param {Object} credentials - 合并后的凭据对象,需包含 clientId 和 clientSecret + * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) + * @returns {Promise} 导入结果 + */ +export async function importAwsCredentials(credentials, skipDuplicateCheck = false) { + try { + // 验证必需字段 - 需要四个字段都存在 + const missingFields = []; + if (!credentials.clientId) missingFields.push('clientId'); + if (!credentials.clientSecret) missingFields.push('clientSecret'); + if (!credentials.accessToken) missingFields.push('accessToken'); + if (!credentials.refreshToken) missingFields.push('refreshToken'); + + if (missingFields.length > 0) { + return { + success: false, + error: `Missing required fields: ${missingFields.join(', ')}` + }; + } + + // 检查重复凭据 + if (!skipDuplicateCheck) { + const duplicateCheck = await checkKiroCredentialsDuplicate(credentials.refreshToken); + if (duplicateCheck.isDuplicate) { + return { + success: false, + error: 'duplicate', + existingPath: duplicateCheck.existingPath + }; + } + } + + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`); + + // 准备凭据数据 - 四个字段都是必需的 + const credentialsData = { + clientId: credentials.clientId, + clientSecret: credentials.clientSecret, + accessToken: credentials.accessToken, + refreshToken: credentials.refreshToken, + authMethod: credentials.authMethod || 'builder-id', + region: credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION + }; + + // 可选字段 + if (credentials.expiresAt) { + credentialsData.expiresAt = credentials.expiresAt; + } + if (credentials.startUrl) { + credentialsData.startUrl = credentials.startUrl; + } + if (credentials.registrationExpiresAt) { + credentialsData.registrationExpiresAt = credentials.registrationExpiresAt; + } + + // 尝试刷新获取最新的 accessToken + try { + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`); + + const region = credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION; + const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', region); + + const refreshResponse = await fetch(refreshUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + refreshToken: credentials.refreshToken, + clientId: credentials.clientId, + clientSecret: credentials.clientSecret, + grantType: 'refresh_token' + }) + }); + + if (refreshResponse.ok) { + const tokenData = await refreshResponse.json(); + credentialsData.accessToken = tokenData.accessToken; + credentialsData.refreshToken = tokenData.refreshToken; + const expiresIn = tokenData.expiresIn || 3600; + credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`); + } else { + console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`); + } + } catch (refreshError) { + console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message); + // 继续保存原始凭据 + } + + // 生成文件路径: 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(credentialsData, null, 2)); + + const relativePath = path.relative(process.cwd(), credPath); + + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`); + + // 广播事件 + broadcastEvent('oauth_success', { + provider: 'claude-kiro-oauth', + relativePath: relativePath, + timestamp: new Date().toISOString() + }); + + // 自动关联新生成的凭据到 Pools + await autoLinkProviderConfigs(CONFIG); + + return { + success: true, + path: relativePath + }; + + } catch (error) { + console.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error); + return { + success: false, + error: error.message + }; + } +} + diff --git a/src/ui-manager.js b/src/ui-manager.js index fe42515..8c15a64 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, batchImportKiroRefreshTokens } from './oauth-handlers.js'; +import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth, handleIFlowOAuth, batchImportKiroRefreshTokens, batchImportKiroRefreshTokensStream, importAwsCredentials } from './oauth-handlers.js'; import { generateUUID, normalizePath, @@ -2126,8 +2126,8 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return true; } - // Batch import Kiro refresh tokens - // 批量导入 Kiro refreshToken + // Batch import Kiro refresh tokens with SSE (real-time progress) + // 批量导入 Kiro refreshToken(带实时进度 SSE) if (method === 'POST' && pathParam === '/api/kiro/batch-import-tokens') { try { const body = await getRequestBody(req); @@ -2142,21 +2142,125 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return true; } - console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens...`); + console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens with SSE...`); - const result = await batchImportKiroRefreshTokens(refreshTokens, region || 'us-east-1'); + // 设置 SSE 响应头 + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' + }); + + // 发送 SSE 事件的辅助函数 + const sendSSE = (event, data) => { + res.write(`event: ${event}\n`); + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + // 发送开始事件 + sendSSE('start', { total: refreshTokens.length }); + + // 执行流式批量导入 + const result = await batchImportKiroRefreshTokensStream( + refreshTokens, + region || 'us-east-1', + (progress) => { + // 每处理完一个 token 发送进度更新 + sendSSE('progress', progress); + } + ); console.log(`[Kiro Batch Import] Completed: ${result.success} success, ${result.failed} failed`); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ + // 发送完成事件 + sendSSE('complete', { success: true, - ...result - })); + total: result.total, + successCount: result.success, + failedCount: result.failed, + details: result.details + }); + + res.end(); return true; } catch (error) { console.error('[Kiro Batch Import] Error:', error); + // 如果已经开始发送 SSE,则发送错误事件 + if (res.headersSent) { + res.write(`event: error\n`); + res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); + res.end(); + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: error.message + })); + } + return true; + } + } + + // Import AWS SSO credentials for Kiro + // 导入 AWS SSO 凭据用于 Kiro + if (method === 'POST' && pathParam === '/api/kiro/import-aws-credentials') { + try { + const body = await getRequestBody(req); + const { credentials } = body; + + if (!credentials || typeof credentials !== 'object') { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: 'credentials object is required' + })); + return true; + } + + // 验证必需字段 - 需要四个字段都存在 + const missingFields = []; + if (!credentials.clientId) missingFields.push('clientId'); + if (!credentials.clientSecret) missingFields.push('clientSecret'); + if (!credentials.accessToken) missingFields.push('accessToken'); + if (!credentials.refreshToken) missingFields.push('refreshToken'); + + if (missingFields.length > 0) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: `Missing required fields: ${missingFields.join(', ')}` + })); + return true; + } + + console.log('[Kiro AWS Import] Starting AWS credentials import...'); + + const result = await importAwsCredentials(credentials); + + if (result.success) { + console.log(`[Kiro AWS Import] Successfully imported credentials to: ${result.path}`); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + path: result.path, + message: 'AWS credentials imported successfully' + })); + } else { + // 重复凭据返回 409 Conflict,其他错误返回 500 + const statusCode = result.error === 'duplicate' ? 409 : 500; + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: result.error, + existingPath: result.existingPath || null + })); + } + return true; + + } catch (error) { + console.error('[Kiro AWS Import] Error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, diff --git a/static/app/i18n.js b/static/app/i18n.js index f36bf78..9794f7c 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -133,18 +133,49 @@ const translations = { 'oauth.kiro.step3': '授权完成后页面会自动关闭', 'oauth.kiro.step4': '刷新本页面查看凭据文件', 'oauth.kiro.batchImport': '批量导入 refreshToken', - 'oauth.kiro.batchImportDesc': '批量导入已有的 refreshToken 生成凭据文件', + 'oauth.kiro.batchImportDesc': '批量导入已有的 refreshToken 生成凭据文件,该模式不支持 AWS 账号。', 'oauth.kiro.batchImportInstructions': '请输入 refreshToken,每行一个。系统将自动刷新并生成凭据文件。', + 'oauth.kiro.awsImport': '导入 AWS 账号', + 'oauth.kiro.awsImportDesc': '从 AWS SSO cache 目录导入凭据文件,适用于 AWS Builder ID 模式。', + 'oauth.kiro.awsImportInstructions': '请上传 AWS SSO cache 目录中的 JSON 文件,需包含 clientId、clientSecret、accessToken、refreshToken 四个字段。', + 'oauth.kiro.awsModeFile': '文件上传', + 'oauth.kiro.awsModeJson': 'JSON 粘贴', + 'oauth.kiro.awsUploadFiles': '上传凭据文件', + 'oauth.kiro.awsDragDrop': '拖拽文件到此处', + 'oauth.kiro.awsClickUpload': '或点击选择文件', + 'oauth.kiro.awsFileHint': '如果一个文件不包含全部字段,可以多次上传不同的文件进行补全', + 'oauth.kiro.awsSelectedFiles': '已选择的文件', + 'oauth.kiro.awsClearFiles': '清空全部', + 'oauth.kiro.awsFileReplaced': '已替换同名文件: {filename}', + 'oauth.kiro.awsJsonInput': '粘贴 JSON 凭据', + 'oauth.kiro.awsJsonPlaceholderSimple': '在此粘贴包含 clientId、clientSecret、accessToken、refreshToken 的 JSON...', + 'oauth.kiro.awsJsonExample': '查看 JSON 格式示例', + 'oauth.kiro.awsJsonHint': '可以直接粘贴合并后的 JSON,或从 AWS SSO cache 文件复制内容', + 'oauth.kiro.awsJsonParseError': 'JSON 格式错误', + 'oauth.kiro.awsParseError': '解析文件 {filename} 失败', + 'oauth.kiro.awsValidationSuccess': '验证通过!已找到全部必需字段', + 'oauth.kiro.awsValidationFailed': '验证失败!缺少必需字段', + 'oauth.kiro.awsMissingFields': '缺少 {count} 个字段', + 'oauth.kiro.awsUploadMore': '请上传包含缺失字段的文件,或切换到 JSON 模式手动补全', + 'oauth.kiro.awsPreviewJson': '合并后的凭据预览', + 'oauth.kiro.awsConfirmImport': '确认导入', + 'oauth.kiro.awsNoCredentials': '没有可导入的凭据', + 'oauth.kiro.awsImporting': '正在导入...', + 'oauth.kiro.awsImportSuccess': 'AWS 凭据导入成功!', + 'oauth.kiro.awsImportFailed': 'AWS 凭据导入失败', 'oauth.kiro.refreshTokensLabel': 'RefreshToken 列表', 'oauth.kiro.refreshTokensPlaceholder': '每行输入一个 refreshToken\n例如:\naorAxxxxxxxx\naorAyyyyyyyy\naorAzzzzzzzz', 'oauth.kiro.tokenCount': '待导入数量:', 'oauth.kiro.importing': '正在导入中,请稍候...', + 'oauth.kiro.importingProgress': '正在导入 {current}/{total}...', '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.kiro.duplicateToken': '重复凭据 - 此 refreshToken 已存在', + 'oauth.kiro.duplicateCredentials': '该凭据已存在,请勿重复导入', 'oauth.iflow.step1': '点击下方按钮在浏览器中打开 iFlow 授权页面', 'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权', 'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key', @@ -415,6 +446,8 @@ const translations = { 'common.loading': '加载中...', 'common.upload': '上传', 'common.generate': '生成', + 'common.found': '已找到', + 'common.missing': '缺失', 'common.search': '搜索', 'common.welcome': '欢迎使用AIClient2API管理控制台!', 'common.fileType': '不支持的文件类型,请选择 JSON、TXT、KEY、PEM、P12 或 PFX 文件', @@ -574,18 +607,49 @@ const translations = { '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.batchImportDesc': 'Batch import existing refresh tokens to generate credential files. This mode does not support AWS accounts.', 'oauth.kiro.batchImportInstructions': 'Enter refreshTokens, one per line. The system will automatically refresh and generate credential files.', + 'oauth.kiro.awsImport': 'Import AWS Account', + 'oauth.kiro.awsImportDesc': 'Import credential files from AWS SSO cache directory. For AWS Builder ID mode.', + 'oauth.kiro.awsImportInstructions': 'Upload JSON files from AWS SSO cache directory. Must contain clientId, clientSecret, accessToken, and refreshToken.', + 'oauth.kiro.awsModeFile': 'File Upload', + 'oauth.kiro.awsModeJson': 'Paste JSON', + 'oauth.kiro.awsUploadFiles': 'Upload Credential Files', + 'oauth.kiro.awsDragDrop': 'Drag and drop files here', + 'oauth.kiro.awsClickUpload': 'or click to select files', + 'oauth.kiro.awsFileHint': 'If one file doesn\'t contain all fields, you can upload multiple files to complete them', + 'oauth.kiro.awsSelectedFiles': 'Selected Files', + 'oauth.kiro.awsClearFiles': 'Clear All', + 'oauth.kiro.awsFileReplaced': 'Replaced file: {filename}', + 'oauth.kiro.awsJsonInput': 'Paste JSON Credentials', + 'oauth.kiro.awsJsonPlaceholderSimple': 'Paste JSON containing clientId, clientSecret, accessToken, refreshToken here...', + 'oauth.kiro.awsJsonExample': 'View JSON format example', + 'oauth.kiro.awsJsonHint': 'You can paste merged JSON directly, or copy content from AWS SSO cache files', + 'oauth.kiro.awsJsonParseError': 'Invalid JSON format', + 'oauth.kiro.awsParseError': 'Failed to parse file {filename}', + 'oauth.kiro.awsValidationSuccess': 'Validation passed! All required fields found', + 'oauth.kiro.awsValidationFailed': 'Validation failed! Required fields missing', + 'oauth.kiro.awsMissingFields': '{count} field(s) missing', + 'oauth.kiro.awsUploadMore': 'Please upload files containing the missing fields, or switch to JSON mode to complete manually', + 'oauth.kiro.awsPreviewJson': 'Merged Credentials Preview', + 'oauth.kiro.awsConfirmImport': 'Confirm Import', + 'oauth.kiro.awsNoCredentials': 'No credentials to import', + 'oauth.kiro.awsImporting': 'Importing...', + 'oauth.kiro.awsImportSuccess': 'AWS credentials imported successfully!', + 'oauth.kiro.awsImportFailed': 'AWS credentials import failed', '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.importingProgress': 'Importing {current}/{total}...', '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.kiro.duplicateToken': 'Duplicate - this refreshToken already exists', + 'oauth.kiro.duplicateCredentials': 'This credential already exists, please do not import duplicates', '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', @@ -857,6 +921,8 @@ const translations = { 'common.loading': 'Loading...', 'common.upload': 'Upload', 'common.generate': 'Generate', + 'common.found': 'Found', + 'common.missing': 'Missing', 'common.search': 'Search', 'common.welcome': 'Welcome to AIClient2API Management Console!', 'common.fileType': 'Unsupported file type. Please select JSON, TXT, KEY, PEM, P12, or PFX.', diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index eff5d9d..3d671c8 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -496,6 +496,13 @@ function showKiroAuthMethodSelector(providerType) {
${t('oauth.kiro.batchImportDesc')}
+