Compare commits

..

No commits in common. "v1.8.3" and "main" have entirely different histories.
v1.8.3 ... main

62 changed files with 5718 additions and 5709 deletions

1
.gitignore vendored
View File

@ -8,4 +8,3 @@ out
*.log*
.idea
*.ttf
party.md

View File

@ -23,7 +23,6 @@ package() {
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-smart
install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"

View File

@ -29,7 +29,6 @@ package() {
cp -r $srcdir/opt/mihomo-party/resources/files ${pkgdir}/opt/mihomo-party/resources/
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-smart
install -Dm755 "${srcdir}/${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
install -Dm644 "${pkgdir}/opt/mihomo-party/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"

View File

@ -39,7 +39,6 @@ package() {
cp -r $srcdir/${_pkgname}-${pkgver}/extra/files ${pkgdir}/opt/mihomo-party/resources/
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-smart
install -Dm755 "${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
install -Dm644 "${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
install -Dm644 "${pkgdir}/opt/mihomo-party/resources/icon.png" "${pkgdir}/usr/share/icons/hicolor/512x512/apps/${_pkgname}.png"

View File

@ -41,7 +41,6 @@ package() {
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-smart
install -Dm755 "${srcdir}/../${_pkgname}.sh" "${pkgdir}/usr/bin/${_pkgname}"
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${_pkgname}.desktop"

View File

@ -36,7 +36,6 @@ package() {
chmod +x ${pkgdir}/opt/mihomo-party/mihomo-party
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-alpha
chmod +sx ${pkgdir}/opt/mihomo-party/resources/sidecar/mihomo-smart
install -Dm755 "${srcdir}/../${pkgname}.sh" "${pkgdir}/usr/bin/${pkgname}"
sed -i '3s!/opt/mihomo-party/mihomo-party!mihomo-party!' "${pkgdir}/usr/share/applications/${pkgname}.desktop"

View File

@ -8,7 +8,5 @@
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@ -13,7 +13,6 @@ fi
chmod 4755 '/opt/mihomo-party/chrome-sandbox' || true
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo-alpha
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo-smart
if hash update-mime-database 2>/dev/null; then
update-mime-database /usr/share/mime || true

View File

@ -51,14 +51,6 @@ else
log "Warning: mihomo-alpha binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
fi
if [ -f "$APP_PATH/Contents/Resources/sidecar/mihomo-smart" ]; then
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo-smart"
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo-smart"
log "Set permissions for mihomo-smart"
else
log "Warning: mihomo-smart binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo-smart"
fi
# 复制 helper 工具
log "Installing helper tool..."
if [ -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" ]; then

View File

@ -1,52 +1,72 @@
## 1.8.3
**本次更新移除了 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件,安全性更好,更灵活,给无法使用管理员模式运行软件的企业用户提供了更大的便利**
## 1.7.7
### 新功能 (Feat)
- 移除 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件
### 重构 (Refactor)
- Geodata 文件只有在源文件更新的时候才会在启动时覆盖更新
- Mihomo 内核升级 v1.19.12
- 新增 Webdav 最大备数设置和清理逻辑
### 修复 (Fix)
- 修复 DNS/嗅探覆写开关逻辑,修改设置不再会直接写入运行时配置,增加了“仅保存”按钮
- 悬浮窗改为纯矩形,修复 windows 下兼容性问题带来的白边
- 修复 MacOS 下无法启动的问题(重置工作目录权限)
- 尝试修复不同版本 MacOS 下安装软件时候的报错Input/output error
- 部分遗漏的多国语言翻译
## 1.8.2
## 1.7.6
**本次更新主要集中在重大内核更新和依赖升级后所产生的 bug 修复解决了自1.7版以后首次安装无法启动的问题,推荐更新**
### 新功能 (Feat)
- 重构 域名嗅探 卡片模块,改为“覆写”逻辑,当开关打开后,使用 嗅探覆写 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置
- 订阅/覆写卡片可右键呼出菜单
- MacOS 下“轻触(tap)”触控板可进行开关操作(之前必须“按下(click)”)
**此版本修复了 1.7.5 中的几个严重 bug推荐所有人更新**
### 修复 (Fix)
- **因多国语言带来的在 Windows 下首次安装无法启动的问题**
- 1.8.1升级依赖导致的节点圆角显示失效
- DNS 覆写模块的逻辑冲突
- 点击订阅卡片功能区导致的选中订阅问题
- 覆写卡片可以双击编辑
- 因代码不规范导致的控制台警告
- Linux 下没有设置 Smart 内核权限导致的“外部控制监听错误”
- 修复了内核1.19.8更新后gist同步失效的问题(#780)
- 部分遗漏的多国语言翻译
- MacOS 下启动Error: EACCES: permission denied
- MacOS 系统代理 bypass 不生效
- MacOS 系统代理开启时 500 报错
## 1.8.1
## 1.7.5
### 新功能 (Feat)
- 重构 DNS 卡片模块改为“覆写”逻辑当开关打开后使用DNS 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置
### 性能提升(Perf)
- 更新依赖,提升页面响应性
- 优化订阅切换逻辑,大幅提升切换速度和稳定性
### 修复 (Fix)
- “使用自动 Smart 规则覆写”没有覆盖兜底的 MATCH 规则
- 移除默认的 Smart "policy-priority" 规则
- 增加组延迟测试时的动画
- 订阅卡片可右键点击
-
## 1.8.0
### 修复 (Fix)
- 1.7.4引入的内核启动错误
- 无法手动设置内核权限
- 完善 系统代理socket 重建和检测机制
## 1.7.4
### 新功能 (Feat)
**重大更新:本次更新增加了 Smart Core可以根据用户使用习惯和节点质量自动选择符合您的最优节点。并内置了“一键开启”适合不想折腾自定义规则的用户
“一键开启”内置 Smart规则的功能在“内核设置”下的“使用自动 Smart 规则覆写”,原理:当开关开启后,自动载入覆写脚本,新增 Smart Group并替换当前配置文件下的默认出站规则为"Smart Group",您的所有代理流量都将从此分组下的节点流出。如果使用“全局模式”请选择名称为"Smart Group"的节点,以使用该功能。**
- Mihomo 内核升级 v1.19.10
- 改进 socket创建机制防止 MacOS 下系统代理开启无法找到 socket 文件的问题
- mihomo-party-helper增加更多日志以方便调试
- 改进 MacOS 下签名和公正流程
- 增加 MacOS 下 plist 权限设置
- 改进安装流程
-
注意:本功能还在测试中,如遇到问题请发 issue 反馈
### 修复 (Fix)
- 修复mihomo-party-helper本地提权漏洞
- 修复 MacOS 下安装失败的问题
- 移除节点页面的滚动位置记忆,解决页面溢出的问题
- DNS hosts 设置在 useHosts 不为 true 时也会被错误应用的问题(#742)
- 当用户在 Profile 设置中修改了更新间隔并保存后,新的间隔时间不会立即生效(#671)
- 禁止选择器组件选择空值
- 修复proxy-provider
## 1.7.3
**注意:如安装后为英文,请在设置中反复选择几次不同语言以写入配置文件**
### 新功能 (Feat)
- Mihomo 内核升级 v1.19.5
- MacOS 下添加 Dock 图标动态展现方式 (#594)
- 更改默认 UA 并添加版本
- 添加固定间隔的配置文件更新按钮 (#670)
- 重构Linux上的手动授权内核方式
- 将sub-store迁移到工作目录下(#552)
- 重置软件增加警告提示
### 修复 (Fix)
- 修复代理节点页面因为重复刷新导致的溢出问题
- 修复由于 Mihomo 核心错误导致启动时窗口丢失 (#601)
- 修复macOS下的sub-store更新问题 (#552)
- 修复多语言翻译
- 修复 defaultBypass 几乎总是 Windows 默认绕过设置 (#602)
- 修复重置防火墙时发生的错误,因为没有指定防火墙规则 (#650)

View File

@ -39,14 +39,12 @@ mac:
target:
- pkg
entitlementsInherit: build/entitlements.mac.plist
hardenedRuntime: true
gatekeeperAssess: false
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
notarize: true
artifactName: ${name}-macos-${version}-${arch}.${ext}
pkg:
allowAnywhere: false
@ -56,9 +54,8 @@ pkg:
file: build/background.png
linux:
desktop:
entry:
Name: Mihomo Party
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
Name: Mihomo Party
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
target:
- deb
- rpm

View File

@ -1,7 +1,6 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://github.com/vdesjs/vite-plugin-monaco-editor/issues/21#issuecomment-1827562674
import monacoEditorPluginModule from 'vite-plugin-monaco-editor'
const isObjectWithDefaultFunction = (
@ -38,7 +37,6 @@ export default defineConfig({
},
plugins: [
react(),
tailwindcss(),
monacoEditorPlugin({
languageWorkers: ['editorWorkerService', 'typescript', 'css'],
customDistPath: (_, out) => `${out}/monacoeditorwork`,

View File

@ -1,6 +1,6 @@
{
"name": "mihomo-party",
"version": "1.8.3",
"version": "1.7.7",
"description": "Mihomo Party",
"main": "./out/main/index.js",
"author": "mihomo-party-org",
@ -23,76 +23,75 @@
"build:linux": "electron-vite build && electron-builder --publish never --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@heroui/react": "^2.8.2",
"@mihomo-party/sysproxy": "^2.0.8",
"@mihomo-party/sysproxy-darwin-arm64": "^2.0.8",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@heroui/react": "^2.6.14",
"@mihomo-party/sysproxy": "^2.0.7",
"@mihomo-party/sysproxy-darwin-arm64": "^2.0.7",
"@types/crypto-js": "^4.2.2",
"adm-zip": "^0.5.16",
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"chokidar": "^4.0.3",
"axios": "^1.7.7",
"chokidar": "^4.0.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"express": "^5.1.0",
"i18next": "^25.3.2",
"express": "^5.0.1",
"i18next": "^24.2.2",
"iconv-lite": "^0.6.3",
"react-chartjs-2": "^5.3.0",
"react-i18next": "^15.6.1",
"webdav": "^5.8.0",
"ws": "^8.18.3",
"yaml": "^2.8.0"
"react-i18next": "^15.4.0",
"webdav": "^5.7.1",
"ws": "^8.18.0",
"yaml": "^2.6.0"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@tailwindcss/vite": "^4.1.11",
"@types/adm-zip": "^0.5.7",
"@types/express": "^5.0.3",
"@types/node": "^24.1.0",
"@types/adm-zip": "^0.5.6",
"@types/express": "^5.0.0",
"@types/node": "^22.13.1",
"@types/pubsub-js": "^1.8.6",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.7.0",
"cron-validator": "^1.4.0",
"driver.js": "^1.3.6",
"electron": "^37.2.5",
"electron-builder": "26.0.12",
"electron-vite": "^4.0.0",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@types/ws": "^8.5.13",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"cron-validator": "^1.3.1",
"driver.js": "^1.3.5",
"electron": "^34.0.2",
"electron-builder": "25.1.8",
"electron-vite": "^2.3.0",
"electron-window-state": "^5.0.3",
"eslint": "9.32.0",
"eslint-plugin-react": "^7.37.5",
"form-data": "^4.0.4",
"framer-motion": "12.23.12",
"eslint": "8.57.1",
"eslint-plugin-react": "^7.37.2",
"form-data": "^4.0.1",
"framer-motion": "12.0.11",
"lodash": "^4.17.21",
"meta-json-schema": "^1.19.12",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"meta-json-schema": "^1.18.9",
"monaco-yaml": "^5.2.3",
"nanoid": "^5.0.8",
"next-themes": "^0.4.3",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"pubsub-js": "^1.9.5",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-error-boundary": "^6.0.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-monaco-editor": "^0.59.0",
"react-router-dom": "^7.7.1",
"react-virtuoso": "^4.13.0",
"swr": "^2.3.4",
"tailwindcss": "^4.1.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^5.0.0",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"react-monaco-editor": "^0.58.0",
"react-router-dom": "^7.1.5",
"react-virtuoso": "^4.12.0",
"recharts": "^2.13.3",
"swr": "^2.2.5",
"tailwindcss": "^3.4.17",
"tar": "^7.4.3",
"tsx": "^4.20.3",
"tsx": "^4.19.2",
"types-pac": "^1.0.3",
"typescript": "^5.9.2",
"vite": "^7.0.6",
"typescript": "^5.6.3",
"vite": "^6.0.7",
"vite-plugin-monaco-editor": "^1.1.0"
},
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"

9268
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

View File

@ -45,36 +45,6 @@ async function getLatestAlphaVersion() {
}
}
/* ======= mihomo smart ======= */
const MIHOMO_SMART_VERSION_URL =
'https://github.com/vernesong/mihomo/releases/download/Prerelease-Alpha/version.txt'
const MIHOMO_SMART_URL_PREFIX = `https://github.com/vernesong/mihomo/releases/download/Prerelease-Alpha`
let MIHOMO_SMART_VERSION
const MIHOMO_SMART_MAP = {
'win32-x64': 'mihomo-windows-amd64-v2-go120',
'win32-ia32': 'mihomo-windows-386-go120',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-v2-go120',
'darwin-arm64': 'mihomo-darwin-arm64',
'linux-x64': 'mihomo-linux-amd64-v2-go120',
'linux-arm64': 'mihomo-linux-arm64'
}
async function getLatestSmartVersion() {
try {
const response = await fetch(MIHOMO_SMART_VERSION_URL, {
method: 'GET'
})
let v = await response.text()
MIHOMO_SMART_VERSION = v.trim() // Trim to remove extra whitespaces
console.log(`Latest smart version: ${MIHOMO_SMART_VERSION}`)
} catch (error) {
console.error('Error fetching latest smart version:', error.message)
process.exit(1)
}
}
/* ======= mihomo release ======= */
const MIHOMO_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
@ -117,10 +87,6 @@ if (!MIHOMO_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(`unsupported platform "${platform}-${arch}"`)
}
if (!MIHOMO_SMART_MAP[`${platform}-${arch}`]) {
throw new Error(`unsupported platform "${platform}-${arch}"`)
}
/**
* core info
*/
@ -157,23 +123,6 @@ function mihomo() {
downloadURL
}
}
function mihomoSmart() {
const name = MIHOMO_SMART_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
const downloadURL = `${MIHOMO_SMART_URL_PREFIX}/${name}-${MIHOMO_SMART_VERSION}.${urlExt}`
const exeFile = `${name}${isWin ? '.exe' : ''}`
const zipFile = `${name}-${MIHOMO_SMART_VERSION}.${urlExt}`
return {
name: 'mihomo-smart',
targetFile: `mihomo-smart${isWin ? '.exe' : ''}`,
exeFile,
zipFile,
downloadURL
}
}
/**
* download sidecar and rename
*/
@ -322,6 +271,11 @@ const resolveSysproxy = () =>
file: '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 tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
@ -406,11 +360,6 @@ const tasks = [
func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
retry: 5
},
{
name: 'mihomo-smart',
func: () => getLatestSmartVersion().then(() => resolveSidecar(mihomoSmart())),
retry: 5
},
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
{ name: 'metadb', func: resolveMetadb, retry: 5 },
{ name: 'geosite', func: resolveGeosite, retry: 5 },
@ -433,6 +382,12 @@ const tasks = [
retry: 5,
winOnly: true
},
{
name: 'runner',
func: resolveRunner,
retry: 5,
winOnly: true
},
{
name: 'monitor',
func: resolveMonitor,
@ -477,14 +432,7 @@ async function runTask() {
break
} catch (err) {
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message)
if (i === task.retry - 1) {
if (task.optional) {
console.log(`[WARN]: Optional task::${task.name} failed, skipping...`)
break
} else {
throw err
}
}
if (i === task.retry - 1) throw err
}
}
return runTask()

View File

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

View File

@ -27,9 +27,3 @@ export {
setOverride,
updateOverrideItem
} from './override'
export {
createSmartOverride,
removeSmartOverride,
manageSmartOverride,
isSmartOverrideExists
} from './smartOverride'

View File

@ -37,21 +37,15 @@ export async function getProfileItem(id: string | undefined): Promise<IProfileIt
export async function changeCurrentProfile(id: string): Promise<void> {
const config = await getProfileConfig()
const current = config.current
if (current === id) {
return
}
config.current = id
await setProfileConfig(config)
try {
await restartCore()
} catch (e) {
// 如果重启失败,恢复原来的配置
config.current = current
await setProfileConfig(config)
throw e
} finally {
await setProfileConfig(config)
}
}

View File

@ -1,283 +0,0 @@
import { getAppConfig } from './app'
import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override'
const SMART_OVERRIDE_ID = 'smart-core-override'
/**
* Smart
*/
function generateSmartOverrideTemplate(useLightGBM: boolean, collectData: boolean, strategy: string): string {
return `
// 配置会在启用 Smart 内核时自动应用
function main(config) {
try {
// 确保配置对象存在
if (!config || typeof config !== 'object') {
console.log('[Smart Override] Invalid config object')
return config
}
// 确保代理组配置存在
if (!config['proxy-groups']) {
config['proxy-groups'] = []
}
// 确保代理组是数组
if (!Array.isArray(config['proxy-groups'])) {
console.log('[Smart Override] proxy-groups is not an array, converting...')
config['proxy-groups'] = []
}
// 查找现有的 Smart 代理组并更新
let smartGroupExists = false
for (let i = 0; i < config['proxy-groups'].length; i++) {
const group = config['proxy-groups'][i]
if (group && group.type === 'smart') {
smartGroupExists = true
console.log('[Smart Override] Found existing smart group:', group.name)
if (!group['policy-priority']) {
group['policy-priority'] = '' // policy-priority: <1 means lower priority, >1 means higher priority, the default is 1, pattern support regex and string
}
group.uselightgbm = ${useLightGBM}
group.collectdata = ${collectData}
group.strategy = '${strategy}'
break
}
}
// 如果没有 Smart 组且有可用代理,创建示例组
if (!smartGroupExists && config.proxies && Array.isArray(config.proxies) && config.proxies.length > 0) {
console.log('[Smart Override] Creating new smart group with', config.proxies.length, 'proxies')
// 获取所有代理的名称
const proxyNames = config.proxies
.filter(proxy => proxy && typeof proxy === 'object' && proxy.name)
.map(proxy => proxy.name)
if (proxyNames.length > 0) {
const smartGroup = {
name: 'Smart Group',
type: 'smart',
'policy-priority': '', // policy-priority: <1 means lower priority, >1 means higher priority, the default is 1, pattern support regex and string
uselightgbm: ${useLightGBM},
collectdata: ${collectData},
strategy: '${strategy}',
proxies: proxyNames
}
config['proxy-groups'].unshift(smartGroup)
console.log('[Smart Override] Created smart group at first position with proxies:', proxyNames)
} else {
console.log('[Smart Override] No valid proxies found, skipping smart group creation')
}
} else if (!smartGroupExists) {
console.log('[Smart Override] No proxies available, skipping smart group creation')
}
// 处理规则替换
if (config.rules && Array.isArray(config.rules)) {
console.log('[Smart Override] Processing rules, original count:', config.rules.length)
// 收集所有代理组名称
const proxyGroupNames = new Set()
if (config['proxy-groups'] && Array.isArray(config['proxy-groups'])) {
config['proxy-groups'].forEach(group => {
if (group && group.name) {
proxyGroupNames.add(group.name)
}
})
}
// 添加常见的内置目标
const builtinTargets = new Set([
'DIRECT',
'REJECT',
'REJECT-DROP',
'PASS',
'COMPATIBLE'
])
// 添加常见的规则参数,不应该替换
const ruleParams = new Set(['no-resolve', 'force-remote-dns', 'prefer-ipv6'])
console.log('[Smart Override] Found', proxyGroupNames.size, 'proxy groups:', Array.from(proxyGroupNames))
let replacedCount = 0
config.rules = config.rules.map(rule => {
if (typeof rule === 'string') {
// 检查是否是复杂规则格式(包含括号的嵌套规则)
if (rule.includes('((') || rule.includes('))')) {
console.log('[Smart Override] Skipping complex nested rule:', rule)
return rule
}
// 处理字符串格式的规则
const parts = rule.split(',').map(part => part.trim())
if (parts.length >= 2) {
// 找到代理组名称的位置
let targetIndex = -1
let targetValue = ''
// 处理 MATCH 规则
if (parts[0] === 'MATCH' && parts.length === 2) {
targetIndex = 1
targetValue = parts[1]
} else if (parts.length >= 3) {
// 处理其他规则
for (let i = 2; i < parts.length; i++) {
const part = parts[i]
if (!ruleParams.has(part)) {
targetIndex = i
targetValue = part
break
}
}
}
if (targetIndex !== -1 && targetValue) {
// 检查是否应该替换
const shouldReplace = !builtinTargets.has(targetValue) &&
(proxyGroupNames.has(targetValue) ||
!ruleParams.has(targetValue))
if (shouldReplace) {
parts[targetIndex] = 'Smart Group'
replacedCount++
console.log('[Smart Override] Replaced rule target:', targetValue, '→ Smart Group')
return parts.join(',')
}
}
}
} else if (typeof rule === 'object' && rule !== null) {
// 处理对象格式
let targetField = ''
let targetValue = ''
if (rule.target) {
targetField = 'target'
targetValue = rule.target
} else if (rule.proxy) {
targetField = 'proxy'
targetValue = rule.proxy
}
if (targetField && targetValue) {
const shouldReplace = !builtinTargets.has(targetValue) &&
(proxyGroupNames.has(targetValue) ||
!ruleParams.has(targetValue))
if (shouldReplace) {
rule[targetField] = 'Smart Group'
replacedCount++
console.log('[Smart Override] Replaced rule target:', targetValue, '→ Smart Group')
}
}
}
return rule
})
console.log('[Smart Override] Rules processed, replaced', replacedCount, 'non-DIRECT rules with Smart Group')
} else {
console.log('[Smart Override] No rules found or rules is not an array')
}
console.log('[Smart Override] Configuration processed successfully')
return config
} catch (error) {
console.error('[Smart Override] Error processing config:', error)
// 发生错误时返回原始配置,避免破坏整个配置
return config
}
}
`
}
/**
* Smart
*/
export async function createSmartOverride(): Promise<void> {
try {
// 获取应用配置
const {
smartCoreUseLightGBM = false,
smartCoreCollectData = false,
smartCoreStrategy = 'sticky-sessions'
} = await getAppConfig()
// 生成覆写模板
const template = generateSmartOverrideTemplate(
smartCoreUseLightGBM,
smartCoreCollectData,
smartCoreStrategy
)
// 检查是否已存在 Smart 覆写配置
const existingOverride = await getOverrideItem(SMART_OVERRIDE_ID)
if (existingOverride) {
// 如果已存在,更新配置
await addOverrideItem({
id: SMART_OVERRIDE_ID,
name: 'Smart Core Override',
type: 'local',
ext: 'js',
global: true,
file: template
})
} else {
// 如果不存在,创建新的覆写配置
await addOverrideItem({
id: SMART_OVERRIDE_ID,
name: 'Smart Core Override',
type: 'local',
ext: 'js',
global: true,
file: template
})
}
} catch (error) {
console.error('Failed to create Smart override:', error)
throw error
}
}
/**
* Smart
*/
export async function removeSmartOverride(): Promise<void> {
try {
const existingOverride = await getOverrideItem(SMART_OVERRIDE_ID)
if (existingOverride) {
await removeOverrideItem(SMART_OVERRIDE_ID)
}
} catch (error) {
console.error('Failed to remove Smart override:', error)
throw error
}
}
/**
* Smart
*/
export async function manageSmartOverride(): Promise<void> {
const { enableSmartCore = true, enableSmartOverride = true, core } = await getAppConfig()
if (enableSmartCore && enableSmartOverride && core === 'mihomo-smart') {
await createSmartOverride()
} else {
await removeSmartOverride()
}
}
/**
* Smart
*/
export async function isSmartOverrideExists(): Promise<boolean> {
try {
const override = await getOverrideItem(SMART_OVERRIDE_ID)
return !!override
} catch {
return false
}
}

View File

@ -26,23 +26,9 @@ let runtimeConfig: IMihomoConfig
export async function generateProfile(): Promise<void> {
const { current } = await getProfileConfig()
const { diffWorkDir = false, controlDns = true, controlSniff = true, useNameserverPolicy } = await getAppConfig()
const { diffWorkDir = false } = await getAppConfig()
const currentProfile = await overrideProfile(current, await getProfile(current))
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 controledMihomoConfig = await getControledMihomoConfig()
const profile = deepMerge(currentProfile, controledMihomoConfig)
// 确保可以拿到基础日志信息
// 使用 debug 可以调试内核相关问题 `debug/pprof`

View File

@ -15,10 +15,9 @@ import {
getControledMihomoConfig,
getProfileConfig,
patchAppConfig,
patchControledMihomoConfig,
manageSmartOverride
patchControledMihomoConfig
} from '../config'
import { app, ipcMain, net } from 'electron'
import { app, dialog, ipcMain, net } from 'electron'
import {
startMihomoTraffic,
startMihomoConnections,
@ -39,7 +38,6 @@ import os from 'os'
import { createWriteStream, existsSync } from 'fs'
import { uploadRuntimeConfig } from '../resolve/gistApi'
import { startMonitor } from '../resolve/trafficMonitor'
import { safeShowErrorBox } from '../utils/init'
import i18next from '../../shared/i18n'
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
@ -47,7 +45,7 @@ chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', as
await stopCore(true)
await startCore()
} catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
}
})
@ -85,10 +83,6 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
const { current } = await getProfileConfig()
const { tun } = await getControledMihomoConfig()
const corePath = mihomoCorePath(core)
// 管理 Smart 内核覆写配置
await manageSmartOverride()
await generateProfile()
await checkProfile()
await stopCore()
@ -213,12 +207,7 @@ export async function restartCore(): Promise<void> {
try {
await startCore()
} catch (e) {
// 记录错误到日志而不是显示阻塞对话框
await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, {
flag: 'a'
})
// 重新抛出错误,让调用者处理
throw e
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
}
}
@ -229,7 +218,7 @@ export async function keepCoreAlive(): Promise<void> {
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
}
} catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
}
}
@ -260,75 +249,29 @@ async function checkProfile(): Promise<void> {
mihomoTestDir()
], { env })
} catch (error) {
console.error('Profile check failed:', error)
if (error instanceof Error && 'stdout' in error) {
const { stdout, stderr } = error as { stdout: string; stderr?: string }
console.log('Profile check stdout:', stdout)
console.log('Profile check stderr:', stderr)
const { stdout } = error as { stdout: string }
const errorLines = stdout
.split('\n')
.filter((line) => line.includes('level=error') || line.includes('error'))
.map((line) => {
if (line.includes('level=error')) {
return line.split('level=error')[1]?.trim() || line
}
return line.trim()
})
.filter(line => line.length > 0)
if (errorLines.length === 0) {
const allLines = stdout.split('\n').filter(line => line.trim().length > 0)
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${allLines.join('\n')}`)
} else {
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`)
}
.filter((line) => line.includes('level=error'))
.map((line) => line.split('level=error')[1])
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`)
} else {
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}: ${error}`)
throw error
}
}
}
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> {
export async function manualGrantCorePermition(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
const execPromise = promisify(exec)
const execFilePromise = promisify(execFile)
if (process.platform === 'darwin') {
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`)
}
if (process.platform === 'linux') {
await execFilePromise('pkexec', [
'bash',
@ -336,146 +279,6 @@ export async function grantTunPermissions(): Promise<void> {
`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> {

View File

@ -168,21 +168,6 @@ export const mihomoUpgrade = async (): Promise<void> => {
return await instance.post('/upgrade')
}
// Smart 内核 API
export const mihomoSmartGroupWeights = async (groupName: string): Promise<Record<string, number>> => {
const instance = await getAxios()
return await instance.get(`/group/${encodeURIComponent(groupName)}/weights`)
}
export const mihomoSmartFlushCache = async (configName?: string): Promise<void> => {
const instance = await getAxios()
if (configName) {
return await instance.post(`/cache/smart/flush/${encodeURIComponent(configName)}`)
} else {
return await instance.post('/cache/smart/flush')
}
}
export const startMihomoTraffic = async (): Promise<void> => {
await mihomoTraffic()
}

View File

@ -3,43 +3,27 @@ import { registerIpcMainHandlers } from './utils/ipc'
import windowStateKeeper from 'electron-window-state'
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
import { addProfileItem, getAppConfig, patchAppConfig } from './config'
import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun } from './core/manager'
import { quitWithoutCore, startCore, stopCore } from './core/manager'
import { triggerSysProxy } from './sys/sysproxy'
import icon from '../../resources/icon.png?asset'
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
import { init } from './utils/init'
import { join } from 'path'
import { initShortcut } from './resolve/shortcut'
import { spawn, exec } from 'child_process'
import { execSync, spawn, exec } from 'child_process'
import { createElevateTask } from './sys/misc'
import { promisify } from 'util'
import { stat } from 'fs/promises'
import { initProfileUpdater } from './core/profileUpdater'
import { existsSync } from 'fs'
import { exePath } from './utils/dirs'
import { existsSync, writeFileSync } from 'fs'
import { exePath, taskDir } from './utils/dirs'
import path from 'path'
import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow'
import iconv from 'iconv-lite'
import { initI18n } from '../shared/i18n'
import i18next from 'i18next'
// 错误处理
function showSafeErrorBox(titleKey: string, message: string): void {
let title: string
try {
title = i18next.t(titleKey)
if (!title || title === titleKey) throw new Error('Translation not ready')
} catch {
const isZh = app.getLocale().startsWith('zh')
const fallbacks: Record<string, { zh: string; en: string }> = {
'common.error.initFailed': { zh: '应用初始化失败', en: 'Application initialization failed' },
'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' },
'profiles.error.importFailed': { zh: '配置导入失败', en: 'Profile import failed' },
'common.error.adminRequired': { zh: '需要管理员权限', en: 'Administrator privileges required' }
}
title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error')
}
dialog.showErrorBox(title, message)
}
async function fixUserDataPermissions(): Promise<void> {
if (process.platform !== 'darwin') return
@ -66,6 +50,39 @@ async function fixUserDataPermissions(): Promise<void> {
let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null
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
}
dialog.showErrorBox(
i18next.t('common.error.adminRequired'),
`${i18next.t('common.error.adminRequired')}\n${createErrorStr}\n${eStr}`
)
} finally {
app.exit()
}
}
}
async function initApp(): Promise<void> {
await fixUserDataPermissions()
@ -154,9 +171,6 @@ app.whenReady().then(async () => {
electronApp.setAppUserModelId('party.mihomo.app')
try {
// 首先等待初始化完成,确保配置文件和目录都已创建
await initPromise
const appConfig = await getAppConfig()
// 如果配置中没有语言设置,则使用系统语言
if (!appConfig.language) {
@ -165,19 +179,18 @@ app.whenReady().then(async () => {
appConfig.language = systemLanguage
}
await initI18n({ lng: appConfig.language })
await initPromise
} catch (e) {
showSafeErrorBox('common.error.initFailed', `${e}`)
dialog.showErrorBox(i18next.t('common.error.initFailed'), `${e}`)
app.quit()
}
try {
const [startPromise] = await startCore()
startPromise.then(async () => {
await initProfileUpdater()
// 上次是否为了开启 TUN 而重启
await checkAdminRestartForTun()
})
} catch (e) {
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)
dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
}
try {
await startMonitor()
@ -229,7 +242,7 @@ async function handleDeepLink(url: string): Promise<void> {
new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show()
break
} catch (e) {
showSafeErrorBox('profiles.error.importFailed', `${url}\n${e}`)
dialog.showErrorBox(i18next.t('profiles.error.importFailed'), `${url}\n${e}`)
}
}
}

View File

@ -19,7 +19,7 @@ import { mainWindow, showMainWindow, triggerMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy'
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager'
import { quitWithoutCore, restartCore } from '../core/manager'
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
import { t } from 'i18next'
@ -178,35 +178,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
const enable = item.checked
try {
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 } })
} else {
await patchControledMihomoConfig({ tun: { enable } })

View File

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

View File

@ -1,4 +1,4 @@
import { exec, execFile, spawn } from 'child_process'
import { exec, execFile, execSync, spawn } from 'child_process'
import { app, dialog, nativeTheme, shell } from 'electron'
import { readFile } from 'fs/promises'
import path from 'path'
@ -9,8 +9,11 @@ import {
mihomoCorePath,
overridePath,
profilePath,
resourcesDir
resourcesDir,
resourcesFilesDir,
taskDir
} from '../utils/dirs'
import { copyFileSync, writeFileSync } from 'fs'
export function getFilePath(ext: string[]): string[] | undefined {
return dialog.showOpenDialogSync({
@ -65,7 +68,56 @@ export function setNativeTheme(theme: 'system' | 'light' | 'dark'): void {
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 {
if (process.platform === 'win32') {

View File

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

View File

@ -69,10 +69,6 @@ export function mihomoCoreDir(): string {
export function mihomoCorePath(core: string): string {
const isWin = process.platform === 'win32'
// 处理 Smart 内核
if (core === 'mihomo-smart') {
return path.join(mihomoCoreDir(), `mihomo-smart${isWin ? '.exe' : ''}`)
}
return path.join(mihomoCoreDir(), `${core}${isWin ? '.exe' : ''}`)
}

View File

@ -39,25 +39,8 @@ import {
patchAppConfig,
patchControledMihomoConfig
} from '../config'
import { app, dialog } from 'electron'
import { app } from 'electron'
import { startSSIDCheck } from '../sys/ssid'
import i18next from '../../shared/i18n'
// 安全错误处理
export function safeShowErrorBox(titleKey: string, message: string): void {
let title: string
try {
title = i18next.t(titleKey)
if (!title || title === titleKey) throw new Error('Translation not ready')
} catch {
const isZh = process.env.LANG?.startsWith('zh') || process.env.LC_ALL?.startsWith('zh')
const fallbacks: Record<string, { zh: string; en: string }> = {
'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' }
}
title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error')
}
dialog.showErrorBox(title, message)
}
async function fixDataDirPermissions(): Promise<void> {
if (process.platform !== 'darwin') return
@ -82,63 +65,50 @@ 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> {
await fixDataDirPermissions()
// 按依赖顺序创建目录
const dirsToCreate = [
dataDir(),
themesDir(),
profilesDir(),
overrideDir(),
mihomoWorkDir(),
logDir(),
mihomoTestDir(),
subStoreDir()
]
for (const dir of dirsToCreate) {
try {
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true })
}
} catch (error) {
console.error(`Failed to create directory ${dir}:`, error)
throw new Error(`Failed to create directory ${dir}: ${error}`)
}
if (!existsSync(dataDir())) {
await mkdir(dataDir())
}
if (!existsSync(themesDir())) {
await mkdir(themesDir())
}
if (!existsSync(profilesDir())) {
await mkdir(profilesDir())
}
if (!existsSync(overrideDir())) {
await mkdir(overrideDir())
}
if (!existsSync(mihomoWorkDir())) {
await mkdir(mihomoWorkDir())
}
if (!existsSync(logDir())) {
await mkdir(logDir())
}
if (!existsSync(mihomoTestDir())) {
await mkdir(mihomoTestDir())
}
if (!existsSync(subStoreDir())) {
await mkdir(subStoreDir())
}
}
async function initConfig(): Promise<void> {
const configs = [
{ path: appConfigPath(), content: defaultConfig, name: 'app config' },
{ path: profileConfigPath(), content: defaultProfileConfig, name: 'profile config' },
{ path: overrideConfigPath(), content: defaultOverrideConfig, name: 'override config' },
{ path: profilePath('default'), content: defaultProfile, name: 'default profile' },
{ path: controledMihomoConfigPath(), content: defaultControledMihomoConfig, name: 'mihomo config' }
]
for (const config of configs) {
try {
if (!existsSync(config.path)) {
await writeFile(config.path, yaml.stringify(config.content))
}
} catch (error) {
console.error(`Failed to create ${config.name} at ${config.path}:`, error)
throw new Error(`Failed to create ${config.name}: ${error}`)
}
if (!existsSync(appConfigPath())) {
await writeFile(appConfigPath(), yaml.stringify(defaultConfig))
}
if (!existsSync(profileConfigPath())) {
await writeFile(profileConfigPath(), yaml.stringify(defaultProfileConfig))
}
if (!existsSync(overrideConfigPath())) {
await writeFile(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
}
if (!existsSync(profilePath('default'))) {
await writeFile(profilePath('default'), yaml.stringify(defaultProfile))
}
if (!existsSync(controledMihomoConfigPath())) {
await writeFile(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
}
}
@ -147,37 +117,13 @@ async function initFiles(): Promise<void> {
const targetPath = path.join(mihomoWorkDir(), file)
const testTargetPath = path.join(mihomoTestDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file)
try {
// 检查是否需要复制
if (existsSync(sourcePath)) {
const shouldCopyToWork = !existsSync(targetPath) || await isSourceNewer(sourcePath, targetPath)
if (shouldCopyToWork) {
await cp(sourcePath, targetPath, { recursive: true })
}
}
if (existsSync(sourcePath)) {
const shouldCopyToTest = !existsSync(testTargetPath) || await isSourceNewer(sourcePath, testTargetPath)
if (shouldCopyToTest) {
await cp(sourcePath, testTargetPath, { recursive: true })
}
}
} catch (error) {
console.error(`Failed to copy ${file}:`, error)
if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) {
throw new Error(`Failed to copy critical file ${file}: ${error}`)
}
if (!existsSync(targetPath) && existsSync(sourcePath)) {
await cp(sourcePath, targetPath, { recursive: true })
}
if (!existsSync(testTargetPath) && existsSync(sourcePath)) {
await cp(sourcePath, testTargetPath, { recursive: true })
}
}
// 确保工作目录存在
if (!existsSync(mihomoWorkDir())) {
await mkdir(mihomoWorkDir(), { recursive: true })
}
if (!existsSync(mihomoTestDir())) {
await mkdir(mihomoTestDir(), { recursive: true })
}
await Promise.all([
copy('country.mmdb'),
copy('geoip.metadb'),

View File

@ -16,9 +16,7 @@ import {
mihomoUpgrade,
mihomoUpgradeGeo,
mihomoVersion,
patchMihomoConfig,
mihomoSmartGroupWeights,
mihomoSmartFlushCache
patchMihomoConfig
} from '../core/mihomoApi'
import { checkAutoRun, disableAutoRun, enableAutoRun } from '../sys/autoRun'
import {
@ -56,17 +54,7 @@ import {
subStoreFrontendPort,
subStorePort
} from '../resolve/server'
import {
quitWithoutCore,
restartCore,
checkTunPermissions,
grantTunPermissions,
manualGrantCorePermition,
checkAdminPrivileges,
restartAsAdmin,
checkMihomoCorePermissions,
requestTunPermissions
} from '../core/manager'
import { manualGrantCorePermition, quitWithoutCore, restartCore } from '../core/manager'
import { triggerSysProxy } from '../sys/sysproxy'
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
import {
@ -153,13 +141,6 @@ export function registerIpcMainHandlers(): void {
ipcErrorWrapper(mihomoGroupDelay)(group, url)
)
ipcMain.handle('patchMihomoConfig', (_e, patch) => ipcErrorWrapper(patchMihomoConfig)(patch))
// Smart 内核 API
ipcMain.handle('mihomoSmartGroupWeights', (_e, groupName) =>
ipcErrorWrapper(mihomoSmartGroupWeights)(groupName)
)
ipcMain.handle('mihomoSmartFlushCache', (_e, configName) =>
ipcErrorWrapper(mihomoSmartFlushCache)(configName)
)
ipcMain.handle('checkAutoRun', ipcErrorWrapper(checkAutoRun))
ipcMain.handle('enableAutoRun', ipcErrorWrapper(enableAutoRun))
ipcMain.handle('disableAutoRun', ipcErrorWrapper(disableAutoRun))
@ -196,13 +177,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
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('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
@ -277,18 +251,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('alert', (_e, msg) => {
dialog.showErrorBox('Mihomo Party', msg)
})
ipcMain.handle('showDetailedError', (_e, title, message) => {
dialog.showErrorBox(title, message)
})
ipcMain.handle('getSmartOverrideContent', async () => {
const { getOverrideItem } = await import('../config')
try {
const override = await getOverrideItem('smart-core-override')
return override?.file || null
} catch (error) {
return null
}
})
ipcMain.handle('resetAppConfig', resetAppConfig)
ipcMain.handle('relaunchApp', () => {
app.relaunch()

View File

@ -1,10 +1,5 @@
export const defaultConfig: IAppConfig = {
core: 'mihomo',
enableSmartCore: true,
enableSmartOverride: true,
smartCoreUseLightGBM: false,
smartCoreCollectData: false,
smartCoreStrategy: 'sticky-sessions',
silentStart: false,
appTheme: 'system',
useWindowFrame: false,
@ -79,7 +74,6 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
'fake-ip-filter': ['*', '+.lan', '+.local', 'time.*.com', 'ntp.*.com', '+.market.xiaomi.com'],
'use-hosts': false,
'use-system-hosts': false,
'default-nameserver': ['tls://223.5.5.5'],
nameserver: ['https://120.53.53.53/dns-query', 'https://223.5.5.5/dns-query'],
'proxy-server-nameserver': ['https://120.53.53.53/dns-query', 'https://223.5.5.5/dns-query'],
'direct-nameserver': []

View File

@ -373,13 +373,13 @@ const App: React.FC = () => {
setSiderWidthValue(e.clientX)
}
}}
className={`w-full h-screen flex ${resizing ? 'cursor-ew-resize' : ''}`}
className={`w-full h-[100vh] flex ${resizing ? 'cursor-ew-resize' : ''}`}
>
{siderWidthValue === narrowWidth ? (
<div style={{ width: `${narrowWidth}px` }} className="side h-full">
<div className="app-drag flex justify-center items-center z-40 bg-transparent h-[49px]">
{platform !== 'darwin' && (
<MihomoIcon className="h-[32px] leading-[32px] text-lg mx-px" />
<MihomoIcon className="h-[32px] leading-[32px] text-lg mx-[1px]" />
)}
<UpdaterButton iconOnly={true} />
</div>
@ -417,7 +417,7 @@ const App: React.FC = () => {
className={`flex justify-between p-2 ${!useWindowFrame && platform === 'darwin' ? 'ml-[60px]' : ''}`}
>
<div className="flex ml-1">
<MihomoIcon className="h-[32px] leading-[32px] text-lg mx-px" />
<MihomoIcon className="h-[32px] leading-[32px] text-lg mx-[1px]" />
<h3 className="text-lg font-bold leading-[32px]">ihomo Party</h3>
</div>
<UpdaterButton />

View File

@ -59,9 +59,9 @@ const FloatingApp: React.FC = () => {
}, [])
return (
<div className="app-drag h-screen w-screen overflow-hidden">
<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="app-drag h-[100vh] w-[100vw] overflow-hidden">
<div className="floating-bg border-1 border-divider flex rounded-full bg-content1 h-[calc(100%-2px)] w-[calc(100%-2px)]">
<div className="flex justify-center items-center h-[100%] aspect-square">
<div
onContextMenu={(e) => {
e.preventDefault()
@ -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 h-[calc(100%-4px)] aspect-square`}
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`}
>
<MihomoIcon className="floating-icon text-primary-foreground h-full leading-full text-[22px] mx-auto" />
</div>

View File

@ -1,8 +1,6 @@
@import 'tailwindcss';
@plugin './hero.ts';
@source '../**/*.{js,ts,jsx,tsx}';
@source '../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@tailwind base;
@tailwind components;
@tailwind utilities;
.floating-text {
font-family:

View File

@ -1,2 +0,0 @@
import { heroui } from '@heroui/react'
export default heroui()

View File

@ -1,12 +1,6 @@
@import 'tailwindcss';
@plugin './hero.ts';
@source '../**/*.{js,ts,jsx,tsx}';
@source '../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@theme {
--default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Noto Color Emoji';

View File

@ -1,7 +1,5 @@
.border-switch {
overflow: hidden;
}
.border-switch input[type='checkbox'] {
width: 100%;
input[type='checkbox'] {
width: 100%;
}
}

View File

@ -10,7 +10,7 @@ import {
} from '@heroui/react'
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
import dayjs from '@renderer/utils/dayjs'
import React, { Key, useMemo, useState } from 'react'
import React, { Key, useEffect, useMemo, useState } from 'react'
import EditFileModal from './edit-file-modal'
import EditInfoModal from './edit-info-modal'
import { useSortable } from '@dnd-kit/sortable'
@ -54,7 +54,7 @@ const OverrideItem: React.FC<Props> = (props) => {
id: info.id
})
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const [dropdownOpen, setDropdownOpen] = useState(false)
const [disableOpen, setDisableOpen] = useState(false)
const menuItems: MenuItem[] = useMemo(() => {
const list = [
{
@ -124,13 +124,17 @@ const OverrideItem: React.FC<Props> = (props) => {
}
}
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setDropdownOpen(true)
}
useEffect(() => {
if (isDragging) {
setTimeout(() => {
setDisableOpen(true)
}, 200)
} else {
setTimeout(() => {
setDisableOpen(false)
}, 200)
}
}, [isDragging])
return (
<div
@ -160,21 +164,13 @@ const OverrideItem: React.FC<Props> = (props) => {
<Card
as="div"
fullWidth
className="cursor-pointer"
onContextMenu={handleContextMenu}
onDoubleClick={(e) => {
if ((e.target as Element)?.closest('button, [role="menu"], [role="menuitem"]')) {
return
}
isPressable
onPress={() => {
if (disableOpen) return
setOpenFileEditor(true)
}}
>
<div
ref={setNodeRef}
{...attributes}
{...listeners}
className="h-full w-full"
>
<div ref={setNodeRef} {...attributes} {...listeners} className="h-full w-full">
<CardBody>
<div className="flex justify-between h-[32px]">
<h3
@ -210,10 +206,7 @@ const OverrideItem: React.FC<Props> = (props) => {
</Button>
)}
<Dropdown
isOpen={dropdownOpen}
onOpenChange={setDropdownOpen}
>
<Dropdown>
<DropdownTrigger>
<Button isIconOnly size="sm" variant="light" color="default">
<IoMdMore color="default" className={`text-[24px]`} />

View File

@ -14,7 +14,7 @@ import {
import { calcPercent, calcTraffic } from '@renderer/utils/calc'
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
import dayjs from '@renderer/utils/dayjs'
import React, { Key, useMemo, useState } from 'react'
import React, { Key, useEffect, useMemo, useState } from 'react'
import EditFileModal from './edit-file-modal'
import EditInfoModal from './edit-info-modal'
import { useSortable } from '@dnd-kit/sortable'
@ -72,8 +72,7 @@ const ProfileItem: React.FC<Props> = (props) => {
id: info.id
})
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const [isActuallyDragging, setIsActuallyDragging] = useState(false)
const [clickStartPos, setClickStartPos] = useState<{ x: number; y: number } | null>(null)
const [disableSelect, setDisableSelect] = useState(false)
const menuItems: MenuItem[] = useMemo(() => {
const list = [
@ -151,46 +150,19 @@ const ProfileItem: React.FC<Props> = (props) => {
setDropdownOpen(true)
}
const handleMouseDown = (e: React.MouseEvent) => {
if (e.button === 0) {
setClickStartPos({ x: e.clientX, y: e.clientY })
setIsActuallyDragging(false)
useEffect(() => {
if (isDragging) {
setTimeout(() => {
setDisableSelect(true)
}, 200)
} else {
setTimeout(() => {
setDisableSelect(false)
}, 200)
}
}
}, [isDragging])
const handleMouseMove = (e: React.MouseEvent) => {
if (!clickStartPos) return
const dx = e.clientX - clickStartPos.x
const dy = e.clientY - clickStartPos.y
if (dx * dx + dy * dy > 25) {
setIsActuallyDragging(true)
}
}
const handleMouseUp = (e: React.MouseEvent) => {
const cleanup = () => {
setClickStartPos(null)
setTimeout(() => setIsActuallyDragging(false), 100)
}
// 只处理左键点击
if (e.button !== 0) return cleanup()
// 检查功能按钮点击
const target = e.target as Element
if (target?.closest('button, [role="menu"], [role="menuitem"], [data-slot="trigger"]')) {
return cleanup()
}
// 处理卡片选中
if (!isActuallyDragging && !isDragging && clickStartPos) {
setSelecting(true)
onPress().finally(() => setSelecting(false))
}
cleanup()
}
return (
<div
@ -214,19 +186,18 @@ const ProfileItem: React.FC<Props> = (props) => {
<Card
as="div"
fullWidth
isPressable={false}
isPressable
onPress={() => {
if (disableSelect) return
setSelecting(true)
onPress().finally(() => {
setSelecting(false)
})
}}
onContextMenu={handleContextMenu}
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''} cursor-pointer`}
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`}
>
<div
ref={setNodeRef}
{...attributes}
{...listeners}
className="w-full h-full"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
<div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full">
<CardBody className="pb-1">
<div className="flex justify-between h-[32px]">
<h3
@ -263,12 +234,7 @@ const ProfileItem: React.FC<Props> = (props) => {
onOpenChange={setDropdownOpen}
>
<DropdownTrigger>
<Button
isIconOnly
size="sm"
variant="light"
color="default"
>
<Button isIconOnly size="sm" variant="light" color="default">
<IoMdMore
color="default"
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}

View File

@ -60,7 +60,7 @@ const ProxyItem: React.FC<Props> = (props) => {
onPress={() => onSelect(group.name, proxy.name)}
isPressable
fullWidth
shadow="xs"
shadow="sm"
className={`${
fixed
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'

View File

@ -4,7 +4,7 @@ import SettingItem from '../base/base-setting-item'
import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import debounce from '@renderer/utils/debounce'
import { getGistUrl, restartCore } from '@renderer/utils/ipc'
import { getGistUrl, patchControledMihomoConfig, restartCore } from '@renderer/utils/ipc'
import { MdDeleteForever } from 'react-icons/md'
import { BiCopy } from 'react-icons/bi'
import { IoIosHelpCircle } from 'react-icons/io'
@ -16,6 +16,8 @@ const MihomoConfig: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig()
const {
diffWorkDir = false,
controlDns = true,
controlSniff = true,
delayTestConcurrency,
delayTestTimeout,
githubToken = '',
@ -191,8 +193,36 @@ const MihomoConfig: React.FC = () => {
}}
/>
</SettingItem>
<SettingItem title={t('mihomo.controlDns')} divider>
<Switch
size="sm"
isSelected={controlDns}
onValueChange={async (v) => {
try {
await patchAppConfig({ controlDns: v })
await patchControledMihomoConfig({})
await restartCore()
} catch (e) {
alert(e)
}
}}
/>
</SettingItem>
<SettingItem title={t('mihomo.controlSniff')} divider>
<Switch
size="sm"
isSelected={controlSniff}
onValueChange={async (v) => {
try {
await patchAppConfig({ controlSniff: v })
await patchControledMihomoConfig({})
await restartCore()
} catch (e) {
alert(e)
}
}}
/>
</SettingItem>
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
<Switch
size="sm"

View File

@ -2,29 +2,16 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import React, { useEffect, useState, useMemo } from 'react'
import React, { useEffect, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { IoLink } from 'react-icons/io5'
import { useTheme } from 'next-themes'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { platform } from '@renderer/utils/init'
import { Line } from 'react-chartjs-2'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Filler,
ChartOptions,
ScriptableContext
} from 'chart.js'
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
import { useTranslation } from 'react-i18next'
// 注册 Chart.js 组件
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler)
let currentUpload: number | undefined = undefined
let currentDownload: number | undefined = undefined
let hasShowTraffic = false
@ -34,9 +21,10 @@ interface Props {
iconOnly?: boolean
}
const ConnCard: React.FC<Props> = (props) => {
const { theme = 'system', systemTheme = 'dark' } = useTheme()
const { iconOnly } = props
const { appConfig } = useAppConfig()
const { showTraffic = false, connectionCardStatus = 'col-span-2' } = appConfig || {}
const { showTraffic = false, connectionCardStatus = 'col-span-2', customTheme } = appConfig || {}
const location = useLocation()
const navigate = useNavigate()
const match = location.pathname.includes('/connections')
@ -55,69 +43,25 @@ const ConnCard: React.FC<Props> = (props) => {
id: 'connection'
})
const [series, setSeries] = useState(Array(10).fill(0))
const [chartColor, setChartColor] = useState('rgba(255,255,255)')
// Chart.js 配置
const chartData = useMemo(() => {
return {
labels: Array(10).fill(''),
datasets: [
{
data: series,
fill: true,
backgroundColor: (context: ScriptableContext<'line'>) => {
const chart = context.chart
const { ctx, chartArea } = chart
if (!chartArea) {
return 'transparent'
}
useEffect(() => {
setChartColor(
match
? `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-primary-foreground')})`
: `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-foreground')})`
)
}, [theme, systemTheme, match])
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom)
// 颜色处理
const isMatch = location.pathname.includes('/connections')
const baseColor = isMatch ? '6, 182, 212' : '161, 161, 170' // primary vs foreground 的近似 RGB 值
gradient.addColorStop(0, `rgba(${baseColor}, 0.8)`)
gradient.addColorStop(1, `rgba(${baseColor}, 0)`)
return gradient
},
borderColor: 'transparent',
pointRadius: 0,
pointHoverRadius: 0,
tension: 0.4
}
]
}
}, [series, location.pathname])
const chartOptions: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
display: false
},
y: {
display: false
}
},
elements: {
line: {
borderWidth: 0
}
},
interaction: {
intersect: false
},
animation: {
duration: 0
}
}
useEffect(() => {
setTimeout(() => {
setChartColor(
match
? `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-primary-foreground')})`
: `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-foreground')})`
)
}, 200)
}, [customTheme])
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
useEffect(() => {
@ -224,9 +168,30 @@ const ConnCard: React.FC<Props> = (props) => {
</h3>
</CardFooter>
</Card>
<div className="w-full h-full absolute top-0 left-0 pointer-events-none overflow-hidden rounded-[14px]">
<Line data={chartData} options={chartOptions} />
</div>
<ResponsiveContainer
height="100%"
width="100%"
className="w-full h-full absolute top-0 left-0 pointer-events-none overflow-hidden rounded-[14px]"
>
<AreaChart
data={series.map((v) => ({ traffic: v }))}
margin={{ top: 50, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={chartColor} stopOpacity={0.8} />
<stop offset="100%" stopColor={chartColor} stopOpacity={0} />
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey="traffic"
stroke="none"
fill="url(#gradient)"
/>
</AreaChart>
</ResponsiveContainer>
</>
) : (
<Card

View File

@ -3,7 +3,7 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import BorderSwitch from '@renderer/components/base/border-swtich'
import { LuServer } from 'react-icons/lu'
import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore } from '@renderer/utils/ipc'
import { patchMihomoConfig } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -15,13 +15,15 @@ interface Props {
}
const DNSCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
const location = useLocation()
const navigate = useNavigate()
const match = location.pathname.includes('/dns')
const { patchControledMihomoConfig } = useControledMihomoConfig()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { dns, tun } = controledMihomoConfig || {}
const { enable = true } = dns || {}
const {
attributes,
listeners,
@ -33,19 +35,14 @@ const DNSCard: React.FC<Props> = (props) => {
id: 'dns'
})
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (controlDns: boolean): Promise<void> => {
try {
await patchAppConfig({ controlDns })
await patchControledMihomoConfig({})
await restartCore()
} catch (e) {
alert(e)
}
const onChange = async (enable: boolean): Promise<void> => {
await patchControledMihomoConfig({ dns: { enable } })
await patchMihomoConfig({ dns: { enable } })
}
if (iconOnly) {
return (
<div className={`${dnsCardStatus} flex justify-center`}>
<div className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} flex justify-center`}>
<Tooltip content={t('sider.cards.dns')} placement="right">
<Button
size="sm"
@ -71,7 +68,7 @@ const DNSCard: React.FC<Props> = (props) => {
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className={`${dnsCardStatus} dns-card`}
className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} dns-card`}
>
<Card
fullWidth
@ -93,9 +90,9 @@ const DNSCard: React.FC<Props> = (props) => {
/>
</Button>
<BorderSwitch
isShowBorder={match && controlDns}
isSelected={controlDns}
isDisabled={false}
isShowBorder={match && enable}
isSelected={enable}
isDisabled={tun?.enable}
onValueChange={onChange}
/>
</div>

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
import BorderSwitch from '@renderer/components/base/border-swtich'
import { RiScan2Fill } from 'react-icons/ri'
import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore } from '@renderer/utils/ipc'
import { patchMihomoConfig } from '@renderer/utils/ipc'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
@ -15,13 +15,15 @@ interface Props {
}
const SniffCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
const { appConfig } = useAppConfig()
const { iconOnly } = props
const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {}
const location = useLocation()
const navigate = useNavigate()
const match = location.pathname.includes('/sniffer')
const { patchControledMihomoConfig } = useControledMihomoConfig()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { sniffer } = controledMihomoConfig || {}
const { enable } = sniffer || {}
const {
attributes,
listeners,
@ -33,19 +35,14 @@ const SniffCard: React.FC<Props> = (props) => {
id: 'sniff'
})
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (controlSniff: boolean): Promise<void> => {
try {
await patchAppConfig({ controlSniff })
await patchControledMihomoConfig({})
await restartCore()
} catch (e) {
alert(e)
}
const onChange = async (enable: boolean): Promise<void> => {
await patchControledMihomoConfig({ sniffer: { enable } })
await patchMihomoConfig({ sniffer: { enable } })
}
if (iconOnly) {
return (
<div className={`${sniffCardStatus} flex justify-center`}>
<div className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''} flex justify-center`}>
<Tooltip content={t('sider.cards.sniff')} placement="right">
<Button
size="sm"
@ -71,7 +68,7 @@ const SniffCard: React.FC<Props> = (props) => {
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className={`${sniffCardStatus} sniff-card`}
className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''} sniff-card`}
>
<Card
fullWidth
@ -94,8 +91,8 @@ const SniffCard: React.FC<Props> = (props) => {
/>
</Button>
<BorderSwitch
isShowBorder={match && controlSniff}
isSelected={controlSniff}
isShowBorder={match && enable}
isSelected={enable}
onValueChange={onChange}
/>
</div>

View File

@ -38,42 +38,6 @@ const TunSwitcher: React.FC<Props> = (props) => {
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (enable: boolean): Promise<void> => {
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 } })
} else {
await patchControledMihomoConfig({ tun: { enable } })

View File

@ -71,34 +71,13 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
}
const changeCurrentProfile = async (id: string): Promise<void> => {
if (profileConfig?.current === id) {
return
}
// 乐观更新:立即更新 UI 状态,提供即时反馈
if (profileConfig) {
const optimisticUpdate = { ...profileConfig, current: id }
mutateProfileConfig(optimisticUpdate, false)
}
try {
// 异步执行后台切换,不阻塞 UI
change(id).then(() => {
window.electron.ipcRenderer.send('updateTrayMenu')
mutateProfileConfig()
}).catch((e) => {
const errorMsg = e?.message || String(e)
// 处理 IPC 超时错误
if (errorMsg.includes('reply was never sent')) {
setTimeout(() => mutateProfileConfig(), 1000)
} else {
alert(`切换 Profile 失败: ${errorMsg}`)
mutateProfileConfig()
}
})
await change(id)
} catch (e) {
alert(`切换 Profile 失败: ${e}`)
alert(e)
} finally {
mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu')
}
}

View File

@ -98,6 +98,7 @@
"mihomo.cpuPriority.low": "Low",
"mihomo.workDir.title": "Separate Work Directory for Different Subscriptions",
"mihomo.workDir.tooltip": "Enable to avoid conflicts when different subscriptions have proxy groups with the same name",
"mihomo.controlDns": "Control DNS Settings",
"mihomo.controlSniff": "Control Domain Sniffing",
"mihomo.autoCloseConnection": "Auto Close Connection",
"mihomo.pauseSSID.title": "Direct Connection for Specific WiFi SSIDs",
@ -113,15 +114,6 @@
"mihomo.selectCoreVersion": "Select Core Version",
"mihomo.stableVersion": "Stable",
"mihomo.alphaVersion": "Alpha",
"mihomo.smartVersion": "Smart",
"mihomo.enableSmartCore": "Enable Smart Core",
"mihomo.enableSmartOverride": "Use Auto Smart Rule Override",
"mihomo.smartOverrideTooltip": "Use Party's built-in smart override script to extract all nodes from subscriptions and replace default rules. Perfect for users who don't want to tinker, one-click functionality; if using global mode, please select the node named 'Smart Group'",
"mihomo.smartCoreUseLightGBM": "Use LightGBM",
"mihomo.smartCoreCollectData": "Collect Data",
"mihomo.smartCoreStrategy": "Strategy Mode",
"mihomo.smartCoreStrategyStickySession": "Sticky Sessions",
"mihomo.smartCoreStrategyRoundRobin": "Round Robin",
"mihomo.mixedPort": "Mixed Port",
"mihomo.confirm": "Confirm",
"mihomo.socksPort": "Socks Port",
@ -215,8 +207,8 @@
"sider.cards.override": "Override",
"sider.cards.connections": "Connections",
"sider.cards.core": "Core Settings",
"sider.cards.dns": "DNS Override",
"sider.cards.sniff": "Sniff OVRD",
"sider.cards.dns": "DNS",
"sider.cards.sniff": "Sniffing",
"sider.cards.logs": "Logs",
"sider.cards.substore": "Sub-Store",
"sider.cards.config": "Runtime Config",
@ -269,7 +261,6 @@
"proxies.search.placeholder": "Search Proxies",
"proxies.locate": "Locate Current Proxy",
"sniffer.title": "Domain Sniffing Settings",
"sniffer.enable": "Enable Domain Sniffing",
"sniffer.parsePureIP": "Sniff Unmapped IP Addresses",
"sniffer.forceDNSMapping": "Sniff Real IP Mappings",
"sniffer.overrideDestination": "Override Connection Address",
@ -285,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "Example: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "Skip Source Address Sniffing",
"sniffer.skipSrcAddress.placeholder": "Example: 192.168.1.1/24",
"sniffer.saveOnly": "Save Only",
"sysproxy.title": "System Proxy",
"sysproxy.host.title": "Proxy Host",
"sysproxy.host.placeholder": "Default 127.0.0.1, do not modify unless necessary",
@ -316,13 +306,7 @@
"tun.notifications.coreAuthSuccess": "Core Authorization Successful",
"tun.notifications.firewallResetSuccess": "Firewall Reset Successful",
"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.enable": "Enable DNS",
"dns.enhancedMode.title": "Domain Mapping Mode",
"dns.enhancedMode.fakeIp": "Fake IP",
"dns.enhancedMode.redirHost": "Real IP",
@ -349,7 +333,6 @@
"dns.customHosts.list": "Hosts List",
"dns.customHosts.domainPlaceholder": "Domain",
"dns.customHosts.valuePlaceholder": "Domain or IP",
"dns.saveOnly": "Save Only",
"profiles.title": "Profile Management",
"profiles.updateAll": "Update All Profiles",
"profiles.useProxy": "Proxy",
@ -520,8 +503,8 @@
"guide.tunSetting.description": "Here you can modify virtual network card settings. Mihomo Party has theoretically solved all permission issues. If your virtual network card is still not working, try resetting the firewall (Windows) or manually authorizing the core (MacOS/Linux) and then restart the core.",
"guide.override.title": "Override",
"guide.override.description": "Mihomo Party provides powerful override functionality to customize your imported subscription configurations, such as adding rules and customizing proxy groups. You can directly import override files written by others or write your own. <b>Remember to enable the override file on the subscription you want to override</b>. For override file syntax, please refer to the <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">official documentation</a>.",
"guide.dns.title": "DNS OVRD",
"guide.dns.description": "The software defaults to using the application's DNS settings to override the subscription configuration. If you need to use the DNS settings from the subscription configuration, please disable this feature. The same applies to domain sniffing.",
"guide.dns.title": "DNS",
"guide.dns.description": "The software takes control of the core's DNS settings by default. If you need to use the DNS settings from your subscription configuration, you can disable 'Control DNS Settings' in the application settings. The same applies to domain sniffing.",
"guide.end.title": "Tutorial Complete",
"guide.end.description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news."
}

View File

@ -101,6 +101,7 @@
"mihomo.cpuPriority.low": "پایین",
"mihomo.workDir.title": "استفاده از پوشه کاری مجزا برای اشتراک‌های مختلف",
"mihomo.workDir.tooltip": "برای جلوگیری از تداخل گروه‌های پراکسی با نام یکسان در اشتراک‌های مختلف",
"mihomo.controlDns": "کنترل تنظیمات DNS",
"mihomo.controlSniff": "کنترل تشخیص دامنه",
"mihomo.autoCloseConnection": "بستن خودکار اتصال",
"mihomo.pauseSSID.title": "اتصال مستقیم برای SSIDهای خاص",
@ -206,8 +207,8 @@
"sider.cards.override": "جایگزینی",
"sider.cards.connections": "اتصالات",
"sider.cards.core": "تنظیمات هسته",
"sider.cards.dns": "بازنویسی دی‌ان‌اس",
"sider.cards.sniff": "لغو بو کشیدن",
"sider.cards.dns": "DNS",
"sider.cards.sniff": "تشخیص دامنه",
"sider.cards.logs": "گزارش‌ها",
"sider.cards.substore": "ساب استور",
"sider.cards.config": "پیکربندی اجرا",
@ -260,7 +261,6 @@
"proxies.search.placeholder": "جستجوی پراکسی‌ها",
"proxies.locate": "یافتن پراکسی فعلی",
"sniffer.title": "تنظیمات تشخیص دامنه",
"sniffer.enable": "فعال کردن قابلیت شنود دامنه",
"sniffer.parsePureIP": "تشخیص آدرس‌های IP بدون نگاشت",
"sniffer.forceDNSMapping": "تشخیص نگاشت‌های IP واقعی",
"sniffer.overrideDestination": "جایگزینی آدرس اتصال",
@ -276,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "مثال: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "رد کردن تشخیص آدرس مبدا",
"sniffer.skipSrcAddress.placeholder": "مثال: 192.168.1.1/24",
"sniffer.saveOnly": "فقط ذخیره",
"sysproxy.title": "پراکسی سیستم",
"sysproxy.host.title": "میزبان پراکسی",
"sysproxy.host.placeholder": "پیش‌فرض 127.0.0.1، در صورت عدم نیاز تغییر ندهید",
@ -308,7 +307,6 @@
"tun.notifications.firewallResetSuccess": "بازنشانی دیوار آتش با موفقیت انجام شد",
"tun.error.tunPermissionDenied": "راه‌اندازی رابط TUN با شکست مواجه شد، لطفا به صورت دستی به هسته مجوز دهید",
"dns.title": "تنظیمات DNS",
"dns.enable": "فعال‌سازی DNS",
"dns.enhancedMode.title": "حالت نگاشت دامنه",
"dns.enhancedMode.fakeIp": "IP جعلی",
"dns.enhancedMode.redirHost": "IP واقعی",
@ -335,7 +333,6 @@
"dns.customHosts.list": "لیست Hosts",
"dns.customHosts.domainPlaceholder": "دامنه",
"dns.customHosts.valuePlaceholder": "دامنه یا IP",
"dns.saveOnly": "فقط ذخیره",
"profiles.title": "مدیریت پروفایل",
"profiles.updateAll": "به‌روزرسانی همه پروفایل‌ها",
"profiles.useProxy": "پراکسی",
@ -507,7 +504,7 @@
"guide.override.title": "Override",
"guide.override.description": "میهومو پارتی قابلیت‌های قدرتمند جایگزینی را برای سفارشی‌سازی پیکربندی‌های اشتراک وارد شده، مانند افزودن قوانین و سفارشی‌سازی گروه‌های پراکسی ارائه می‌دهد. می‌توانید مستقیماً فایل‌های جایگزینی نوشته شده توسط دیگران را وارد کنید یا فایل‌های خود را بنویسید. <b>فراموش نکنید که فایل جایگزینی را روی اشتراکی که می‌خواهید جایگزین کنید فعال کنید</b>. برای نحو فایل جایگزینی، لطفاً به <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">مستندات رسمی</a> مراجعه کنید.",
"guide.dns.title": "DNS",
"guide.dns.description": "نرم‌افزار به‌طور پیش‌فرض از تنظیمات DNS برنامه برای بازنویسی پیکربندی اشتراک استفاده می‌کند. اگر نیاز به استفاده از تنظیمات DNS موجود در پیکربندی اشتراک دارید، لطفاً این قابلیت را غیرفعال کنید. همین موضوع برای شناسایی دامنه نیز صدق می‌کند.",
"guide.dns.description": "نرم‌افزار به طور پیش‌فرض کنترل تنظیمات DNS هسته را در دست می‌گیرد. اگر نیاز دارید از تنظیمات DNS پیکربندی اشتراک خود استفاده کنید، می‌توانید 'کنترل تنظیمات DNS' را در تنظیمات برنامه غیرفعال کنید. همین مورد برای تشخیص دامنه نیز صدق می‌کند.",
"guide.end.title": "پایان آموزش",
"guide.end.description": "اکنون که با استفاده اساسی از نرم‌افزار آشنا شدید، اشتراک خود را وارد کنید و از آن استفاده کنید. لذت ببرید!\nهمچنین می‌توانید برای آخرین اخبار به <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">گروه تلگرام</a> ما بپیوندید."
}

View File

@ -101,6 +101,7 @@
"mihomo.cpuPriority.low": "Низкий",
"mihomo.workDir.title": "Отдельные рабочие каталоги для разных подписок",
"mihomo.workDir.tooltip": "Включите для избежания конфликтов при наличии групп прокси с одинаковыми именами в разных подписках",
"mihomo.controlDns": "Управление настройками DNS",
"mihomo.controlSniff": "Управление сниффингом доменов",
"mihomo.autoCloseConnection": "Автозакрытие соединений",
"mihomo.pauseSSID.title": "Прямое подключение для определённых WiFi SSID",
@ -206,8 +207,8 @@
"sider.cards.override": "Переопределения",
"sider.cards.connections": "Подключения",
"sider.cards.core": "Настройки ядра",
"sider.cards.dns": "Переопределение DNS",
"sider.cards.sniff": "переопределение сниффинга",
"sider.cards.dns": "DNS",
"sider.cards.sniff": "Анализ трафика",
"sider.cards.logs": "Журналы",
"sider.cards.substore": "Sub-Store",
"sider.cards.config": "Конфигурация",
@ -260,7 +261,6 @@
"proxies.search.placeholder": "Поиск прокси",
"proxies.locate": "Найти текущий прокси",
"sniffer.title": "Настройки анализа доменов",
"sniffer.enable": "Включить анализ домена",
"sniffer.parsePureIP": "Анализировать немаппированные IP-адреса",
"sniffer.forceDNSMapping": "Анализировать реальные IP-маппинги",
"sniffer.overrideDestination": "Переопределить адрес подключения",
@ -276,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "Пример: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "Пропустить анализ исходных адресов",
"sniffer.skipSrcAddress.placeholder": "Пример: 192.168.1.1/24",
"sniffer.saveOnly": "Только сохранить",
"sysproxy.title": "Системный прокси",
"sysproxy.host.title": "Хост прокси",
"sysproxy.host.placeholder": "По умолчанию 127.0.0.1, не изменяйте без необходимости",
@ -308,7 +307,6 @@
"tun.notifications.firewallResetSuccess": "Брандмауэр успешно сброшен",
"tun.error.tunPermissionDenied": "Ошибка запуска TUN, попробуйте вручную предоставить разрешения ядру",
"dns.title": "Настройки DNS",
"dns.enable": "Включить DNS",
"dns.enhancedMode.title": "Режим маппинга доменов",
"dns.enhancedMode.fakeIp": "Фиктивный IP",
"dns.enhancedMode.redirHost": "Реальный IP",
@ -335,7 +333,6 @@
"dns.customHosts.list": "Список Hosts",
"dns.customHosts.domainPlaceholder": "Домен",
"dns.customHosts.valuePlaceholder": "Домен или IP",
"dns.saveOnly": "Только сохранить",
"profiles.title": "Управление профилями",
"profiles.updateAll": "Обновить все профили",
"profiles.useProxy": "Прокси",
@ -507,7 +504,7 @@
"guide.override.title": "Переопределение",
"guide.override.description": "Mihomo Party предоставляет мощную функцию переопределения для настройки импортированных конфигураций подписки, таких как добавление правил и настройка групп прокси. Вы можете напрямую импортировать файлы переопределения, написанные другими, или написать свои собственные. <b>Не забудьте включить файл переопределения для подписки, которую вы хотите переопределить</b>. Синтаксис файла переопределения см. в <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">официальной документации</a>.",
"guide.dns.title": "DNS",
"guide.dns.description": рограммное обеспечение по умолчанию использует настройки DNS приложения для переопределения конфигурации подписки. Если вам нужно использовать настройки DNS из конфигурации подписки, пожалуйста, отключите эту функцию. То же самое относится к определению домена.",
"guide.dns.description": о умолчанию программа контролирует настройки DNS ядра. Если вам нужно использовать настройки DNS из конфигурации подписки, вы можете отключить 'Контроль настроек DNS' в настройках приложения. То же самое относится к сниффингу доменов.",
"guide.end.title": "Руководство завершено",
"guide.end.description": "Теперь, когда вы понимаете основы использования программы, импортируйте свою подписку и начните использовать ее. Приятного использования!\nВы также можете присоединиться к нашей официальной <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">группе Telegram</a> для получения последних новостей."
}

View File

@ -101,6 +101,7 @@
"mihomo.cpuPriority.low": "低",
"mihomo.workDir.title": "不同订阅使用独立工作目录",
"mihomo.workDir.tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突",
"mihomo.controlDns": "控制 DNS 设置",
"mihomo.controlSniff": "控制域名嗅探",
"mihomo.autoCloseConnection": "自动关闭连接",
"mihomo.pauseSSID.title": "指定 WiFi SSID 直连",
@ -113,15 +114,6 @@
"mihomo.selectCoreVersion": "选择内核版本",
"mihomo.stableVersion": "稳定版",
"mihomo.alphaVersion": "预览版",
"mihomo.smartVersion": "Smart 版",
"mihomo.enableSmartCore": "启用 Smart 内核",
"mihomo.enableSmartOverride": "使用自动 Smart 规则覆写",
"mihomo.smartOverrideTooltip": "使用 Party 自带的智能覆写脚本提取订阅中的所有节点并替换默认规则适合不想折腾的用户功能一键生效如果使用全局模式请选择名称为“Smart Group 的节点",
"mihomo.smartCoreUseLightGBM": "使用 LightGBM",
"mihomo.smartCoreCollectData": "收集数据",
"mihomo.smartCoreStrategy": "策略模式",
"mihomo.smartCoreStrategyStickySession": "粘性会话",
"mihomo.smartCoreStrategyRoundRobin": "轮询",
"mihomo.mixedPort": "混合端口",
"mihomo.confirm": "确认",
"mihomo.socksPort": "Socks 端口",
@ -215,8 +207,8 @@
"sider.cards.override": "覆写",
"sider.cards.connections": "连接",
"sider.cards.core": "内核设置",
"sider.cards.dns": "DNS覆写",
"sider.cards.sniff": "嗅探覆写",
"sider.cards.dns": "DNS",
"sider.cards.sniff": "域名嗅探",
"sider.cards.logs": "日志",
"sider.cards.substore": "Sub-Store",
"sider.cards.config": "运行时配置",
@ -269,7 +261,6 @@
"proxies.search.placeholder": "搜索节点",
"proxies.locate": "定位到当前节点",
"sniffer.title": "域名嗅探设置",
"sniffer.enable": "启用域名嗅探",
"sniffer.parsePureIP": "对未映射 IP 地址嗅探",
"sniffer.forceDNSMapping": "对真实 IP 映射嗅探",
"sniffer.overrideDestination": "覆盖连接地址",
@ -285,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "例1.1.1.1/32",
"sniffer.skipSrcAddress.title": "跳过来源地址嗅探",
"sniffer.skipSrcAddress.placeholder": "例192.168.1.1/24",
"sniffer.saveOnly": "仅保存",
"sysproxy.title": "系统代理",
"sysproxy.host.title": "代理主机",
"sysproxy.host.placeholder": "默认 127.0.0.1 若无特殊需求请勿修改",
@ -316,13 +306,7 @@
"tun.notifications.coreAuthSuccess": "内核授权成功",
"tun.notifications.firewallResetSuccess": "防火墙重设成功",
"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.enable": "启用 DNS",
"dns.enhancedMode.title": "域名映射模式",
"dns.enhancedMode.fakeIp": "虚假 IP",
"dns.enhancedMode.redirHost": "真实 IP",
@ -349,7 +333,6 @@
"dns.customHosts.list": "Hosts 列表",
"dns.customHosts.domainPlaceholder": "域名",
"dns.customHosts.valuePlaceholder": "域名或 IP",
"dns.saveOnly": "仅保存",
"profiles.title": "订阅管理",
"profiles.updateAll": "更新全部订阅",
"profiles.useProxy": "代理",
@ -521,7 +504,7 @@
"guide.override.title": "覆写",
"guide.override.description": "Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">官方文档</a>",
"guide.dns.title": "DNS",
"guide.dns.description": "软件默认使用应用的 DNS 设置覆盖订阅配置,如果您需要使用订阅配置中的 DNS 设置,请关闭此功能,域名嗅探同理",
"guide.dns.description": "软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭\"接管 DNS 设置\",域名嗅探同理",
"guide.end.title": "教程结束",
"guide.end.description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群组</a> 获取最新资讯"
}

View File

@ -273,7 +273,7 @@ const Connections: React.FC = () => {
</div>
<Divider />
</div>
<div className="h-[calc(100vh-100px)] mt-px">
<div className="h-[calc(100vh-100px)] mt-[1px]">
<Virtuoso
data={filteredConnections}
itemContent={(i, connection) => (

View File

@ -5,7 +5,7 @@ import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { restartCore, patchMihomoConfig } from '@renderer/utils/ipc'
import { restartCore } from '@renderer/utils/ipc'
import React, { Key, ReactNode, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -13,10 +13,9 @@ const DNS: React.FC = () => {
const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { appConfig, patchAppConfig } = useAppConfig()
const { nameserverPolicy, useNameserverPolicy, controlDns = true } = appConfig || {}
const { nameserverPolicy, useNameserverPolicy } = appConfig || {}
const { dns, hosts } = controledMihomoConfig || {}
const {
enable = true,
ipv6 = false,
'fake-ip-range': fakeIPRange = '198.18.0.1/16',
'fake-ip-filter': fakeIPFilter = [
@ -41,7 +40,6 @@ const DNS: React.FC = () => {
} = dns || {}
const [changed, setChanged] = useState(false)
const [values, originSetValues] = useState({
enable,
ipv6,
useHosts,
enhancedMode,
@ -128,10 +126,7 @@ const DNS: React.FC = () => {
try {
setChanged(false)
await patchControledMihomoConfig(patch)
if (controlDns) {
await patchMihomoConfig(patch)
await restartCore()
}
await restartCore()
} catch (e) {
alert(e)
}
@ -148,7 +143,6 @@ const DNS: React.FC = () => {
color="primary"
onPress={() => {
const dnsConfig = {
enable: values.enable,
ipv6: values.ipv6,
'fake-ip-range': values.fakeIPRange,
'fake-ip-filter': values.fakeIPFilter,
@ -177,21 +171,12 @@ const DNS: React.FC = () => {
onSave(result)
}}
>
{controlDns ? t('common.save') : t('dns.saveOnly')}
{t('common.save')}
</Button>
)
}
>
<SettingCard>
<SettingItem title={t('dns.enable')} divider>
<Switch
size="sm"
isSelected={values.enable}
onValueChange={(v) => {
setValues({ ...values, enable: v })
}}
/>
</SettingItem>
<SettingItem title={t('dns.enhancedMode.title')} divider>
<Tabs
size="sm"
@ -279,7 +264,7 @@ const DNS: React.FC = () => {
{[...values.nameserverPolicy, { domain: '', value: '' }].map(
({ domain, value }, index) => (
<div key={index} className="flex mb-2">
<div className="flex-4">
<div className="flex-[4]">
<Input
size="sm"
fullWidth
@ -296,7 +281,7 @@ const DNS: React.FC = () => {
/>
</div>
<span className="mx-2">:</span>
<div className="flex-6 flex">
<div className="flex-[6] flex">
<Input
size="sm"
fullWidth
@ -347,7 +332,7 @@ const DNS: React.FC = () => {
<h3 className="mb-2">{t('dns.customHosts.list')}</h3>
{[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => (
<div key={index} className="flex mb-2">
<div className="flex-4">
<div className="flex-[4]">
<Input
size="sm"
fullWidth
@ -364,7 +349,7 @@ const DNS: React.FC = () => {
/>
</div>
<span className="mx-2">:</span>
<div className="flex-6 flex">
<div className="flex-[6] flex">
<Input
size="sm"
fullWidth

View File

@ -109,7 +109,7 @@ const Logs: React.FC = () => {
</div>
<Divider />
</div>
<div className="h-[calc(100vh-100px)] mt-px">
<div className="h-[calc(100vh-100px)] mt-[1px]">
<Virtuoso
ref={virtuosoRef}
data={filteredLogs}

View File

@ -1,4 +1,4 @@
import { Button, Divider, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react'
import { Button, Divider, Input, Select, SelectItem, Switch } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page'
import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
@ -6,14 +6,13 @@ import { useAppConfig } from '@renderer/hooks/use-app-config'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { platform } from '@renderer/utils/init'
import { FaNetworkWired } from 'react-icons/fa'
import { IoMdCloudDownload, IoMdInformationCircleOutline } from 'react-icons/io'
import { IoMdCloudDownload } from 'react-icons/io'
import PubSub from 'pubsub-js'
import {
mihomoUpgrade,
restartCore,
startSubStoreBackendServer,
triggerSysProxy,
showDetailedError
triggerSysProxy
} from '@renderer/utils/ipc'
import React, { useState } from 'react'
import InterfaceModal from '@renderer/components/mihomo/interface-modal'
@ -22,8 +21,7 @@ import { useTranslation } from 'react-i18next'
const CoreMap = {
mihomo: 'mihomo.stableVersion',
'mihomo-alpha': 'mihomo.alphaVersion',
'mihomo-smart': 'mihomo.smartVersion'
'mihomo-alpha': 'mihomo.alphaVersion'
}
const Mihomo: React.FC = () => {
@ -31,11 +29,6 @@ const Mihomo: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig()
const {
core = 'mihomo',
enableSmartCore = true,
enableSmartOverride = true,
smartCoreUseLightGBM = false,
smartCoreCollectData = false,
smartCoreStrategy = 'sticky-sessions',
maxLogDays = 7,
sysProxy,
disableLoopbackDetector,
@ -89,14 +82,7 @@ const Mihomo: React.FC = () => {
await patchAppConfig({ [key]: value })
await restartCore()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e)
console.error('Core restart failed:', errorMessage)
if (errorMessage.includes('配置检查失败') || errorMessage.includes('Profile Check Failed')) {
await showDetailedError(t('mihomo.error.profileCheckFailed'), errorMessage)
} else {
alert(errorMessage)
}
alert(e)
} finally {
PubSub.publish('mihomo-core-changed')
}
@ -106,188 +92,59 @@ const Mihomo: React.FC = () => {
<>
{lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />}
<BasePage title={t('mihomo.title')}>
{/* Smart 内核设置 */}
<SettingCard>
<div className={`rounded-md border p-2 transition-all duration-200 ${
enableSmartCore
? 'border-blue-300 bg-blue-50/30 dark:border-blue-700 dark:bg-blue-950/20'
: 'border-gray-300 bg-gray-50/30 dark:border-gray-600 dark:bg-gray-800/20'
}`}>
<SettingItem
title={t('mihomo.enableSmartCore')}
divider
>
<Switch
<SettingItem
title={t('mihomo.coreVersion')}
actions={
<Button
size="sm"
isSelected={enableSmartCore}
color={enableSmartCore ? 'primary' : 'default'}
onValueChange={async (v) => {
await patchAppConfig({ enableSmartCore: v })
if (v && core !== 'mihomo-smart') {
await handleConfigChangeWithRestart('core', 'mihomo-smart')
} else if (!v && core === 'mihomo-smart') {
await handleConfigChangeWithRestart('core', 'mihomo')
isIconOnly
title={t('mihomo.upgradeCore')}
variant="light"
isLoading={upgrading}
onPress={async () => {
try {
setUpgrading(true)
await mihomoUpgrade()
setTimeout(() => {
PubSub.publish('mihomo-core-changed')
}, 2000)
if (platform !== 'win32') {
new Notification(t('mihomo.coreAuthLost'), {
body: t('mihomo.coreUpgradeSuccess')
})
}
} catch (e) {
if (typeof e === 'string' && e.includes('already using latest version')) {
new Notification(t('mihomo.alreadyLatestVersion'))
} else {
alert(e)
}
} finally {
setUpgrading(false)
}
}}
/>
</SettingItem>
{/* Smart 覆写开关 */}
{enableSmartCore && (
<SettingItem
title={
<div className="flex items-center gap-2">
<span>{t('mihomo.enableSmartOverride')}</span>
<Tooltip
content={t('mihomo.smartOverrideTooltip')}
placement="top"
className="max-w-xs"
>
<IoMdInformationCircleOutline className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 cursor-help" />
</Tooltip>
</div>
}
divider={core === 'mihomo-smart'}
>
<Switch
size="sm"
isSelected={enableSmartOverride}
color="primary"
onValueChange={async (v) => {
await patchAppConfig({ enableSmartOverride: v })
await restartCore()
}}
/>
</SettingItem>
)}
<SettingItem
title={t('mihomo.coreVersion')}
actions={
<Button
size="sm"
isIconOnly
title={t('mihomo.upgradeCore')}
variant="light"
isLoading={upgrading}
onPress={async () => {
try {
setUpgrading(true)
await mihomoUpgrade()
setTimeout(() => {
PubSub.publish('mihomo-core-changed')
}, 2000)
if (platform !== 'win32') {
new Notification(t('mihomo.coreAuthLost'), {
body: t('mihomo.coreUpgradeSuccess')
})
}
} catch (e) {
if (typeof e === 'string' && e.includes('already using latest version')) {
new Notification(t('mihomo.alreadyLatestVersion'))
} else {
alert(e)
}
} finally {
setUpgrading(false)
}
}}
>
<IoMdCloudDownload className="text-lg" />
</Button>
}
divider={enableSmartCore && core === 'mihomo-smart'}
<IoMdCloudDownload className="text-lg" />
</Button>
}
divider
>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[100px]"
size="sm"
aria-label={t('mihomo.selectCoreVersion')}
selectedKeys={new Set([core])}
disallowEmptySelection={true}
onSelectionChange={async (v) => {
handleConfigChangeWithRestart('core', v.currentKey as 'mihomo' | 'mihomo-alpha')
}}
>
<Select
classNames={{
trigger: enableSmartCore
? 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50'
: 'data-[hover=true]:bg-default-200'
}}
className="w-[100px]"
size="sm"
aria-label={t('mihomo.selectCoreVersion')}
selectedKeys={new Set([
enableSmartCore
? 'mihomo-smart'
: (core === 'mihomo-smart' ? 'mihomo' : core)
])}
disallowEmptySelection={true}
onSelectionChange={async (v) => {
handleConfigChangeWithRestart('core', v.currentKey as 'mihomo' | 'mihomo-alpha' | 'mihomo-smart')
}}
>
{enableSmartCore ? (
<SelectItem key="mihomo-smart">{t(CoreMap['mihomo-smart'])}</SelectItem>
) : (
<>
<SelectItem key="mihomo">{t(CoreMap['mihomo'])}</SelectItem>
<SelectItem key="mihomo-alpha">{t(CoreMap['mihomo-alpha'])}</SelectItem>
</>
)}
</Select>
</SettingItem>
{/* Smart 内核配置项 */}
{enableSmartCore && core === 'mihomo-smart' && (
<>
<SettingItem
title={t('mihomo.smartCoreUseLightGBM')}
divider
>
<Switch
size="sm"
color="primary"
isSelected={smartCoreUseLightGBM}
onValueChange={async (v) => {
await patchAppConfig({ smartCoreUseLightGBM: v })
await restartCore()
}}
/>
</SettingItem>
<SettingItem
title={t('mihomo.smartCoreCollectData')}
divider
>
<Switch
size="sm"
color="primary"
isSelected={smartCoreCollectData}
onValueChange={async (v) => {
await patchAppConfig({ smartCoreCollectData: v })
await restartCore()
}}
/>
</SettingItem>
<SettingItem
title={t('mihomo.smartCoreStrategy')}
>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50' }}
className="w-[150px]"
size="sm"
aria-label={t('mihomo.smartCoreStrategy')}
selectedKeys={new Set([smartCoreStrategy])}
disallowEmptySelection={true}
onSelectionChange={async (v) => {
const strategy = v.currentKey as 'sticky-sessions' | 'round-robin'
await patchAppConfig({ smartCoreStrategy: strategy })
await restartCore()
}}
>
<SelectItem key="sticky-sessions">{t('mihomo.smartCoreStrategyStickySession')}</SelectItem>
<SelectItem key="round-robin">{t('mihomo.smartCoreStrategyRoundRobin')}</SelectItem>
</Select>
</SettingItem>
</>
)}
</div>
</SettingCard>
{/* 常规内核设置 */}
<SettingCard>
<SelectItem key="mihomo">{t(CoreMap['mihomo'])}</SelectItem>
<SelectItem key="mihomo-alpha">{t(CoreMap['mihomo-alpha'])}</SelectItem>
</Select>
</SettingItem>
<SettingItem title={t('mihomo.mixedPort')} divider>
<div className="flex">
{mixedPortInput !== mixedPort && (
@ -646,7 +503,7 @@ const Mihomo: React.FC = () => {
const [user, pass] = auth.split(':')
return (
<div key={index} className="flex mb-2">
<div className="flex-4">
<div className="flex-[4]">
<Input
size="sm"
fullWidth
@ -666,7 +523,7 @@ const Mihomo: React.FC = () => {
/>
</div>
<span className="mx-2">:</span>
<div className="flex-6 flex">
<div className="flex-[6] flex">
<Input
size="sm"
fullWidth

View File

@ -38,7 +38,7 @@ const Rules: React.FC = () => {
</div>
<Divider />
</div>
<div className="h-[calc(100vh-100px)] mt-px">
<div className="h-[calc(100vh-100px)] mt-[1px]">
<Virtuoso
data={filteredRules}
itemContent={(i, rule) => (

View File

@ -3,8 +3,7 @@ import BasePage from '@renderer/components/base/base-page'
import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { restartCore, patchMihomoConfig } from '@renderer/utils/ipc'
import { restartCore } from '@renderer/utils/ipc'
import React, { ReactNode, useState } from 'react'
import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
@ -12,11 +11,8 @@ import { useTranslation } from 'react-i18next'
const Sniffer: React.FC = () => {
const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { appConfig } = useAppConfig()
const { controlSniff = true } = appConfig || {}
const { sniffer } = controledMihomoConfig || {}
const {
enable = true,
'parse-pure-ip': parsePureIP = true,
'force-dns-mapping': forceDNSMapping = true,
'override-destination': overrideDestination = false,
@ -45,7 +41,6 @@ const Sniffer: React.FC = () => {
} = sniffer || {}
const [changed, setChanged] = useState(false)
const [values, originSetValues] = useState({
enable,
parsePureIP,
forceDNSMapping,
overrideDestination,
@ -64,11 +59,7 @@ const Sniffer: React.FC = () => {
try {
setChanged(false)
await patchControledMihomoConfig(patch)
if (controlSniff) {
await patchMihomoConfig(patch)
await restartCore()
}
await restartCore()
} catch (e) {
alert(e)
}
@ -139,34 +130,22 @@ const Sniffer: React.FC = () => {
onPress={() =>
onSave({
sniffer: {
enable: values.enable,
'parse-pure-ip': values.parsePureIP,
'force-dns-mapping': values.forceDNSMapping,
'override-destination': values.overrideDestination,
sniff: values.sniff,
'skip-domain': values.skipDomain,
'force-domain': values.forceDomain,
'skip-dst-address': values.skipDstAddress,
'skip-src-address': values.skipSrcAddress
'force-domain': values.forceDomain
}
})
}
>
{controlSniff ? t('common.save') : t('sniffer.saveOnly')}
{t('common.save')}
</Button>
)
}
>
<SettingCard>
<SettingItem title={t('sniffer.enable')} divider>
<Switch
size="sm"
isSelected={values.enable}
onValueChange={(v) => {
setValues({ ...values, enable: v })
}}
/>
</SettingItem>
<SettingItem title={t('sniffer.overrideDestination')} divider>
<Switch
size="sm"

View File

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

View File

@ -79,22 +79,6 @@ export async function mihomoGroupDelay(group: string, url?: string): Promise<IMi
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoGroupDelay', group, url))
}
export async function mihomoSmartGroupWeights(groupName: string): Promise<Record<string, number>> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoSmartGroupWeights', groupName))
}
export async function mihomoSmartFlushCache(configName?: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoSmartFlushCache', configName))
}
export async function showDetailedError(title: string, message: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showDetailedError', title, message))
}
export async function getSmartOverrideContent(): Promise<string | null> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getSmartOverrideContent'))
}
export async function patchMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch))
}
@ -227,16 +211,6 @@ export async function triggerSysProxy(enable: boolean): Promise<void> {
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> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
}

View File

@ -216,12 +216,7 @@ interface ISysProxyConfig {
}
interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha' | 'mihomo-smart'
enableSmartCore: boolean
enableSmartOverride: boolean
smartCoreUseLightGBM: boolean
smartCoreCollectData: boolean
smartCoreStrategy: 'sticky-sessions' | 'round-robin'
core: 'mihomo' | 'mihomo-alpha'
disableLoopbackDetector: boolean
disableEmbedCA: boolean
disableSystemCA: boolean

14
tailwind.config.js Normal file
View File

@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
const { heroui } = require('@heroui/react')
module.exports = {
content: [
'./src/renderer/src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {}
},
darkMode: 'class',
plugins: [heroui()]
}

View File

@ -3,7 +3,6 @@
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"],
"moduleResolution": "bundler"
"types": ["electron-vite/node"]
}
}