feat(updater): add app update checks and refine the kernel dashboard

This commit is contained in:
lzt404 committed 2026-07-19 19:03:40 +08:00
1 parent 4e8a02fdfc
commit d1833e63ad
7 files changed
+247 -44

No files matched your search

-4
View File
@@ -166,10 +166,6 @@ function AppContent() {
<span>联系我们</span>
<ExternalLink size={13} aria-hidden="true" />
</button>
<div className="sidebar-footer">
<span>Easy_CLIProxyAPI</span>
<strong>v0.1.9</strong>
</div>
</div>
</aside>
+113 -13
View File
@@ -1,12 +1,14 @@
import { useEffect, useRef, useState } from 'react';
import { getVersion } from '@tauri-apps/api/app';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { AlertCircle, Check, Copy, Info } from 'lucide-react';
import { AlertCircle, Check, Copy, ExternalLink, Info, RefreshCw } from 'lucide-react';
import { type CoreStatus, useCoreRuntime } from '../coreRuntime';
import openaiIcon from '../assets/icons/openai-light.svg';
import claudeIcon from '../assets/icons/claude.svg';
import geminiIcon from '../assets/icons/gemini.svg';
import { clientApiProfiles, DEFAULT_CLIENT_API_KEY } from '../services/clientAccess';
import packageMetadata from '../../package.json';
type CorePlatform = {
os: string;
@@ -54,10 +56,30 @@ type GuiSettings = {
runOnStartup: boolean;
};
type AppUpdateInfo = {
currentVersion: string;
latestVersion: string;
updateAvailable: boolean;
releaseUrl: string;
};
const APP_RELEASE_URL = 'https://github.com/lzt404/Easy_CLIProxyAPI/releases/latest';
let latestAutoCheckStarted = false;
let cachedLatest: CoreLatest | null = null;
let cachedLatestError = '';
let latestCheckPromise: Promise<CoreLatest> | null = null;
let initialAppUpdateCheck: Promise<AppUpdateInfo> | null = null;
function displayAppVersion(version: string) {
const resolvedVersion = version.trim() || packageMetadata.version;
return resolvedVersion.startsWith('v') ? resolvedVersion : `v${resolvedVersion}`;
}
function requestInitialAppUpdate() {
initialAppUpdateCheck ??= invoke<AppUpdateInfo>('check_app_update');
return initialAppUpdateCheck;
}
function requestLatestCore() {
if (latestCheckPromise) {
@@ -90,6 +112,10 @@ export function KernelPage() {
publishStatus,
} = useCoreRuntime();
const [platform, setPlatform] = useState<CorePlatform | null>(null);
const [currentAppVersion, setCurrentAppVersion] = useState(() => displayAppVersion(packageMetadata.version));
const [appUpdate, setAppUpdate] = useState<AppUpdateInfo | null>(null);
const [appUpdateError, setAppUpdateError] = useState('');
const [checkingAppUpdate, setCheckingAppUpdate] = useState(true);
const [platformError, setPlatformError] = useState('');
const [latest, setLatest] = useState<CoreLatest | null>(cachedLatest);
const [latestError, setLatestError] = useState(cachedLatestError);
@@ -212,6 +238,36 @@ export function KernelPage() {
};
}, []);
useEffect(() => {
let disposed = false;
void getVersion()
.then((version) => {
if (!disposed) setCurrentAppVersion(displayAppVersion(version));
})
.catch((error) => console.warn('读取当前软件版本失败', error));
void requestInitialAppUpdate()
.then((info) => {
if (disposed) return;
setCurrentAppVersion(displayAppVersion(info.currentVersion));
setAppUpdate(info);
setAppUpdateError('');
})
.catch((error) => {
if (disposed) return;
console.warn('自动检查软件更新失败', error);
setAppUpdateError(String(error));
})
.finally(() => {
if (!disposed) setCheckingAppUpdate(false);
});
return () => {
disposed = true;
};
}, []);
useEffect(() => {
if (!installDialogOpen) {
return;
@@ -565,12 +621,35 @@ export function KernelPage() {
}
};
const checkAppUpdate = async () => {
setCheckingAppUpdate(true);
setAppUpdateError('');
try {
const info = await invoke<AppUpdateInfo>('check_app_update');
setCurrentAppVersion(displayAppVersion(info.currentVersion));
setAppUpdate(info);
} catch (error) {
setAppUpdateError(String(error));
} finally {
setCheckingAppUpdate(false);
}
};
const openAppUpdate = async () => {
try {
await invoke('open_external_url', { url: appUpdate?.releaseUrl || APP_RELEASE_URL });
} catch (error) {
setAppUpdateError(`打开更新页面失败:${String(error)}`);
}
};
const latestVersion = latest?.version ?? '';
const currentVersion = coreStatus?.currentVersion ?? '';
const coreInstalled = Boolean(coreStatus?.installed);
const coreRunning = Boolean(coreStatus?.running);
const busy = checkingLatest || installing || processBusy;
const installDisabled = busy || Boolean(coreStatus?.running);
const offlineInstallDisabled = installing || processBusy || coreRunning;
const computedPercent =
progress?.percent ??
(progress?.total && progress.total > 0 ? (progress.downloaded / progress.total) * 100 : null);
@@ -597,6 +676,12 @@ export function KernelPage() {
: statusError
? '检测失败'
: '检测中';
const appUpdateLabel = appUpdateError
? '检查失败,重试'
: appUpdate?.updateAvailable
? `更新至 ${displayAppVersion(appUpdate.latestVersion)}`
: '';
const appUpdateTone = appUpdateError ? 'error' : 'update';
const latestLabel = checkingLatest
? '检查中'
: latestVersion || (latestError ? '检查失败' : '未检查');
@@ -609,17 +694,21 @@ export function KernelPage() {
: currentVersion === latestVersion
? '已是最新版本'
: '可安装新版本';
const platformLabel = platform ? `${platform.os} / ${platform.arch}` : platformError || '检测中';
const platformOsLabel = platform?.os || (platformError ? '检测失败' : '检测中');
const platformArchLabel = platform?.arch || (platformError ? '检测失败' : '检测中');
const installTaskRunning = Boolean(installing || progress?.running);
const offlineInstallRequired = Boolean(coreStatus && !coreInstalled && latestError);
const versionStatusLabel = installTaskRunning
? cancellingInstall
? '正在取消下载'
: progress?.phase || '正在安装'
: message || updateStateLabel;
: offlineInstallRequired
? 'Github连接失败,请使用“离线安装”'
: message || updateStateLabel;
const versionStatusTone: MessageType = installTaskRunning
? 'info'
: offlineInstallRequired
? 'error'
: message
? messageType
: latestError
@@ -677,17 +766,28 @@ export function KernelPage() {
</div>
</div>
<div className="status-card">
<span>当前版本</span>
<span>软件版本</span>
<div className="software-version-line">
<strong>{currentAppVersion}</strong>
{appUpdateLabel ? (
<button
type="button"
className={`software-version-action ${appUpdateTone}`}
title={appUpdateError || '打开 GitHub 最新版本页面'}
disabled={checkingAppUpdate}
onClick={() => void (appUpdateError ? checkAppUpdate() : openAppUpdate())}
>
{appUpdateError ? <RefreshCw size={11} aria-hidden="true" /> : null}
<span>{appUpdateLabel}</span>
{appUpdate?.updateAvailable && !appUpdateError ? <ExternalLink size={11} aria-hidden="true" /> : null}
</button>
) : null}
</div>
</div>
<div className="status-card">
<span>内核版本</span>
<strong>{currentVersion || '未安装'}</strong>
</div>
<div className="status-card">
<span>最新版本</span>
<strong>{latestLabel}</strong>
</div>
<div className="status-card">
<span>运行平台</span>
<strong>{platformLabel}</strong>
</div>
</div>
<div className="kernel-layout">
@@ -864,7 +964,7 @@ export function KernelPage() {
type="button"
className="primary-button"
title={(bundledCore?.assetName ?? bundledCoreError) || '当前发行包未包含内置内核'}
disabled={!bundledCore || installDisabled}
disabled={!bundledCore || offlineInstallDisabled}
onClick={() => void installBundledCore()}
>
离线安装
+60 -26
View File
@@ -222,19 +222,6 @@ input:focus-visible {
white-space: nowrap;
}
.sidebar-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: #7a746c;
font-size: 12px;
}
.sidebar-footer strong {
color: #2d2a26;
}
.workspace {
min-width: 0;
}
@@ -336,16 +323,16 @@ input:focus-visible {
.status-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.status-card {
display: grid;
gap: 6px;
gap: 4px;
min-width: 0;
min-height: 84px;
padding: 16px;
min-height: 72px;
padding: 12px 14px;
border: 1px solid #e3e1db;
border-radius: 8px;
background: #fffdf8;
@@ -354,12 +341,12 @@ input:focus-visible {
.status-card-primary {
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 12px;
gap: 10px;
}
.status-card-primary > div {
display: grid;
gap: 6px;
gap: 4px;
min-width: 0;
}
@@ -374,12 +361,63 @@ input:focus-visible {
.status-card strong {
overflow: hidden;
color: #2d2a26;
font-size: 17px;
font-size: 16px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.software-version-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.software-version-line > strong {
flex: none;
}
.software-version-action {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
min-width: 0;
max-width: 62%;
padding: 0;
border: 0;
background: transparent;
color: #817b73;
font-size: 10px;
font-weight: 600;
}
.software-version-action span {
color: inherit;
font-size: 10px;
font-weight: 600;
}
.software-version-action:hover:not(:disabled) span {
text-decoration: underline;
text-underline-offset: 2px;
}
.software-version-action:disabled {
cursor: wait;
opacity: 0.72;
}
.software-version-action.update {
color: #866b4d;
}
.software-version-action.error {
color: #8a625d;
}
.status-dot {
display: block;
width: 12px;
@@ -453,7 +491,7 @@ input:focus-visible {
}
.client-api-panel {
margin-top: 18px;
margin-top: 0;
}
.client-api-heading {
@@ -4819,7 +4857,7 @@ input:focus-visible {
}
.status-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.kernel-layout {
@@ -4911,10 +4949,6 @@ input:focus-visible {
grid-template-columns: 17px minmax(0, 1fr);
}
.sidebar-footer {
display: none;
}
.sidebar-bottom {
margin-top: 0;
padding-top: 0;