// 提供商管理功能模块 import { providerStats, updateProviderStats } from './constants.js'; import { showToast, formatUptime } from './utils.js'; import { fileUploadHandler } from './file-upload.js'; import { t, getCurrentLanguage } from './i18n.js'; import { loadConfigList } from './upload-config-manager.js'; import { setServiceMode } from './event-handlers.js'; // 保存初始服务器时间和运行时间 let initialServerTime = null; let initialUptime = null; let initialLoadTime = null; /** * 加载系统信息 */ async function loadSystemInfo() { try { const data = await window.apiClient.get('/system'); const appVersionEl = document.getElementById('appVersion'); const nodeVersionEl = document.getElementById('nodeVersion'); const serverTimeEl = document.getElementById('serverTime'); const memoryUsageEl = document.getElementById('memoryUsage'); const cpuUsageEl = document.getElementById('cpuUsage'); const uptimeEl = document.getElementById('uptime'); if (appVersionEl) appVersionEl.textContent = data.appVersion ? `v${data.appVersion}` : '--'; // 自动检查更新 if (data.appVersion) { checkUpdate(true); } if (nodeVersionEl) nodeVersionEl.textContent = data.nodeVersion || '--'; if (memoryUsageEl) memoryUsageEl.textContent = data.memoryUsage || '--'; if (cpuUsageEl) cpuUsageEl.textContent = data.cpuUsage || '--'; // 保存初始时间用于本地计算 if (data.serverTime && data.uptime !== undefined) { initialServerTime = new Date(data.serverTime); initialUptime = data.uptime; initialLoadTime = Date.now(); } // 初始显示 if (serverTimeEl) serverTimeEl.textContent = data.serverTime || '--'; if (uptimeEl) uptimeEl.textContent = data.uptime ? formatUptime(data.uptime) : '--'; // 加载服务模式信息 await loadServiceModeInfo(); } catch (error) { console.error('Failed to load system info:', error); } } /** * 加载服务运行模式信息 */ async function loadServiceModeInfo() { try { const data = await window.apiClient.get('/service-mode'); const serviceModeEl = document.getElementById('serviceMode'); const processPidEl = document.getElementById('processPid'); const platformInfoEl = document.getElementById('platformInfo'); // 更新服务模式到 event-handlers setServiceMode(data.mode || 'worker'); // 更新重启/重载按钮显示 updateRestartButton(data.mode); if (serviceModeEl) { const modeText = data.mode === 'worker' ? t('dashboard.serviceMode.worker') : t('dashboard.serviceMode.standalone'); const canRestartIcon = data.canAutoRestart ? '' : ''; serviceModeEl.innerHTML = modeText; } if (processPidEl) { processPidEl.textContent = data.pid || '--'; } if (platformInfoEl) { // 格式化平台信息 const platformMap = { 'win32': 'Windows', 'darwin': 'macOS', 'linux': 'Linux', 'freebsd': 'FreeBSD' }; platformInfoEl.textContent = platformMap[data.platform] || data.platform || '--'; } } catch (error) { console.error('Failed to load service mode info:', error); } } /** * 根据服务模式更新重启/重载按钮显示 * @param {string} mode - 服务模式 ('worker' 或 'standalone') */ function updateRestartButton(mode) { const restartBtn = document.getElementById('restartBtn'); const restartBtnIcon = document.getElementById('restartBtnIcon'); const restartBtnText = document.getElementById('restartBtnText'); if (!restartBtn) return; if (mode === 'standalone') { // 独立模式:显示"重载"按钮 if (restartBtnIcon) { restartBtnIcon.className = 'fas fa-sync-alt'; } if (restartBtnText) { restartBtnText.textContent = t('header.reload'); restartBtnText.setAttribute('data-i18n', 'header.reload'); } restartBtn.setAttribute('aria-label', t('header.reload')); restartBtn.setAttribute('data-i18n-aria-label', 'header.reload'); restartBtn.title = t('header.reload'); } else { // 子进程模式:显示"重启"按钮 if (restartBtnIcon) { restartBtnIcon.className = 'fas fa-redo'; } if (restartBtnText) { restartBtnText.textContent = t('header.restart'); restartBtnText.setAttribute('data-i18n', 'header.restart'); } restartBtn.setAttribute('aria-label', t('header.restart')); restartBtn.setAttribute('data-i18n-aria-label', 'header.restart'); restartBtn.title = t('header.restart'); } } /** * 更新服务器时间和运行时间显示(本地计算) */ function updateTimeDisplay() { if (!initialServerTime || initialUptime === null || !initialLoadTime) { return; } const serverTimeEl = document.getElementById('serverTime'); const uptimeEl = document.getElementById('uptime'); // 计算经过的秒数 const elapsedSeconds = Math.floor((Date.now() - initialLoadTime) / 1000); // 更新服务器时间 if (serverTimeEl) { const currentServerTime = new Date(initialServerTime.getTime() + elapsedSeconds * 1000); serverTimeEl.textContent = currentServerTime.toLocaleString(getCurrentLanguage(), { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } // 更新运行时间 if (uptimeEl) { const currentUptime = initialUptime + elapsedSeconds; uptimeEl.textContent = formatUptime(currentUptime); } } /** * 加载提供商列表 */ async function loadProviders() { try { const data = await window.apiClient.get('/providers'); renderProviders(data); } catch (error) { console.error('Failed to load providers:', error); } } /** * 渲染提供商列表 * @param {Object} providers - 提供商数据 */ function renderProviders(providers) { const container = document.getElementById('providersList'); if (!container) return; container.innerHTML = ''; // 检查是否有提供商池数据 const hasProviders = Object.keys(providers).length > 0; const statsGrid = document.querySelector('#providers .stats-grid'); // 始终显示统计卡片 if (statsGrid) statsGrid.style.display = 'grid'; // 定义所有支持的提供商配置(顺序、显示名称、是否显示) const providerConfigs = [ { id: 'forward-api', name: 'NewAPI', visible: false }, { id: 'gemini-cli-oauth', name: 'Gemini CLI OAuth', visible: true }, { id: 'gemini-antigravity', name: 'Gemini Antigravity', visible: true }, { id: 'openai-custom', name: 'OpenAI Custom', visible: true }, { id: 'claude-custom', name: 'Claude Custom', visible: true }, { id: 'claude-kiro-oauth', name: 'Claude Kiro OAuth', visible: true }, { id: 'openai-qwen-oauth', name: 'OpenAI Qwen OAuth', visible: true }, { id: 'openaiResponses-custom', name: 'OpenAI Responses', visible: true }, { id: 'openai-iflow', name: 'OpenAI iFlow', visible: true }, { id: 'openai-codex-oauth', name: 'OpenAI Codex OAuth', visible: true }, ]; // 提取显示的 ID 顺序 const providerDisplayOrder = providerConfigs.filter(c => c.visible !== false).map(c => c.id); // 建立 ID 到配置的映射,方便获取显示名称 const configMap = providerConfigs.reduce((map, config) => { map[config.id] = config; return map; }, {}); // 获取所有提供商类型并按指定顺序排序 // 优先显示预定义的所有提供商类型,即使某些提供商没有数据也要显示 let allProviderTypes; if (hasProviders) { // 合并预定义类型和实际存在的类型,确保显示所有预定义提供商 const actualProviderTypes = Object.keys(providers); // 只保留配置中标记为 visible 的,或者不在配置中的(默认显示) allProviderTypes = [...new Set([...providerDisplayOrder, ...actualProviderTypes])]; } else { allProviderTypes = providerDisplayOrder; } // 过滤掉明确设置为不显示的提供商 const sortedProviderTypes = providerDisplayOrder.filter(type => allProviderTypes.includes(type)) .concat(allProviderTypes.filter(type => !providerDisplayOrder.some(t => t === type) && !configMap[type]?.visible === false)); // 计算总统计 let totalAccounts = 0; let totalHealthy = 0; // 按照排序后的提供商类型渲染 sortedProviderTypes.forEach((providerType) => { // 如果配置中明确设置为不显示,则跳过 if (configMap[providerType] && configMap[providerType].visible === false) { return; } const accounts = hasProviders ? providers[providerType] || [] : []; const providerDiv = document.createElement('div'); providerDiv.className = 'provider-item'; providerDiv.dataset.providerType = providerType; providerDiv.style.cursor = 'pointer'; const healthyCount = accounts.filter(acc => acc.isHealthy).length; const totalCount = accounts.length; const usageCount = accounts.reduce((sum, acc) => sum + (acc.usageCount || 0), 0); const errorCount = accounts.reduce((sum, acc) => sum + (acc.errorCount || 0), 0); totalAccounts += totalCount; totalHealthy += healthyCount; // 更新全局统计变量 if (!providerStats.providerTypeStats[providerType]) { providerStats.providerTypeStats[providerType] = { totalAccounts: 0, healthyAccounts: 0, totalUsage: 0, totalErrors: 0, lastUpdate: null }; } const typeStats = providerStats.providerTypeStats[providerType]; typeStats.totalAccounts = totalCount; typeStats.healthyAccounts = healthyCount; typeStats.totalUsage = usageCount; typeStats.totalErrors = errorCount; typeStats.lastUpdate = new Date().toISOString(); // 为无数据状态设置特殊样式 const isEmptyState = !hasProviders || totalCount === 0; const statusClass = isEmptyState ? 'status-empty' : (healthyCount === totalCount ? 'status-healthy' : 'status-unhealthy'); const statusIcon = isEmptyState ? 'fa-info-circle' : (healthyCount === totalCount ? 'fa-check-circle' : 'fa-exclamation-triangle'); const statusText = isEmptyState ? t('providers.status.empty') : t('providers.status.healthy', { healthy: healthyCount, total: totalCount }); // 获取显示名称 const displayName = configMap[providerType]?.name || providerType; providerDiv.innerHTML = `
${displayName}
${generateAuthButton(providerType)}
${statusText}
${t('providers.stat.totalAccounts')} ${totalCount}
${t('providers.stat.healthyAccounts')} ${healthyCount}
${t('providers.stat.usageCount')} ${usageCount}
${t('providers.stat.errorCount')} ${errorCount}
`; // 如果是空状态,添加特殊样式 if (isEmptyState) { providerDiv.classList.add('empty-provider'); } // 添加点击事件 - 整个提供商组都可以点击 providerDiv.addEventListener('click', (e) => { e.preventDefault(); openProviderManager(providerType); }); container.appendChild(providerDiv); // 为授权按钮添加事件监听 const authBtn = providerDiv.querySelector('.generate-auth-btn'); if (authBtn) { authBtn.addEventListener('click', (e) => { e.stopPropagation(); // 阻止事件冒泡到父元素 handleGenerateAuthUrl(providerType); }); } }); // 更新统计卡片数据 const activeProviders = hasProviders ? Object.keys(providers).length : 0; updateProviderStatsDisplay(activeProviders, totalHealthy, totalAccounts); } /** * 更新提供商统计信息 * @param {number} activeProviders - 活跃提供商数 * @param {number} healthyProviders - 健康提供商数 * @param {number} totalAccounts - 总账户数 */ function updateProviderStatsDisplay(activeProviders, healthyProviders, totalAccounts) { // 更新全局统计变量 const newStats = { activeProviders, healthyProviders, totalAccounts, lastUpdateTime: new Date().toISOString() }; updateProviderStats(newStats); // 计算总请求数和错误数 let totalUsage = 0; let totalErrors = 0; Object.values(providerStats.providerTypeStats).forEach(typeStats => { totalUsage += typeStats.totalUsage || 0; totalErrors += typeStats.totalErrors || 0; }); const finalStats = { ...newStats, totalRequests: totalUsage, totalErrors: totalErrors }; updateProviderStats(finalStats); // 修改:根据使用次数统计"活跃提供商"和"活动连接" // "活跃提供商":统计有使用次数(usageCount > 0)的提供商类型数量 let activeProvidersByUsage = 0; Object.entries(providerStats.providerTypeStats).forEach(([providerType, typeStats]) => { if (typeStats.totalUsage > 0) { activeProvidersByUsage++; } }); // "活动连接":统计所有提供商账户的使用次数总和 const activeConnections = totalUsage; // 更新页面显示 const activeProvidersEl = document.getElementById('activeProviders'); const healthyProvidersEl = document.getElementById('healthyProviders'); const activeConnectionsEl = document.getElementById('activeConnections'); if (activeProvidersEl) activeProvidersEl.textContent = activeProvidersByUsage; if (healthyProvidersEl) healthyProvidersEl.textContent = healthyProviders; if (activeConnectionsEl) activeConnectionsEl.textContent = activeConnections; // 打印调试信息到控制台 console.log('Provider Stats Updated:', { activeProviders, activeProvidersByUsage, healthyProviders, totalAccounts, totalUsage, totalErrors, providerTypeStats: providerStats.providerTypeStats }); } /** * 打开提供商管理模态框 * @param {string} providerType - 提供商类型 */ async function openProviderManager(providerType) { try { const data = await window.apiClient.get(`/providers/${encodeURIComponent(providerType)}`); showProviderManagerModal(data); } catch (error) { console.error('Failed to load provider details:', error); showToast(t('common.error'), t('modal.provider.load.failed'), 'error'); } } /** * 生成授权按钮HTML * @param {string} providerType - 提供商类型 * @returns {string} 授权按钮HTML */ function generateAuthButton(providerType) { // 只为支持OAuth的提供商显示授权按钮 const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'openai-iflow', 'openai-codex-oauth']; if (!oauthProviders.includes(providerType)) { return ''; } // Codex 提供商使用特殊图标 if (providerType === 'openai-codex-oauth') { return ` `; } return ` `; } /** * 处理生成授权链接 * @param {string} providerType - 提供商类型 */ async function handleGenerateAuthUrl(providerType) { // 如果是 Kiro OAuth,先显示认证方式选择对话框 if (providerType === 'claude-kiro-oauth') { showKiroAuthMethodSelector(providerType); return; } // 如果是 Gemini OAuth 或 Antigravity,显示认证方式选择对话框 if (providerType === 'gemini-cli-oauth' || providerType === 'gemini-antigravity') { showGeminiAuthMethodSelector(providerType); return; } await executeGenerateAuthUrl(providerType, {}); } /** * 显示 Kiro OAuth 认证方式选择对话框 * @param {string} providerType - 提供商类型 */ function showKiroAuthMethodSelector(providerType) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); // 关闭按钮事件 const closeBtn = modal.querySelector('.modal-close'); const cancelBtn = modal.querySelector('.modal-cancel'); [closeBtn, cancelBtn].forEach(btn => { btn.addEventListener('click', () => { modal.remove(); }); }); // 认证方式选择按钮事件 const methodBtns = modal.querySelectorAll('.auth-method-btn'); methodBtns.forEach(btn => { btn.addEventListener('mouseenter', () => { btn.style.borderColor = '#00a67e'; btn.style.background = '#f8fffe'; }); btn.addEventListener('mouseleave', () => { btn.style.borderColor = '#e0e0e0'; btn.style.background = 'white'; }); btn.addEventListener('click', async () => { const method = btn.dataset.method; modal.remove(); if (method === 'batch-import') { showKiroBatchImportModal(); } else if (method === 'aws-import') { showKiroAwsImportModal(); } else { await executeGenerateAuthUrl(providerType, { method }); } }); }); } /** * 显示 Gemini OAuth 认证方式选择对话框 * @param {string} providerType - 提供商类型 */ function showGeminiAuthMethodSelector(providerType) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); // 关闭按钮事件 const closeBtn = modal.querySelector('.modal-close'); const cancelBtn = modal.querySelector('.modal-cancel'); [closeBtn, cancelBtn].forEach(btn => { btn.addEventListener('click', () => { modal.remove(); }); }); // 认证方式选择按钮事件 const methodBtns = modal.querySelectorAll('.auth-method-btn'); methodBtns.forEach(btn => { btn.addEventListener('mouseenter', () => { btn.style.borderColor = '#4285f4'; btn.style.background = '#f8faff'; }); btn.addEventListener('mouseleave', () => { btn.style.borderColor = '#e0e0e0'; btn.style.background = 'white'; }); btn.addEventListener('click', async () => { const method = btn.dataset.method; modal.remove(); if (method === 'batch-import') { showGeminiBatchImportModal(providerType); } else { await executeGenerateAuthUrl(providerType, {}); } }); }); } /** * 显示 Gemini 批量导入模态框 * @param {string} providerType - 提供商类型 */ function showGeminiBatchImportModal(providerType) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); const textarea = modal.querySelector('#batchGeminiTokens'); const statsDiv = modal.querySelector('#geminiBatchStats'); const tokenCountValue = modal.querySelector('#geminiTokenCountValue'); const progressDiv = modal.querySelector('#geminiBatchProgress'); const progressBar = modal.querySelector('#geminiImportProgressBar'); const resultDiv = modal.querySelector('#geminiBatchResult'); const submitBtn = modal.querySelector('#geminiBatchSubmit'); const closeBtn = modal.querySelector('.modal-close'); const cancelBtn = modal.querySelector('.modal-cancel'); // 实时统计 token 数量 textarea.addEventListener('input', () => { try { const val = textarea.value.trim(); if (!val) { statsDiv.style.display = 'none'; return; } const data = JSON.parse(val); const tokens = Array.isArray(data) ? data : [data]; statsDiv.style.display = 'block'; tokenCountValue.textContent = tokens.length; } catch (e) { statsDiv.style.display = 'none'; } }); // 关闭按钮事件 [closeBtn, cancelBtn].forEach(btn => { btn.addEventListener('click', () => { modal.remove(); }); }); // 提交按钮事件 submitBtn.addEventListener('click', async () => { let tokens = []; try { const val = textarea.value.trim(); const data = JSON.parse(val); tokens = Array.isArray(data) ? data : [data]; } catch (e) { showToast(t('common.error'), t('oauth.gemini.noTokens'), 'error'); return; } if (tokens.length === 0) { showToast(t('common.warning'), t('oauth.gemini.noTokens'), 'warning'); return; } // 禁用输入和按钮 textarea.disabled = true; submitBtn.disabled = true; cancelBtn.disabled = true; progressDiv.style.display = 'block'; resultDiv.style.display = 'none'; progressBar.style.width = '0%'; // 创建实时结果显示区域 resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;'; resultDiv.innerHTML = `
${t('oauth.gemini.importingProgress', { current: 0, total: tokens.length })}
`; const progressText = resultDiv.querySelector('#geminiBatchProgressText'); const resultsList = resultDiv.querySelector('#geminiBatchResultsList'); let importSuccess = false; // 标记是否导入成功 try { const response = await fetch('/api/gemini/batch-import-tokens', { method: 'POST', headers: window.apiClient ? window.apiClient.getAuthHeaders() : { 'Content-Type': 'application/json' }, body: JSON.stringify({ providerType, tokens }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; let eventType = ''; let eventData = ''; for (const line of lines) { if (line.startsWith('event: ')) { eventType = line.substring(7).trim(); } else if (line.startsWith('data: ')) { eventData = line.substring(6).trim(); if (eventType && eventData) { try { const data = JSON.parse(eventData); if (eventType === 'progress') { const { index, total, current } = data; const percentage = Math.round((index / total) * 100); progressBar.style.width = `${percentage}%`; progressText.textContent = t('oauth.gemini.importingProgress', { current: index, total: total }); const resultItem = document.createElement('div'); resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);'; if (current.success) { resultItem.innerHTML = `Token ${current.index}: ✓ ${current.path}`; } else if (current.error === 'duplicate') { resultItem.innerHTML = `Token ${current.index}: ⚠ ${t('oauth.kiro.duplicateToken')} ${current.existingPath ? `(${current.existingPath})` : ''}`; } else { resultItem.innerHTML = `Token ${current.index}: ✗ ${current.error}`; } resultsList.appendChild(resultItem); resultsList.scrollTop = resultsList.scrollHeight; } else if (eventType === 'complete') { progressBar.style.width = '100%'; progressDiv.style.display = 'none'; const isAllSuccess = data.failedCount === 0; const isAllFailed = data.successCount === 0; let resultClass, resultIcon, resultMessage; if (isAllSuccess) { resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;'; resultIcon = 'fa-check-circle'; resultMessage = t('oauth.gemini.importSuccess', { count: data.successCount }); } else if (isAllFailed) { resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; resultIcon = 'fa-times-circle'; resultMessage = t('oauth.gemini.importAllFailed', { count: data.failedCount }); } else { resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;'; resultIcon = 'fa-exclamation-triangle'; resultMessage = t('oauth.gemini.importPartial', { success: data.successCount, failed: data.failedCount }); } resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`; const headerDiv = resultDiv.querySelector('div:first-child'); headerDiv.innerHTML = ` ${resultMessage}`; if (data.successCount > 0) { importSuccess = true; loadProviders(); loadConfigList(); } } else if (eventType === 'error') { throw new Error(data.error); } } catch (parseError) { console.warn('Failed to parse SSE data:', parseError); } eventType = ''; eventData = ''; } } } } } catch (error) { console.error('[Gemini Batch Import] Failed:', error); progressDiv.style.display = 'none'; resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; resultDiv.innerHTML = `
${t('oauth.gemini.importError')}: ${error.message}
`; } finally { cancelBtn.disabled = false; if (!importSuccess) { textarea.disabled = false; submitBtn.disabled = false; submitBtn.innerHTML = ` ${t('oauth.gemini.startImport')}`; } else { submitBtn.innerHTML = ` ${t('common.success')}`; } } }); } /** * 显示 Kiro 批量导入 refreshToken 模态框 */ function showKiroBatchImportModal() { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); const textarea = modal.querySelector('#batchRefreshTokens'); const statsDiv = modal.querySelector('#batchImportStats'); const tokenCountValue = modal.querySelector('#tokenCountValue'); const progressDiv = modal.querySelector('#batchImportProgress'); const progressBar = modal.querySelector('#importProgressBar'); const resultDiv = modal.querySelector('#batchImportResult'); const submitBtn = modal.querySelector('#batchImportSubmit'); const closeBtn = modal.querySelector('.modal-close'); const cancelBtn = modal.querySelector('.modal-cancel'); // 实时统计 token 数量 textarea.addEventListener('input', () => { const tokens = textarea.value.split('\n').filter(line => line.trim()); if (tokens.length > 0) { statsDiv.style.display = 'block'; tokenCountValue.textContent = tokens.length; } else { statsDiv.style.display = 'none'; } }); // 关闭按钮事件 [closeBtn, cancelBtn].forEach(btn => { btn.addEventListener('click', () => { modal.remove(); }); }); // 提交按钮事件 - 使用 SSE 流式响应实时显示进度 submitBtn.addEventListener('click', async () => { const tokens = textarea.value.split('\n').filter(line => line.trim()); if (tokens.length === 0) { showToast(t('common.warning'), t('oauth.kiro.noTokens'), 'warning'); return; } // 禁用输入和按钮 textarea.disabled = true; submitBtn.disabled = true; cancelBtn.disabled = true; progressDiv.style.display = 'block'; resultDiv.style.display = 'none'; progressBar.style.width = '0%'; // 创建实时结果显示区域 resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;'; resultDiv.innerHTML = `
${t('oauth.kiro.importingProgress', { current: 0, total: tokens.length })}
`; const progressText = resultDiv.querySelector('#batchProgressText'); const resultsList = resultDiv.querySelector('#batchResultsList'); let successCount = 0; let failedCount = 0; const details = []; let importSuccess = false; // 标记是否导入成功 try { // 使用 fetch + SSE 获取流式响应(需要带认证头) const response = await fetch('/api/kiro/batch-import-tokens', { method: 'POST', headers: window.apiClient ? window.apiClient.getAuthHeaders() : { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshTokens: tokens }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 解析 SSE 事件 const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 保留最后一个可能不完整的行 let eventType = ''; let eventData = ''; for (const line of lines) { if (line.startsWith('event: ')) { eventType = line.substring(7).trim(); } else if (line.startsWith('data: ')) { eventData = line.substring(6).trim(); if (eventType && eventData) { try { const data = JSON.parse(eventData); if (eventType === 'start') { // 开始事件 console.log(`[Batch Import] Starting import of ${data.total} tokens`); } else if (eventType === 'progress') { // 进度更新 const { index, total, current, successCount: sc, failedCount: fc } = data; successCount = sc; failedCount = fc; details.push(current); // 更新进度条 const percentage = Math.round((index / total) * 100); progressBar.style.width = `${percentage}%`; // 更新进度文本 progressText.textContent = t('oauth.kiro.importingProgress', { current: index, total: total }); // 添加结果项 const resultItem = document.createElement('div'); resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);'; if (current.success) { resultItem.innerHTML = `Token ${current.index}: ✓ ${current.path}`; } else if (current.error === 'duplicate') { resultItem.innerHTML = `Token ${current.index}: ⚠ ${t('oauth.kiro.duplicateToken')} ${current.existingPath ? `(${current.existingPath})` : ''}`; } else { resultItem.innerHTML = `Token ${current.index}: ✗ ${current.error}`; } resultsList.appendChild(resultItem); // 自动滚动到底部 resultsList.scrollTop = resultsList.scrollHeight; } else if (eventType === 'complete') { // 完成事件 progressBar.style.width = '100%'; progressDiv.style.display = 'none'; const isAllSuccess = data.failedCount === 0; const isAllFailed = data.successCount === 0; let resultClass, resultIcon, resultMessage; if (isAllSuccess) { resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;'; resultIcon = 'fa-check-circle'; resultMessage = t('oauth.kiro.importSuccess', { count: data.successCount }); } else if (isAllFailed) { resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; resultIcon = 'fa-times-circle'; resultMessage = t('oauth.kiro.importAllFailed', { count: data.failedCount }); } else { resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;'; resultIcon = 'fa-exclamation-triangle'; resultMessage = t('oauth.kiro.importPartial', { success: data.successCount, failed: data.failedCount }); } // 更新结果区域样式 resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`; // 更新标题 const headerDiv = resultDiv.querySelector('div:first-child'); headerDiv.innerHTML = ` ${resultMessage}`; // 如果有成功的,刷新提供商列表 if (data.successCount > 0) { importSuccess = true; loadProviders(); loadConfigList(); } } else if (eventType === 'error') { throw new Error(data.error); } } catch (parseError) { console.warn('Failed to parse SSE data:', parseError); } eventType = ''; eventData = ''; } } } } } catch (error) { console.error('[Kiro Batch Import] Failed:', error); progressDiv.style.display = 'none'; resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; resultDiv.innerHTML = `
${t('oauth.kiro.importError')}: ${error.message}
`; } finally { // 重新启用按钮 cancelBtn.disabled = false; if (!importSuccess) { textarea.disabled = false; submitBtn.disabled = false; submitBtn.innerHTML = ` ${t('oauth.kiro.startImport')}`; } else { submitBtn.innerHTML = ` ${t('common.success')}`; } } }); } /** * 显示 Kiro AWS 账号导入模态框 * 支持从 AWS SSO cache 目录导入凭据文件,或直接粘贴 JSON */ function showKiroAwsImportModal() { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); const fileInput = modal.querySelector('#awsFilesInput'); const uploadArea = modal.querySelector('.aws-file-upload-area'); const filesListDiv = modal.querySelector('#awsFilesList'); const filesContainer = modal.querySelector('#awsFilesContainer'); const clearFilesBtn = modal.querySelector('#clearFilesBtn'); const validationResult = modal.querySelector('#awsValidationResult'); const jsonPreview = modal.querySelector('#awsJsonPreview'); const jsonContent = modal.querySelector('#awsJsonContent'); const submitBtn = modal.querySelector('#awsImportSubmit'); const closeBtn = modal.querySelector('.modal-close'); const cancelBtn = modal.querySelector('.modal-cancel'); const modeBtns = modal.querySelectorAll('.mode-btn'); const fileModeSection = modal.querySelector('#fileModeSection'); const jsonModeSection = modal.querySelector('#jsonModeSection'); const jsonInputTextarea = modal.querySelector('#awsJsonInput'); let uploadedFiles = []; let mergedCredentials = null; let currentMode = 'file'; // 清空文件按钮事件 clearFilesBtn.addEventListener('click', () => { uploadedFiles = []; filesContainer.innerHTML = ''; filesListDiv.style.display = 'none'; validationResult.style.display = 'none'; jsonPreview.style.display = 'none'; submitBtn.disabled = true; mergedCredentials = null; // 清空 file input fileInput.value = ''; }); // 清空按钮 hover 效果 clearFilesBtn.addEventListener('mouseenter', () => { clearFilesBtn.style.background = '#fef2f2'; }); clearFilesBtn.addEventListener('mouseleave', () => { clearFilesBtn.style.background = 'none'; }); // 模式切换 modeBtns.forEach(btn => { btn.addEventListener('click', () => { const mode = btn.dataset.mode; if (mode === currentMode) return; currentMode = mode; // 更新按钮样式 modeBtns.forEach(b => { if (b.dataset.mode === mode) { b.style.borderColor = '#ff9900'; b.style.background = '#fff7ed'; b.style.color = '#9a3412'; b.classList.add('active'); } else { b.style.borderColor = '#d1d5db'; b.style.background = 'white'; b.style.color = '#6b7280'; b.classList.remove('active'); } }); // 切换显示区域 if (mode === 'file') { fileModeSection.style.display = 'block'; jsonModeSection.style.display = 'none'; // 重新验证文件模式的内容 validateAndPreview(); } else { fileModeSection.style.display = 'none'; jsonModeSection.style.display = 'block'; // 验证 JSON 输入 validateJsonInput(); } }); }); // JSON 输入实时验证 jsonInputTextarea.addEventListener('input', () => { validateJsonInput(); }); // 验证 JSON 输入 function validateJsonInput() { const inputValue = jsonInputTextarea.value.trim(); if (!inputValue) { validationResult.style.display = 'none'; jsonPreview.style.display = 'none'; submitBtn.disabled = true; mergedCredentials = null; return; } try { mergedCredentials = JSON.parse(inputValue); validateAndShowResult(); } catch (error) { validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; validationResult.innerHTML = `
${t('oauth.kiro.awsJsonParseError')}

