Compare commits

...

11 Commits

13 changed files with 169 additions and 82 deletions

View File

@ -196,12 +196,9 @@ jobs:
run: |
pnpm install
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
# Electron 22 requires CJS format
(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
if: github.event_name == 'workflow_dispatch'
env:
@ -213,6 +210,7 @@ jobs:
env:
npm_config_arch: ${{ matrix.arch }}
npm_config_target_arch: ${{ matrix.arch }}
LEGACY_BUILD: 'true'
run: pnpm build:win --${{ matrix.arch }}
- name: Add Portable Flag
run: |
@ -590,7 +588,7 @@ jobs:
- mihomo-party-bin
- mihomo-party
if: startsWith(github.ref, 'refs/tags/v')
needs: linux
needs: updater
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -601,14 +599,14 @@ jobs:
- name: Update Checksums
if: matrix.pkgname == 'mihomo-party' || matrix.pkgname == 'mihomo-party-electron'
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-electron/PKGBUILD
- name: Update Checksums
if: matrix.pkgname == 'mihomo-party-bin' || matrix.pkgname == 'mihomo-party-electron-bin'
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/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')-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')-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_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

View File

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

View File

@ -15,14 +15,22 @@ const monacoEditorPlugin = isObjectWithDefaultFunction(monacoEditorPluginModule)
? monacoEditorPluginModule.default
: 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({
main: {
plugins: [externalizeDepsPlugin()]
plugins: isLegacyBuild ? [] : [externalizeDepsPlugin()],
build: isLegacyBuild
? { rollupOptions: { external: legacyExternal, output: { format: 'cjs' } } }
: undefined
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: isLegacyBuild ? [] : [externalizeDepsPlugin()],
build: {
rollupOptions: {
external: isLegacyBuild ? legacyExternal : undefined,
output: {
format: 'cjs',
entryFileNames: '[name].cjs'

View File

@ -1,6 +1,6 @@
{
"name": "mihomo-party",
"version": "1.9.0",
"version": "1.9.1",
"description": "Clash Party",
"type": "module",
"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}`
function getSysproxyNodeName() {
// 检测是否为 musl 系统(与 src/native/sysproxy/index.js 保持一致)
const isMusl = (() => {
if (platform !== 'linux') return false
try {
// 通过 ldd --version 输出判断是否为 musl
const output = execSync('ldd --version 2>&1 || true').toString()
return output.includes('musl')
} 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')
}
// 先停止 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 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 escapedExePath = exePath.replace(/'/g, "''")
const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '")
// 使用 Start-Sleep 延迟启动,确保旧进程完全退出后再启动新进程
const command =
restartArgs.length > 0
? `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs -Wait:$false; exit 0"`
: `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -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-Sleep -Milliseconds 1000; Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
managerLogger.info('Restarting as administrator with command', command)
return new Promise((resolve, reject) => {
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
if (error) {
managerLogger.error('PowerShell execution error', 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()
})
// 先启动 PowerShell它会等待 1 秒),然后立即退出当前进程
exec(command, { windowsHide: true }, (error) => {
if (error) {
managerLogger.error('Failed to start PowerShell for admin restart', error)
}
})
managerLogger.info('PowerShell command started, quitting app immediately')
app.exit(0)
}
export async function requestTunPermissions(): Promise<void> {
@ -342,21 +348,15 @@ export async function validateTunPermissionsOnStartup(_restartCore: () => Promis
const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) {
managerLogger.warn('TUN is enabled but insufficient permissions detected, prompting user...')
const confirmed = await showTunPermissionDialog()
if (confirmed) {
await restartAsAdmin()
return
}
managerLogger.warn('User declined admin restart, auto-disabling TUN...')
// 启动时没有权限,静默禁用 TUN不弹窗打扰用户
managerLogger.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
await patchControledMihomoConfig({ tun: { enable: false } })
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
managerLogger.info('TUN auto-disabled due to insufficient permissions')
managerLogger.info('TUN auto-disabled due to insufficient permissions on startup')
} else {
managerLogger.info('TUN permissions validated successfully')
}

View File

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

View File

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

View File

@ -56,10 +56,22 @@ function getTaskXml(asAdmin: boolean): string {
export async function checkAutoRun(): Promise<boolean> {
if (process.platform === 'win32') {
const execPromise = promisify(exec)
// 先检查任务计划程序
try {
const { stdout } = await execPromise(
`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)
} catch {
return false
@ -87,17 +99,45 @@ export async function enableAutoRun(): Promise<void> {
const { checkAdminPrivileges } = await import('../core/manager')
const isAdmin = await checkAdminPrivileges()
await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml(isAdmin)}`, 'utf-16le'))
let taskCreated = false
if (isAdmin) {
await execPromise(
`%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`
)
try {
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 {
try {
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 {
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 { checkAdminPrivileges } = await import('../core/manager')
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(
`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') {

View File

@ -97,10 +97,15 @@ async function enableSysProxy(): Promise<void> {
}
} else {
// Windows / Linux 直接使用 sysproxy-rs
if (mode === 'auto') {
triggerAutoProxy(true, `http://${proxyHost}:${pacPort}/pac`)
} else {
triggerManualProxy(true, proxyHost, port, bypass.join(','))
try {
if (mode === 'auto') {
triggerAutoProxy(true, `http://${proxyHost}:${pacPort}/pac`)
} 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 {
// Windows / Linux 直接使用 sysproxy-rs
triggerAutoProxy(false, '')
triggerManualProxy(false, '', 0, '')
try {
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 })
} catch (error: unknown) {
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
}
throw error

View File

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

View File

@ -161,7 +161,7 @@ const ConnCard: React.FC<Props> = (props) => {
}
} else if (hasShowTrafficRef.current) {
// 只在从 showTraffic=true 切换到 false 时恢复一次原始图标
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64, false)
hasShowTrafficRef.current = false
}
}
@ -305,7 +305,7 @@ const drawSvg = async (
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 image = await loadImage(svg)
window.electron.ipcRenderer.send('trayIconUpdate', image)
window.electron.ipcRenderer.send('trayIconUpdate', image, true)
}
const loadImage = (url: string): Promise<string> => {