Compare commits

...

9 Commits

Author SHA1 Message Date
ezequielnick
294dd75b48 1.8.3 Released 2025-08-09 10:15:33 +08:00
ezequielnick
f56c585818 refactor: copy geodata only if source files are newer 2025-08-09 10:05:09 +08:00
ezequielnick
ef96819621 fix: restore mistakenly removed trafficmonitor download component 2025-08-09 09:49:02 +08:00
ezequielnick
5e0c5b6e69 fix: change floating window to rectangle to avoid white border issue 2025-08-09 09:48:09 +08:00
ezequielnick
8cfee2f5e5 fix: resolve Windows admin mode restart issue 2025-08-06 22:49:39 +08:00
ezequielnick
b5f6658b72 fix: dns/sniffer override button logic2 2025-08-06 22:24:57 +08:00
ezequielnick
6b93a59616 fix: privilege check and elevation restart logic 2025-08-06 21:41:30 +08:00
ezequielnick
e27ddbd16e feat: request admin privileges when enabling TUN 2025-08-06 20:36:08 +08:00
ezequielnick
5c1d30b454 feat: remove enforced admin mode requirement 2025-08-06 18:34:59 +08:00
19 changed files with 354 additions and 149 deletions

View File

@ -1,4 +1,18 @@
## 1.8.3
**本次更新移除了 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件,安全性更好,更灵活,给无法使用管理员模式运行软件的企业用户提供了更大的便利**
### 新功能 (Feat)
- 移除 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件
### 重构 (Refactor)
- Geodata 文件只有在源文件更新的时候才会在启动时覆盖更新
### 修复 (Fix)
- 修复 DNS/嗅探覆写开关逻辑,修改设置不再会直接写入运行时配置,增加了“仅保存”按钮
- 悬浮窗改为纯矩形,修复 windows 下兼容性问题带来的白边
## 1.8.2 ## 1.8.2
**本次更新主要集中在重大内核更新和依赖升级后所产生的 bug 修复解决了自1.7版以后首次安装无法启动的问题,推荐更新** **本次更新主要集中在重大内核更新和依赖升级后所产生的 bug 修复解决了自1.7版以后首次安装无法启动的问题,推荐更新**
### 新功能 (Feat) ### 新功能 (Feat)

View File