${error.message}

`; jsonPreview.style.display = 'none'; submitBtn.disabled = true; mergedCredentials = null; } } // 文件上传区域交互 uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#ff9900'; uploadArea.style.background = '#fffbeb'; }); uploadArea.addEventListener('dragleave', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#d1d5db'; uploadArea.style.background = 'transparent'; }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#d1d5db'; uploadArea.style.background = 'transparent'; const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.json')); if (files.length > 0) { processFiles(files); } }); fileInput.addEventListener('change', () => { const files = Array.from(fileInput.files); if (files.length > 0) { processFiles(files); } }); // 处理上传的文件(支持追加) async function processFiles(files) { for (const file of files) { // 检查是否已存在同名文件 const existingIndex = uploadedFiles.findIndex(f => f.name === file.name); try { const content = await readFileAsText(file); const json = JSON.parse(content); if (existingIndex >= 0) { // 替换已存在的同名文件 uploadedFiles[existingIndex] = { name: file.name, content: json }; showToast(t('common.info'), t('oauth.kiro.awsFileReplaced', { filename: file.name }), 'info'); } else { // 追加新文件 uploadedFiles.push({ name: file.name, content: json }); } } catch (error) { console.error(`Failed to parse ${file.name}:`, error); showToast(t('common.error'), t('oauth.kiro.awsParseError', { filename: file.name }), 'error'); } } // 重新渲染文件列表 renderFilesList(); filesListDiv.style.display = uploadedFiles.length > 0 ? 'block' : 'none'; // 清空 file input 以便可以再次选择相同文件 fileInput.value = ''; validateAndPreview(); } // 渲染文件列表 function renderFilesList() { filesContainer.innerHTML = ''; for (const file of uploadedFiles) { const fileDiv = document.createElement('div'); fileDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center; padding: 8px; background: white; border-radius: 4px; margin-bottom: 4px;'; fileDiv.dataset.filename = file.name; const fields = Object.keys(file.content).slice(0, 5).join(', '); const moreFields = Object.keys(file.content).length > 5 ? '...' : ''; fileDiv.innerHTML = `
${file.name}
${fields}${moreFields}
`; filesContainer.appendChild(fileDiv); } // 添加删除文件按钮事件 filesContainer.querySelectorAll('.remove-file-btn').forEach(btn => { btn.addEventListener('click', (e) => { const filename = e.currentTarget.dataset.filename; uploadedFiles = uploadedFiles.filter(f => f.name !== filename); renderFilesList(); filesListDiv.style.display = uploadedFiles.length > 0 ? 'block' : 'none'; validateAndPreview(); }); }); } // 验证并预览(文件模式) function validateAndPreview() { if (currentMode !== 'file') return; if (uploadedFiles.length === 0) { validationResult.style.display = 'none'; jsonPreview.style.display = 'none'; submitBtn.disabled = true; mergedCredentials = null; return; } // 智能合并所有文件的内容 // 如果多个文件都有 expiresAt,使用包含 refreshToken 的文件中的 expiresAt mergedCredentials = {}; let expiresAtFromRefreshTokenFile = null; for (const file of uploadedFiles) { // 如果这个文件包含 refreshToken,记录它的 expiresAt if (file.content.refreshToken && file.content.expiresAt) { expiresAtFromRefreshTokenFile = file.content.expiresAt; } Object.assign(mergedCredentials, file.content); } // 如果找到了包含 refreshToken 的文件的 expiresAt,使用它 if (expiresAtFromRefreshTokenFile) { mergedCredentials.expiresAt = expiresAtFromRefreshTokenFile; } validateAndShowResult(); } // 验证并显示结果(通用) function validateAndShowResult() { if (!mergedCredentials) { validationResult.style.display = 'none'; jsonPreview.style.display = 'none'; submitBtn.disabled = true; return; } // 检查是否为批量导入(数组) const isBatchImport = Array.isArray(mergedCredentials); if (isBatchImport) { // 批量导入模式:验证数组中的每个对象 let allValid = true; const credentialsValidation = mergedCredentials.map((cred, index) => { const hasClientId = !!cred.clientId; const hasClientSecret = !!cred.clientSecret; const hasAccessToken = !!cred.accessToken; const hasRefreshToken = !!cred.refreshToken; const isValid = hasClientId && hasClientSecret && hasAccessToken && hasRefreshToken; if (!isValid) allValid = false; return { index: index + 1, isValid, fields: [ { key: 'clientId', has: hasClientId }, { key: 'clientSecret', has: hasClientSecret }, { key: 'accessToken', has: hasAccessToken }, { key: 'refreshToken', has: hasRefreshToken } ] }; }); // 构建批量验证结果HTML const credentialsHtml = credentialsValidation.map(cv => { const statusIcon = cv.isValid ? '✓' : '✗'; const statusColor = cv.isValid ? '#166534' : '#991b1b'; const fieldsHtml = cv.fields.map(f => ` ${f.key}: ${f.has ? `` : `` } `).join(''); return `
${statusIcon} 凭据 ${cv.index}
${fieldsHtml}
`; }).join(''); if (allValid) { validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;'; validationResult.innerHTML = `
批量验证通过 (${mergedCredentials.length} 个凭据)
${credentialsHtml}
`; submitBtn.disabled = false; } else { const validCount = credentialsValidation.filter(cv => cv.isValid).length; const invalidCount = credentialsValidation.length - validCount; validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; validationResult.innerHTML = `
批量验证失败 (${invalidCount} 个凭据缺少必需字段)
${credentialsHtml}

