Compare commits

...

11 Commits

13 changed files with 169 additions and 82 deletions

View File

@ -196,12 +196,9 @@ jobs:
run: | run: |
pnpm install pnpm install
pnpm add -D electron@22.3.27 pnpm add -D electron@22.3.27
# Downgrade packages incompatible with Node.js 16 (Electron 22)
pnpm add express@4.21.2 chokidar@3.6.0
(Get-Content electron-builder.yml) -replace 'windows', 'win7' | Set-Content electron-builder.yml (Get-Content electron-builder.yml) -replace 'windows', 'win7' | Set-Content electron-builder.yml
# Electron 22 requires CJS format # Electron 22 requires CJS format
(Get-Content package.json) -replace '"type": "module"', '"type": "commonjs"' | Set-Content package.json (Get-Content package.json) -replace '"type": "module"', '"type": "commonjs"' | Set-Content package.json
(Get-Content electron.vite.config.ts) -replace 'plugins: \[externalizeDepsPlugin\(\)\]', "plugins: [externalizeDepsPlugin()],`n build: { rollupOptions: { output: { format: 'cjs' } } }" | Set-Content electron.vite.config.ts
- name: Update Version for Dev Build - name: Update Version for Dev Build
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
env: env:
@ -213,6 +210,7 @@ jobs:
env: env:
npm_config_arch: ${{ matrix.arch }} npm_config_arch: ${{ matrix.arch }}
npm_config_target_arch: ${{ matrix.arch }} npm_config_target_arch: ${{ matrix.arch }}
LEGACY_BUILD: 'true'
run: pnpm build:win --${{ matrix.arch }} run: pnpm build:win --${{ matrix.arch }}
- name: Add Portable Flag - name: Add Portable Flag
run: | run: |
@ -590,7 +588,7 @@ jobs:
- mihomo-party-bin - mihomo-party-bin
- mihomo-party - mihomo-party
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: linux needs: updater
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -601,14 +599,14 @@ jobs:
- name: Update Checksums - name: Update Checksums
if: matrix.pkgname == 'mihomo-party' || matrix.pkgname == 'mihomo-party-electron' if: matrix.pkgname == 'mihomo-party' || matrix.pkgname == 'mihomo-party-electron'
run: | run: |
wget https://github.com/mihomo-party-org/mihomo-party/archive/refs/tags/$(echo ${{ github.ref }} | tr -d 'refs/tags/').tar.gz -O release.tar.gz wget https://github.com/${{ github.repository }}/archive/refs/tags/$(echo ${{ github.ref }} | tr -d 'refs/tags/').tar.gz -O release.tar.gz
sed -i "s/sha256sums=.*/sha256sums=(\"$(sha256sum ./release.tar.gz | awk '{print $1}')\"/" aur/mihomo-party/PKGBUILD sed -i "s/sha256sums=.*/sha256sums=(\"$(sha256sum ./release.tar.gz | awk '{print $1}')\"/" aur/mihomo-party/PKGBUILD
sed -i "s/sha256sums=.*/sha256sums=(\"$(sha256sum ./release.tar.gz | awk '{print $1}')\"/" aur/mihomo-party-electron/PKGBUILD sed -i "s/sha256sums=.*/sha256sums=(\"$(sha256sum ./release.tar.gz | awk '{print $1}')\"/" aur/mihomo-party-electron/PKGBUILD
- name: Update Checksums - name: Update Checksums
if: matrix.pkgname == 'mihomo-party-bin' || matrix.pkgname == 'mihomo-party-electron-bin' if: matrix.pkgname == 'mihomo-party-bin' || matrix.pkgname == 'mihomo-party-electron-bin'
run: | run: |
wget https://github.com/mihomo-party-org/mihomo-party/releases/download/$(echo ${{ github.ref }} | tr -d 'refs/tags/')/mihomo-party-linux-$(echo ${{ github.ref }} | tr -d 'refs/tags/v')-amd64.deb -O amd64.deb wget https://github.com/${{ github.repository }}/releases/download/$(echo ${{ github.ref }} | tr -d 'refs/tags/')/clash-party-linux-$(echo ${{ github.ref }} | tr -d 'refs/tags/v')-amd64.deb -O amd64.deb
wget https://github.com/mihomo-party-org/mihomo-party/releases/download/$(echo ${{ github.ref }} | tr -d 'refs/tags/')/mihomo-party-linux-$(echo ${{ github.ref }} | tr -d 'refs/tags/v')-arm64.deb -O arm64.deb wget https://github.com/${{ github.repository }}/releases/download/$(echo ${{ github.ref }} | tr -d 'refs/tags/')/clash-party-linux-$(echo ${{ github.ref }} | tr -d 'refs/tags/v')-arm64.deb -O arm64.deb
sed -i "s/sha256sums_x86_64=.*/sha256sums_x86_64=(\"$(sha256sum ./amd64.deb | awk '{print $1}')\")/" aur/mihomo-party-bin/PKGBUILD sed -i "s/sha256sums_x86_64=.*/sha256sums_x86_64=(\"$(sha256sum ./amd64.deb | awk '{print $1}')\")/" aur/mihomo-party-bin/PKGBUILD
sed -i "s/sha256sums_aarch64=.*/sha256sums_aarch64=(\"$(sha256sum ./arm64.deb | awk '{print $1}')\")/" aur/mihomo-party-bin/PKGBUILD sed -i "s/sha256sums_aarch64=.*/sha256sums_aarch64=(\"$(sha256sum ./arm64.deb | awk '{print $1}')\")/" aur/mihomo-party-bin/PKGBUILD
sed -i "s/sha256sums_x86_64=.*/sha256sums_x86_64=(\"$(sha256sum ./amd64.deb | awk '{print $1}')\")/" aur/mihomo-party-electron-bin/PKGBUILD sed -i "s/sha256sums_x86_64=.*/sha256sums_x86_64=(\"$(sha256sum ./amd64.deb | awk '{print $1}')\")/" aur/mihomo-party-electron-bin/PKGBUILD

