添加 iFlow API 提供商支持,包括: 1. 新增 MODEL_PROVIDER.IFLOW_API 常量 2. 实现 IFlowApiService 和适配器 3. 添加 OAuth 认证流程及令牌刷新机制 4. 更新相关配置文件、路由和前端界面 5. 扩展多语言支持 6. 修改 Docker 端口映射范围以包含 iFlow 回调端口
1111 lines
No EOL
47 KiB
JavaScript
1111 lines
No EOL
47 KiB
JavaScript
// 提供商管理功能模块
|
||
|
||
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
|
||
? '<i class="fas fa-check-circle" style="color: #10b981; margin-left: 4px;" title="' + t('dashboard.serviceMode.canRestart') + '"></i>'
|
||
: '';
|
||
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 providerDisplayOrder = [
|
||
'gemini-cli-oauth',
|
||
'gemini-antigravity',
|
||
'openai-custom',
|
||
'claude-custom',
|
||
'claude-kiro-oauth',
|
||
'openai-qwen-oauth',
|
||
'openaiResponses-custom',
|
||
'openai-iflow'
|
||
];
|
||
|
||
// 获取所有提供商类型并按指定顺序排序
|
||
// 优先显示预定义的所有提供商类型,即使某些提供商没有数据也要显示
|
||
let allProviderTypes;
|
||
if (hasProviders) {
|
||
// 合并预定义类型和实际存在的类型,确保显示所有预定义提供商
|
||
const actualProviderTypes = Object.keys(providers);
|
||
allProviderTypes = [...new Set([...providerDisplayOrder, ...actualProviderTypes])];
|
||
} else {
|
||
allProviderTypes = providerDisplayOrder;
|
||
}
|
||
const sortedProviderTypes = providerDisplayOrder.filter(type => allProviderTypes.includes(type))
|
||
.concat(allProviderTypes.filter(type => !providerDisplayOrder.includes(type)));
|
||
|
||
// 计算总统计
|
||
let totalAccounts = 0;
|
||
let totalHealthy = 0;
|
||
|
||
// 按照排序后的提供商类型渲染
|
||
sortedProviderTypes.forEach((providerType) => {
|
||
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 });
|
||
|
||
providerDiv.innerHTML = `
|
||
<div class="provider-header">
|
||
<div class="provider-name">
|
||
<span class="provider-type-text">${providerType}</span>
|
||
</div>
|
||
<div class="provider-header-right">
|
||
${generateAuthButton(providerType)}
|
||
<div class="provider-status ${statusClass}">
|
||
<i class="fas fa-${statusIcon}"></i>
|
||
<span>${statusText}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="provider-stats">
|
||
<div class="provider-stat">
|
||
<span class="provider-stat-label" data-i18n="providers.stat.totalAccounts">${t('providers.stat.totalAccounts')}</span>
|
||
<span class="provider-stat-value">${totalCount}</span>
|
||
</div>
|
||
<div class="provider-stat">
|
||
<span class="provider-stat-label" data-i18n="providers.stat.healthyAccounts">${t('providers.stat.healthyAccounts')}</span>
|
||
<span class="provider-stat-value">${healthyCount}</span>
|
||
</div>
|
||
<div class="provider-stat">
|
||
<span class="provider-stat-label" data-i18n="providers.stat.usageCount">${t('providers.stat.usageCount')}</span>
|
||
<span class="provider-stat-value">${usageCount}</span>
|
||
</div>
|
||
<div class="provider-stat">
|
||
<span class="provider-stat-label" data-i18n="providers.stat.errorCount">${t('providers.stat.errorCount')}</span>
|
||
<span class="provider-stat-value">${errorCount}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 如果是空状态,添加特殊样式
|
||
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'];
|
||
|
||
if (!oauthProviders.includes(providerType)) {
|
||
return '';
|
||
}
|
||
|
||
return `
|
||
<button class="generate-auth-btn" title="生成OAuth授权链接">
|
||
<i class="fas fa-key"></i>
|
||
<span data-i18n="providers.auth.generate">${t('providers.auth.generate')}</span>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 处理生成授权链接
|
||
* @param {string} providerType - 提供商类型
|
||
*/
|
||
async function handleGenerateAuthUrl(providerType) {
|
||
// 如果是 Kiro OAuth,先显示认证方式选择对话框
|
||
if (providerType === 'claude-kiro-oauth') {
|
||
showKiroAuthMethodSelector(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 = `
|
||
<div class="modal-content" style="max-width: 500px;">
|
||
<div class="modal-header">
|
||
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
|
||
<button class="modal-close">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
|
||
<!-- <button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
|
||
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
|
||
<div style="text-align: left;">
|
||
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.google">${t('oauth.kiro.google')}</div>
|
||
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.googleDesc">${t('oauth.kiro.googleDesc')}</div>
|
||
</div>
|
||
</button>
|
||
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
|
||
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
|
||
<div style="text-align: left;">
|
||
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.github">${t('oauth.kiro.github')}</div>
|
||
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.githubDesc">${t('oauth.kiro.githubDesc')}</div>
|
||
</div>
|
||
</button> -->
|
||
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
|
||
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
|
||
<div style="text-align: left;">
|
||
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.awsBuilder">${t('oauth.kiro.awsBuilder')}</div>
|
||
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.awsBuilderDesc">${t('oauth.kiro.awsBuilderDesc')}</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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();
|
||
await executeGenerateAuthUrl(providerType, { method });
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行生成授权链接
|
||
* @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 = `
|
||
<div class="auth-instructions">
|
||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||
<ol>
|
||
<li data-i18n="oauth.modal.step1">${t('oauth.modal.step1')}</li>
|
||
<li data-i18n="oauth.modal.step2.qwen">${t('oauth.modal.step2.qwen')}</li>
|
||
<li data-i18n="oauth.modal.step3">${t('oauth.modal.step3')}</li>
|
||
<li data-i18n="oauth.modal.step4.qwen" data-i18n-params='{"min":"${Math.floor(authInfo.expiresIn / 60)}"}'>${t('oauth.modal.step4.qwen', { min: Math.floor(authInfo.expiresIn / 60) })}</li>
|
||
</ol>
|
||
</div>
|
||
`;
|
||
} 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 = `
|
||
<div class="auth-instructions">
|
||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||
<p><strong data-i18n="oauth.kiro.authMethodLabel">${t('oauth.kiro.authMethodLabel')}</strong> ${methodDisplay}</p>
|
||
<ol>
|
||
<li data-i18n="oauth.kiro.step1">${t('oauth.kiro.step1')}</li>
|
||
<li data-i18n="oauth.kiro.step2" data-i18n-params='{"method":"${methodAccount}"}'>${t('oauth.kiro.step2', { method: methodAccount })}</li>
|
||
<li data-i18n="oauth.kiro.step3">${t('oauth.kiro.step3')}</li>
|
||
<li data-i18n="oauth.kiro.step4">${t('oauth.kiro.step4')}</li>
|
||
</ol>
|
||
</div>
|
||
`;
|
||
} else if (authInfo.provider === 'openai-iflow') {
|
||
instructionsHtml = `
|
||
<div class="auth-instructions">
|
||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||
<ol>
|
||
<li data-i18n="oauth.iflow.step1">${t('oauth.iflow.step1')}</li>
|
||
<li data-i18n="oauth.iflow.step2">${t('oauth.iflow.step2')}</li>
|
||
<li data-i18n="oauth.iflow.step3">${t('oauth.iflow.step3')}</li>
|
||
<li data-i18n="oauth.iflow.step4">${t('oauth.iflow.step4')}</li>
|
||
</ol>
|
||
</div>
|
||
`;
|
||
} else {
|
||
instructionsHtml = `
|
||
<div class="auth-instructions">
|
||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||
<ol>
|
||
<li data-i18n="oauth.modal.step1">${t('oauth.modal.step1')}</li>
|
||
<li data-i18n="oauth.modal.step2.google">${t('oauth.modal.step2.google')}</li>
|
||
<li data-i18n="oauth.modal.step4.google">${t('oauth.modal.step4.google')}</li>
|
||
<li data-i18n="oauth.modal.step3">${t('oauth.modal.step3')}</li>
|
||
</ol>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.modal.title">${t('oauth.modal.title')}</span></h3>
|
||
<button class="modal-close">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="auth-info">
|
||
<p><strong data-i18n="oauth.modal.provider">${t('oauth.modal.provider')}</strong> ${authInfo.provider}</p>
|
||
<div class="port-info-section" style="margin: 12px 0; padding: 12px; background: #fef3c7; border: 1px solid #fcd34d; border-radius: 8px;">
|
||
<div style="margin: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||
<i class="fas fa-network-wired" style="color: #d97706;"></i>
|
||
<strong data-i18n="oauth.modal.requiredPort">${t('oauth.modal.requiredPort')}</strong>
|
||
${isDeviceFlow ?
|
||
`<code style="background: #fff; padding: 2px 8px; border-radius: 4px; font-weight: bold; color: #d97706;">${requiredPort}</code>` :
|
||
`<div style="display: flex; align-items: center; gap: 4px;">
|
||
<input type="number" class="auth-port-input" value="${requiredPort}" style="width: 80px; padding: 2px 8px; border: 1px solid #d97706; border-radius: 4px; font-weight: bold; color: #d97706; background: white;">
|
||
<button class="regenerate-port-btn" title="${t('common.generate')}" style="background: none; border: 1px solid #d97706; border-radius: 4px; cursor: pointer; color: #d97706; padding: 2px 6px;">
|
||
<i class="fas fa-sync-alt"></i>
|
||
</button>
|
||
</div>`
|
||
}
|
||
</div>
|
||
<p style="margin: 8px 0 0 0; font-size: 0.85rem; color: #92400e;" data-i18n="oauth.modal.portNote">${t('oauth.modal.portNote')}</p>
|
||
</div>
|
||
${instructionsHtml}
|
||
<div class="auth-url-section">
|
||
<label data-i18n="oauth.modal.urlLabel">${t('oauth.modal.urlLabel')}</label>
|
||
<div class="auth-url-container">
|
||
<input type="text" readonly value="${authUrl}" class="auth-url-input">
|
||
<button class="copy-btn" data-i18n="oauth.modal.copyTitle" title="复制链接">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
|
||
<button class="open-auth-btn">
|
||
<i class="fas fa-external-link-alt"></i>
|
||
<span data-i18n="oauth.modal.openInBrowser">${t('oauth.modal.openInBrowser')}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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);
|
||
}
|
||
};
|
||
}
|
||
|
||
// 复制链接按钮
|
||
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 = `
|
||
<div class="manual-callback-section" style="margin-top: 20px; padding: 15px; background: #fffbeb; border: 1px solid #fef3c7; border-radius: 8px;">
|
||
<h4 style="color: #92400e; margin-bottom: 8px;"><i class="fas fa-exclamation-circle"></i> <span data-i18n="oauth.manual.title">${t('oauth.manual.title')}</span></h4>
|
||
<p style="font-size: 0.875rem; color: #b45309; margin-bottom: 10px;" data-i18n-html="oauth.manual.desc">${t('oauth.manual.desc')}</p>
|
||
<div class="auth-url-container" style="display: flex; gap: 5px;">
|
||
<input type="text" class="manual-callback-input" data-i18n="oauth.manual.placeholder" placeholder="粘贴回调 URL (包含 code=...)" style="flex: 1; padding: 8px; border: 1px solid #fcd34d; border-radius: 4px; background: white; color: black;">
|
||
<button class="btn btn-success apply-callback-btn" style="padding: 8px 15px; white-space: nowrap; background: #059669; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||
<i class="fas fa-check"></i> <span data-i18n="oauth.manual.submit">${t('oauth.manual.submit')}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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: localUrl.href,
|
||
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 = `
|
||
<div class="modal-content restart-modal-content" style="max-width: 420px;">
|
||
<div class="modal-header restart-modal-header">
|
||
<h3><i class="fas fa-check-circle" style="color: #10b981;"></i> <span data-i18n="dashboard.update.restartTitle">${t('dashboard.update.restartTitle')}</span></h3>
|
||
<button class="modal-close">×</button>
|
||
</div>
|
||
<div class="modal-body" style="text-align: center; padding: 20px;">
|
||
<p style="font-size: 1rem; color: #374151; margin: 0;" data-i18n="dashboard.update.restartMsg" data-i18n-params='{"version":"${version}"}'>${t('dashboard.update.restartMsg', { version })}</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn restart-confirm-btn">
|
||
<i class="fas fa-check"></i>
|
||
<span data-i18n="common.confirm">${t('common.confirm')}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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,
|
||
checkUpdate,
|
||
performUpdate
|
||
}; |