feat(ui): 增强提供商刷新状态显示和版本选择功能
- 在提供商管理界面添加刷新状态徽章,显示“刷新中”状态 - 为更新功能添加版本选择下拉框,支持选择特定版本进行更新 - 在提供商状态中新增 needsRefresh 字段用于跟踪刷新状态 - 修复冷启动时刷新状态重置逻辑,避免持久化状态影响新会话 - 为刷新操作添加超时保护机制,防止适配器调用无限挂起 - 完善国际化翻译,添加相关状态和版本标签
This commit is contained in:
parent
85d7b50cb1
commit
1ee4ca37d1
9 changed files with 201 additions and 74 deletions
|
|
@ -72,6 +72,7 @@ export class ProviderPoolManager {
|
|||
this.refreshBufferQueues = {}; // 按 providerType 分组的缓冲队列
|
||||
this.refreshBufferTimers = {}; // 按 providerType 分组的定时器
|
||||
this.bufferDelay = options.globalConfig?.REFRESH_BUFFER_DELAY ?? 5000; // 默认5秒缓冲延迟
|
||||
this.refreshTaskTimeoutMs = options.globalConfig?.REFRESH_TASK_TIMEOUT_MS ?? 60000; // 默认60秒刷新超时
|
||||
|
||||
// 用于并发选点时的原子排序辅助(自增序列)
|
||||
this._selectionSequence = 0;
|
||||
|
|
@ -184,6 +185,12 @@ export class ProviderPoolManager {
|
|||
_enqueueRefresh(providerType, providerStatus, force = false) {
|
||||
const uuid = providerStatus.uuid;
|
||||
|
||||
// 如果节点被禁用,不进行刷新
|
||||
if (providerStatus.config.isDisabled) {
|
||||
this._log('debug', `Skipping refresh for disabled node ${uuid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经在刷新中,直接返回
|
||||
if (this.refreshingUuids.has(uuid)) {
|
||||
this._log('debug', `Node ${uuid} is already in refresh queue.`);
|
||||
|
|
@ -405,16 +412,18 @@ export class ProviderPoolManager {
|
|||
// 调用适配器的 refreshToken 方法(内部封装了具体的刷新逻辑)
|
||||
if (typeof serviceAdapter.refreshToken === 'function') {
|
||||
const startTime = Date.now();
|
||||
let refreshOperation;
|
||||
if (force) {
|
||||
if (typeof serviceAdapter.forceRefreshToken === 'function') {
|
||||
await serviceAdapter.forceRefreshToken();
|
||||
refreshOperation = serviceAdapter.forceRefreshToken();
|
||||
} else {
|
||||
this._log('warn', `forceRefreshToken not implemented for ${providerType}, falling back to refreshToken`);
|
||||
await serviceAdapter.refreshToken();
|
||||
refreshOperation = serviceAdapter.refreshToken();
|
||||
}
|
||||
} else {
|
||||
await serviceAdapter.refreshToken();
|
||||
refreshOperation = serviceAdapter.refreshToken();
|
||||
}
|
||||
await this._awaitRefreshWithTimeout(refreshOperation, providerType, providerStatus.uuid);
|
||||
const duration = Date.now() - startTime;
|
||||
this._log('info', `Token refresh successful for node ${providerStatus.uuid} (Duration: ${duration}ms)`);
|
||||
|
||||
|
|
@ -422,6 +431,8 @@ export class ProviderPoolManager {
|
|||
config.needsRefresh = false;
|
||||
config.refreshCount = 0;
|
||||
config.lastRefreshTime = Date.now(); // 记录最后刷新成功时间
|
||||
|
||||
this._debouncedSave(providerType);
|
||||
} else {
|
||||
throw new Error(`refreshToken method not implemented for ${providerType}`);
|
||||
}
|
||||
|
|
@ -433,6 +444,31 @@ export class ProviderPoolManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为刷新任务附加超时保护,避免单个适配器调用无限挂起。
|
||||
* @private
|
||||
*/
|
||||
async _awaitRefreshWithTimeout(refreshOperation, providerType, uuid) {
|
||||
if (this.refreshTaskTimeoutMs <= 0) {
|
||||
return await refreshOperation;
|
||||
}
|
||||
|
||||
let timeoutId = null;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Refresh timeout after ${this.refreshTaskTimeoutMs}ms for node ${uuid} (${providerType})`));
|
||||
}, this.refreshTaskTimeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([Promise.resolve(refreshOperation), timeoutPromise]);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算节点的权重/评分,用于排序
|
||||
* 分数越低,优先级越高
|
||||
|
|
@ -627,6 +663,7 @@ export class ProviderPoolManager {
|
|||
*/
|
||||
initializeProviderStatus() {
|
||||
const oldFullStatus = this.providerStatus || {};
|
||||
const isColdStart = Object.keys(oldFullStatus).length === 0;
|
||||
this.providerStatus = {}; // Tracks health and usage for each provider instance
|
||||
for (const providerType in this.providerPools) {
|
||||
const oldStatus = oldFullStatus[providerType] || [];
|
||||
|
|
@ -652,8 +689,13 @@ export class ProviderPoolManager {
|
|||
providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0;
|
||||
|
||||
// --- V2: 刷新监控字段 ---
|
||||
providerConfig.needsRefresh = providerConfig.needsRefresh !== undefined ? providerConfig.needsRefresh : false;
|
||||
providerConfig.refreshCount = providerConfig.refreshCount !== undefined ? providerConfig.refreshCount : 0;
|
||||
const persistedNeedsRefresh = providerConfig.needsRefresh !== undefined ? providerConfig.needsRefresh : false;
|
||||
const persistedRefreshCount = providerConfig.refreshCount !== undefined ? providerConfig.refreshCount : 0;
|
||||
if (isColdStart && (persistedNeedsRefresh || persistedRefreshCount > 0)) {
|
||||
this._log('info', `Resetting stale refresh state for provider ${providerConfig.uuid} (${providerType}) on startup.`);
|
||||
}
|
||||
providerConfig.needsRefresh = isColdStart ? false : persistedNeedsRefresh;
|
||||
providerConfig.refreshCount = isColdStart ? 0 : persistedRefreshCount;
|
||||
|
||||
// 优化2: 简化 lastErrorTime 处理逻辑
|
||||
providerConfig.lastErrorTime = providerConfig.lastErrorTime instanceof Date
|
||||
|
|
@ -680,6 +722,9 @@ export class ProviderPoolManager {
|
|||
logger.error(`[ProviderPoolManager] Error initializing node for ${providerType}: ${nodeError.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 确保初始化时的默认值补全也能写盘
|
||||
this._debouncedSave(providerType);
|
||||
}
|
||||
this._log('info', `Initialized provider statuses: ok (maxErrorCount: ${this.maxErrorCount})`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -563,7 +563,8 @@ export async function getProviderStatus(config, options = {}) {
|
|||
'customName',
|
||||
'isHealthy',
|
||||
'lastErrorTime',
|
||||
'lastErrorMessage'
|
||||
'lastErrorMessage',
|
||||
'needsRefresh'
|
||||
];
|
||||
// identify 字段映射表
|
||||
const identifyFieldMap = {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function sanitizeProviderData(provider, maskSensitive = false) {
|
|||
if (maskSensitive) {
|
||||
for (const key in sanitized) {
|
||||
// 排除已知非敏感字段
|
||||
if (key === 'uuid' || key === 'customName' || key === 'isHealthy' || key === 'isDisabled') continue;
|
||||
if (key === 'uuid' || key === 'customName' || key === 'isHealthy' || key === 'isDisabled' || key === 'needsRefresh') continue;
|
||||
|
||||
const val = sanitized[key];
|
||||
if (typeof val !== 'string' || !val) continue;
|
||||
|
|
@ -138,8 +138,18 @@ export async function handleGetProviders(req, res, currentConfig, providerPoolMa
|
|||
const poolsData = JSON.parse(readFileSync(filePath, 'utf-8'));
|
||||
poolTypes = Object.keys(poolsData);
|
||||
poolTypes.forEach(type => {
|
||||
if (!providerStatus[type]) {
|
||||
providerStatus[type] = [];
|
||||
// 如果管理器中没有该组,或者该组是空的,则从文件中补全
|
||||
if (!providerStatus[type] || providerStatus[type].length === 0) {
|
||||
const fileProviders = poolsData[type] || [];
|
||||
if (fileProviders.length > 0) {
|
||||
providerStatus[type] = fileProviders.map(p => ({
|
||||
...p,
|
||||
activeRequests: 0,
|
||||
waitingRequests: 0
|
||||
}));
|
||||
} else if (!providerStatus[type]) {
|
||||
providerStatus[type] = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { exec } from 'child_process';
|
|||
import { promisify } from 'util';
|
||||
import { CONFIG } from '../core/config-manager.js';
|
||||
import { parseProxyUrl } from '../utils/proxy-utils.js';
|
||||
import { getRequestBody } from '../utils/common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const GITHUB_REPO = 'justlovemaki/AIClient-2-API';
|
||||
|
|
@ -149,16 +150,16 @@ function compareVersions(v1, v2) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 通过 GitHub API 获取最新版本
|
||||
* @returns {Promise<string|null>} 最新版本号或 null
|
||||
* 通过 GitHub API 获取最近的版本列表
|
||||
* @param {number} limit - 限制返回的版本数量
|
||||
* @returns {Promise<string[]>} 版本列表
|
||||
*/
|
||||
async function getLatestVersionFromGitHub() {
|
||||
async function getVersionsFromGitHub(limit = 10) {
|
||||
const candidates = buildGitHubApiCandidates(GITHUB_REPO);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
logger.info(`[Update] Fetching latest version from GitHub API via ${candidate.name}...`);
|
||||
logger.info(`[Update] Request URL: ${candidate.url}`);
|
||||
logger.info(`[Update] Fetching versions from GitHub API via ${candidate.name}...`);
|
||||
const response = await fetchWithProxy(candidate.url, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
|
|
@ -189,15 +190,23 @@ async function getLatestVersionFromGitHub() {
|
|||
}
|
||||
|
||||
versions.sort((a, b) => compareVersions(b, a));
|
||||
logger.info(`[Update] Latest version fetched successfully via ${candidate.name}: ${versions[0]}`);
|
||||
return versions[0];
|
||||
return versions.slice(0, limit);
|
||||
} catch (error) {
|
||||
logger.warn(`[Update] Failed to fetch latest version via ${candidate.name}: ${error.message}`);
|
||||
logger.warn(`[Update] Failed to fetch versions via ${candidate.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn('[Update] All GitHub API proxy attempts failed');
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 GitHub API 获取最新版本
|
||||
* @returns {Promise<string|null>} 最新版本号或 null
|
||||
*/
|
||||
async function getLatestVersionFromGitHub() {
|
||||
const versions = await getVersionsFromGitHub(1);
|
||||
return versions.length > 0 ? versions[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -231,10 +240,11 @@ export async function checkForUpdates() {
|
|||
}
|
||||
|
||||
let latestTag = null;
|
||||
let availableVersions = [];
|
||||
let updateMethod = 'unknown';
|
||||
|
||||
if (isGitRepo) {
|
||||
// Git 仓库模式:使用 git 命令
|
||||
// Git 仓库模式:使用 git命令
|
||||
updateMethod = 'git';
|
||||
|
||||
// 获取远程 tags
|
||||
|
|
@ -244,45 +254,33 @@ export async function checkForUpdates() {
|
|||
} catch (error) {
|
||||
logger.warn('[Update] Failed to fetch tags via git, falling back to GitHub API:', error.message);
|
||||
// 如果 git fetch 失败,回退到 GitHub API
|
||||
latestTag = await getLatestVersionFromGitHub();
|
||||
availableVersions = await getVersionsFromGitHub(10);
|
||||
latestTag = availableVersions.length > 0 ? availableVersions[0] : null;
|
||||
updateMethod = 'github_api';
|
||||
}
|
||||
|
||||
// 如果 git fetch 成功,获取最新的 tag
|
||||
// 如果 git fetch 成功,获取最新的 tag 和可用的 tags
|
||||
if (!latestTag && updateMethod === 'git') {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
try {
|
||||
if (isWindows) {
|
||||
// Windows: 使用 git for-each-ref,这是跨平台兼容的方式
|
||||
const { stdout } = await execAsync('git for-each-ref --sort=-v:refname --format="%(refname:short)" refs/tags --count=1');
|
||||
latestTag = stdout.trim();
|
||||
} else {
|
||||
// Linux/macOS: 使用 head 命令,更高效
|
||||
const { stdout } = await execAsync('git tag --sort=-v:refname | head -n 1');
|
||||
latestTag = stdout.trim();
|
||||
// 获取最近的 10 个 tag
|
||||
const { stdout } = await execAsync('git tag --sort=-v:refname');
|
||||
const tags = stdout.trim().split('\n').filter(t => t);
|
||||
if (tags.length > 0) {
|
||||
availableVersions = tags.slice(0, 10);
|
||||
latestTag = availableVersions[0];
|
||||
}
|
||||
} catch (error) {
|
||||
// 备用方案:获取所有 tags 并在 JavaScript 中排序
|
||||
try {
|
||||
const { stdout } = await execAsync('git tag');
|
||||
const tags = stdout.trim().split('\n').filter(t => t);
|
||||
if (tags.length > 0) {
|
||||
// 按版本号排序(降序)
|
||||
tags.sort((a, b) => compareVersions(b, a));
|
||||
latestTag = tags[0];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('[Update] Failed to get latest tag via git, falling back to GitHub API:', e.message);
|
||||
latestTag = await getLatestVersionFromGitHub();
|
||||
updateMethod = 'github_api';
|
||||
}
|
||||
logger.warn('[Update] Failed to get tags via git, falling back to GitHub API:', error.message);
|
||||
availableVersions = await getVersionsFromGitHub(10);
|
||||
latestTag = availableVersions.length > 0 ? availableVersions[0] : null;
|
||||
updateMethod = 'github_api';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非 Git 仓库模式(如 Docker 容器):使用 GitHub API
|
||||
updateMethod = 'github_api';
|
||||
latestTag = await getLatestVersionFromGitHub();
|
||||
availableVersions = await getVersionsFromGitHub(10);
|
||||
latestTag = availableVersions.length > 0 ? availableVersions[0] : null;
|
||||
}
|
||||
|
||||
if (!latestTag) {
|
||||
|
|
@ -290,6 +288,7 @@ export async function checkForUpdates() {
|
|||
hasUpdate: false,
|
||||
localVersion,
|
||||
latestVersion: null,
|
||||
availableVersions: [],
|
||||
updateMethod,
|
||||
error: 'Unable to get latest version information'
|
||||
};
|
||||
|
|
@ -305,6 +304,7 @@ export async function checkForUpdates() {
|
|||
hasUpdate,
|
||||
localVersion,
|
||||
latestVersion: latestTag,
|
||||
availableVersions,
|
||||
updateMethod,
|
||||
error: null
|
||||
};
|
||||
|
|
@ -312,9 +312,10 @@ export async function checkForUpdates() {
|
|||
|
||||
/**
|
||||
* 执行更新操作
|
||||
* @param {string} targetTag - 目标版本 tag,如果未提供则更新到最新版本
|
||||
* @returns {Promise<Object>} 更新结果
|
||||
*/
|
||||
export async function performUpdate() {
|
||||
export async function performUpdate(targetTag = null) {
|
||||
// 首先检查是否有更新
|
||||
const updateInfo = await checkForUpdates();
|
||||
|
||||
|
|
@ -322,7 +323,12 @@ export async function performUpdate() {
|
|||
throw new Error(updateInfo.error);
|
||||
}
|
||||
|
||||
if (!updateInfo.hasUpdate) {
|
||||
// 如果未提供 targetTag,使用最新版本
|
||||
const latestTag = updateInfo.latestVersion;
|
||||
const finalTag = targetTag || latestTag;
|
||||
|
||||
// 如果是更新到最新版本,且当前已是最新版本
|
||||
if (!targetTag && !updateInfo.hasUpdate) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Already at the latest version',
|
||||
|
|
@ -332,16 +338,25 @@ export async function performUpdate() {
|
|||
};
|
||||
}
|
||||
|
||||
const latestTag = updateInfo.latestVersion;
|
||||
// 如果指定了 tag,但与本地版本相同
|
||||
if (targetTag && (targetTag === updateInfo.localVersion || targetTag === `v${updateInfo.localVersion}`)) {
|
||||
return {
|
||||
success: true,
|
||||
message: `Already at version ${targetTag}`,
|
||||
localVersion: updateInfo.localVersion,
|
||||
latestVersion: updateInfo.latestVersion,
|
||||
updated: false
|
||||
};
|
||||
}
|
||||
|
||||
// 检查更新方式 - 如果是通过 GitHub API 获取的版本信息,说明不在 Git 仓库中
|
||||
if (updateInfo.updateMethod === 'github_api') {
|
||||
// Docker/非 Git 环境,通过下载 tarball 更新
|
||||
logger.info('[Update] Running in Docker/non-Git environment, will download and extract tarball');
|
||||
return await performTarballUpdate(updateInfo.localVersion, latestTag);
|
||||
logger.info(`[Update] Running in Docker/non-Git environment, will download and extract tarball for ${finalTag}`);
|
||||
return await performTarballUpdate(updateInfo.localVersion, finalTag);
|
||||
}
|
||||
|
||||
logger.info(`[Update] Starting update to ${latestTag}...`);
|
||||
logger.info(`[Update] Starting update to ${finalTag}...`);
|
||||
|
||||
// 检查是否有未提交的更改
|
||||
try {
|
||||
|
|
@ -355,19 +370,19 @@ export async function performUpdate() {
|
|||
logger.warn('[Update] Failed to check git status:', error.message);
|
||||
}
|
||||
|
||||
// 执行 checkout 到最新 tag
|
||||
// 执行 checkout 到目标 tag
|
||||
try {
|
||||
logger.info(`[Update] Checking out to ${latestTag}...`);
|
||||
await execAsync(`git checkout ${latestTag}`);
|
||||
logger.info(`[Update] Checking out to ${finalTag}...`);
|
||||
await execAsync(`git checkout ${finalTag}`);
|
||||
} catch (error) {
|
||||
logger.error('[Update] Failed to checkout:', error.message);
|
||||
throw new Error('Failed to switch to new version: ' + error.message);
|
||||
throw new Error(`Failed to switch to version ${finalTag}: ` + error.message);
|
||||
}
|
||||
|
||||
// 更新 VERSION 文件(如果 tag 和 VERSION 文件不同步)
|
||||
const versionFilePath = path.join(process.cwd(), 'VERSION');
|
||||
try {
|
||||
const newVersion = latestTag.replace(/^v/, '');
|
||||
const newVersion = finalTag.replace(/^v/, '');
|
||||
writeFileSync(versionFilePath, newVersion, 'utf-8');
|
||||
logger.info(`[Update] VERSION file updated to ${newVersion}`);
|
||||
} catch (error) {
|
||||
|
|
@ -379,7 +394,7 @@ export async function performUpdate() {
|
|||
try {
|
||||
// 确保本地版本号有 v 前缀,以匹配 git tag 格式
|
||||
const localVersionTag = updateInfo.localVersion.startsWith('v') ? updateInfo.localVersion : `v${updateInfo.localVersion}`;
|
||||
const { stdout: diffOutput } = await execAsync(`git diff ${localVersionTag}..${latestTag} --name-only`);
|
||||
const { stdout: diffOutput } = await execAsync(`git diff ${localVersionTag}..${finalTag} --name-only`);
|
||||
if (diffOutput.includes('package.json') || diffOutput.includes('package-lock.json')) {
|
||||
logger.info('[Update] package.json changed, running npm install...');
|
||||
await execAsync('npm install');
|
||||
|
|
@ -389,13 +404,14 @@ export async function performUpdate() {
|
|||
logger.warn('[Update] Failed to check package changes:', error.message);
|
||||
}
|
||||
|
||||
logger.info(`[Update] Update completed successfully to ${latestTag}`);
|
||||
logger.info(`[Update] Update completed successfully to ${finalTag}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully updated to version ${latestTag}`,
|
||||
message: `Successfully updated to version ${finalTag}`,
|
||||
localVersion: updateInfo.localVersion,
|
||||
latestVersion: latestTag,
|
||||
targetVersion: finalTag,
|
||||
updated: true,
|
||||
updateMethod: 'git',
|
||||
needsRestart: needsRestart,
|
||||
|
|
@ -626,7 +642,10 @@ export async function handleCheckUpdate(req, res) {
|
|||
*/
|
||||
export async function handlePerformUpdate(req, res) {
|
||||
try {
|
||||
const updateResult = await performUpdate();
|
||||
const body = await getRequestBody(req);
|
||||
const version = body?.version || null;
|
||||
|
||||
const updateResult = await performUpdate(version);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(updateResult));
|
||||
return true;
|
||||
|
|
@ -640,4 +659,4 @@ export async function handlePerformUpdate(req, res) {
|
|||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ const translations = {
|
|||
'dashboard.update.performTitle': '更新到最新版本',
|
||||
'dashboard.update.checking': '正在检查...',
|
||||
'dashboard.update.upToDate': '已是最新',
|
||||
'dashboard.update.latest': '最新',
|
||||
'dashboard.update.current': '当前',
|
||||
'dashboard.update.hasUpdate': '发现新版本: {version}',
|
||||
'dashboard.update.updating': '正在更新...',
|
||||
'dashboard.update.success': '更新成功',
|
||||
|
|
@ -464,6 +466,7 @@ const translations = {
|
|||
'providers.healthyProviders': '健康提供商',
|
||||
'providers.status.healthy': '{healthy}/{total} 可用',
|
||||
'providers.status.empty': '0/0 节点',
|
||||
'providers.status.needsRefresh': '刷新中',
|
||||
'providers.stat.totalAccounts': '总账户',
|
||||
'providers.stat.healthyAccounts': '健康账户',
|
||||
'providers.stat.usageCount': '使用次数',
|
||||
|
|
@ -914,6 +917,8 @@ const translations = {
|
|||
'dashboard.update.performTitle': 'Update to latest version',
|
||||
'dashboard.update.checking': 'Checking...',
|
||||
'dashboard.update.upToDate': 'Up to date',
|
||||
'dashboard.update.latest': 'Latest',
|
||||
'dashboard.update.current': 'Current',
|
||||
'dashboard.update.hasUpdate': 'New version available: {version}',
|
||||
'dashboard.update.updating': 'Updating...',
|
||||
'dashboard.update.success': 'Update successful',
|
||||
|
|
@ -1332,6 +1337,7 @@ const translations = {
|
|||
'providers.healthyProviders': 'Healthy Providers',
|
||||
'providers.status.healthy': '{healthy}/{total} Available',
|
||||
'providers.status.empty': '0/0 Nodes',
|
||||
'providers.status.needsRefresh': 'Refreshing',
|
||||
'providers.stat.totalAccounts': 'Total Accounts',
|
||||
'providers.stat.healthyAccounts': 'Healthy Accounts',
|
||||
'providers.stat.usageCount': 'Usage Count',
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ function renderProviderList(providers) {
|
|||
const toggleButtonText = isDisabled ? t('modal.provider.enabled') : t('modal.provider.disabled');
|
||||
const toggleButtonIcon = isDisabled ? 'fas fa-play' : 'fas fa-ban';
|
||||
const toggleButtonClass = isDisabled ? 'btn-success' : 'btn-warning';
|
||||
const needsRefresh = !!provider.needsRefresh;
|
||||
|
||||
// 构建错误信息显示
|
||||
let errorInfoHtml = '';
|
||||
|
|
@ -388,7 +389,10 @@ function renderProviderList(providers) {
|
|||
<div class="provider-item-detail ${healthClass} ${disabledClass}" data-uuid="${provider.uuid}">
|
||||
<div class="provider-item-header" onclick="window.toggleProviderDetails('${provider.uuid}')">
|
||||
<div class="provider-info">
|
||||
<div class="provider-name">${provider.customName || provider.uuid}</div>
|
||||
<div class="provider-name">
|
||||
${provider.customName || provider.uuid}
|
||||
${needsRefresh ? `<span class="badge badge-warning" style="font-size: 10px; margin-left: 8px; vertical-align: middle;"><i class="fas fa-sync-alt fa-spin"></i> <span data-i18n="providers.status.needsRefresh">${t('providers.status.needsRefresh')}</span></span>` : ''}
|
||||
</div>
|
||||
<div class="provider-meta">
|
||||
<span class="health-status">
|
||||
<i class="${healthIcon}"></i>
|
||||
|
|
|
|||
|
|
@ -3108,6 +3108,8 @@ async function checkUpdate(silent = false) {
|
|||
const updateBtn = document.getElementById('performUpdateBtn');
|
||||
const updateBadge = document.getElementById('updateBadge');
|
||||
const latestVersionText = document.getElementById('latestVersionText');
|
||||
const versionSelectWrapper = document.getElementById('versionSelectWrapper');
|
||||
const versionSelect = document.getElementById('versionSelect');
|
||||
const checkBtnIcon = checkBtn?.querySelector('i');
|
||||
const checkBtnText = checkBtn?.querySelector('span');
|
||||
|
||||
|
|
@ -3120,16 +3122,46 @@ async function checkUpdate(silent = false) {
|
|||
|
||||
const data = await window.apiClient.get('/check-update');
|
||||
|
||||
// 处理版本列表
|
||||
if (versionSelect && data.availableVersions && data.availableVersions.length > 0) {
|
||||
versionSelect.innerHTML = '';
|
||||
data.availableVersions.forEach(version => {
|
||||
const option = document.createElement('option');
|
||||
option.value = version;
|
||||
option.textContent = version;
|
||||
// 如果是最新版本,增加标识
|
||||
if (version === data.latestVersion) {
|
||||
option.textContent += ` (${t('dashboard.update.latest') || 'Latest'})`;
|
||||
}
|
||||
// 如果是当前版本,增加标识
|
||||
if (version === data.localVersion || version === `v${data.localVersion}`) {
|
||||
option.textContent += ` (${t('dashboard.update.current') || 'Current'})`;
|
||||
option.selected = true;
|
||||
}
|
||||
versionSelect.appendChild(option);
|
||||
});
|
||||
|
||||
if (versionSelectWrapper) versionSelectWrapper.style.display = 'block';
|
||||
if (updateBtn) {
|
||||
updateBtn.style.display = 'inline-flex';
|
||||
// 如果是回退,修改按钮文字
|
||||
updateBtn.querySelector('span').textContent = t('dashboard.update.perform');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.hasUpdate) {
|
||||
if (updateBtn) updateBtn.style.display = 'inline-flex';
|
||||
if (updateBadge) updateBadge.style.display = 'inline-flex';
|
||||
if (latestVersionText) latestVersionText.textContent = data.latestVersion;
|
||||
|
||||
// 如果有新版本且未选择特定版本,默认选中最新
|
||||
if (versionSelect && data.latestVersion) {
|
||||
versionSelect.value = 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');
|
||||
|
|
@ -3154,10 +3186,10 @@ async function checkUpdate(silent = false) {
|
|||
*/
|
||||
async function performUpdate() {
|
||||
const updateBtn = document.getElementById('performUpdateBtn');
|
||||
const latestVersionText = document.getElementById('latestVersionText');
|
||||
const version = latestVersionText?.textContent || '';
|
||||
const versionSelect = document.getElementById('versionSelect');
|
||||
const selectedVersion = versionSelect?.value || '';
|
||||
|
||||
if (!confirm(t('dashboard.update.confirmMsg', { version }))) {
|
||||
if (!confirm(t('dashboard.update.confirmMsg', { version: selectedVersion }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -3173,7 +3205,7 @@ async function performUpdate() {
|
|||
|
||||
showToast(t('common.info'), t('dashboard.update.updating'), 'info');
|
||||
|
||||
const data = await window.apiClient.post('/update');
|
||||
const data = await window.apiClient.post('/update', { version: selectedVersion });
|
||||
|
||||
if (data.success) {
|
||||
if (data.updated) {
|
||||
|
|
@ -3183,8 +3215,8 @@ async function performUpdate() {
|
|||
// 自动重启服务
|
||||
await restartServiceAfterUpdate();
|
||||
} else {
|
||||
// 已是最新版本
|
||||
showToast(t('common.info'), t('dashboard.update.upToDate'), 'info');
|
||||
// 已是目标版本
|
||||
showToast(t('common.info'), data.message || t('dashboard.update.upToDate'), 'info');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@
|
|||
<div class="system-info-header">
|
||||
<h3 data-i18n="dashboard.systemInfo">系统信息</h3>
|
||||
<div class="update-controls">
|
||||
<div id="versionSelectWrapper" style="display: none; margin-right: 8px;">
|
||||
<select id="versionSelect" class="form-control select-sm" style="height: 32px; padding: 0 8px; font-size: 13px; min-width: 120px;">
|
||||
<!-- Versions will be loaded here -->
|
||||
</select>
|
||||
</div>
|
||||
<button id="checkUpdateBtn" class="btn btn-outline btn-sm" data-i18n-title="dashboard.update.checkTitle" title="检查更新">
|
||||
<i class="fas fa-sync-alt"></i> <span data-i18n="dashboard.update.check">检查更新</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -198,9 +198,14 @@
|
|||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.provider-badge.official { background: var(--info-bg); color: var(--info-text); }
|
||||
.provider-badge.oauth { background: var(--success-bg); color: var(--success-text); }
|
||||
.provider-badge.responses { background: var(--warning-bg); color: var(--warning-text); }
|
||||
.badge-warning {
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.routing-card-content {
|
||||
padding: 1.5rem;
|
||||
|
|
|
|||
Loading…
Reference in a new issue