View File

@ -1,3 +1,11 @@
# 1.9.1
## 修复 (Fix)
- 修复 Windows 下以管理员重启开启 TUN 时因单实例锁冲突导致的闪退问题
- 修复托盘菜单开启 TUN 时管理员重启后继续执行导致的竞态问题
- 修复关键资源文件复制失败时静默跳过导致内核启动异常的问题
# 1.9.0 # 1.9.0
## 新功能 (Feat) ## 新功能 (Feat)

View File

@ -15,14 +15,22 @@ const monacoEditorPlugin = isObjectWithDefaultFunction(monacoEditorPluginModule)
? monacoEditorPluginModule.default ? monacoEditorPluginModule.default
: monacoEditorPluginModule : monacoEditorPluginModule
// Win7 build: bundle all deps (Vite converts ESM→CJS), only externalize native modules
const isLegacyBuild = process.env.LEGACY_BUILD === 'true'
const legacyExternal = ['sysproxy-rs', 'electron']
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin()] plugins: isLegacyBuild ? [] : [externalizeDepsPlugin()],
build: isLegacyBuild
? { rollupOptions: { external: legacyExternal, output: { format: 'cjs' } } }
: undefined
}, },
preload: { preload: {
plugins: [externalizeDepsPlugin()], plugins: isLegacyBuild ? [] : [externalizeDepsPlugin()],
build: { build: {
rollupOptions: { rollupOptions: {
external: isLegacyBuild ? legacyExternal : undefined,
output: { output: {
format: 'cjs', format: 'cjs',
entryFileNames: '[name].cjs' entryFileNames: '[name].cjs'

View File

@ -1,6 +1,6 @@
{ {
"name": "mihomo-party", "name": "mihomo-party",
"version": "1.9.0", "version": "1.9.1",
"description": "Clash Party", "description": "Clash Party",
"type": "module", "type": "module",
"main": "./out/main/index.js", "main": "./out/main/index.js",

View File

@ -321,10 +321,10 @@ const SYSPROXY_RS_VERSION = 'v0.1.0'
const SYSPROXY_RS_URL_PREFIX = `https://github.com/mihomo-party-org/sysproxy-rs-opti/releases/download/${SYSPROXY_RS_VERSION}` const SYSPROXY_RS_URL_PREFIX = `https://github.com/mihomo-party-org/sysproxy-rs-opti/releases/download/${SYSPROXY_RS_VERSION}`
function getSysproxyNodeName() { function getSysproxyNodeName() {
// 检测是否为 musl 系统(与 src/native/sysproxy/index.js 保持一致)
const isMusl = (() => { const isMusl = (() => {
if (platform !== 'linux') return false if (platform !== 'linux') return false
try { try {
// 通过 ldd --version 输出判断是否为 musl
const output = execSync('ldd --version 2>&1 || true').toString() const output = execSync('ldd --version 2>&1 || true').toString()
return output.includes('musl') return output.includes('musl')
} catch { } catch {

View File

@ -256,33 +256,39 @@ export async function restartAsAdmin(forTun: boolean = true): Promise<void> {
throw new Error('This function is only available on Windows') throw new Error('This function is only available on Windows')
} }
// 先停止 Core避免新旧进程冲突
try {
const { stopCore } = await import('./manager')
managerLogger.info('Stopping core before admin restart...')
await stopCore(true)
await new Promise((resolve) => setTimeout(resolve, 500))
} catch (error) {
managerLogger.warn('Failed to stop core before restart:', error)
}
const exePath = process.execPath const exePath = process.execPath
const args = process.argv.slice(1) const args = process.argv.slice(1).filter((arg) => arg !== '--admin-restart-for-tun')
const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args
const escapedExePath = exePath.replace(/'/g, "''") const escapedExePath = exePath.replace(/'/g, "''")
const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '") const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '")
// 使用 Start-Sleep 延迟启动,确保旧进程完全退出后再启动新进程
const command = const command =
restartArgs.length > 0 restartArgs.length > 0
? `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs -Wait:$false; exit 0"` ? `powershell -NoProfile -Command "Start-Sleep -Milliseconds 1000; Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs"`
: `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs -Wait:$false; exit 0"` : `powershell -NoProfile -Command "Start-Sleep -Milliseconds 1000; Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
managerLogger.info('Restarting as administrator with command', command) managerLogger.info('Restarting as administrator with command', command)
return new Promise((resolve, reject) => { // 先启动 PowerShell它会等待 1 秒),然后立即退出当前进程
exec(command, { windowsHide: true }, (error, _stdout, stderr) => { exec(command, { windowsHide: true }, (error) => {
if (error) { if (error) {
managerLogger.error('PowerShell execution error', error) managerLogger.error('Failed to start PowerShell for admin restart', error)
managerLogger.error('stderr', stderr) }
reject(new Error(`Failed to restart as administrator: ${error.message}`))
return
}
managerLogger.info('PowerShell command executed successfully, quitting app')
setTimeout(() => app.quit(), 500)
resolve()
})
}) })
managerLogger.info('PowerShell command started, quitting app immediately')
app.exit(0)
} }
export async function requestTunPermissions(): Promise<void> { export async function requestTunPermissions(): Promise<void> {
@ -342,21 +348,15 @@ export async function validateTunPermissionsOnStartup(_restartCore: () => Promis
const hasPermissions = await checkMihomoCorePermissions() const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) { if (!hasPermissions) {
managerLogger.warn('TUN is enabled but insufficient permissions detected, prompting user...') // 启动时没有权限,静默禁用 TUN不弹窗打扰用户
const confirmed = await showTunPermissionDialog() managerLogger.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
if (confirmed) {
await restartAsAdmin()
return
}
managerLogger.warn('User declined admin restart, auto-disabling TUN...')
await patchControledMihomoConfig({ tun: { enable: false } }) await patchControledMihomoConfig({ tun: { enable: false } })
const { mainWindow } = await import('../index') const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
managerLogger.info('TUN auto-disabled due to insufficient permissions') managerLogger.info('TUN auto-disabled due to insufficient permissions on startup')
} else { } else {
managerLogger.info('TUN permissions validated successfully') managerLogger.info('TUN permissions validated successfully')
} }

View File

@ -87,13 +87,13 @@ async function checkHighPrivilegeCoreEarly(): Promise<void> {
if (choice === 0) { if (choice === 0) {
try { try {
await restartAsAdmin(false) await restartAsAdmin(false)
process.exit(0) app.exit(0)
} catch (error) { } catch (error) {
safeShowErrorBox('common.error.adminRequired', `${error}`) safeShowErrorBox('common.error.adminRequired', `${error}`)
process.exit(1) app.exit(1)
} }
} else { } else {
process.exit(0) app.exit(0)
} }
} catch (e) { } catch (e) {
mainLogger.error('Failed to check high privilege core', e) mainLogger.error('Failed to check high privilege core', e)
@ -151,12 +151,14 @@ app.whenReady().then(async () => {
try { try {
initCoreWatcher() initCoreWatcher()
const [startPromise] = await startCore() const startPromises = await startCore()
startPromise.then(async () => { if (startPromises.length > 0) {
await initProfileUpdater() startPromises[0].then(async () => {
await initWebdavBackupScheduler() await initProfileUpdater()
await checkAdminRestartForTun() await initWebdavBackupScheduler()
}) await checkAdminRestartForTun()
})
}
} catch (e) { } catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
} }

View File

@ -39,6 +39,8 @@ import { trayLogger } from '../utils/logger'
import { floatingWindow, triggerFloatingWindow } from './floatingWindow' import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
export let tray: Tray | null = null export let tray: Tray | null = null
// macOS 流量显示状态,避免异步读取配置导致的时序问题
let macTrafficIconEnabled = false
export const buildContextMenu = async (): Promise<Menu> => { export const buildContextMenu = async (): Promise<Menu> => {
// 添加调试日志 // 添加调试日志
@ -231,6 +233,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
if (process.platform === 'win32') { if (process.platform === 'win32') {
try { try {
await restartAsAdmin() await restartAsAdmin()
return
} catch (error) { } catch (error) {
await trayLogger.error('Failed to restart as admin from tray', error) await trayLogger.error('Failed to restart as admin from tray', error)
item.checked = false item.checked = false
@ -250,6 +253,9 @@ export const buildContextMenu = async (): Promise<Menu> => {
} }
} catch (error) { } catch (error) {
await trayLogger.warn('Permission check failed in tray', error) await trayLogger.warn('Permission check failed in tray', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
} }
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
@ -392,7 +398,8 @@ export async function createTray(): Promise<void> {
} }
// 移除旧监听器防止累积 // 移除旧监听器防止累积
ipcMain.removeAllListeners('trayIconUpdate') ipcMain.removeAllListeners('trayIconUpdate')
ipcMain.on('trayIconUpdate', async (_, png: string) => { ipcMain.on('trayIconUpdate', async (_, png: string, enabled: boolean) => {
macTrafficIconEnabled = enabled
const image = nativeImage.createFromDataURL(png).resize({ height: 16 }) const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
image.setTemplateImage(true) image.setTemplateImage(true)
tray?.setImage(image) tray?.setImage(image)
@ -524,14 +531,15 @@ const getIconPaths = () => {
export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: boolean): void { export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: boolean): void {
if (!tray) return if (!tray) return
// macOS 流量显示开启时,由 trayIconUpdate 负责图标更新
if (process.platform === 'darwin' && macTrafficIconEnabled) return
const status = calculateTrayIconStatus(sysProxyEnabled, tunEnabled) const status = calculateTrayIconStatus(sysProxyEnabled, tunEnabled)
const iconPaths = getIconPaths() const iconPaths = getIconPaths()
getAppConfig().then(({ disableTrayIconColor = false, showTraffic = false }) => { getAppConfig().then(({ disableTrayIconColor = false }) => {
if (!tray) return if (!tray) return
// macOS 开启流量显示时,由 trayIconUpdate 负责图标更新 if (process.platform === 'darwin' && macTrafficIconEnabled) return
if (process.platform === 'darwin' && showTraffic) return
const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status] const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status]
try { try {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@ -550,10 +558,10 @@ export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: bo
export async function updateTrayIcon(): Promise<void> { export async function updateTrayIcon(): Promise<void> {
if (!tray) return if (!tray) return
// macOS 流量显示开启时,由 trayIconUpdate 负责图标更新
if (process.platform === 'darwin' && macTrafficIconEnabled) return
const { disableTrayIconColor = false, showTraffic = false } = await getAppConfig() const { disableTrayIconColor = false } = await getAppConfig()
// macOS 开启流量显示时,由 trayIconUpdate 负责图标更新
if (process.platform === 'darwin' && showTraffic) return
const status = await getTrayIconStatus() const status = await getTrayIconStatus()
const iconPaths = getIconPaths() const iconPaths = getIconPaths()
const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status] const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status]

View File

@ -56,10 +56,22 @@ function getTaskXml(asAdmin: boolean): string {
export async function checkAutoRun(): Promise<boolean> { export async function checkAutoRun(): Promise<boolean> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
// 先检查任务计划程序
try { try {
const { stdout } = await execPromise( const { stdout } = await execPromise(
`chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"` `chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"`
) )
if (stdout.includes(appName)) {
return true
}
} catch {
// 任务计划程序中不存在,继续检查注册表
}
// 检查注册表备用方案
try {
const regPath = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
const { stdout } = await execPromise(`reg query "${regPath}" /v "${appName}"`)
return stdout.includes(appName) return stdout.includes(appName)
} catch { } catch {
return false return false
@ -87,17 +99,45 @@ export async function enableAutoRun(): Promise<void> {
const { checkAdminPrivileges } = await import('../core/manager') const { checkAdminPrivileges } = await import('../core/manager')
const isAdmin = await checkAdminPrivileges() const isAdmin = await checkAdminPrivileges()
await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml(isAdmin)}`, 'utf-16le')) await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml(isAdmin)}`, 'utf-16le'))
let taskCreated = false
if (isAdmin) { if (isAdmin) {
await execPromise( try {
`%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f` await execPromise(
) `%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`
)
taskCreated = true
} catch (error) {
await managerLogger.warn('Failed to create scheduled task as admin:', error)
}
} else { } else {
try { try {
await execPromise( await execPromise(
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"` `powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden -Wait"`
) )
// 验证任务是否创建成功
await new Promise((resolve) => setTimeout(resolve, 1000))
const created = await checkAutoRun()
taskCreated = created
if (!created) {
await managerLogger.warn('Scheduled task creation may have failed or been rejected')
}
} catch { } catch {
await managerLogger.info('Maybe the user rejected the UAC dialog?') await managerLogger.info('Scheduled task creation failed, trying registry fallback')
}
}
// 任务计划程序失败时使用注册表备用方案(适用于 Windows IoT LTSC 等受限环境)
if (!taskCreated) {
await managerLogger.info('Using registry fallback for auto-run')
try {
const regPath = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
const regValue = `"${exePath()}"`
await execPromise(`reg add "${regPath}" /v "${appName}" /t REG_SZ /d ${regValue} /f`)
await managerLogger.info('Registry auto-run entry created successfully')
} catch (regError) {
await managerLogger.error('Failed to create registry auto-run entry:', regError)
} }
} }
} }
@ -137,16 +177,26 @@ export async function disableAutoRun(): Promise<void> {
const execPromise = promisify(exec) const execPromise = promisify(exec)
const { checkAdminPrivileges } = await import('../core/manager') const { checkAdminPrivileges } = await import('../core/manager')
const isAdmin = await checkAdminPrivileges() const isAdmin = await checkAdminPrivileges()
if (isAdmin) {
await execPromise(`%SystemRoot%\\System32\\schtasks.exe /delete /tn "${appName}" /f`) // 删除任务计划程序中的任务
} else { try {
try { if (isAdmin) {
await execPromise(`%SystemRoot%\\System32\\schtasks.exe /delete /tn "${appName}" /f`)
} else {
await execPromise( await execPromise(
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"` `powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden -Wait"`
) )
} catch {
await managerLogger.info('Maybe the user rejected the UAC dialog?')
} }
} catch {
// 任务可能不存在,忽略错误
}
// 同时删除注册表备用方案
try {
const regPath = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
await execPromise(`reg delete "${regPath}" /v "${appName}" /f`)
} catch {
// 注册表项可能不存在,忽略错误
} }
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {

View File

@ -97,10 +97,15 @@ async function enableSysProxy(): Promise<void> {
} }
} else { } else {
// Windows / Linux 直接使用 sysproxy-rs // Windows / Linux 直接使用 sysproxy-rs
if (mode === 'auto') { try {
triggerAutoProxy(true, `http://${proxyHost}:${pacPort}/pac`) if (mode === 'auto') {
} else { triggerAutoProxy(true, `http://${proxyHost}:${pacPort}/pac`)
triggerManualProxy(true, proxyHost, port, bypass.join(',')) } else {
triggerManualProxy(true, proxyHost, port, bypass.join(','))
}
} catch (error) {
await proxyLogger.error('Failed to enable system proxy', error)
throw error
} }
} }
} }
@ -114,8 +119,13 @@ async function disableSysProxy(): Promise<void> {
) )
} else { } else {
// Windows / Linux 直接使用 sysproxy-rs // Windows / Linux 直接使用 sysproxy-rs
triggerAutoProxy(false, '') try {
triggerManualProxy(false, '', 0, '') triggerAutoProxy(false, '')
triggerManualProxy(false, '', 0, '')
} catch (error) {
await proxyLogger.error('Failed to disable system proxy', error)
throw error
}
} }
} }

View File

@ -189,8 +189,9 @@ async function initFiles(): Promise<void> {
await cp(sourcePath, targetPath, { recursive: true, force: true }) await cp(sourcePath, targetPath, { recursive: true, force: true })
} catch (error: unknown) { } catch (error: unknown) {
const code = (error as NodeJS.ErrnoException).code const code = (error as NodeJS.ErrnoException).code
if (code === 'EPERM' || code === 'EBUSY') { // 文件被占用或权限问题,如果目标已存在则跳过
await initLogger.warn(`Skipping ${file}: file is in use`) if ((code === 'EPERM' || code === 'EBUSY' || code === 'EACCES') && existsSync(targetPath)) {
await initLogger.warn(`Skipping ${file}: file is in use or permission denied`)
return return
} }
throw error throw error

View File

@ -1,4 +1,4 @@
const { existsSync, readFileSync } = require('fs') const { existsSync } = require('fs')
const { join, dirname } = require('path') const { join, dirname } = require('path')
const { platform, arch } = process const { platform, arch } = process
@ -7,17 +7,19 @@ let nativeBinding = null
let loadError = null let loadError = null
function isMusl() { function isMusl() {
if (!process.report || typeof process.report.getReport !== 'function') { // 优先使用 process.reportNode.js 12+,最可靠)
try { if (process.report && typeof process.report.getReport === 'function') {
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime return !glibcVersionRuntime
} }
// 备选:检查 ldd --version 输出
try {
const { execSync } = require('child_process')
const output = execSync('ldd --version 2>&1 || true').toString()
return output.includes('musl')
} catch {
return false
}
} }
function getBindingName() { function getBindingName() {

View File

@ -161,7 +161,7 @@ const ConnCard: React.FC<Props> = (props) => {
} }
} else if (hasShowTrafficRef.current) { } else if (hasShowTrafficRef.current) {
// 只在从 showTraffic=true 切换到 false 时恢复一次原始图标 // 只在从 showTraffic=true 切换到 false 时恢复一次原始图标
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64) window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64, false)
hasShowTrafficRef.current = false hasShowTrafficRef.current = false
} }
} }
@ -305,7 +305,7 @@ const drawSvg = async (
currentDownloadRef.current = download currentDownloadRef.current = download
const svg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 36"><image height="36" width="36" href="${trayIconBase64}"/><text x="140" y="15" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(upload)}/s</text><text x="140" y="34" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(download)}/s</text></svg>` const svg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 36"><image height="36" width="36" href="${trayIconBase64}"/><text x="140" y="15" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(upload)}/s</text><text x="140" y="34" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(download)}/s</text></svg>`
const image = await loadImage(svg) const image = await loadImage(svg)
window.electron.ipcRenderer.send('trayIconUpdate', image) window.electron.ipcRenderer.send('trayIconUpdate', image, true)
} }
const loadImage = (url: string): Promise<string> => { const loadImage = (url: string): Promise<string> => {