feat: add GitHub proxy selection with auto multi-source fallback

This commit is contained in:
Memory 2026-04-04 19:27:06 +08:00 committed by GitHub
parent 0f1cd352db
commit 06591b50f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 131 additions and 62 deletions

View File

@ -10,25 +10,47 @@ import i18next from 'i18next'
import { mainWindow } from '../window'
import { appLogger } from '../utils/logger'
import { dataDir, exeDir, exePath, isPortable, resourcesFilesDir } from '../utils/dirs'
import { getControledMihomoConfig } from '../config'
import { getAppConfig, getControledMihomoConfig } from '../config'
import { checkAdminPrivileges } from '../core/manager'
import { parse } from '../utils/yaml'
import * as chromeRequest from '../utils/chromeRequest'
export async function checkUpdate(): Promise<IAppVersion | undefined> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const res = await chromeRequest.get(
'https://github.com/mihomo-party-org/mihomo-party/releases/latest/download/latest.yml',
{
headers: { 'Content-Type': 'application/octet-stream' },
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: mixedPort
},
responseType: 'text'
const GITHUB_PROXIES = ['https://gh-proxy.org', 'https://ghfast.top', 'https://down.clashparty.org']
function buildDownloadUrls(githubUrl: string, proxyPref = ''): string[] {
if (proxyPref === 'direct') return [githubUrl]
if (proxyPref && proxyPref !== 'auto') return [`${proxyPref}/${githubUrl}`]
// auto: try each proxy then fall back to direct
return [...GITHUB_PROXIES.map((p) => `${p}/${githubUrl}`), githubUrl]
}
async function tryDownload(
urls: string[],
options: Parameters<typeof chromeRequest.get>[1]
): Promise<Awaited<ReturnType<typeof chromeRequest.get>>> {
let lastError: unknown
for (const url of urls) {
try {
return await chromeRequest.get(url, options)
} catch (e) {
lastError = e
}
)
}
throw lastError
}
export async function checkUpdate(): Promise<IAppVersion | undefined> {
const [{ 'mixed-port': mixedPort = 7890 }, { githubProxy = '' }] = await Promise.all([
getControledMihomoConfig(),
getAppConfig()
])
const githubUrl =
'https://github.com/mihomo-party-org/mihomo-party/releases/latest/download/latest.yml'
const res = await tryDownload(buildDownloadUrls(githubUrl, githubProxy), {
headers: { 'Content-Type': 'application/octet-stream' },
proxy: { protocol: 'http', host: '127.0.0.1', port: mixedPort },
responseType: 'text'
})
const latest = parse(res.data as string) as IAppVersion
const currentVersion = app.getVersion()
if (compareVersions(latest.version, currentVersion) > 0) {
@ -57,8 +79,11 @@ function compareVersions(a: string, b: string): number {
}
export async function downloadAndInstallUpdate(version: string): Promise<void> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const baseUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}/`
const [{ 'mixed-port': mixedPort = 7890 }, { githubProxy = '' }] = await Promise.all([
getControledMihomoConfig(),
getAppConfig()
])
const githubBase = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}/`
const fileMap = {
'win32-x64': `clash-party-windows-${version}-x64-setup.exe`,
'win32-ia32': `clash-party-windows-${version}-ia32-setup.exe`,
@ -84,34 +109,27 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
file = file.replace('macos', 'catalina')
}
}
const proxy = { protocol: 'http' as const, host: '127.0.0.1', port: mixedPort }
try {
if (!existsSync(path.join(dataDir(), file))) {
const sha256Res = await chromeRequest.get(`${baseUrl}${file}.sha256`, {
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: mixedPort
},
responseType: 'text'
})
const sha256Res = await tryDownload(
buildDownloadUrls(`${githubBase}${file}.sha256`, githubProxy),
{ proxy, responseType: 'text' }
)
const expectedHash = (sha256Res.data as string).trim().split(/\s+/)[0]
const res = await chromeRequest.get(`${baseUrl}${file}`, {
responseType: 'arraybuffer',
timeout: 0,
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: mixedPort
},
headers: {
'Content-Type': 'application/octet-stream'
},
onProgress: (loaded, total) => {
mainWindow?.webContents.send('updateDownloadProgress', {
status: 'downloading',
percent: Math.round((loaded / total) * 100)
})
}
const res = await tryDownload(
buildDownloadUrls(`${githubBase}${file}`, githubProxy),
{
responseType: 'arraybuffer',
timeout: 0,
proxy,
headers: { 'Content-Type': 'application/octet-stream' },
onProgress: (loaded: number, total: number) => {
mainWindow?.webContents.send('updateDownloadProgress', {
status: 'downloading',
percent: Math.round((loaded / total) * 100)
})
}
})
mainWindow?.webContents.send('updateDownloadProgress', { status: 'verifying' })
const fileBuffer = Buffer.from(res.data as ArrayBuffer)

View File

@ -9,9 +9,18 @@ import { stopCore } from '../core/manager'
import { mihomoCoreDir } from './dirs'
import * as chromeRequest from './chromeRequest'
import { createLogger } from './logger'
import { getAppConfig } from '../config'
const log = createLogger('GitHub')
const GITHUB_PROXIES = ['https://gh-proxy.org', 'https://ghfast.top', 'https://down.clashparty.org']
function buildDownloadUrls(githubUrl: string, proxyPref = ''): string[] {
if (proxyPref === 'direct') return [githubUrl]
if (proxyPref && proxyPref !== 'auto') return [`${proxyPref}/${githubUrl}`]
return [...GITHUB_PROXIES.map((p) => `${p}/${githubUrl}`), githubUrl]
}
export interface GitHubTag {
name: string
zipball_url: string
@ -115,22 +124,28 @@ export function clearVersionCache(owner: string, repo: string): void {
* @param outputPath
*/
async function downloadGitHubAsset(url: string, outputPath: string): Promise<void> {
try {
log.debug(`Downloading asset from ${url}`)
const response = await chromeRequest.get(url, {
responseType: 'arraybuffer',
timeout: 30000
})
await writeFile(outputPath, Buffer.from(response.data as Buffer))
log.debug(`Successfully downloaded asset to ${outputPath}`)
} catch (error) {
log.error(`Failed to download asset from ${url}`, error)
if (error instanceof Error) {
throw new Error(`Download error: ${error.message}`)
const { githubProxy = '' } = await getAppConfig()
const urls = buildDownloadUrls(url, githubProxy)
let lastError: unknown
for (const candidate of urls) {
try {
log.debug(`Downloading asset from ${candidate}`)
const response = await chromeRequest.get(candidate, {
responseType: 'arraybuffer',
timeout: 30000
})
await writeFile(outputPath, Buffer.from(response.data as Buffer))
log.debug(`Successfully downloaded asset to ${outputPath}`)
return
} catch (error) {
log.warn(`Download failed from ${candidate}, trying next`, error)
lastError = error
}
throw new Error('Failed to download core file')
}
log.error(`Failed to download asset from all sources`, lastError)
throw lastError instanceof Error
? new Error(`Download error: ${lastError.message}`)
: new Error('Failed to download core file')
}
/**

View File

@ -66,6 +66,7 @@ const GeneralConfig: React.FC = () => {
customTheme = 'default.css',
envType = [platform === 'win32' ? 'powershell' : 'bash'],
autoCheckUpdate,
githubProxy = 'auto',
appTheme = 'system',
language = 'zh-CN',
triggerMainWindowBehavior = 'show',
@ -171,6 +172,24 @@ const GeneralConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title={t('settings.githubProxy')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-50"
size="sm"
selectedKeys={[githubProxy]}
aria-label={t('settings.githubProxy')}
onSelectionChange={(v) => {
patchAppConfig({ githubProxy: Array.from(v)[0] as string })
}}
>
<SelectItem key="auto">{t('settings.githubProxy.auto')}</SelectItem>
<SelectItem key="direct">{t('settings.githubProxy.direct')}</SelectItem>
<SelectItem key="https://gh-proxy.org">gh-proxy.org</SelectItem>
<SelectItem key="https://ghfast.top">ghfast.top</SelectItem>
<SelectItem key="https://down.clashparty.org">down.clashparty.org</SelectItem>
</Select>
</SettingItem>
<SettingItem title={t('settings.silentStart')} divider>
<Switch
size="sm"
@ -204,7 +223,7 @@ const GeneralConfig: React.FC = () => {
<div className="flex items-center gap-2">
<Input
size="sm"
className="w-[100px]"
className="w-25"
type="number"
value={autoQuitWithoutCoreDelay.toString()}
onValueChange={async (v: string) => {
@ -240,7 +259,7 @@ const GeneralConfig: React.FC = () => {
>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
className="w-37.5"
size="sm"
selectionMode="multiple"
selectedKeys={new Set(envType)}

View File

@ -707,5 +707,9 @@
"network.topology.sourcePort": "Source Port",
"network.topology.waiting": "Waiting for connections...",
"network.topology.pause": "Pause",
"network.topology.resume": "Resume"
"network.topology.resume": "Resume",
"guide.end.description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news.",
"settings.githubProxy": "GitHub Download Proxy",
"settings.githubProxy.auto": "Auto (proxy first)",
"settings.githubProxy.direct": "Direct"
}

View File

@ -671,5 +671,8 @@
"network.topology.sourcePort": "پورت منبع",
"network.topology.waiting": "در انتظار اتصال...",
"network.topology.pause": "مکث",
"network.topology.resume": "ادامه"
"network.topology.resume": "ادامه",
"settings.githubProxy": "پروکسی دانلود GitHub",
"settings.githubProxy.auto": "خودکار (پروکسی اول)",
"settings.githubProxy.direct": "مستقیم"
}

View File

@ -673,5 +673,8 @@
"network.topology.sourcePort": "Порт источника",
"network.topology.waiting": "Ожидание подключений...",
"network.topology.pause": "Пауза",
"network.topology.resume": "Возобновить"
"network.topology.resume": "Возобновить",
"settings.githubProxy": "Прокси для загрузки GitHub",
"settings.githubProxy.auto": "Авто (сначала прокси)",
"settings.githubProxy.direct": "Прямое подключение"
}

View File

@ -707,5 +707,8 @@
"network.topology.sourcePort": "来源端口",
"network.topology.waiting": "等待连接数据...",
"network.topology.pause": "暂停",
"network.topology.resume": "恢复"
"network.topology.resume": "恢复",
"settings.githubProxy": "GitHub 下载代理",
"settings.githubProxy.auto": "自动(优先代理)",
"settings.githubProxy.direct": "直连"
}

View File

@ -707,5 +707,8 @@
"network.topology.sourcePort": "來源連接埠",
"network.topology.waiting": "等待連線資料...",
"network.topology.pause": "暫停",
"network.topology.resume": "繼續"
"network.topology.resume": "繼續",
"settings.githubProxy": "GitHub 下載代理",
"settings.githubProxy.auto": "自動(優先代理)",
"settings.githubProxy.direct": "直連"
}

View File

@ -303,6 +303,7 @@ interface IAppConfig {
appTheme: AppTheme
customTheme?: string
autoCheckUpdate: boolean
githubProxy?: string
silentStart: boolean
autoCloseConnection: boolean
sysProxy: ISysProxyConfig