@ -1,6 +1,6 @@
{ {
"name": "mihomo-party", "name": "mihomo-party",
"version": "1.8.2", "version": "1.8.3",
"description": "Mihomo Party", "description": "Mihomo Party",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "mihomo-party-org", "author": "mihomo-party-org",

View File

@ -322,11 +322,6 @@ const resolveSysproxy = () =>
file: 'sysproxy.exe', file: 'sysproxy.exe',
downloadURL: `https://github.com/mihomo-party-org/sysproxy/releases/download/${arch}/sysproxy.exe` downloadURL: `https://github.com/mihomo-party-org/sysproxy/releases/download/${arch}/sysproxy.exe`
}) })
const resolveRunner = () =>
resolveResource({
file: 'mihomo-party-run.exe',
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-run/releases/download/${arch}/mihomo-party-run.exe`
})
const resolveMonitor = async () => { const resolveMonitor = async () => {
const tempDir = path.join(TEMP_DIR, 'TrafficMonitor') const tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
@ -438,12 +433,6 @@ const tasks = [
retry: 5, retry: 5,
winOnly: true winOnly: true
}, },
{
name: 'runner',
func: resolveRunner,
retry: 5,
winOnly: true
},
{ {
name: 'monitor', name: 'monitor',
func: resolveMonitor, func: resolveMonitor,

View File

@ -19,7 +19,7 @@ export async function getControledMihomoConfig(force = false): Promise<Partial<I
} }
export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> { export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
const { useNameserverPolicy, controlDns = true, controlSniff = true } = await getAppConfig() const { controlDns = true, controlSniff = true } = await getAppConfig()
if (patch.hosts) { if (patch.hosts) {
controledMihomoConfig.hosts = patch.hosts controledMihomoConfig.hosts = patch.hosts
@ -30,36 +30,18 @@ export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>):
} }
controledMihomoConfig = deepMerge(controledMihomoConfig, patch) controledMihomoConfig = deepMerge(controledMihomoConfig, patch)
// 覆写开关控制 // 从不接管状态恢复
let configForProfile = { ...controledMihomoConfig } if (controlDns && controledMihomoConfig.dns?.ipv6 === undefined) {
controledMihomoConfig.dns = defaultControledMihomoConfig.dns
if (!controlDns) {
delete configForProfile.dns
delete configForProfile.hosts
} else {
if (configForProfile.dns?.ipv6 === undefined) {
configForProfile.dns = defaultControledMihomoConfig.dns
}
} }
if (!controlSniff) { if (controlSniff && !controledMihomoConfig.sniffer) {
delete configForProfile.sniffer controledMihomoConfig.sniffer = defaultControledMihomoConfig.sniffer
} else {
if (!configForProfile.sniffer) {
configForProfile.sniffer = defaultControledMihomoConfig.sniffer
}
} }
if (!useNameserverPolicy) {
delete configForProfile?.dns?.['nameserver-policy']
}
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
delete configForProfile?.tun?.device delete controledMihomoConfig?.tun?.device
} }
const originalConfig = controledMihomoConfig
controledMihomoConfig = configForProfile
await generateProfile() await generateProfile()
controledMihomoConfig = originalConfig
await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8') await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8')
} }

View File

@ -26,9 +26,23 @@ let runtimeConfig: IMihomoConfig
export async function generateProfile(): Promise<void> { export async function generateProfile(): Promise<void> {
const { current } = await getProfileConfig() const { current } = await getProfileConfig()
const { diffWorkDir = false } = await getAppConfig() const { diffWorkDir = false, controlDns = true, controlSniff = true, useNameserverPolicy } = await getAppConfig()
const currentProfile = await overrideProfile(current, await getProfile(current)) const currentProfile = await overrideProfile(current, await getProfile(current))
const controledMihomoConfig = await getControledMihomoConfig() let controledMihomoConfig = await getControledMihomoConfig()
// 根据开关状态过滤控制配置
controledMihomoConfig = { ...controledMihomoConfig }
if (!controlDns) {
delete controledMihomoConfig.dns
delete controledMihomoConfig.hosts
}
if (!controlSniff) {
delete controledMihomoConfig.sniffer
}
if (!useNameserverPolicy) {
delete controledMihomoConfig?.dns?.['nameserver-policy']
}
const profile = deepMerge(currentProfile, controledMihomoConfig) const profile = deepMerge(currentProfile, controledMihomoConfig)
// 确保可以拿到基础日志信息 // 确保可以拿到基础日志信息
// 使用 debug 可以调试内核相关问题 `debug/pprof` // 使用 debug 可以调试内核相关问题 `debug/pprof`

View File

@ -18,7 +18,7 @@ import {
patchControledMihomoConfig, patchControledMihomoConfig,
manageSmartOverride manageSmartOverride
} from '../config' } from '../config'
import { app, dialog, ipcMain, net } from 'electron' import { app, ipcMain, net } from 'electron'
import { import {
startMihomoTraffic, startMihomoTraffic,
startMihomoConnections, startMihomoConnections,
@ -290,16 +290,45 @@ async function checkProfile(): Promise<void> {
} }
} }
export async function manualGrantCorePermition(): Promise<void> { export async function checkTunPermissions(): Promise<boolean> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
try {
if (process.platform === 'win32') {
const execPromise = promisify(exec)
try {
await execPromise('net session')
return true
} catch {
return false
}
}
if (process.platform === 'darwin' || process.platform === 'linux') {
const { stat } = await import('fs/promises')
const stats = await stat(corePath)
return (stats.mode & 0o4000) !== 0 && stats.uid === 0
}
} catch {
return false
}
return false
}
export async function grantTunPermissions(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig() const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
const execPromise = promisify(exec) const execPromise = promisify(exec)
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}` const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
const command = `do shell script "${shell}" with administrator privileges` const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`) await execPromise(`osascript -e '${command}'`)
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
await execFilePromise('pkexec', [ await execFilePromise('pkexec', [
'bash', 'bash',
@ -307,6 +336,146 @@ export async function manualGrantCorePermition(): Promise<void> {
`chown root:root "${corePath}" && chmod +sx "${corePath}"` `chown root:root "${corePath}" && chmod +sx "${corePath}"`
]) ])
} }
if (process.platform === 'win32') {
throw new Error('Windows platform requires running as administrator')
}
}
export async function checkAdminPrivileges(): Promise<boolean> {
if (process.platform !== 'win32') {
return true
}
try {
const execPromise = promisify(exec)
await execPromise('net session')
return true
} catch {
return false
}
}
export async function restartAsAdmin(): Promise<void> {
if (process.platform !== 'win32') {
throw new Error('This function is only available on Windows')
}
const exePath = process.execPath
const args = process.argv.slice(1)
const restartArgs = [...args, '--admin-restart-for-tun']
try {
// 处理路径和参数的引号
const escapedExePath = exePath.replace(/'/g, "''")
const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '")
let command: string
if (restartArgs.length > 0) {
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs"`
} else {
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
}
console.log('Restarting as administrator with command:', command)
// 执行PowerShell命令
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
if (error) {
console.error('PowerShell execution error:', error)
console.error('stderr:', stderr)
} else {
console.log('PowerShell command executed successfully')
}
})
await new Promise(resolve => setTimeout(resolve, 1500))
const { app } = await import('electron')
app.quit()
} catch (error) {
console.error('Failed to restart as administrator:', error)
throw new Error(`Failed to restart as administrator: ${error}`)
}
}
export async function checkMihomoCorePermissions(): Promise<boolean> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
try {
if (process.platform === 'win32') {
// Windows权限检查
return await checkAdminPrivileges()
}
if (process.platform === 'darwin' || process.platform === 'linux') {
const { stat } = await import('fs/promises')
const stats = await stat(corePath)
return (stats.mode & 0o4000) !== 0 && stats.uid === 0
}
} catch {
return false
}
return false
}
// TUN模式获取权限
export async function requestTunPermissions(): Promise<void> {
if (process.platform === 'win32') {
await restartAsAdmin()
} else {
const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) {
await grantTunPermissions()
}
}
}
export async function checkAdminRestartForTun(): Promise<void> {
if (process.argv.includes('--admin-restart-for-tun')) {
console.log('Detected admin restart for TUN mode, auto-enabling TUN...')
try {
if (process.platform === 'win32') {
const hasAdminPrivileges = await checkAdminPrivileges()
if (hasAdminPrivileges) {
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
await restartCore()
console.log('TUN mode auto-enabled after admin restart')
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
} else {
console.warn('Admin restart detected but no admin privileges found')
}
}
} catch (error) {
console.error('Failed to auto-enable TUN after admin restart:', error)
}
} else if (process.platform === 'win32') {
try {
const hasAdminPrivileges = await checkAdminPrivileges()
const { tun } = await getControledMihomoConfig()
if (hasAdminPrivileges && !tun?.enable) {
console.log('Running with admin privileges but TUN is disabled')
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('adminPrivilegesDetected', { tunEnabled: false })
}
} catch (error) {
console.error('Failed to check admin privileges on startup:', error)
}
}
}
export async function manualGrantCorePermition(): Promise<void> {
return grantTunPermissions()
} }
export async function getDefaultDevice(): Promise<string> { export async function getDefaultDevice(): Promise<string> {

View File

@ -3,22 +3,19 @@ import { registerIpcMainHandlers } from './utils/ipc'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron' import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
import { addProfileItem, getAppConfig, patchAppConfig } from './config' import { addProfileItem, getAppConfig, patchAppConfig } from './config'
import { quitWithoutCore, startCore, stopCore } from './core/manager' import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun } from './core/manager'
import { triggerSysProxy } from './sys/sysproxy' import { triggerSysProxy } from './sys/sysproxy'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray' import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
import { init } from './utils/init' import { init } from './utils/init'
import { join } from 'path' import { join } from 'path'
import { initShortcut } from './resolve/shortcut' import { initShortcut } from './resolve/shortcut'
import { execSync, spawn, exec } from 'child_process' import { spawn, exec } from 'child_process'
import { createElevateTask } from './sys/misc'
import { promisify } from 'util' import { promisify } from 'util'
import { stat } from 'fs/promises' import { stat } from 'fs/promises'
import { initProfileUpdater } from './core/profileUpdater' import { initProfileUpdater } from './core/profileUpdater'
import { existsSync, writeFileSync } from 'fs' import { existsSync } from 'fs'
import { exePath, taskDir } from './utils/dirs' import { exePath } from './utils/dirs'
import path from 'path'
import iconv from 'iconv-lite'
import { startMonitor } from './resolve/trafficMonitor' import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow' import { showFloatingWindow } from './resolve/floatingWindow'
import { initI18n } from '../shared/i18n' import { initI18n } from '../shared/i18n'
@ -69,37 +66,6 @@ async function fixUserDataPermissions(): Promise<void> {
let quitTimeout: NodeJS.Timeout | null = null let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null export let mainWindow: BrowserWindow | null = null
// Windows 管理员权限检查(仅在生产模式下)
if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')) {
try {
createElevateTask()
} catch (createError) {
try {
if (process.argv.slice(1).length > 0) {
writeFileSync(path.join(taskDir(), 'param.txt'), process.argv.slice(1).join(' '))
} else {
writeFileSync(path.join(taskDir(), 'param.txt'), 'empty')
}
if (!existsSync(path.join(taskDir(), 'mihomo-party-run.exe'))) {
throw new Error('mihomo-party-run.exe not found')
} else {
execSync('%SystemRoot%\\System32\\schtasks.exe /run /tn mihomo-party-run')
}
} catch (e) {
let createErrorStr = `${createError}`
let eStr = `${e}`
try {
createErrorStr = iconv.decode((createError as { stderr: Buffer }).stderr, 'gbk')
eStr = iconv.decode((e as { stderr: Buffer }).stderr, 'gbk')
} catch {
// ignore
}
showSafeErrorBox('common.error.adminRequired', `${createErrorStr}\n${eStr}`)
} finally {
app.exit()
}
}
}
async function initApp(): Promise<void> { async function initApp(): Promise<void> {
await fixUserDataPermissions() await fixUserDataPermissions()
@ -207,6 +173,8 @@ app.whenReady().then(async () => {
const [startPromise] = await startCore() const [startPromise] = await startCore()
startPromise.then(async () => { startPromise.then(async () => {
await initProfileUpdater() await initProfileUpdater()
// 上次是否为了开启 TUN 而重启
await checkAdminRestartForTun()
}) })
} catch (e) { } catch (e) {
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`) showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)

View File

@ -19,7 +19,7 @@ import { mainWindow, showMainWindow, triggerMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron' import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs' import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { quitWithoutCore, restartCore } from '../core/manager' import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager'
import { floatingWindow, triggerFloatingWindow } from './floatingWindow' import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
import { t } from 'i18next' import { t } from 'i18next'
@ -178,6 +178,35 @@ export const buildContextMenu = async (): Promise<Menu> => {
const enable = item.checked const enable = item.checked
try { try {
if (enable) { if (enable) {
// 检查权限
try {
const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) {
if (process.platform === 'win32') {
try {
await restartAsAdmin()
} catch (error) {
console.error('Failed to restart as admin from tray:', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
} else {
try {
await requestTunPermissions()
} catch (error) {
console.error('Failed to grant TUN permissions from tray:', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
}
}
} catch (error) {
console.warn('Permission check failed in tray:', error)
}
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
} else { } else {
await patchControledMihomoConfig({ tun: { enable } }) await patchControledMihomoConfig({ tun: { enable } })

View File

@ -1,4 +1,5 @@
import { exePath, homeDir, taskDir } from '../utils/dirs' import { exePath, homeDir } from '../utils/dirs'
import { tmpdir } from 'os'
import { mkdir, readFile, rm, writeFile } from 'fs/promises' import { mkdir, readFile, rm, writeFile } from 'fs/promises'
import { exec } from 'child_process' import { exec } from 'child_process'
import { existsSync } from 'fs' import { existsSync } from 'fs'
@ -19,7 +20,7 @@ function getTaskXml(): string {
<Principals> <Principals>
<Principal id="Author"> <Principal id="Author">
<LogonType>InteractiveToken</LogonType> <LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel> <RunLevel>LeastPrivilege</RunLevel>
</Principal> </Principal>
</Principals> </Principals>
<Settings> <Settings>
@ -43,8 +44,7 @@ function getTaskXml(): string {
</Settings> </Settings>
<Actions Context="Author"> <Actions Context="Author">
<Exec> <Exec>
<Command>"${path.join(taskDir(), `mihomo-party-run.exe`)}"</Command> <Command>"${exePath()}"</Command>
<Arguments>"${exePath()}"</Arguments>
</Exec> </Exec>
</Actions> </Actions>
</Task> </Task>
@ -81,7 +81,7 @@ export async function checkAutoRun(): Promise<boolean> {
export async function enableAutoRun(): Promise<void> { export async function enableAutoRun(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
const taskFilePath = path.join(taskDir(), `${appName}.xml`) const taskFilePath = path.join(tmpdir(), `${appName}.xml`)
await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml()}`, 'utf-16le')) await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml()}`, 'utf-16le'))
await execPromise( await execPromise(
`%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f` `%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`

View File

@ -1,4 +1,4 @@
import { exec, execFile, execSync, spawn } from 'child_process' import { exec, execFile, spawn } from 'child_process'
import { app, dialog, nativeTheme, shell } from 'electron' import { app, dialog, nativeTheme, shell } from 'electron'
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises'
import path from 'path' import path from 'path'
@ -9,11 +9,8 @@ import {
mihomoCorePath, mihomoCorePath,
overridePath, overridePath,
profilePath, profilePath,
resourcesDir, resourcesDir
resourcesFilesDir,
taskDir
} from '../utils/dirs' } from '../utils/dirs'
import { copyFileSync, writeFileSync } from 'fs'
export function getFilePath(ext: string[]): string[] | undefined { export function getFilePath(ext: string[]): string[] | undefined {
return dialog.showOpenDialogSync({ return dialog.showOpenDialogSync({
@ -68,56 +65,7 @@ export function setNativeTheme(theme: 'system' | 'light' | 'dark'): void {
nativeTheme.themeSource = theme nativeTheme.themeSource = theme
} }
function getElevateTaskXml(): string {
return `<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers />
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>3</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"${path.join(taskDir(), `mihomo-party-run.exe`)}"</Command>
<Arguments>"${exePath()}"</Arguments>
</Exec>
</Actions>
</Task>
`
}
export function createElevateTask(): void {
const taskFilePath = path.join(taskDir(), `mihomo-party-run.xml`)
writeFileSync(taskFilePath, Buffer.from(`\ufeff${getElevateTaskXml()}`, 'utf-16le'))
copyFileSync(
path.join(resourcesFilesDir(), 'mihomo-party-run.exe'),
path.join(taskDir(), 'mihomo-party-run.exe')
)
execSync(
`%SystemRoot%\\System32\\schtasks.exe /create /tn "mihomo-party-run" /xml "${taskFilePath}" /f`
)
}
export function resetAppConfig(): void { export function resetAppConfig(): void {
if (process.platform === 'win32') { if (process.platform === 'win32') {

View File

@ -167,7 +167,7 @@ async function requestSocketRecreation(): Promise<void> {
const { promisify } = require('util') const { promisify } = require('util')
const execPromise = promisify(exec) const execPromise = promisify(exec)
// Use osascript with administrator privileges (same pattern as manualGrantCorePermition) // Use osascript with administrator privileges (same pattern as grantTunPermissions)
const shell = `pkill -USR1 -f party.mihomo.helper` const shell = `pkill -USR1 -f party.mihomo.helper`
const command = `do shell script "${shell}" with administrator privileges` const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`) await execPromise(`osascript -e '${command}'`)

View File

@ -82,6 +82,18 @@ async function fixDataDirPermissions(): Promise<void> {
} }
} }
// 比较修改geodata文件修改时间
async function isSourceNewer(sourcePath: string, targetPath: string): Promise<boolean> {
try {
const sourceStats = await stat(sourcePath)
const targetStats = await stat(targetPath)
return sourceStats.mtime > targetStats.mtime
} catch {
return true
}
}
async function initDirs(): Promise<void> { async function initDirs(): Promise<void> {
await fixDataDirPermissions() await fixDataDirPermissions()
@ -137,11 +149,18 @@ async function initFiles(): Promise<void> {
const sourcePath = path.join(resourcesFilesDir(), file) const sourcePath = path.join(resourcesFilesDir(), file)
try { try {
if (!existsSync(targetPath) && existsSync(sourcePath)) { // 检查是否需要复制
await cp(sourcePath, targetPath, { recursive: true }) if (existsSync(sourcePath)) {
const shouldCopyToWork = !existsSync(targetPath) || await isSourceNewer(sourcePath, targetPath)
if (shouldCopyToWork) {
await cp(sourcePath, targetPath, { recursive: true })
}
} }
if (!existsSync(testTargetPath) && existsSync(sourcePath)) { if (existsSync(sourcePath)) {
await cp(sourcePath, testTargetPath, { recursive: true }) const shouldCopyToTest = !existsSync(testTargetPath) || await isSourceNewer(sourcePath, testTargetPath)
if (shouldCopyToTest) {
await cp(sourcePath, testTargetPath, { recursive: true })
}
} }
} catch (error) { } catch (error) {
console.error(`Failed to copy ${file}:`, error) console.error(`Failed to copy ${file}:`, error)

View File

@ -56,7 +56,17 @@ import {
subStoreFrontendPort, subStoreFrontendPort,
subStorePort subStorePort
} from '../resolve/server' } from '../resolve/server'
import { manualGrantCorePermition, quitWithoutCore, restartCore } from '../core/manager' import {
quitWithoutCore,
restartCore,
checkTunPermissions,
grantTunPermissions,
manualGrantCorePermition,
checkAdminPrivileges,
restartAsAdmin,
checkMihomoCorePermissions,
requestTunPermissions
} from '../core/manager'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
import { import {
@ -186,6 +196,13 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached)) ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable)) ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)()) ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)())
ipcMain.handle('checkAdminPrivileges', () => ipcErrorWrapper(checkAdminPrivileges)())
ipcMain.handle('restartAsAdmin', () => ipcErrorWrapper(restartAsAdmin)())
ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)())
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr)) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))

