feat(updater): add app update checks and refine the kernel dashboard
This commit is contained in:
1 parent
4e8a02fdfc
commit
d1833e63ad
7 files changed
+247
-44
No files matched your search
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user