feat: add multi-source downloads with automatic fallback

- support GitHub, GitCode, gh-proxy, and ghfast sources
- automatically try other sources when version checks fail
- persist the working fallback source and sync the UI immediately
- update translations, configuration migration, and source tests
This commit is contained in:
lzt404 committed 2026-08-26 01:56:46 +08:00
1 parent e8093928c0
commit 03be4acf93
16 files changed
+507 -157

No files matched your search

+7 -4
View File
@@ -943,9 +943,8 @@ export const jaOverrides = {
'kernel.versions.appCardTitle': 'デスクトップアプリ',
'kernel.versions.coreCardTitle': 'プロキシコア',
'kernel.versions.catalogCardTitle': 'Codex モデルカタログ',
'kernel.versions.gitcodeEnabled': 'GitCode 国内ミラー有効',
'kernel.versions.gitcodeDisabled': '公式 GitHub ソース',
'kernel.versions.gitcodeSwitchSuccess': 'ダウンロード元を変更しバージョンを再確認しました',
'kernel.versions.sourceAutoSwitched': '現在のダウンロード元で確認できなかったため、{source} に自動切り替えしました',
'kernel.versions.bundledPackage': '内蔵オフラインパッケージ',
'kernel.versions.offlineRecommend': 'ネットワーク制限がありますか?インターネット接続なしで内蔵オフラインコアを直接インストールできます。',
'kernel.versions.autoUpdateSupported': 'アプリ内自動更新対応',
@@ -973,8 +972,12 @@ export const jaOverrides = {
'kernel.versions.reinstall': '再インストール',
'kernel.versions.noBundled': 'このリリースには組み込みコアが含まれていません',
'kernel.versions.offlineInstall': 'オフラインインストール',
'kernel.versions.gitcodeSource': 'GitCode 中国向けダウンロード元',
'kernel.versions.gitcodeSourceHint': '有効にすると、ソフトウェアとコアの更新確認・ダウンロードで GitCode を優先し、失敗時は GitHub に切り替えます。',
'kernel.versions.downloadSource': 'ダウンロード元を切り替える',
'kernel.versions.downloadSourceHint': 'ソフトウェアとコアの更新確認・ダウンロードでは選択した経路を優先し、失敗時は他の利用可能なソースに切り替えます。ミラープロキシは第三者が提供します。',
'kernel.versions.source.github': 'GitHub 公式',
'kernel.versions.source.gitcode': 'GitCode 中国向けミラー',
'kernel.versions.source.ghProxy': 'GitHub ミラー · gh-proxy.com',
'kernel.versions.source.ghFast': 'GitHub ミラー · ghfast.top',
'kernel.versions.gitcodeUnavailable': 'このビルドには完全な GitCode ミラー設定が含まれていません。',
'kernel.versions.gitcodeSaveFailed': 'ダウンロード元の切り替えに失敗しました: {error}',
'kernel.access.firstKey': '最初のキー',
+7 -4
View File
@@ -943,9 +943,8 @@ export const en: Record<MessageKey, string> = {
'kernel.versions.appCardTitle': 'Desktop Application',
'kernel.versions.coreCardTitle': 'Proxy Core Kernel',
'kernel.versions.catalogCardTitle': 'Codex Model Catalog',
'kernel.versions.gitcodeEnabled': 'GitCode China Mirror Active',
'kernel.versions.gitcodeDisabled': 'Official GitHub Source',
'kernel.versions.gitcodeSwitchSuccess': 'Download source updated and versions rechecked',
'kernel.versions.sourceAutoSwitched': 'The current source failed; automatically switched to {source}',
'kernel.versions.bundledPackage': 'Bundled Offline Package',
'kernel.versions.offlineRecommend': 'Network restricted? You can install the bundled offline core directly without an internet connection.',
'kernel.versions.autoUpdateSupported': 'In-app auto-update supported',
@@ -973,8 +972,12 @@ export const en: Record<MessageKey, string> = {
'kernel.versions.reinstall': 'Reinstall',
'kernel.versions.noBundled': 'This release does not include a bundled core',
'kernel.versions.offlineInstall': 'Offline Install',
'kernel.versions.gitcodeSource': 'GitCode China Download Source',
'kernel.versions.gitcodeSourceHint': 'When enabled, software and core update checks and downloads prefer GitCode, with GitHub as fallback.',
'kernel.versions.downloadSource': 'Switch Download Source',
'kernel.versions.downloadSourceHint': 'Software and core update checks and downloads prefer the selected channel, then fall back to other available sources. Mirror proxies are third-party services.',
'kernel.versions.source.github': 'Official GitHub',
'kernel.versions.source.gitcode': 'GitCode China Mirror',
'kernel.versions.source.ghProxy': 'GitHub Mirror · gh-proxy.com',
'kernel.versions.source.ghFast': 'GitHub Mirror · ghfast.top',
'kernel.versions.gitcodeUnavailable': 'This build does not include complete GitCode mirror configuration.',
'kernel.versions.gitcodeSaveFailed': 'Failed to switch download source: {error}',
'kernel.access.firstKey': 'First key',
+7 -4
View File
@@ -941,9 +941,8 @@ export const zhCN = {
'kernel.versions.appCardTitle': '桌面客户端',
'kernel.versions.coreCardTitle': '代理内核',
'kernel.versions.catalogCardTitle': 'Codex 模型目录',
'kernel.versions.gitcodeEnabled': '已启用 GitCode 国内加速',
'kernel.versions.gitcodeDisabled': '官方 GitHub 源',
'kernel.versions.gitcodeSwitchSuccess': '已切换下载源并重新检测版本',
'kernel.versions.sourceAutoSwitched': '当前下载源检测失败,已自动切换至 {source}',
'kernel.versions.bundledPackage': '内置离线安装包',
'kernel.versions.offlineRecommend': '网络连接受限?可直接一键安装内置离线内核,无需联网。',
'kernel.versions.autoUpdateSupported': '支持应用内自动升级',
@@ -971,8 +970,12 @@ export const zhCN = {
'kernel.versions.reinstall': '重新安装',
'kernel.versions.noBundled': '当前发行包未包含内置内核',
'kernel.versions.offlineInstall': '离线安装',
'kernel.versions.gitcodeSource': 'GitCode 国内下载源',
'kernel.versions.gitcodeSourceHint': '开启后,软件和内核的版本检查及下载将优先使用 GitCode,失败时回退 GitHub。',
'kernel.versions.downloadSource': '切换下载源',
'kernel.versions.downloadSourceHint': '软件和内核的版本检查及下载将优先使用所选渠道,失败时自动回退到其他可用源。镜像代理由第三方提供。',
'kernel.versions.source.github': 'GitHub 官方源',
'kernel.versions.source.gitcode': 'GitCode 国内源',
'kernel.versions.source.ghProxy': 'GitHub 镜像 · gh-proxy.com',
'kernel.versions.source.ghFast': 'GitHub 镜像 · ghfast.top',
'kernel.versions.gitcodeUnavailable': '当前构建未配置完整的 GitCode 镜像源。',
'kernel.versions.gitcodeSaveFailed': '切换下载源失败:{error}',
'kernel.access.firstKey': '第一个密钥',
+50 -22
View File
@@ -39,10 +39,22 @@ export type CoreInstallTask = {
};
export type VersionSourceSettings = {
preferGitcodeDownloads: boolean;
source: VersionDownloadSource;
gitcodeAvailable: boolean;
};
export type VersionDownloadSource = 'github' | 'gitcode' | 'gh-proxy' | 'gh-fast';
function downloadSourceLabel(source: VersionDownloadSource, t: ReturnType<typeof useI18n>['t']) {
const keys = {
github: 'kernel.versions.source.github',
gitcode: 'kernel.versions.source.gitcode',
'gh-proxy': 'kernel.versions.source.ghProxy',
'gh-fast': 'kernel.versions.source.ghFast',
} as const;
return t(keys[source]);
}
export type MessageType = 'info' | 'success' | 'error';
const APP_RELEASE_URL = 'https://github.com/router-for-me/EasyCLIProxyAPI/releases/latest';
@@ -210,11 +222,11 @@ export function VersionManagementPage() {
}
};
const updateVersionSource = async (enabled: boolean) => {
const updateVersionSource = async (source: VersionDownloadSource) => {
setVersionSourceSaving(true);
setVersionSourceError('');
try {
const settings = await invoke<VersionSourceSettings>('set_prefer_gitcode_downloads', { enabled });
const settings = await invoke<VersionSourceSettings>('set_download_source', { source });
setVersionSource(settings);
cachedLatest = null;
cachedLatestError = '';
@@ -312,6 +324,7 @@ export function VersionManagementPage() {
let disposed = false;
let unlisten: (() => void) | null = null;
let unlistenConfig: (() => void) | null = null;
let unlistenVersionSource: (() => void) | null = null;
listen<CoreInstallTask>('core-install-progress', (event) => {
applyInstallTask(event.payload);
@@ -331,6 +344,18 @@ export function VersionManagementPage() {
else unlistenConfig = stop;
});
void listen<VersionSourceSettings>('version-download-source-changed', (event) => {
if (disposed) return;
setVersionSource(event.payload);
setVersionSourceError('');
showToast(t('kernel.versions.sourceAutoSwitched', {
source: downloadSourceLabel(event.payload.source, t),
}), 'info');
}).then((stop) => {
if (disposed) stop();
else unlistenVersionSource = stop;
});
loadInstallTask();
loadVersionSourceSettings();
@@ -351,6 +376,7 @@ export function VersionManagementPage() {
disposed = true;
unlisten?.();
unlistenConfig?.();
unlistenVersionSource?.();
if (toastTimerRef.current !== null) {
window.clearTimeout(toastTimerRef.current);
}
@@ -471,39 +497,41 @@ export function VersionManagementPage() {
const installDialogActionDisabled = (installing || progress?.running) && (cancellingInstall || !progress?.cancellable);
const isGitcodeActive = Boolean(versionSource?.preferGitcodeDownloads);
return (
<section className="page management-page version-management-page">
<section className="panel version-list">
<div className="version-source-row" aria-label={t('kernel.versions.gitcodeSource')}>
<div className="version-source-row" aria-label={t('kernel.versions.downloadSource')}>
<div className="version-source-copy">
<strong>{t('kernel.versions.gitcodeSource')}</strong>
<span>
{versionSource?.gitcodeAvailable === false
? t('kernel.versions.gitcodeUnavailable')
: t('kernel.versions.gitcodeSourceHint')}
</span>
<strong>{t('kernel.versions.downloadSource')}</strong>
<span>{t('kernel.versions.downloadSourceHint')}</span>
{versionSource?.gitcodeAvailable === false ? (
<span>{t('kernel.versions.gitcodeUnavailable')}</span>
) : null}
{versionSourceError ? (
<span className="version-source-error" role="alert">{versionSourceError}</span>
) : null}
</div>
<div className="version-source-control">
<label className="switch-control" title={t('kernel.versions.gitcodeSource')}>
<input
type="checkbox"
role="switch"
checked={isGitcodeActive}
<label>
<span className="sr-only">{t('kernel.versions.downloadSource')}</span>
<select
value={versionSource?.source ?? 'github'}
disabled={
(!versionSource?.gitcodeAvailable && !versionSource?.preferGitcodeDownloads)
!versionSource
|| versionSourceSaving
|| appUpdateTask.running
|| installing
}
aria-label={t('kernel.versions.gitcodeSource')}
onChange={(event) => void updateVersionSource(event.currentTarget.checked)}
/>
<span className="switch-track" />
aria-label={t('kernel.versions.downloadSource')}
onChange={(event) => void updateVersionSource(event.currentTarget.value as VersionDownloadSource)}
>
<option value="github">{t('kernel.versions.source.github')}</option>
<option value="gitcode" disabled={!versionSource?.gitcodeAvailable}>
{t('kernel.versions.source.gitcode')}
</option>
<option value="gh-proxy">{t('kernel.versions.source.ghProxy')}</option>
<option value="gh-fast">{t('kernel.versions.source.ghFast')}</option>
</select>
</label>
</div>
</div>
+37
View File
@@ -1073,6 +1073,33 @@ textarea:focus-visible {
color: var(--theme-9f2d20);
}
.version-source-control,
.version-source-control label {
display: grid;
}
.version-source-control select {
width: 250px;
max-width: 100%;
height: 42px;
padding: 0 36px 0 12px;
border: 1px solid var(--theme-d5d2cb);
border-radius: 8px;
background: var(--theme-fffdf8);
color: var(--theme-2d2a26);
cursor: pointer;
font-size: var(--font-size-body);
}
.version-source-control select:hover:not(:disabled) {
border-color: var(--theme-9b958c);
}
.version-source-control select:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.version-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1639,6 +1666,16 @@ textarea:focus-visible {
}
}
@container workspace (max-width: 560px) {
.version-source-row {
grid-template-columns: minmax(0, 1fr);
}
.version-source-control select {
width: 100%;
}
}
.info-list {
display: grid;
gap: 10px;