View File

@ -60,7 +60,7 @@ const FloatingApp: React.FC = () => {
return ( return (
<div className="app-drag h-screen w-screen overflow-hidden"> <div className="app-drag h-screen w-screen overflow-hidden">
<div className="floating-bg border border-divider flex rounded-full bg-content1 h-[calc(100%-2px)] w-[calc(100%-2px)]"> <div className="floating-bg border border-divider flex bg-content1 h-full w-full">
<div className="flex justify-center items-center h-full aspect-square"> <div className="flex justify-center items-center h-full aspect-square">
<div <div
onContextMenu={(e) => { onContextMenu={(e) => {
@ -78,7 +78,7 @@ const FloatingApp: React.FC = () => {
} }
: {} : {}
} }
className={`app-nodrag cursor-pointer floating-thumb ${tunEnabled ? 'bg-secondary' : sysProxyEnabled ? 'bg-primary' : 'bg-default'} hover:opacity-hover rounded-full h-[calc(100%-4px)] aspect-square`} className={`app-nodrag cursor-pointer floating-thumb ${tunEnabled ? 'bg-secondary' : sysProxyEnabled ? 'bg-primary' : 'bg-default'} hover:opacity-hover h-[calc(100%-4px)] aspect-square`}
> >
<MihomoIcon className="floating-icon text-primary-foreground h-full leading-full text-[22px] mx-auto" /> <MihomoIcon className="floating-icon text-primary-foreground h-full leading-full text-[22px] mx-auto" />
</div> </div>

View File

@ -38,6 +38,42 @@ const TunSwitcher: React.FC<Props> = (props) => {
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (enable: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
if (enable) { if (enable) {
try {
// 检查内核权限
const hasPermissions = await window.electron.ipcRenderer.invoke('checkMihomoCorePermissions')
if (!hasPermissions) {
if (window.electron.process.platform === 'win32') {
const confirmed = confirm(t('tun.permissions.required'))
if (confirmed) {
try {
const notification = new Notification(t('tun.permissions.restarting'))
await window.electron.ipcRenderer.invoke('restartAsAdmin')
notification.close()
return
} catch (error) {
console.error('Failed to restart as admin:', error)
alert(t('tun.permissions.failed') + ': ' + error)
return
}
} else {
return
}
} else {
// macOS/Linux下尝试自动获取权限
try {
await window.electron.ipcRenderer.invoke('requestTunPermissions')
} catch (error) {
console.warn('Permission grant failed:', error)
alert(t('tun.permissions.failed') + ': ' + error)
return
}
}
}
} catch (error) {
console.warn('Permission check failed:', error)
}
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
} else { } else {
await patchControledMihomoConfig({ tun: { enable } }) await patchControledMihomoConfig({ tun: { enable } })

View File

@ -316,6 +316,11 @@
"tun.notifications.coreAuthSuccess": "Core Authorization Successful", "tun.notifications.coreAuthSuccess": "Core Authorization Successful",
"tun.notifications.firewallResetSuccess": "Firewall Reset Successful", "tun.notifications.firewallResetSuccess": "Firewall Reset Successful",
"tun.error.tunPermissionDenied": "TUN interface start failed, please try to manually grant core permissions", "tun.error.tunPermissionDenied": "TUN interface start failed, please try to manually grant core permissions",
"tun.permissions.required": "TUN mode requires administrator privileges. Restart the application now to get permissions?",
"tun.permissions.failed": "Permission authorization failed",
"tun.permissions.windowsRestart": "On Windows, you need to restart the application as administrator to use TUN mode",
"tun.permissions.requesting": "Requesting administrator privileges, please click 'Yes' in the UAC dialog...",
"tun.permissions.restarting": "Restarting application with administrator privileges, please click 'Yes' in the UAC dialog...",
"dns.title": "DNS Settings", "dns.title": "DNS Settings",
"dns.enable": "Enable DNS", "dns.enable": "Enable DNS",
"dns.enhancedMode.title": "Domain Mapping Mode", "dns.enhancedMode.title": "Domain Mapping Mode",

View File

@ -316,6 +316,11 @@
"tun.notifications.coreAuthSuccess": "内核授权成功", "tun.notifications.coreAuthSuccess": "内核授权成功",
"tun.notifications.firewallResetSuccess": "防火墙重设成功", "tun.notifications.firewallResetSuccess": "防火墙重设成功",
"tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限", "tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限",
"tun.permissions.required": "启用TUN模式需要管理员权限是否现在重启应用获取权限",
"tun.permissions.failed": "权限授权失败",
"tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式",
"tun.permissions.requesting": "正在请求管理员权限请在UAC对话框中点击'是'...",
"tun.permissions.restarting": "正在以管理员权限重启应用请在UAC对话框中点击'是'...",
"dns.title": "DNS 设置", "dns.title": "DNS 设置",
"dns.enable": "启用 DNS", "dns.enable": "启用 DNS",
"dns.enhancedMode.title": "域名映射模式", "dns.enhancedMode.title": "域名映射模式",

View File

@ -3,7 +3,7 @@ import BasePage from '@renderer/components/base/base-page'
import SettingCard from '@renderer/components/base/base-setting-card' import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc' import { grantTunPermissions, restartCore, setupFirewall } from '@renderer/utils/ipc'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import React, { Key, useState } from 'react' import React, { Key, useState } from 'react'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -129,7 +129,7 @@ const Tun: React.FC = () => {
color="primary" color="primary"
onPress={async () => { onPress={async () => {
try { try {
await manualGrantCorePermition() await grantTunPermissions()
new Notification(t('tun.notifications.coreAuthSuccess')) new Notification(t('tun.notifications.coreAuthSuccess'))
await restartCore() await restartCore()
} catch (e) { } catch (e) {

View File

@ -227,6 +227,16 @@ export async function triggerSysProxy(enable: boolean): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable))
} }
export async function checkTunPermissions(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkTunPermissions'))
}
export async function grantTunPermissions(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('grantTunPermissions'))
}
export async function manualGrantCorePermition(): Promise<void> { export async function manualGrantCorePermition(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
} }