请确保每个凭据都包含所有必需字段:clientId, clientSecret, accessToken, refreshToken

`; submitBtn.disabled = true; } // 显示 JSON 预览(批量模式) jsonPreview.style.display = 'block'; const previewData = mergedCredentials.map(cred => { const preview = { ...cred }; if (preview.clientSecret) { preview.clientSecret = preview.clientSecret.substring(0, 8) + '...' + preview.clientSecret.slice(-4); } if (preview.accessToken) { preview.accessToken = preview.accessToken.substring(0, 20) + '...' + preview.accessToken.slice(-10); } if (preview.refreshToken) { preview.refreshToken = preview.refreshToken.substring(0, 10) + '...' + preview.refreshToken.slice(-6); } return preview; }); jsonContent.textContent = JSON.stringify(previewData, null, 2); } else { // 单个导入模式:原有逻辑 const hasClientId = !!mergedCredentials.clientId; const hasClientSecret = !!mergedCredentials.clientSecret; const hasAccessToken = !!mergedCredentials.accessToken; const hasRefreshToken = !!mergedCredentials.refreshToken; // 所有四个字段都必须存在 const isValid = hasClientId && hasClientSecret && hasAccessToken && hasRefreshToken; // 构建字段状态列表 const fieldsList = [ { key: 'clientId', has: hasClientId }, { key: 'clientSecret', has: hasClientSecret }, { key: 'accessToken', has: hasAccessToken }, { key: 'refreshToken', has: hasRefreshToken } ]; const fieldsHtml = fieldsList.map(f => `
  • ${f.key}: ${f.has ? `✓ ${t('common.found')}` : `✗ ${t('common.missing')}` }
  • `).join(''); if (isValid) { validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;'; validationResult.innerHTML = `
    ${t('oauth.kiro.awsValidationSuccess')}
    `; submitBtn.disabled = false; } else { const missingCount = fieldsList.filter(f => !f.has).length; validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; validationResult.innerHTML = `
    ${t('oauth.kiro.awsValidationFailed')} (${t('oauth.kiro.awsMissingFields', { count: missingCount })})

    ${t('oauth.kiro.awsUploadMore')}

    `; submitBtn.disabled = true; } // 显示 JSON 预览(单个模式) jsonPreview.style.display = 'block'; // 隐藏敏感信息的部分内容 const previewData = { ...mergedCredentials }; if (previewData.clientSecret) { previewData.clientSecret = previewData.clientSecret.substring(0, 8) + '...' + previewData.clientSecret.slice(-4); } if (previewData.accessToken) { previewData.accessToken = previewData.accessToken.substring(0, 20) + '...' + previewData.accessToken.slice(-10); } if (previewData.refreshToken) { previewData.refreshToken = previewData.refreshToken.substring(0, 10) + '...' + previewData.refreshToken.slice(-6); } jsonContent.textContent = JSON.stringify(previewData, null, 2); } } // 读取文件内容 function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (e) => reject(e); reader.readAsText(file); }); } // 关闭按钮事件 [closeBtn, cancelBtn].forEach(btn => { btn.addEventListener('click', () => { modal.remove(); }); }); // 提交按钮事件 submitBtn.addEventListener('click', async () => { if (!mergedCredentials) { showToast(t('common.warning'), t('oauth.kiro.awsNoCredentials'), 'warning'); return; } // 检查是否为批量导入(数组) const isBatchImport = Array.isArray(mergedCredentials); // 禁用按钮和输入 submitBtn.disabled = true; cancelBtn.disabled = true; submitBtn.innerHTML = ` ${t('oauth.kiro.awsImporting')}`; if (currentMode === 'json') { jsonInputTextarea.disabled = true; } let importSuccess = false; // 标记是否导入成功 try { if (isBatchImport) { // 批量导入模式 - 使用 SSE 流式响应 // 确保每个凭据都有 authMethod const credentialsToImport = mergedCredentials.map(cred => ({ ...cred, authMethod: cred.authMethod || 'builder-id' })); // 创建进度显示区域 validationResult.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;'; validationResult.innerHTML = `
    ${t('oauth.kiro.importingProgress', { current: 0, total: credentialsToImport.length })}
    `; const progressText = validationResult.querySelector('#awsBatchProgressText'); const progressBar = validationResult.querySelector('#awsImportProgressBar'); const resultsList = validationResult.querySelector('#awsBatchResultsList'); // 使用 fetch + SSE 获取流式响应 const response = await fetch('/api/kiro/import-aws-credentials', { method: 'POST', headers: window.apiClient ? window.apiClient.getAuthHeaders() : { 'Content-Type': 'application/json' }, body: JSON.stringify({ credentials: credentialsToImport }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let successCount = 0; let failedCount = 0; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 解析 SSE 事件 const lines = buffer.split('\n'); buffer = lines.pop() || ''; let eventType = ''; let eventData = ''; for (const line of lines) { if (line.startsWith('event: ')) { eventType = line.substring(7).trim(); } else if (line.startsWith('data: ')) { eventData = line.substring(6).trim(); if (eventType && eventData) { try { const data = JSON.parse(eventData); if (eventType === 'start') { console.log(`[AWS Batch Import] Starting import of ${data.total} credentials`); } else if (eventType === 'progress') { const { index, total, current, successCount: sc, failedCount: fc } = data; successCount = sc; failedCount = fc; // 更新进度条 const percentage = Math.round((index / total) * 100); progressBar.style.width = `${percentage}%`; // 更新进度文本 progressText.textContent = t('oauth.kiro.importingProgress', { current: index, total: total }); // 添加结果项 const resultItem = document.createElement('div'); resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);'; if (current.success) { resultItem.innerHTML = `凭据 ${current.index}: ✓ ${current.path}`; } else if (current.error === 'duplicate') { resultItem.innerHTML = `凭据 ${current.index}: ⚠ ${t('oauth.kiro.duplicateCredentials')} ${current.existingPath ? `(${current.existingPath})` : ''}`; } else { resultItem.innerHTML = `凭据 ${current.index}: ✗ ${current.error}`; } resultsList.appendChild(resultItem); resultsList.scrollTop = resultsList.scrollHeight; } else if (eventType === 'complete') { progressBar.style.width = '100%'; const isAllSuccess = data.failedCount === 0; const isAllFailed = data.successCount === 0; let resultClass, resultIcon, resultMessage; if (isAllSuccess) { resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;'; resultIcon = 'fa-check-circle'; resultMessage = t('oauth.kiro.awsImportSuccess') + ` (${data.successCount})`; } else if (isAllFailed) { resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; resultIcon = 'fa-times-circle'; resultMessage = t('oauth.kiro.awsImportAllFailed', { count: data.failedCount }); } else { resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;'; resultIcon = 'fa-exclamation-triangle'; resultMessage = t('oauth.kiro.importPartial', { success: data.successCount, failed: data.failedCount }); } validationResult.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`; const headerDiv = validationResult.querySelector('div:first-child'); headerDiv.innerHTML = ` ${resultMessage}`; // 如果有成功的,标记为成功并刷新提供商列表 if (data.successCount > 0) { importSuccess = true; loadProviders(); loadConfigList(); } } else if (eventType === 'error') { throw new Error(data.error); } } catch (parseError) { console.warn('Failed to parse SSE data:', parseError); } eventType = ''; eventData = ''; } } } } } else { // 单个导入模式 // 确保 authMethod 为 builder-id(AWS 账号模式) if (!mergedCredentials.authMethod) { mergedCredentials.authMethod = 'builder-id'; } const response = await window.apiClient.post('/kiro/import-aws-credentials', { credentials: mergedCredentials }); if (response.success) { importSuccess = true; showToast(t('common.success'), t('oauth.kiro.awsImportSuccess'), 'success'); modal.remove(); // 刷新提供商列表和配置列表 loadProviders(); loadConfigList(); } else if (response.error === 'duplicate') { // 显示重复凭据警告 const existingPath = response.existingPath || ''; showToast(t('common.warning'), t('oauth.kiro.duplicateCredentials') + (existingPath ? ` (${existingPath})` : ''), 'warning'); } else { showToast(t('common.error'), response.error || t('oauth.kiro.awsImportFailed'), 'error'); } } } catch (error) { console.error('AWS import failed:', error); // 更新错误显示 validationResult.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; validationResult.innerHTML = `
    ${t('oauth.kiro.awsImportFailed')}: ${error.message}
    `; showToast(t('common.error'), t('oauth.kiro.awsImportFailed') + ': ' + error.message, 'error'); } finally { // 取消按钮始终可用 cancelBtn.disabled = false; // 只有在导入失败时才重新启用提交按钮 if (!importSuccess) { submitBtn.disabled = false; submitBtn.innerHTML = ` ${t('oauth.kiro.awsConfirmImport')}`; if (currentMode === 'json') { jsonInputTextarea.disabled = false; } } else { // 导入成功后,保持提交按钮禁用状态,并显示成功图标 submitBtn.innerHTML = ` ${t('common.success')}`; } } }); } /** * 执行生成授权链接 * @param {string} providerType - 提供商类型 * @param {Object} extraOptions - 额外选项 */ async function executeGenerateAuthUrl(providerType, extraOptions = {}) { try { showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info'); // 使用 fileUploadHandler 中的 getProviderKey 获取目录名称 const providerDir = fileUploadHandler.getProviderKey(providerType); const response = await window.apiClient.post( `/providers/${encodeURIComponent(providerType)}/generate-auth-url`, { saveToConfigs: true, providerDir: providerDir, ...extraOptions } ); if (response.success && response.authUrl) { // 如果提供了 targetInputId,设置成功监听器 if (extraOptions.targetInputId) { const targetInputId = extraOptions.targetInputId; const handleSuccess = (e) => { const data = e.detail; if (data.provider === providerType && data.relativePath) { const input = document.getElementById(targetInputId); if (input) { input.value = data.relativePath; input.dispatchEvent(new Event('input', { bubbles: true })); showToast(t('common.success'), t('modal.provider.auth.success'), 'success'); } window.removeEventListener('oauth_success_event', handleSuccess); } }; window.addEventListener('oauth_success_event', handleSuccess); } // 显示授权信息模态框 showAuthModal(response.authUrl, response.authInfo); } else { showToast(t('common.error'), t('modal.provider.auth.failed'), 'error'); } } catch (error) { console.error('生成授权链接失败:', error); showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error'); } } /** * 获取提供商的授权文件路径 * @param {string} provider - 提供商类型 * @returns {string} 授权文件路径 */ function getAuthFilePath(provider) { const authFilePaths = { 'gemini-cli-oauth': '~/.gemini/oauth_creds.json', 'gemini-antigravity': '~/.antigravity/oauth_creds.json', 'openai-qwen-oauth': '~/.qwen/oauth_creds.json', 'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json', 'openai-iflow': '~/.iflow/oauth_creds.json' }; return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径'); } /** * 显示授权信息模态框 * @param {string} authUrl - 授权URL * @param {Object} authInfo - 授权信息 */ function showAuthModal(authUrl, authInfo) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.display = 'flex'; // 获取授权文件路径 const authFilePath = getAuthFilePath(authInfo.provider); // 获取需要开放的端口号(从 authInfo 或当前页面 URL) const requiredPort = authInfo.callbackPort || authInfo.port || window.location.port || '3000'; const isDeviceFlow = authInfo.provider === 'openai-qwen-oauth' || (authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id'); let instructionsHtml = ''; if (authInfo.provider === 'openai-qwen-oauth') { instructionsHtml = `

    ${t('oauth.modal.steps')}

    1. ${t('oauth.modal.step1')}
    2. ${t('oauth.modal.step2.qwen')}
    3. ${t('oauth.modal.step3')}
    4. ${t('oauth.modal.step4.qwen', { min: Math.floor(authInfo.expiresIn / 60) })}
    `; } else if (authInfo.provider === 'claude-kiro-oauth') { const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`; const methodAccount = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'; instructionsHtml = `

    ${t('oauth.modal.steps')}

    ${t('oauth.kiro.authMethodLabel')} ${methodDisplay}

    1. ${t('oauth.kiro.step1')}
    2. ${t('oauth.kiro.step2', { method: methodAccount })}
    3. ${t('oauth.kiro.step3')}
    4. ${t('oauth.kiro.step4')}
    `; } else if (authInfo.provider === 'openai-iflow') { instructionsHtml = `

    ${t('oauth.modal.steps')}

    1. ${t('oauth.iflow.step1')}
    2. ${t('oauth.iflow.step2')}
    3. ${t('oauth.iflow.step3')}
    4. ${t('oauth.iflow.step4')}
    `; } else { instructionsHtml = `

    ${t('oauth.modal.steps')}

    1. ${t('oauth.modal.step1')}
    2. ${t('oauth.modal.step2.google')}
    3. ${t('oauth.modal.step4.google')}
    4. ${t('oauth.modal.step3')}
    `; } modal.innerHTML = ` `; document.body.appendChild(modal); // 关闭按钮事件 const closeBtn = modal.querySelector('.modal-close'); const cancelBtn = modal.querySelector('.modal-cancel'); [closeBtn, cancelBtn].forEach(btn => { btn.addEventListener('click', () => { modal.remove(); }); }); // 重新生成按钮事件 const regenerateBtn = modal.querySelector('.regenerate-port-btn'); if (regenerateBtn) { regenerateBtn.onclick = async () => { const newPort = modal.querySelector('.auth-port-input').value; if (newPort && newPort !== requiredPort) { modal.remove(); // 构造重新请求的参数 const options = { ...authInfo, port: newPort }; // 移除不需要传递回后端的字段 delete options.provider; delete options.redirectUri; delete options.callbackPort; await executeGenerateAuthUrl(authInfo.provider, options); } }; } // Builder ID Start URL 重新生成按钮事件 const regenerateBuilderIdBtn = modal.querySelector('.regenerate-builder-id-btn'); if (regenerateBuilderIdBtn) { regenerateBuilderIdBtn.onclick = async () => { const builderIdStartUrl = modal.querySelector('.builder-id-start-url-input').value.trim(); const region = modal.querySelector('.builder-id-region-input').value.trim(); modal.remove(); // 构造重新请求的参数 const options = { ...authInfo, builderIDStartURL: builderIdStartUrl || 'https://view.awsapps.com/start', region: region || 'us-east-1' }; // 移除不需要传递回后端的字段 delete options.provider; delete options.redirectUri; delete options.callbackPort; await executeGenerateAuthUrl(authInfo.provider, options); }; } // 复制链接按钮 const copyBtn = modal.querySelector('.copy-btn'); copyBtn.addEventListener('click', () => { const input = modal.querySelector('.auth-url-input'); input.select(); document.execCommand('copy'); showToast(t('common.success'), t('oauth.success.msg'), 'success'); }); // 在浏览器中打开按钮 const openBtn = modal.querySelector('.open-auth-btn'); openBtn.addEventListener('click', () => { // 使用子窗口打开,以便监听 URL 变化 const width = 600; const height = 700; const left = (window.screen.width - width) / 2 + 600; const top = (window.screen.height - height) / 2; const authWindow = window.open( authUrl, 'OAuthAuthWindow', `width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes` ); // 监听 OAuth 成功事件,自动关闭窗口和模态框 const handleOAuthSuccess = () => { if (authWindow && !authWindow.closed) { authWindow.close(); } modal.remove(); window.removeEventListener('oauth_success_event', handleOAuthSuccess); // 授权成功后刷新配置和提供商列表 loadProviders(); loadConfigList(); }; window.addEventListener('oauth_success_event', handleOAuthSuccess); if (authWindow) { showToast(t('common.info'), t('oauth.window.opened'), 'info'); // 添加手动输入回调 URL 的 UI const urlSection = modal.querySelector('.auth-url-section'); if (urlSection && !modal.querySelector('.manual-callback-section')) { const manualInputHtml = `

    ${t('oauth.manual.title')}

    ${t('oauth.manual.desc')}

    `; urlSection.insertAdjacentHTML('afterend', manualInputHtml); } const manualInput = modal.querySelector('.manual-callback-input'); const applyBtn = modal.querySelector('.apply-callback-btn'); // 处理回调 URL 的核心逻辑 const processCallback = (urlStr, isManualInput = false) => { try { // 尝试清理 URL(有些用户可能会复制多余的文字) const cleanUrlStr = urlStr.trim().match(/https?:\/\/[^\s]+/)?.[0] || urlStr.trim(); const url = new URL(cleanUrlStr); if (url.searchParams.has('code') || url.searchParams.has('token')) { clearInterval(pollTimer); // 构造本地可处理的 URL,只修改 hostname,保持原始 URL 的端口号不变 const localUrl = new URL(url.href); localUrl.hostname = window.location.hostname; localUrl.protocol = window.location.protocol; showToast(t('common.info'), t('oauth.processing'), 'info'); // 如果是手动输入,直接通过 fetch 请求处理,然后关闭子窗口 if (isManualInput) { // 关闭子窗口 if (authWindow && !authWindow.closed) { authWindow.close(); } // 通过服务端API处理手动输入的回调URL window.apiClient.post('/oauth/manual-callback', { provider: authInfo.provider, callbackUrl: url.href, //使用localhost访问 authMethod: authInfo.authMethod }) .then(response => { if (response.success) { console.log('OAuth 回调处理成功'); showToast(t('common.success'), t('oauth.success.msg'), 'success'); } else { console.error('OAuth 回调处理失败:', response.error); showToast(t('common.error'), response.error || t('oauth.error.process'), 'error'); } }) .catch(err => { console.error('OAuth 回调请求失败:', err); showToast(t('common.error'), t('oauth.error.process'), 'error'); }); } else { // 自动监听模式:优先在子窗口中跳转(如果没关) if (authWindow && !authWindow.closed) { authWindow.location.href = localUrl.href; } else { // 备选方案:通过 fetch 请求 // 通过 fetch 请求本地服务器处理回调 fetch(localUrl.href) .then(response => { if (response.ok) { console.log('OAuth 回调处理成功'); } else { console.error('OAuth 回调处理失败:', response.status); } }) .catch(err => { console.error('OAuth 回调请求失败:', err); }); } } } else { showToast(t('common.warning'), t('oauth.invalid.url'), 'warning'); } } catch (err) { console.error('处理回调失败:', err); showToast(t('common.error'), t('oauth.error.format'), 'error'); } }; applyBtn.addEventListener('click', () => { processCallback(manualInput.value, true); }); // 启动定时器轮询子窗口 URL const pollTimer = setInterval(() => { try { if (authWindow.closed) { clearInterval(pollTimer); return; } // 如果能读到说明回到了同域 const currentUrl = authWindow.location.href; if (currentUrl && (currentUrl.includes('code=') || currentUrl.includes('token='))) { processCallback(currentUrl); } } catch (e) { // 跨域受限是正常的 } }, 1000); } else { showToast(t('common.error'), t('oauth.window.blocked'), 'error'); } }); } /** * 显示需要重启的提示模态框 * @param {string} version - 更新到的版本号 */ function showRestartRequiredModal(version) { const modal = document.createElement('div'); modal.className = 'modal-overlay restart-required-modal'; modal.style.display = 'flex'; modal.innerHTML = ` `; document.body.appendChild(modal); // 关闭按钮事件 const closeBtn = modal.querySelector('.modal-close'); const confirmBtn = modal.querySelector('.restart-confirm-btn'); const closeModal = () => { modal.remove(); }; closeBtn.addEventListener('click', closeModal); confirmBtn.addEventListener('click', closeModal); // 点击遮罩层关闭 modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); } /** * 检查更新 * @param {boolean} silent - 是否静默检查(不显示 Toast) */ async function checkUpdate(silent = false) { const checkBtn = document.getElementById('checkUpdateBtn'); const updateBtn = document.getElementById('performUpdateBtn'); const updateBadge = document.getElementById('updateBadge'); const latestVersionText = document.getElementById('latestVersionText'); const checkBtnIcon = checkBtn?.querySelector('i'); const checkBtnText = checkBtn?.querySelector('span'); try { if (!silent && checkBtn) { checkBtn.disabled = true; if (checkBtnIcon) checkBtnIcon.className = 'fas fa-spinner fa-spin'; if (checkBtnText) checkBtnText.textContent = t('dashboard.update.checking'); } const data = await window.apiClient.get('/check-update'); if (data.hasUpdate) { if (updateBtn) updateBtn.style.display = 'inline-flex'; if (updateBadge) updateBadge.style.display = 'inline-flex'; if (latestVersionText) latestVersionText.textContent = data.latestVersion; if (!silent) { showToast(t('common.info'), t('dashboard.update.hasUpdate', { version: data.latestVersion }), 'info'); } } else { if (updateBtn) updateBtn.style.display = 'none'; if (updateBadge) updateBadge.style.display = 'none'; if (!silent) { showToast(t('common.info'), t('dashboard.update.upToDate'), 'success'); } } } catch (error) { console.error('Check update failed:', error); if (!silent) { showToast(t('common.error'), t('dashboard.update.failed', { error: error.message }), 'error'); } } finally { if (checkBtn) { checkBtn.disabled = false; if (checkBtnIcon) checkBtnIcon.className = 'fas fa-sync-alt'; if (checkBtnText) checkBtnText.textContent = t('dashboard.update.check'); } } } /** * 执行更新 */ async function performUpdate() { const updateBtn = document.getElementById('performUpdateBtn'); const latestVersionText = document.getElementById('latestVersionText'); const version = latestVersionText?.textContent || ''; if (!confirm(t('dashboard.update.confirmMsg', { version }))) { return; } const updateBtnIcon = updateBtn?.querySelector('i'); const updateBtnText = updateBtn?.querySelector('span'); try { if (updateBtn) { updateBtn.disabled = true; if (updateBtnIcon) updateBtnIcon.className = 'fas fa-spinner fa-spin'; if (updateBtnText) updateBtnText.textContent = t('dashboard.update.updating'); } showToast(t('common.info'), t('dashboard.update.updating'), 'info'); const data = await window.apiClient.post('/update'); if (data.success) { if (data.updated) { // 代码已更新,直接调用重启服务 showToast(t('common.success'), t('dashboard.update.success'), 'success'); // 自动重启服务 await restartServiceAfterUpdate(); } else { // 已是最新版本 showToast(t('common.info'), t('dashboard.update.upToDate'), 'info'); } } } catch (error) { console.error('Update failed:', error); showToast(t('common.error'), t('dashboard.update.failed', { error: error.message }), 'error'); } finally { if (updateBtn) { updateBtn.disabled = false; if (updateBtnIcon) updateBtnIcon.className = 'fas fa-download'; if (updateBtnText) updateBtnText.textContent = t('dashboard.update.perform'); } } } /** * 更新后自动重启服务 */ async function restartServiceAfterUpdate() { try { showToast(t('common.info'), t('header.restart.requesting'), 'info'); const token = localStorage.getItem('authToken'); const response = await fetch('/api/restart-service', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' } }); const result = await response.json(); if (response.ok && result.success) { showToast(t('common.success'), result.message || t('header.restart.success'), 'success'); // 如果是 worker 模式,服务会自动重启,等待几秒后刷新页面 if (result.mode === 'worker') { setTimeout(() => { showToast(t('common.info'), t('header.restart.reconnecting'), 'info'); // 等待服务重启后刷新页面 setTimeout(() => { window.location.reload(); }, 3000); }, 2000); } } else { // 显示错误信息 const errorMsg = result.message || result.error?.message || t('header.restart.failed'); showToast(t('common.error'), errorMsg, 'error'); // 如果是独立模式,显示提示 if (result.mode === 'standalone') { showToast(t('common.info'), result.hint, 'warning'); } } } catch (error) { console.error('Restart after update failed:', error); showToast(t('common.error'), t('header.restart.failed') + ': ' + error.message, 'error'); } } export { loadSystemInfo, updateTimeDisplay, loadProviders, renderProviders, updateProviderStatsDisplay, openProviderManager, showAuthModal, executeGenerateAuthUrl, handleGenerateAuthUrl, checkUpdate, performUpdate };