// 提供商管理功能模块
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 = `
${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 = `
${t('oauth.gemini.importInstructions')}
${t('oauth.gemini.tokenCount')}
0
${t('oauth.gemini.importing')}
`;
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 = `
${t('oauth.kiro.batchImportInstructions')}
${t('oauth.kiro.tokenCount')}
0
${t('oauth.kiro.importing')}
`;
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 = `
${t('oauth.kiro.awsImportInstructions')}
C:\\Users\\{username}\\.aws\\sso\\cache
${t('oauth.kiro.awsJsonExample')}
// 单个凭据导入示例:
{
"clientId": "VYZBSTx3Q7QEq1W3Wn8c5nVzLWVhc3QtMQ",
"clientSecret": "eyJraWQi...OAMc",
"expiresAt": "2026-01-09T04:43:18.079944400+00:00",
"accessToken": "aoaAAAAAGlgghoSqRgQK...2tfhmdNZDA",
"authMethod": "IdC",
"provider": "BuilderId",
"refreshToken": "aorAAAAAGn...uKw+E3",
"region": "us-east-1"
}
// 批量导入示例(JSON数组):
[
{
"clientId": "VYZBSTx3Q7QEq1W3Wn8c5nVzLWVhc3QtMQ",
"clientSecret": "eyJraWQi...OAMc",
"accessToken": "aoaAAAAAGlgghoSqRgQK...2tfhmdNZDA",
"refreshToken": "aorAAAAAGn...uKw+E3",
"region": "us-east-1"
},
{
"clientId": "AnotherClientId123",
"clientSecret": "eyJraWQi...xyz",
"accessToken": "aoaAAAAAGlgghoSqRgQK...abc",
"refreshToken": "aorAAAAAGn...def",
"region": "us-west-2",
"idcRegion": "us-west-2"
}
]
注意:AWS企业用户需要额外添加 idcRegion 字段
`;
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')}
- ${t('oauth.modal.step1')}
- ${t('oauth.modal.step2.qwen')}
- ${t('oauth.modal.step3')}
- ${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}
- ${t('oauth.kiro.step1')}
- ${t('oauth.kiro.step2', { method: methodAccount })}
- ${t('oauth.kiro.step3')}
- ${t('oauth.kiro.step4')}
`;
} else if (authInfo.provider === 'openai-iflow') {
instructionsHtml = `
${t('oauth.modal.steps')}
- ${t('oauth.iflow.step1')}
- ${t('oauth.iflow.step2')}
- ${t('oauth.iflow.step3')}
- ${t('oauth.iflow.step4')}
`;
} else {
instructionsHtml = `
${t('oauth.modal.steps')}
- ${t('oauth.modal.step1')}
- ${t('oauth.modal.step2.google')}
- ${t('oauth.modal.step4.google')}
- ${t('oauth.modal.step3')}
`;
}
modal.innerHTML = `
${t('oauth.modal.provider')} ${authInfo.provider}
${(authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id') ? `
` : ''}
${t('oauth.modal.requiredPort')}
${isDeviceFlow ?
`
${requiredPort}` :
`
`
}
${t('oauth.modal.portNote')}
${(authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id') ? `
` : ''}
${instructionsHtml}
`;
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 = `
`;
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 = `
${t('dashboard.update.restartMsg', { version })}
`;
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
};