mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
Compare commits
11 Commits
4a192586fc
...
d8d79f7b7d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d79f7b7d | ||
|
|
a45bc89b33 | ||
|
|
6bdb133cea | ||
|
|
db605f24fc | ||
|
|
f005a4f4cd | ||
|
|
cb3eedfcb8 | ||
|
|
d030a8722d | ||
|
|
a8f8cd0fd3 | ||
|
|
defcbbca5c | ||
|
|
dbfd25f481 | ||
|
|
eb41bae23b |
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
@ -19,16 +19,31 @@ jobs:
|
|||||||
- name: Delete Dev Release Assets
|
- name: Delete Dev Release Assets
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
# Delete the entire dev release to clean up old assets
|
# Get release ID for dev tag
|
||||||
curl -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"https://api.github.com/repos/${{ github.repository }}/releases/tags/dev" || true
|
"https://api.github.com/repos/${{ github.repository }}/releases/tags/dev" | \
|
||||||
|
jq -r '.id // empty')
|
||||||
|
|
||||||
# Delete the dev tag
|
if [ ! -z "$RELEASE_ID" ]; then
|
||||||
curl -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
echo "Found dev release with ID: $RELEASE_ID"
|
||||||
"https://api.github.com/repos/${{ github.repository }}/git/refs/tags/dev" || true
|
|
||||||
|
# Get all assets and delete them
|
||||||
|
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets" | \
|
||||||
|
jq -r '.[].id' | \
|
||||||
|
while read asset_id; do
|
||||||
|
echo "Deleting asset: $asset_id"
|
||||||
|
curl -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All dev release assets deleted"
|
||||||
|
else
|
||||||
|
echo "No existing dev release found"
|
||||||
|
fi
|
||||||
windows:
|
windows:
|
||||||
needs: [cleanup-dev-release]
|
needs: [cleanup-dev-release]
|
||||||
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
|
if: (startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && always())
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -94,11 +109,12 @@ jobs:
|
|||||||
dist/*portable.7z
|
dist/*portable.7z
|
||||||
body: "Development build from ${{ github.sha }}"
|
body: "Development build from ${{ github.sha }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
draft: false
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
windows7:
|
windows7:
|
||||||
needs: [cleanup-dev-release]
|
needs: [cleanup-dev-release]
|
||||||
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
|
if: (startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && always())
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -165,11 +181,12 @@ jobs:
|
|||||||
dist/*portable.7z
|
dist/*portable.7z
|
||||||
body: "Development build from ${{ github.sha }}"
|
body: "Development build from ${{ github.sha }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
draft: false
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
needs: [cleanup-dev-release]
|
needs: [cleanup-dev-release]
|
||||||
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
|
if: (startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && always())
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -229,11 +246,12 @@ jobs:
|
|||||||
dist/*.rpm
|
dist/*.rpm
|
||||||
body: "Development build from ${{ github.sha }}"
|
body: "Development build from ${{ github.sha }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
draft: false
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
macos:
|
macos:
|
||||||
needs: [cleanup-dev-release]
|
needs: [cleanup-dev-release]
|
||||||
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
|
if: (startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && always())
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -315,11 +333,12 @@ jobs:
|
|||||||
dist/*.pkg
|
dist/*.pkg
|
||||||
body: "Development build from ${{ github.sha }}"
|
body: "Development build from ${{ github.sha }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
draft: false
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
macos10:
|
macos10:
|
||||||
needs: [cleanup-dev-release]
|
needs: [cleanup-dev-release]
|
||||||
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
|
if: (startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && always())
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -403,6 +422,7 @@ jobs:
|
|||||||
dist/*.pkg
|
dist/*.pkg
|
||||||
body: "Development build from ${{ github.sha }}"
|
body: "Development build from ${{ github.sha }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
draft: false
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
updater:
|
updater:
|
||||||
@ -438,6 +458,7 @@ jobs:
|
|||||||
files: latest.yml
|
files: latest.yml
|
||||||
body: "Development build updater from ${{ github.sha }}"
|
body: "Development build updater from ${{ github.sha }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
draft: false
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
aur-release-updater:
|
aur-release-updater:
|
||||||
|
|||||||
44
changelog.md
44
changelog.md
@ -1,3 +1,14 @@
|
|||||||
|
## 1.8.4
|
||||||
|
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- 如果当前没有以管理员模式运行,TUN 开关保持关闭
|
||||||
|
- 区分 app 日志与 core 日志输出为不同文件
|
||||||
|
- 完善内核权限鉴别,上一个内核以管理员模式启动的时候,弹出提示
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 修复某些系统下的悬浮窗开启崩溃的问题(开启兼容模式=关闭硬件加速)
|
||||||
|
- 开机自启在非管理员模式下报错的问题
|
||||||
|
|
||||||
## 1.8.3
|
## 1.8.3
|
||||||
**本次更新移除了 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件,安全性更好,更灵活,给无法使用管理员模式运行软件的企业用户提供了更大的便利**
|
**本次更新移除了 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件,安全性更好,更灵活,给无法使用管理员模式运行软件的企业用户提供了更大的便利**
|
||||||
|
|
||||||
@ -18,35 +29,4 @@
|
|||||||
### 新功能 (Feat)
|
### 新功能 (Feat)
|
||||||
- 重构 域名嗅探 卡片模块,改为“覆写”逻辑,当开关打开后,使用 嗅探覆写 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置
|
- 重构 域名嗅探 卡片模块,改为“覆写”逻辑,当开关打开后,使用 嗅探覆写 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置
|
||||||
- 订阅/覆写卡片可右键呼出菜单
|
- 订阅/覆写卡片可右键呼出菜单
|
||||||
- MacOS 下“轻触(tap)”触控板可进行开关操作(之前必须“按下(click)”)
|
- MacOS 下“轻触(tap)”触控板可进行开关操作(之前必须“按下(click)”)
|
||||||
|
|
||||||
### 修复 (Fix)
|
|
||||||
- **因多国语言带来的在 Windows 下首次安装无法启动的问题**
|
|
||||||
- 1.8.1升级依赖导致的节点圆角显示失效
|
|
||||||
- DNS 覆写模块的逻辑冲突
|
|
||||||
- 点击订阅卡片功能区导致的选中订阅问题
|
|
||||||
- 覆写卡片可以双击编辑
|
|
||||||
- 因代码不规范导致的控制台警告
|
|
||||||
- Linux 下没有设置 Smart 内核权限导致的“外部控制监听错误”
|
|
||||||
|
|
||||||
## 1.8.1
|
|
||||||
|
|
||||||
### 新功能 (Feat)
|
|
||||||
- 重构 DNS 卡片模块,改为“覆写”逻辑,当开关打开后,使用DNS 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置
|
|
||||||
|
|
||||||
### 性能提升(Perf)
|
|
||||||
- 更新依赖,提升页面响应性
|
|
||||||
- 优化订阅切换逻辑,大幅提升切换速度和稳定性
|
|
||||||
|
|
||||||
### 修复 (Fix)
|
|
||||||
- “使用自动 Smart 规则覆写”没有覆盖兜底的 MATCH 规则
|
|
||||||
- 移除默认的 Smart "policy-priority" 规则
|
|
||||||
-
|
|
||||||
|
|
||||||
## 1.8.0
|
|
||||||
|
|
||||||
### 新功能 (Feat)
|
|
||||||
**重大更新:本次更新增加了 Smart Core,可以根据用户使用习惯和节点质量自动选择符合您的最优节点。并内置了“一键开启”,适合不想折腾自定义规则的用户
|
|
||||||
“一键开启”内置 Smart规则的功能在“内核设置”下的“使用自动 Smart 规则覆写”,原理:当开关开启后,自动载入覆写脚本,新增 Smart Group,并替换当前配置文件下的默认出站规则为"Smart Group",您的所有代理流量都将从此分组下的节点流出。如果使用“全局模式”请选择名称为"Smart Group"的节点,以使用该功能。**
|
|
||||||
|
|
||||||
注意:本功能还在测试中,如遇到问题请发 issue 反馈
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mihomo-party",
|
"name": "mihomo-party",
|
||||||
"version": "1.8.3",
|
"version": "1.8.4",
|
||||||
"description": "Mihomo Party",
|
"description": "Mihomo Party",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "mihomo-party-org",
|
"author": "mihomo-party-org",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { getAppConfig } from './app'
|
import { getAppConfig } from './app'
|
||||||
import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override'
|
import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override'
|
||||||
|
import { overrideLogger } from '../utils/logger'
|
||||||
|
|
||||||
const SMART_OVERRIDE_ID = 'smart-core-override'
|
const SMART_OVERRIDE_ID = 'smart-core-override'
|
||||||
|
|
||||||
@ -237,7 +238,7 @@ export async function createSmartOverride(): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create Smart override:', error)
|
await overrideLogger.error('Failed to create Smart override', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +253,7 @@ export async function removeSmartOverride(): Promise<void> {
|
|||||||
await removeOverrideItem(SMART_OVERRIDE_ID)
|
await removeOverrideItem(SMART_OVERRIDE_ID)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to remove Smart override:', error)
|
await overrideLogger.error('Failed to remove Smart override', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ChildProcess, exec, execFile, spawn } from 'child_process'
|
import { ChildProcess, exec, execFile, spawn } from 'child_process'
|
||||||
import {
|
import {
|
||||||
dataDir,
|
dataDir,
|
||||||
logPath,
|
coreLogPath,
|
||||||
mihomoCoreDir,
|
mihomoCoreDir,
|
||||||
mihomoCorePath,
|
mihomoCorePath,
|
||||||
mihomoProfileWorkDir,
|
mihomoProfileWorkDir,
|
||||||
@ -41,6 +41,7 @@ import { uploadRuntimeConfig } from '../resolve/gistApi'
|
|||||||
import { startMonitor } from '../resolve/trafficMonitor'
|
import { startMonitor } from '../resolve/trafficMonitor'
|
||||||
import { safeShowErrorBox } from '../utils/init'
|
import { safeShowErrorBox } from '../utils/init'
|
||||||
import i18next from '../../shared/i18n'
|
import i18next from '../../shared/i18n'
|
||||||
|
import { managerLogger } from '../utils/logger'
|
||||||
|
|
||||||
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
|
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
|
||||||
try {
|
try {
|
||||||
@ -96,13 +97,12 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
try {
|
try {
|
||||||
await setPublicDNS()
|
await setPublicDNS()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await writeFile(logPath(), `[Manager]: set dns failed, ${error}`, {
|
await managerLogger.error('set dns failed', error)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stdout = createWriteStream(logPath(), { flags: 'a' })
|
// 内核日志输出到独立的 core-日期.log 文件
|
||||||
const stderr = createWriteStream(logPath(), { flags: 'a' })
|
const stdout = createWriteStream(coreLogPath(), { flags: 'a' })
|
||||||
|
const stderr = createWriteStream(coreLogPath(), { flags: 'a' })
|
||||||
const env = {
|
const env = {
|
||||||
DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector),
|
DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector),
|
||||||
DISABLE_EMBED_CA: String(disableEmbedCA),
|
DISABLE_EMBED_CA: String(disableEmbedCA),
|
||||||
@ -128,11 +128,9 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
child.on('close', async (code, signal) => {
|
child.on('close', async (code, signal) => {
|
||||||
await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, {
|
await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
if (retry) {
|
if (retry) {
|
||||||
await writeFile(logPath(), `[Manager]: Try Restart Core\n`, { flag: 'a' })
|
await managerLogger.info('Try Restart Core')
|
||||||
retry--
|
retry--
|
||||||
await restartCore()
|
await restartCore()
|
||||||
} else {
|
} else {
|
||||||
@ -194,9 +192,7 @@ export async function stopCore(force = false): Promise<void> {
|
|||||||
await recoverDNS()
|
await recoverDNS()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await writeFile(logPath(), `[Manager]: recover dns failed, ${error}`, {
|
await managerLogger.error('recover dns failed', error)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
@ -214,9 +210,7 @@ export async function restartCore(): Promise<void> {
|
|||||||
await startCore()
|
await startCore()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 记录错误到日志而不是显示阻塞对话框
|
// 记录错误到日志而不是显示阻塞对话框
|
||||||
await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, {
|
await managerLogger.error('restart core failed', e)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
// 重新抛出错误,让调用者处理
|
// 重新抛出错误,让调用者处理
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@ -260,12 +254,12 @@ async function checkProfile(): Promise<void> {
|
|||||||
mihomoTestDir()
|
mihomoTestDir()
|
||||||
], { env })
|
], { env })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Profile check failed:', error)
|
await managerLogger.error('Profile check failed', error)
|
||||||
|
|
||||||
if (error instanceof Error && 'stdout' in error) {
|
if (error instanceof Error && 'stdout' in error) {
|
||||||
const { stdout, stderr } = error as { stdout: string; stderr?: string }
|
const { stdout, stderr } = error as { stdout: string; stderr?: string }
|
||||||
console.log('Profile check stdout:', stdout)
|
await managerLogger.info('Profile check stdout', stdout)
|
||||||
console.log('Profile check stderr:', stderr)
|
await managerLogger.info('Profile check stderr', stderr)
|
||||||
|
|
||||||
const errorLines = stdout
|
const errorLines = stdout
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@ -356,14 +350,59 @@ export async function checkAdminPrivileges(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restartAsAdmin(): Promise<void> {
|
// TUN 权限确认框
|
||||||
|
export async function showTunPermissionDialog(): Promise<boolean> {
|
||||||
|
const { dialog } = await import('electron')
|
||||||
|
const i18next = await import('i18next')
|
||||||
|
|
||||||
|
await managerLogger.info('Preparing TUN permission dialog...')
|
||||||
|
await managerLogger.info(`i18next available: ${typeof i18next.t === 'function'}`)
|
||||||
|
|
||||||
|
const title = i18next.t('tun.permissions.title') || '需要管理员权限'
|
||||||
|
const message = i18next.t('tun.permissions.message') || '启用TUN模式需要管理员权限,是否现在重启应用获取权限?'
|
||||||
|
const confirmText = i18next.t('common.confirm') || '确认'
|
||||||
|
const cancelText = i18next.t('common.cancel') || '取消'
|
||||||
|
|
||||||
|
await managerLogger.info(`Dialog texts - Title: "${title}", Message: "${message}", Confirm: "${confirmText}", Cancel: "${cancelText}"`)
|
||||||
|
|
||||||
|
const choice = dialog.showMessageBoxSync({
|
||||||
|
type: 'warning',
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
buttons: [confirmText, cancelText],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
await managerLogger.info(`TUN permission dialog choice: ${choice}`)
|
||||||
|
|
||||||
|
return choice === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误显示框
|
||||||
|
export async function showErrorDialog(title: string, message: string): Promise<void> {
|
||||||
|
const { dialog } = await import('electron')
|
||||||
|
const i18next = await import('i18next')
|
||||||
|
|
||||||
|
const okText = i18next.t('common.confirm') || '确认'
|
||||||
|
|
||||||
|
dialog.showMessageBoxSync({
|
||||||
|
type: 'error',
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
buttons: [okText],
|
||||||
|
defaultId: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartAsAdmin(forTun: boolean = true): Promise<void> {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
throw new Error('This function is only available on Windows')
|
throw new Error('This function is only available on Windows')
|
||||||
}
|
}
|
||||||
|
|
||||||
const exePath = process.execPath
|
const exePath = process.execPath
|
||||||
const args = process.argv.slice(1)
|
const args = process.argv.slice(1)
|
||||||
const restartArgs = [...args, '--admin-restart-for-tun']
|
const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 处理路径和参数的引号
|
// 处理路径和参数的引号
|
||||||
@ -377,15 +416,15 @@ export async function restartAsAdmin(): Promise<void> {
|
|||||||
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Restarting as administrator with command:', command)
|
await managerLogger.info('Restarting as administrator with command', command)
|
||||||
|
|
||||||
// 执行PowerShell命令
|
// 执行PowerShell命令
|
||||||
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
|
exec(command, { windowsHide: true }, async (error, _stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('PowerShell execution error:', error)
|
await managerLogger.error('PowerShell execution error', error)
|
||||||
console.error('stderr:', stderr)
|
await managerLogger.error('stderr', stderr)
|
||||||
} else {
|
} else {
|
||||||
console.log('PowerShell command executed successfully')
|
await managerLogger.info('PowerShell command executed successfully')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -394,13 +433,11 @@ export async function restartAsAdmin(): Promise<void> {
|
|||||||
const { app } = await import('electron')
|
const { app } = await import('electron')
|
||||||
app.quit()
|
app.quit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restart as administrator:', error)
|
await managerLogger.error('Failed to restart as administrator', error)
|
||||||
throw new Error(`Failed to restart as administrator: ${error}`)
|
throw new Error(`Failed to restart as administrator: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function checkMihomoCorePermissions(): Promise<boolean> {
|
export async function checkMihomoCorePermissions(): Promise<boolean> {
|
||||||
const { core = 'mihomo' } = await getAppConfig()
|
const { core = 'mihomo' } = await getAppConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
@ -423,6 +460,142 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测高权限内核
|
||||||
|
export async function checkHighPrivilegeCore(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const { core = 'mihomo' } = await getAppConfig()
|
||||||
|
const corePath = mihomoCorePath(core)
|
||||||
|
|
||||||
|
await managerLogger.info(`Checking high privilege core: ${corePath}`)
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const { existsSync } = await import('fs')
|
||||||
|
if (!existsSync(corePath)) {
|
||||||
|
await managerLogger.info('Core file does not exist')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHighPrivilegeProcess = await checkHighPrivilegeMihomoProcess()
|
||||||
|
if (hasHighPrivilegeProcess) {
|
||||||
|
await managerLogger.info('Found high privilege mihomo process running')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = await checkAdminPrivileges()
|
||||||
|
await managerLogger.info(`Current process admin privileges: ${isAdmin}`)
|
||||||
|
return isAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||||
|
const { stat, existsSync } = await import('fs')
|
||||||
|
const { promisify } = await import('util')
|
||||||
|
const statAsync = promisify(stat)
|
||||||
|
|
||||||
|
if (!existsSync(corePath)) {
|
||||||
|
await managerLogger.info('Core file does not exist')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await statAsync(corePath)
|
||||||
|
const hasSetuid = (stats.mode & 0o4000) !== 0
|
||||||
|
const isOwnedByRoot = stats.uid === 0
|
||||||
|
|
||||||
|
await managerLogger.info(`Core file stats - setuid: ${hasSetuid}, owned by root: ${isOwnedByRoot}, mode: ${stats.mode.toString(8)}`)
|
||||||
|
|
||||||
|
return hasSetuid && isOwnedByRoot
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await managerLogger.error('Failed to check high privilege core', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
|
const mihomoExecutables = ['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe']
|
||||||
|
|
||||||
|
for (const executable of mihomoExecutables) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execPromise(`tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`)
|
||||||
|
const lines = stdout.split('\n').filter(line => line.includes(executable))
|
||||||
|
|
||||||
|
if (lines.length > 0) {
|
||||||
|
await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split(',')
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const pid = parts[1].replace(/"/g, '').trim()
|
||||||
|
try {
|
||||||
|
const { stdout: processInfo } = await execPromise(`wmic process where "ProcessId=${pid}" get Name,ProcessId,ExecutablePath,CommandLine /format:csv`)
|
||||||
|
await managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`)
|
||||||
|
|
||||||
|
if (processInfo.includes('mihomo')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await managerLogger.error(`Failed to check ${executable} processes`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mihomoExecutables = ['mihomo', 'mihomo-alpha', 'mihomo-smart']
|
||||||
|
let foundProcesses = false
|
||||||
|
|
||||||
|
for (const executable of mihomoExecutables) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`)
|
||||||
|
const lines = stdout.split('\n').filter(line => line.trim() && line.includes(executable))
|
||||||
|
|
||||||
|
if (lines.length > 0) {
|
||||||
|
foundProcesses = true
|
||||||
|
await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.trim().split(/\s+/)
|
||||||
|
if (parts.length >= 1) {
|
||||||
|
const user = parts[0]
|
||||||
|
await managerLogger.info(`${executable} process running as user: ${user}`)
|
||||||
|
|
||||||
|
if (user === 'root') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundProcesses) {
|
||||||
|
await managerLogger.info('No mihomo processes found running')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await managerLogger.error('Failed to check mihomo processes on Unix', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await managerLogger.error('Failed to check high privilege mihomo process', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// TUN模式获取权限
|
// TUN模式获取权限
|
||||||
export async function requestTunPermissions(): Promise<void> {
|
export async function requestTunPermissions(): Promise<void> {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@ -437,7 +610,7 @@ export async function requestTunPermissions(): Promise<void> {
|
|||||||
|
|
||||||
export async function checkAdminRestartForTun(): Promise<void> {
|
export async function checkAdminRestartForTun(): Promise<void> {
|
||||||
if (process.argv.includes('--admin-restart-for-tun')) {
|
if (process.argv.includes('--admin-restart-for-tun')) {
|
||||||
console.log('Detected admin restart for TUN mode, auto-enabling TUN...')
|
await managerLogger.info('Detected admin restart for TUN mode, auto-enabling TUN...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@ -446,20 +619,20 @@ export async function checkAdminRestartForTun(): Promise<void> {
|
|||||||
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
|
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
|
||||||
await restartCore()
|
await restartCore()
|
||||||
|
|
||||||
console.log('TUN mode auto-enabled after admin restart')
|
await managerLogger.info('TUN mode auto-enabled after admin restart')
|
||||||
|
|
||||||
const { mainWindow } = await import('../index')
|
const { mainWindow } = await import('../index')
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
} else {
|
} else {
|
||||||
console.warn('Admin restart detected but no admin privileges found')
|
await managerLogger.warn('Admin restart detected but no admin privileges found')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to auto-enable TUN after admin restart:', error)
|
await managerLogger.error('Failed to auto-enable TUN after admin restart', error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 检查TUN配置与权限的匹配
|
// 检查TUN配置与权限的匹配,但不自动开启 TUN
|
||||||
await validateTunPermissionsOnStartup()
|
await validateTunPermissionsOnStartup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,7 +648,7 @@ export async function validateTunPermissionsOnStartup(): Promise<void> {
|
|||||||
const hasPermissions = await checkMihomoCorePermissions()
|
const hasPermissions = await checkMihomoCorePermissions()
|
||||||
|
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
console.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
|
await managerLogger.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
|
||||||
|
|
||||||
await patchControledMihomoConfig({ tun: { enable: false } })
|
await patchControledMihomoConfig({ tun: { enable: false } })
|
||||||
|
|
||||||
@ -483,12 +656,12 @@ export async function validateTunPermissionsOnStartup(): Promise<void> {
|
|||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
|
|
||||||
console.log('TUN auto-disabled due to insufficient permissions')
|
await managerLogger.info('TUN auto-disabled due to insufficient permissions')
|
||||||
} else {
|
} else {
|
||||||
console.log('TUN permissions validated successfully')
|
await managerLogger.info('TUN permissions validated successfully')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to validate TUN permissions on startup:', error)
|
await managerLogger.error('Failed to validate TUN permissions on startup', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,11 @@ 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, checkAdminRestartForTun } from './core/manager'
|
import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun, checkHighPrivilegeCore, restartAsAdmin } 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, initBasic } from './utils/init'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { initShortcut } from './resolve/shortcut'
|
import { initShortcut } from './resolve/shortcut'
|
||||||
import { spawn, exec } from 'child_process'
|
import { spawn, exec } from 'child_process'
|
||||||
@ -20,6 +20,7 @@ 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'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
|
import { logger } from './utils/logger'
|
||||||
|
|
||||||
// 错误处理
|
// 错误处理
|
||||||
function showSafeErrorBox(titleKey: string, message: string): void {
|
function showSafeErrorBox(titleKey: string, message: string): void {
|
||||||
@ -112,7 +113,63 @@ if (process.platform === 'win32' && !exePath().startsWith('C')) {
|
|||||||
app.commandLine.appendSwitch('in-process-gpu')
|
app.commandLine.appendSwitch('in-process-gpu')
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPromise = init()
|
// 内核检测
|
||||||
|
async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await initBasic()
|
||||||
|
|
||||||
|
// 应用管理员权限运行,跳过检测
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const { checkAdminPrivileges } = await import('./core/manager')
|
||||||
|
const isCurrentAppAdmin = await checkAdminPrivileges()
|
||||||
|
|
||||||
|
if (isCurrentAppAdmin) {
|
||||||
|
console.log('Current app is running as administrator, skipping privilege check')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||||
|
if (process.getuid && process.getuid() === 0) {
|
||||||
|
console.log('Current app is running as root, skipping privilege check')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
|
||||||
|
if (hasHighPrivilegeCore) {
|
||||||
|
try {
|
||||||
|
const appConfig = await getAppConfig()
|
||||||
|
const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
||||||
|
await initI18n({ lng: language })
|
||||||
|
} catch {
|
||||||
|
await initI18n({ lng: 'zh-CN' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const choice = dialog.showMessageBoxSync({
|
||||||
|
type: 'warning',
|
||||||
|
title: i18next.t('core.highPrivilege.title'),
|
||||||
|
message: i18next.t('core.highPrivilege.message'),
|
||||||
|
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (choice === 0) {
|
||||||
|
try {
|
||||||
|
// 非TUN重启
|
||||||
|
await restartAsAdmin(false)
|
||||||
|
process.exit(0)
|
||||||
|
} catch (error) {
|
||||||
|
showSafeErrorBox('common.error.adminRequired', `${error}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to check high privilege core:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.on('second-instance', async (_event, commandline) => {
|
app.on('second-instance', async (_event, commandline) => {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
@ -153,9 +210,10 @@ app.whenReady().then(async () => {
|
|||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('party.mihomo.app')
|
electronApp.setAppUserModelId('party.mihomo.app')
|
||||||
|
|
||||||
|
await checkHighPrivilegeCoreEarly()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 首先等待初始化完成,确保配置文件和目录都已创建
|
await init()
|
||||||
await initPromise
|
|
||||||
|
|
||||||
const appConfig = await getAppConfig()
|
const appConfig = await getAppConfig()
|
||||||
// 如果配置中没有语言设置,则使用系统语言
|
// 如果配置中没有语言设置,则使用系统语言
|
||||||
@ -169,6 +227,7 @@ app.whenReady().then(async () => {
|
|||||||
showSafeErrorBox('common.error.initFailed', `${e}`)
|
showSafeErrorBox('common.error.initFailed', `${e}`)
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [startPromise] = await startCore()
|
const [startPromise] = await startCore()
|
||||||
startPromise.then(async () => {
|
startPromise.then(async () => {
|
||||||
@ -198,7 +257,7 @@ app.whenReady().then(async () => {
|
|||||||
try {
|
try {
|
||||||
await showFloatingWindow()
|
await showFloatingWindow()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create floating window on startup:', error)
|
await logger.error('Failed to create floating window on startup', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!disableTray) {
|
if (!disableTray) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
subStoreDir,
|
subStoreDir,
|
||||||
themesDir
|
themesDir
|
||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
|
import { systemLogger } from '../utils/logger'
|
||||||
|
|
||||||
export async function webdavBackup(): Promise<boolean> {
|
export async function webdavBackup(): Promise<boolean> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
@ -75,7 +76,7 @@ export async function webdavBackup(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clean up old backup files:', error)
|
await systemLogger.error('Failed to clean up old backup files', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,15 +5,23 @@ import { join } from 'path'
|
|||||||
import { getAppConfig, patchAppConfig } from '../config'
|
import { getAppConfig, patchAppConfig } from '../config'
|
||||||
import { applyTheme } from './theme'
|
import { applyTheme } from './theme'
|
||||||
import { buildContextMenu, showTrayIcon } from './tray'
|
import { buildContextMenu, showTrayIcon } from './tray'
|
||||||
|
import { floatingWindowLogger } from '../utils/logger'
|
||||||
|
|
||||||
export let floatingWindow: BrowserWindow | null = null
|
export let floatingWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
function logError(message: string, error?: any): void {
|
||||||
|
floatingWindowLogger.log(`FloatingWindow Error: ${message}`, error).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
async function createFloatingWindow(): Promise<void> {
|
async function createFloatingWindow(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const floatingWindowState = windowStateKeeper({
|
const floatingWindowState = windowStateKeeper({ file: 'floating-window-state.json' })
|
||||||
file: 'floating-window-state.json'
|
const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig()
|
||||||
})
|
|
||||||
const { customTheme = 'default.css' } = await getAppConfig()
|
const safeMode = process.env.FLOATING_SAFE_MODE === 'true'
|
||||||
|
const useCompatMode = floatingWindowCompatMode ||
|
||||||
|
process.env.FLOATING_COMPAT_MODE === 'true' ||
|
||||||
|
safeMode
|
||||||
|
|
||||||
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||||||
width: 120,
|
width: 120,
|
||||||
@ -21,16 +29,16 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
x: floatingWindowState.x,
|
x: floatingWindowState.x,
|
||||||
y: floatingWindowState.y,
|
y: floatingWindowState.y,
|
||||||
show: false,
|
show: false,
|
||||||
frame: false,
|
frame: safeMode,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: !safeMode,
|
||||||
resizable: false,
|
resizable: safeMode,
|
||||||
transparent: true,
|
transparent: !safeMode && !useCompatMode,
|
||||||
skipTaskbar: true,
|
skipTaskbar: !safeMode,
|
||||||
minimizable: false,
|
minimizable: safeMode,
|
||||||
maximizable: false,
|
maximizable: safeMode,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
closable: false,
|
closable: safeMode,
|
||||||
backgroundColor: '#00000000',
|
backgroundColor: safeMode ? '#ffffff' : (useCompatMode ? '#f0f0f0' : '#00000000'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
@ -40,47 +48,46 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// windows 添加兼容性处理
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
windowOptions.hasShadow = false
|
windowOptions.hasShadow = !safeMode
|
||||||
windowOptions.webPreferences!.offscreen = false
|
windowOptions.webPreferences!.offscreen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
floatingWindow = new BrowserWindow(windowOptions)
|
floatingWindow = new BrowserWindow(windowOptions)
|
||||||
floatingWindowState.manage(floatingWindow)
|
floatingWindowState.manage(floatingWindow)
|
||||||
|
|
||||||
|
// 事件监听器
|
||||||
floatingWindow.webContents.on('render-process-gone', (_, details) => {
|
floatingWindow.webContents.on('render-process-gone', (_, details) => {
|
||||||
console.error('Floating window render process gone:', details.reason)
|
logError('Render process gone', details.reason)
|
||||||
floatingWindow = null
|
floatingWindow = null
|
||||||
})
|
})
|
||||||
|
|
||||||
floatingWindow.on('ready-to-show', () => {
|
floatingWindow.on('ready-to-show', () => {
|
||||||
try {
|
applyTheme(customTheme)
|
||||||
applyTheme(customTheme)
|
floatingWindow?.show()
|
||||||
floatingWindow?.show()
|
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
|
||||||
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in floating window ready-to-show:', error)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
floatingWindow.on('moved', () => {
|
floatingWindow.on('moved', () => {
|
||||||
if (floatingWindow) floatingWindowState.saveState(floatingWindow)
|
floatingWindow && floatingWindowState.saveState(floatingWindow)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// IPC 监听器
|
||||||
ipcMain.on('updateFloatingWindow', () => {
|
ipcMain.on('updateFloatingWindow', () => {
|
||||||
if (floatingWindow) {
|
if (floatingWindow) {
|
||||||
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
|
floatingWindow.webContents.send('controledMihomoConfigUpdated')
|
||||||
floatingWindow?.webContents.send('appConfigUpdated')
|
floatingWindow.webContents.send('appConfigUpdated')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
// 加载页面
|
||||||
await floatingWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/floating.html`)
|
const url = is.dev && process.env['ELECTRON_RENDERER_URL']
|
||||||
} else {
|
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
||||||
await floatingWindow.loadFile(join(__dirname, '../renderer/floating.html'))
|
: join(__dirname, '../renderer/floating.html')
|
||||||
}
|
|
||||||
|
is.dev ? await floatingWindow.loadURL(url) : await floatingWindow.loadFile(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create floating window:', error)
|
logError('Failed to create floating window', error)
|
||||||
floatingWindow = null
|
floatingWindow = null
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@ -94,8 +101,15 @@ export async function showFloatingWindow(): Promise<void> {
|
|||||||
await createFloatingWindow()
|
await createFloatingWindow()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to show floating window:', error)
|
logError('Failed to show floating window', error)
|
||||||
await patchAppConfig({ showFloatingWindow: false })
|
|
||||||
|
// 如果已经是兼容模式还是崩溃,自动禁用悬浮窗
|
||||||
|
const { floatingWindowCompatMode = true } = await getAppConfig()
|
||||||
|
if (floatingWindowCompatMode) {
|
||||||
|
await patchAppConfig({ showFloatingWindow: false })
|
||||||
|
} else {
|
||||||
|
await patchAppConfig({ floatingWindowCompatMode: true })
|
||||||
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { nativeImage } from 'electron'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
|
import { systemLogger } from '../utils/logger'
|
||||||
|
|
||||||
export let pacPort: number
|
export let pacPort: number
|
||||||
export let subStorePort: number
|
export let subStorePort: number
|
||||||
@ -168,7 +169,6 @@ export async function downloadSubStore(): Promise<void> {
|
|||||||
)
|
)
|
||||||
await writeFile(tempBackendPath, Buffer.from(backendRes.data))
|
await writeFile(tempBackendPath, Buffer.from(backendRes.data))
|
||||||
// 下载前端文件
|
// 下载前端文件
|
||||||
const tempFrontendDir = path.join(tempDir, 'dist')
|
|
||||||
const frontendRes = await axios.get(
|
const frontendRes = await axios.get(
|
||||||
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
|
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
|
||||||
{
|
{
|
||||||
@ -192,7 +192,7 @@ export async function downloadSubStore(): Promise<void> {
|
|||||||
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
||||||
await rm(tempDir, { recursive: true })
|
await rm(tempDir, { recursive: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('substore.downloadFailed:', error)
|
await systemLogger.error('substore.downloadFailed', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,14 +22,15 @@ import { triggerSysProxy } from '../sys/sysproxy'
|
|||||||
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } 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'
|
||||||
|
import { trayLogger } from '../utils/logger'
|
||||||
|
|
||||||
export let tray: Tray | null = null
|
export let tray: Tray | null = null
|
||||||
|
|
||||||
export const buildContextMenu = async (): Promise<Menu> => {
|
export const buildContextMenu = async (): Promise<Menu> => {
|
||||||
// 添加调试日志
|
// 添加调试日志
|
||||||
console.log('Current translation for tray.showWindow:', t('tray.showWindow'))
|
await trayLogger.debug('Current translation for tray.showWindow', t('tray.showWindow'))
|
||||||
console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow'))
|
await trayLogger.debug('Current translation for tray.hideFloatingWindow', t('tray.hideFloatingWindow'))
|
||||||
console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow'))
|
await trayLogger.debug('Current translation for tray.showFloatingWindow', t('tray.showFloatingWindow'))
|
||||||
|
|
||||||
const { mode, tun } = await getControledMihomoConfig()
|
const { mode, tun } = await getControledMihomoConfig()
|
||||||
const {
|
const {
|
||||||
@ -187,7 +188,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
try {
|
try {
|
||||||
await restartAsAdmin()
|
await restartAsAdmin()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.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
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
return
|
return
|
||||||
@ -196,7 +197,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
try {
|
try {
|
||||||
await requestTunPermissions()
|
await requestTunPermissions()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to grant TUN permissions from tray:', error)
|
await trayLogger.error('Failed to grant TUN permissions from tray', error)
|
||||||
item.checked = false
|
item.checked = false
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
return
|
return
|
||||||
@ -204,7 +205,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Permission check failed in tray:', error)
|
await trayLogger.warn('Permission check failed in tray', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
||||||
@ -418,13 +419,13 @@ export async function closeTrayIcon(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showDockIcon(): Promise<void> {
|
export async function showDockIcon(): Promise<void> {
|
||||||
if (process.platform === 'darwin' && !app.dock.isVisible()) {
|
if (process.platform === 'darwin' && app.dock && !app.dock.isVisible()) {
|
||||||
await app.dock.show()
|
await app.dock.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hideDockIcon(): Promise<void> {
|
export async function hideDockIcon(): Promise<void> {
|
||||||
if (process.platform === 'darwin' && app.dock.isVisible()) {
|
if (process.platform === 'darwin' && app.dock && app.dock.isVisible()) {
|
||||||
app.dock.hide()
|
app.dock.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export async function enableAutoRun(): Promise<void> {
|
|||||||
const taskFilePath = path.join(tmpdir(), `${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`
|
`powershell Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f'`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
@ -121,7 +121,7 @@ Categories=Utility;
|
|||||||
export async function disableAutoRun(): Promise<void> {
|
export async function disableAutoRun(): Promise<void> {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
await execPromise(`%SystemRoot%\\System32\\schtasks.exe /delete /tn "${appName}" /f`)
|
await execPromise(`powershell Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f'`)
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { resourcesFilesDir } from '../utils/dirs'
|
|||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import { proxyLogger } from '../utils/logger'
|
||||||
|
|
||||||
let defaultBypass: string[]
|
let defaultBypass: string[]
|
||||||
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
||||||
@ -175,7 +176,7 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
// Wait a bit for socket recreation
|
// Wait a bit for socket recreation
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to send signal to helper:', error)
|
await proxyLogger.error('Failed to send signal to helper', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,16 +198,16 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
|||||||
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||||
(error as Error).message?.includes('ENOENT'))) {
|
(error as Error).message?.includes('ENOENT'))) {
|
||||||
|
|
||||||
console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
|
await proxyLogger.info(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
|
||||||
|
|
||||||
if (!isSocketFileExists()) {
|
if (!isSocketFileExists()) {
|
||||||
console.log('Socket file missing, requesting recreation...')
|
await proxyLogger.info('Socket file missing, requesting recreation...')
|
||||||
try {
|
try {
|
||||||
await requestSocketRecreation()
|
await requestSocketRecreation()
|
||||||
console.log('Socket recreation requested, retrying...')
|
await proxyLogger.info('Socket recreation requested, retrying...')
|
||||||
continue
|
continue
|
||||||
} catch (signalError) {
|
} catch (signalError) {
|
||||||
console.log('Failed to request socket recreation:', signalError)
|
await proxyLogger.warn('Failed to request socket recreation', signalError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,12 +134,27 @@ export function logDir(): string {
|
|||||||
|
|
||||||
export function logPath(): string {
|
export function logPath(): string {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const name = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const name = `mihomo-party-${year}-${month}-${day}`
|
||||||
return path.join(logDir(), `${name}.log`)
|
return path.join(logDir(), `${name}.log`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function substoreLogPath(): string {
|
export function substoreLogPath(): string {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const name = `sub-store-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const name = `sub-store-${year}-${month}-${day}`
|
||||||
|
return path.join(logDir(), `${name}.log`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coreLogPath(): string {
|
||||||
|
const date = new Date()
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const name = `core-${year}-${month}-${day}`
|
||||||
return path.join(logDir(), `${name}.log`)
|
return path.join(logDir(), `${name}.log`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import {
|
|||||||
import { app, dialog } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
import { startSSIDCheck } from '../sys/ssid'
|
import { startSSIDCheck } from '../sys/ssid'
|
||||||
import i18next from '../../shared/i18n'
|
import i18next from '../../shared/i18n'
|
||||||
|
import { initLogger } from './logger'
|
||||||
|
|
||||||
// 安全错误处理
|
// 安全错误处理
|
||||||
export function safeShowErrorBox(titleKey: string, message: string): void {
|
export function safeShowErrorBox(titleKey: string, message: string): void {
|
||||||
@ -115,7 +116,7 @@ async function initDirs(): Promise<void> {
|
|||||||
await mkdir(dir, { recursive: true })
|
await mkdir(dir, { recursive: true })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to create directory ${dir}:`, error)
|
await initLogger.error(`Failed to create directory ${dir}`, error)
|
||||||
throw new Error(`Failed to create directory ${dir}: ${error}`)
|
throw new Error(`Failed to create directory ${dir}: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +137,7 @@ async function initConfig(): Promise<void> {
|
|||||||
await writeFile(config.path, yaml.stringify(config.content))
|
await writeFile(config.path, yaml.stringify(config.content))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to create ${config.name} at ${config.path}:`, error)
|
await initLogger.error(`Failed to create ${config.name} at ${config.path}`, error)
|
||||||
throw new Error(`Failed to create ${config.name}: ${error}`)
|
throw new Error(`Failed to create ${config.name}: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +164,7 @@ async function initFiles(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to copy ${file}:`, error)
|
await initLogger.error(`Failed to copy ${file}`, error)
|
||||||
if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) {
|
if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) {
|
||||||
throw new Error(`Failed to copy critical file ${file}: ${error}`)
|
throw new Error(`Failed to copy critical file ${file}: ${error}`)
|
||||||
}
|
}
|
||||||
@ -317,12 +318,17 @@ function initDeeplink(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function init(): Promise<void> {
|
// 基础初始化
|
||||||
|
export async function initBasic(): Promise<void> {
|
||||||
await initDirs()
|
await initDirs()
|
||||||
await initConfig()
|
await initConfig()
|
||||||
await migration()
|
await migration()
|
||||||
await initFiles()
|
await initFiles()
|
||||||
await cleanup()
|
await cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function init(): Promise<void> {
|
||||||
|
await initBasic()
|
||||||
await startSubStoreFrontendServer()
|
await startSubStoreFrontendServer()
|
||||||
await startSubStoreBackendServer()
|
await startSubStoreBackendServer()
|
||||||
const { sysProxy } = await getAppConfig()
|
const { sysProxy } = await getAppConfig()
|
||||||
|
|||||||
@ -65,7 +65,10 @@ import {
|
|||||||
checkAdminPrivileges,
|
checkAdminPrivileges,
|
||||||
restartAsAdmin,
|
restartAsAdmin,
|
||||||
checkMihomoCorePermissions,
|
checkMihomoCorePermissions,
|
||||||
requestTunPermissions
|
requestTunPermissions,
|
||||||
|
checkHighPrivilegeCore,
|
||||||
|
showTunPermissionDialog,
|
||||||
|
showErrorDialog
|
||||||
} from '../core/manager'
|
} 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'
|
||||||
@ -200,6 +203,9 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('restartAsAdmin', () => ipcErrorWrapper(restartAsAdmin)())
|
ipcMain.handle('restartAsAdmin', () => ipcErrorWrapper(restartAsAdmin)())
|
||||||
ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)())
|
ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)())
|
||||||
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
|
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
|
||||||
|
ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)())
|
||||||
|
ipcMain.handle('showTunPermissionDialog', () => ipcErrorWrapper(showTunPermissionDialog)())
|
||||||
|
ipcMain.handle('showErrorDialog', (_, title: string, message: string) => ipcErrorWrapper(showErrorDialog)(title, message))
|
||||||
|
|
||||||
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
|
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
|
||||||
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())
|
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())
|
||||||
|
|||||||
108
src/main/utils/logger.ts
Normal file
108
src/main/utils/logger.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { writeFile } from 'fs/promises'
|
||||||
|
import { logPath } from './dirs'
|
||||||
|
|
||||||
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private moduleName: string
|
||||||
|
|
||||||
|
constructor(moduleName: string) {
|
||||||
|
this.moduleName = moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatTimestamp(): string {
|
||||||
|
return new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatLogMessage(level: LogLevel, message: string, error?: any): string {
|
||||||
|
const timestamp = this.formatTimestamp()
|
||||||
|
const errorStr = error ? `: ${error}` : ''
|
||||||
|
return `[${timestamp}] [${level.toUpperCase()}] [${this.moduleName}] ${message}${errorStr}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeToFile(level: LogLevel, message: string, error?: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
const appLogPath = logPath()
|
||||||
|
const logMessage = this.formatLogMessage(level, message, error)
|
||||||
|
await writeFile(appLogPath, logMessage, { flag: 'a' })
|
||||||
|
} catch (logError) {
|
||||||
|
// 如果写入日志文件失败,仍然输出到控制台
|
||||||
|
console.error(`[Logger] Failed to write to log file:`, logError)
|
||||||
|
console.error(`[Logger] Original message: [${level.toUpperCase()}] [${this.moduleName}] ${message}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logToConsole(level: LogLevel, message: string, error?: any): void {
|
||||||
|
const prefix = `[${this.moduleName}] ${message}`
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
console.debug(prefix, error || '')
|
||||||
|
break
|
||||||
|
case 'info':
|
||||||
|
console.log(prefix, error || '')
|
||||||
|
break
|
||||||
|
case 'warn':
|
||||||
|
console.warn(prefix, error || '')
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
console.error(prefix, error || '')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async debug(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('debug', message, error)
|
||||||
|
this.logToConsole('debug', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async info(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('info', message, error)
|
||||||
|
this.logToConsole('info', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async warn(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('warn', message, error)
|
||||||
|
this.logToConsole('warn', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async error(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('error', message, error)
|
||||||
|
this.logToConsole('error', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有的 logFloatingWindow 函数签名
|
||||||
|
async log(message: string, error?: any): Promise<void> {
|
||||||
|
if (error) {
|
||||||
|
await this.error(message, error)
|
||||||
|
} else {
|
||||||
|
await this.info(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建不同模块的日志实例
|
||||||
|
export const createLogger = (moduleName: string): Logger => {
|
||||||
|
return new Logger(moduleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的应用日志实例 - 所有模块共享同一个日志文件
|
||||||
|
export const appLogger = createLogger('app')
|
||||||
|
|
||||||
|
// 为了保持向后兼容性,创建各模块的日志实例(都指向同一个应用日志)
|
||||||
|
export const floatingWindowLogger = createLogger('floating-window')
|
||||||
|
export const coreLogger = createLogger('mihomo-core')
|
||||||
|
export const apiLogger = createLogger('mihomo-api')
|
||||||
|
export const configLogger = createLogger('config')
|
||||||
|
export const systemLogger = createLogger('system')
|
||||||
|
export const trafficLogger = createLogger('traffic-monitor')
|
||||||
|
export const trayLogger = createLogger('tray')
|
||||||
|
export const initLogger = createLogger('init')
|
||||||
|
export const ipcLogger = createLogger('ipc')
|
||||||
|
export const proxyLogger = createLogger('sysproxy')
|
||||||
|
export const managerLogger = createLogger('manager')
|
||||||
|
export const factoryLogger = createLogger('factory')
|
||||||
|
export const overrideLogger = createLogger('override')
|
||||||
|
|
||||||
|
// 默认日志实例
|
||||||
|
export const logger = appLogger
|
||||||
@ -21,6 +21,11 @@ export const defaultConfig: IAppConfig = {
|
|||||||
useNameserverPolicy: false,
|
useNameserverPolicy: false,
|
||||||
controlDns: true,
|
controlDns: true,
|
||||||
controlSniff: true,
|
controlSniff: true,
|
||||||
|
floatingWindowCompatMode: true,
|
||||||
|
disableLoopbackDetector: false,
|
||||||
|
disableEmbedCA: false,
|
||||||
|
disableSystemCA: false,
|
||||||
|
skipSafePathCheck: false,
|
||||||
nameserverPolicy: {},
|
nameserverPolicy: {},
|
||||||
siderOrder: [
|
siderOrder: [
|
||||||
'sysproxy',
|
'sysproxy',
|
||||||
|
|||||||
@ -48,6 +48,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
disableTray = false,
|
disableTray = false,
|
||||||
showFloatingWindow: showFloating = false,
|
showFloatingWindow: showFloating = false,
|
||||||
spinFloatingIcon = true,
|
spinFloatingIcon = true,
|
||||||
|
floatingWindowCompatMode = true,
|
||||||
useWindowFrame = false,
|
useWindowFrame = false,
|
||||||
autoQuitWithoutCore = false,
|
autoQuitWithoutCore = false,
|
||||||
autoQuitWithoutCoreDelay = 60,
|
autoQuitWithoutCoreDelay = 60,
|
||||||
@ -236,6 +237,27 @@ const GeneralConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
title={t('settings.floatingWindowCompatMode')}
|
||||||
|
divider
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={floatingWindowCompatMode}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ floatingWindowCompatMode: v })
|
||||||
|
closeFloatingWindow()
|
||||||
|
setTimeout(() => {
|
||||||
|
showFloatingWindow()
|
||||||
|
}, 100)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip content={t('settings.floatingWindowCompatModeTooltip')}>
|
||||||
|
<IoIosHelpCircle className="text-default-500 cursor-help" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
<SettingItem title={t('settings.disableTray')} divider>
|
<SettingItem title={t('settings.disableTray')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
if (window.electron.process.platform === 'win32') {
|
if (window.electron.process.platform === 'win32') {
|
||||||
const confirmed = confirm(t('tun.permissions.required'))
|
const confirmed = await window.electron.ipcRenderer.invoke('showTunPermissionDialog')
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
try {
|
try {
|
||||||
const notification = new Notification(t('tun.permissions.restarting'))
|
const notification = new Notification(t('tun.permissions.restarting'))
|
||||||
@ -53,7 +53,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
|||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restart as admin:', error)
|
console.error('Failed to restart as admin:', error)
|
||||||
alert(t('tun.permissions.failed') + ': ' + error)
|
await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -65,7 +65,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
|||||||
await window.electron.ipcRenderer.invoke('requestTunPermissions')
|
await window.electron.ipcRenderer.invoke('requestTunPermissions')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Permission grant failed:', error)
|
console.warn('Permission grant failed:', error)
|
||||||
alert(t('tun.permissions.failed') + ': ' + error)
|
await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@
|
|||||||
"common.error.shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}",
|
"common.error.shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}",
|
||||||
"common.error.adminRequired": "Please run with administrator privileges for first launch",
|
"common.error.adminRequired": "Please run with administrator privileges for first launch",
|
||||||
"common.error.initFailed": "Application initialization failed",
|
"common.error.initFailed": "Application initialization failed",
|
||||||
|
"core.highPrivilege.title": "High Privilege Core Detected",
|
||||||
|
"core.highPrivilege.message": "A high-privilege core is detected. The application needs to restart in administrator mode to match permissions. Restart now?",
|
||||||
"common.updater.versionReady": "v{{version}} Version Ready",
|
"common.updater.versionReady": "v{{version}} Version Ready",
|
||||||
"common.updater.goToDownload": "Go to Download",
|
"common.updater.goToDownload": "Go to Download",
|
||||||
"common.updater.update": "Update",
|
"common.updater.update": "Update",
|
||||||
@ -54,6 +56,8 @@
|
|||||||
"settings.envType": "Environment Variable Type",
|
"settings.envType": "Environment Variable Type",
|
||||||
"settings.showFloatingWindow": "Show Floating Window",
|
"settings.showFloatingWindow": "Show Floating Window",
|
||||||
"settings.spinFloatingIcon": "Spin Floating Icon Based on Network Speed",
|
"settings.spinFloatingIcon": "Spin Floating Icon Based on Network Speed",
|
||||||
|
"settings.floatingWindowCompatMode": "Floating Window Compatibility Mode (Recommended)",
|
||||||
|
"settings.floatingWindowCompatModeTooltip": "Disables transparency effects to prevent crashes on some Windows systems. Recommended to keep enabled for stability",
|
||||||
"settings.disableTray": "Disable Tray Icon",
|
"settings.disableTray": "Disable Tray Icon",
|
||||||
"settings.proxyInTray": "Show Proxy Info in Tray Menu",
|
"settings.proxyInTray": "Show Proxy Info in Tray Menu",
|
||||||
"settings.showTraffic_windows": "Show Network Speed in Taskbar",
|
"settings.showTraffic_windows": "Show Network Speed in Taskbar",
|
||||||
@ -315,11 +319,9 @@
|
|||||||
"tun.excludeAddress.placeholder": "Example: 172.20.0.0/16",
|
"tun.excludeAddress.placeholder": "Example: 172.20.0.0/16",
|
||||||
"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.permissions.title": "Administrator Privileges Required",
|
||||||
"tun.permissions.required": "TUN mode requires administrator privileges. Restart the application now to get permissions?",
|
"tun.permissions.message": "TUN mode requires administrator privileges. Restart the application now to get permissions?",
|
||||||
"tun.permissions.failed": "Permission authorization failed",
|
"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...",
|
"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",
|
||||||
|
|||||||
@ -36,6 +36,8 @@
|
|||||||
"common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}",
|
"common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}",
|
||||||
"common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید",
|
"common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید",
|
||||||
"common.error.initFailed": "راهاندازی برنامه با خطا مواجه شد",
|
"common.error.initFailed": "راهاندازی برنامه با خطا مواجه شد",
|
||||||
|
"core.highPrivilege.title": "هسته با سطح دسترسی بالا شناسایی شد",
|
||||||
|
"core.highPrivilege.message": "هستهای با سطح دسترسی بالا شناسایی شد. برنامه باید در حالت مدیر سیستم برای تطابق سطح دسترسیها دوباره راهاندازی شود. آیا میخواهید اکنون راهاندازی مجدد شود؟",
|
||||||
"common.updater.versionReady": "نسخه v{{version}} آماده است",
|
"common.updater.versionReady": "نسخه v{{version}} آماده است",
|
||||||
"common.updater.goToDownload": "دانلود",
|
"common.updater.goToDownload": "دانلود",
|
||||||
"common.updater.update": "بهروزرسانی",
|
"common.updater.update": "بهروزرسانی",
|
||||||
|
|||||||
@ -36,7 +36,9 @@
|
|||||||
"common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}",
|
"common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}",
|
||||||
"common.error.adminRequired": "Для первого запуска требуются права администратора",
|
"common.error.adminRequired": "Для первого запуска требуются права администратора",
|
||||||
"common.error.initFailed": "Не удалось инициализировать приложение",
|
"common.error.initFailed": "Не удалось инициализировать приложение",
|
||||||
"common.updater.versionReady": "Версия v{{version}} готова",
|
"core.highPrivilege.title": "High Privilege Core Detected",
|
||||||
|
"core.highPrivilege.message": "Обнаружено ядро с повышенными привилегиями. Приложение необходимо перезапустить в режиме администратора для согласования прав. Перезапустить сейчас?",
|
||||||
|
"common.updater.versionReady": "Обнаружено ядро с повышенными привилегиями",
|
||||||
"common.updater.goToDownload": "Перейти к загрузке",
|
"common.updater.goToDownload": "Перейти к загрузке",
|
||||||
"common.updater.update": "Обновить",
|
"common.updater.update": "Обновить",
|
||||||
"settings.general": "Общие настройки",
|
"settings.general": "Общие настройки",
|
||||||
|
|||||||
@ -36,6 +36,8 @@
|
|||||||
"common.error.shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}",
|
"common.error.shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}",
|
||||||
"common.error.adminRequired": "首次启动请以管理员权限运行",
|
"common.error.adminRequired": "首次启动请以管理员权限运行",
|
||||||
"common.error.initFailed": "应用初始化失败",
|
"common.error.initFailed": "应用初始化失败",
|
||||||
|
"core.highPrivilege.title": "检测到高权限内核",
|
||||||
|
"core.highPrivilege.message": "检测到运行中的高权限内核,需以管理员模式重启应用以匹配权限,确定重启?",
|
||||||
"common.updater.versionReady": "v{{version}} 版本就绪",
|
"common.updater.versionReady": "v{{version}} 版本就绪",
|
||||||
"common.updater.goToDownload": "前往下载",
|
"common.updater.goToDownload": "前往下载",
|
||||||
"common.updater.update": "更新",
|
"common.updater.update": "更新",
|
||||||
@ -54,6 +56,8 @@
|
|||||||
"settings.envType": "环境变量类型",
|
"settings.envType": "环境变量类型",
|
||||||
"settings.showFloatingWindow": "显示悬浮窗",
|
"settings.showFloatingWindow": "显示悬浮窗",
|
||||||
"settings.spinFloatingIcon": "根据网速旋转悬浮窗图标",
|
"settings.spinFloatingIcon": "根据网速旋转悬浮窗图标",
|
||||||
|
"settings.floatingWindowCompatMode": "悬浮窗兼容模式(推荐开启)",
|
||||||
|
"settings.floatingWindowCompatModeTooltip": "禁用透明效果以避免在某些 Windows 系统上崩溃,建议保持开启以确保稳定性",
|
||||||
"settings.disableTray": "禁用托盘图标",
|
"settings.disableTray": "禁用托盘图标",
|
||||||
"settings.proxyInTray": "在托盘菜单显示代理信息",
|
"settings.proxyInTray": "在托盘菜单显示代理信息",
|
||||||
"settings.showTraffic_windows": "在任务栏显示网速",
|
"settings.showTraffic_windows": "在任务栏显示网速",
|
||||||
@ -315,11 +319,9 @@
|
|||||||
"tun.excludeAddress.placeholder": "例: 172.20.0.0/16",
|
"tun.excludeAddress.placeholder": "例: 172.20.0.0/16",
|
||||||
"tun.notifications.coreAuthSuccess": "内核授权成功",
|
"tun.notifications.coreAuthSuccess": "内核授权成功",
|
||||||
"tun.notifications.firewallResetSuccess": "防火墙重设成功",
|
"tun.notifications.firewallResetSuccess": "防火墙重设成功",
|
||||||
"tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限",
|
"tun.permissions.title": "需要管理员权限",
|
||||||
"tun.permissions.required": "启用TUN模式需要管理员权限,是否现在重启应用获取权限?",
|
"tun.permissions.message": "启用TUN模式需要管理员权限,是否现在重启应用获取权限?",
|
||||||
"tun.permissions.failed": "权限授权失败",
|
"tun.permissions.failed": "权限授权失败",
|
||||||
"tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式",
|
|
||||||
"tun.permissions.requesting": "正在请求管理员权限,请在UAC对话框中点击'是'...",
|
|
||||||
"tun.permissions.restarting": "正在以管理员权限重启应用,请在UAC对话框中点击'是'...",
|
"tun.permissions.restarting": "正在以管理员权限重启应用,请在UAC对话框中点击'是'...",
|
||||||
"dns.title": "DNS 设置",
|
"dns.title": "DNS 设置",
|
||||||
"dns.enable": "启用 DNS",
|
"dns.enable": "启用 DNS",
|
||||||
|
|||||||
@ -241,6 +241,26 @@ export async function manualGrantCorePermition(): Promise<void> {
|
|||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkHighPrivilegeCore(): Promise<boolean> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkHighPrivilegeCore'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAdminPrivileges(): Promise<boolean> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkAdminPrivileges'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartAsAdmin(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartAsAdmin'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showTunPermissionDialog(): Promise<boolean> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showTunPermissionDialog'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showErrorDialog(title: string, message: string): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showErrorDialog', title, message))
|
||||||
|
}
|
||||||
|
|
||||||
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext))
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/shared/types.d.ts
vendored
1
src/shared/types.d.ts
vendored
@ -236,6 +236,7 @@ interface IAppConfig {
|
|||||||
spinFloatingIcon?: boolean
|
spinFloatingIcon?: boolean
|
||||||
disableTray?: boolean
|
disableTray?: boolean
|
||||||
showFloatingWindow?: boolean
|
showFloatingWindow?: boolean
|
||||||
|
floatingWindowCompatMode?: boolean
|
||||||
connectionCardStatus?: CardStatus
|
connectionCardStatus?: CardStatus
|
||||||
dnsCardStatus?: CardStatus
|
dnsCardStatus?: CardStatus
|
||||||
logCardStatus?: CardStatus
|
logCardStatus?: CardStatus
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user