Compare commits

..

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

82 changed files with 5918 additions and 6890 deletions

View File

@ -12,41 +12,7 @@ on:
permissions: write-all permissions: write-all
jobs: jobs:
cleanup-dev-release:
runs-on: ubuntu-latest
steps:
- name: Delete Dev Release Assets
if: github.event_name == 'workflow_dispatch'
continue-on-error: true
run: |
# Get release ID for dev tag
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/dev" | \
jq -r '.id // empty')
if [ ! -z "$RELEASE_ID" ]; then
echo "Found dev release with ID: $RELEASE_ID"
# Get all assets and delete them
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets" | \
jq -r '.[].id' | \
while read asset_id; do
echo "Deleting asset: $asset_id"
curl -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id"
done
echo "All dev release assets deleted"
else
echo "No existing dev release found"
fi
- name: Skip for Tag Release
if: startsWith(github.ref, 'refs/tags/v')
run: echo "Skipping cleanup for tag release"
windows: windows:
needs: [cleanup-dev-release]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -99,24 +65,10 @@ jobs:
dist/*.sha256 dist/*.sha256
dist/*setup.exe dist/*setup.exe
dist/*portable.7z dist/*portable.7z
token: ${{ secrets.GITHUB_TOKEN }} body_path: changelog.md
- name: Publish Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
tag_name: dev
files: |
dist/*.sha256
dist/*setup.exe
dist/*portable.7z
body: "Development build from ${{ github.sha }}"
prerelease: true
draft: false
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
windows7: windows7:
needs: [cleanup-dev-release]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -170,24 +122,10 @@ jobs:
dist/*.sha256 dist/*.sha256
dist/*setup.exe dist/*setup.exe
dist/*portable.7z dist/*portable.7z
token: ${{ secrets.GITHUB_TOKEN }} body_path: changelog.md
- name: Publish Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
tag_name: dev
files: |
dist/*.sha256
dist/*setup.exe
dist/*portable.7z
body: "Development build from ${{ github.sha }}"
prerelease: true
draft: false
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
linux: linux:
needs: [cleanup-dev-release]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -234,24 +172,10 @@ jobs:
dist/*.sha256 dist/*.sha256
dist/*.deb dist/*.deb
dist/*.rpm dist/*.rpm
token: ${{ secrets.GITHUB_TOKEN }} body_path: changelog.md
- name: Publish Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
tag_name: dev
files: |
dist/*.sha256
dist/*.deb
dist/*.rpm
body: "Development build from ${{ github.sha }}"
prerelease: true
draft: false
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
macos: macos:
needs: [cleanup-dev-release]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -321,23 +245,10 @@ jobs:
files: | files: |
dist/*.sha256 dist/*.sha256
dist/*.pkg dist/*.pkg
token: ${{ secrets.GITHUB_TOKEN }} body_path: changelog.md
- name: Publish Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
tag_name: dev
files: |
dist/*.sha256
dist/*.pkg
body: "Development build from ${{ github.sha }}"
prerelease: true
draft: false
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
macos10: macos10:
needs: [cleanup-dev-release]
if: always() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -409,23 +320,12 @@ jobs:
files: | files: |
dist/*.sha256 dist/*.sha256
dist/*.pkg dist/*.pkg
token: ${{ secrets.GITHUB_TOKEN }} body_path: changelog.md
- name: Publish Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
tag_name: dev
files: |
dist/*.sha256
dist/*.pkg
body: "Development build from ${{ github.sha }}"
prerelease: true
draft: false
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
updater: updater:
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' if: startsWith(github.ref, 'refs/tags/v')
needs: [windows, windows7, linux, macos, macos10] needs: [windows, macos, windows7, macos10]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -435,29 +335,17 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: pnpm install run: pnpm install
- name: Telegram Notification - name: Telegram Notification
if: startsWith(github.ref, 'refs/tags/v')
env: env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
run: pnpm telegram run: pnpm telegram
- name: Generate latest.yml - name: Generate latest.yml
run: pnpm updater run: pnpm updater
- name: Publish Release - name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: latest.yml files: latest.yml
body_path: changelog.md body_path: changelog.md
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Dev Release
if: github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
tag_name: dev
files: latest.yml
body: "Development build updater from ${{ github.sha }}"
prerelease: true
draft: false
token: ${{ secrets.GITHUB_TOKEN }}
aur-release-updater: aur-release-updater:
strategy: strategy:

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ fi
chmod 4755 '/opt/mihomo-party/chrome-sandbox' || true chmod 4755 '/opt/mihomo-party/chrome-sandbox' || true
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo 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-alpha
chmod +sx /opt/mihomo-party/resources/sidecar/mihomo-smart
if hash update-mime-database 2>/dev/null; then if hash update-mime-database 2>/dev/null; then
update-mime-database /usr/share/mime || true 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" log "Warning: mihomo-alpha binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
fi 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 工具 # 复制 helper 工具
log "Installing helper tool..." log "Installing helper tool..."
if [ -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" ]; then if [ -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" ]; then
@ -125,13 +117,6 @@ macos_version=$(sw_vers -productVersion)
macos_major=$(echo "$macos_version" | cut -d. -f1) macos_major=$(echo "$macos_version" | cut -d. -f1)
log "macOS version: $macos_version" log "macOS version: $macos_version"
# 启用服务(防止安全软件禁用)
if ! launchctl enable system/party.mihomo.helper 2>/dev/null; then
log "Warning: Failed to enable service, continuing installation..."
else
log "Service enabled successfully"
fi
# 清理现有服务 # 清理现有服务
log "Cleaning up existing services..." log "Cleaning up existing services..."
launchctl bootout system "$LAUNCH_DAEMON" 2>/dev/null || true launchctl bootout system "$LAUNCH_DAEMON" 2>/dev/null || true

View File

@ -1,53 +1,72 @@
## 1.8.5 ## 1.7.7
### 新功能 (Feat) ### 新功能 (Feat)
- 支持 cron 表达式以自定义订阅更新频率(#766) - Mihomo 内核升级 v1.19.12
- 修复权限检查并优化TUN与自启联动(#977) - 新增 Webdav 最大备数设置和清理逻辑
- 托盘图标能根据代理状态实时变化颜色
### 修复 (Fix)
- Windows 下当前运行内核权限检测(之前没有正确检测管理员权限运行的内核)
- Windows 下 开机自启 按钮卡顿问题 隐藏运行黑框 现在申请权限会弹通知
- 修复 部分情况下 yaml 文件解析错误 (#889)
- 修复 系统中已经运行的内核名称检测
- 修复 订阅信息中的因格式问题导致的信息缺失(#951)
- 修复 轻量模式延迟启动延时输入数值问题(#962)
- 修复 轻量模式下轻量模式下规则失效的问题(#963)
- 修复 MacOS 下默认虚拟网卡名称没有生效的问题
- 修复 ES Module 兼容性问题
### 样式调整 (Sytle)
- 部分样式微调
## 1.8.4
### 新功能 (Feat)
- 如果当前没有以管理员模式运行TUN 开关保持关闭
- 区分 app 日志与 core 日志输出为不同文件
- 完善内核权限鉴别,上一个内核以管理员模式启动的时候,弹出提示
- 修改 MacOS 默认虚拟网卡名称为 utun1500可自定义
### 修复 (Fix) ### 修复 (Fix)
- 修复某些系统下的悬浮窗开启崩溃的问题(开启兼容模式=关闭硬件加速) - 修复 MacOS 下无法启动的问题(重置工作目录权限)
- 开机自启在非管理员模式下报错的问题 - 尝试修复不同版本 MacOS 下安装软件时候的报错Input/output error
- 解决某些 macos 系统下无法开启虚拟网卡的问题(tun device名称冲突) - 部分遗漏的多国语言翻译
## 1.8.3 ## 1.7.6
**本次更新移除了 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件,安全性更好,更灵活,给无法使用管理员模式运行软件的企业用户提供了更大的便利**
### 新功能 (Feat) **此版本修复了 1.7.5 中的几个严重 bug推荐所有人更新**
- 移除 Windows 下启动必须管理员模式的机制,改为只在启用虚拟网卡模式的时候,申请 UAC 权限重启软件
### 重构 (Refactor)
- Geodata 文件只有在源文件更新的时候才会在启动时覆盖更新
### 修复 (Fix) ### 修复 (Fix)
- 修复 DNS/嗅探覆写开关逻辑,修改设置不再会直接写入运行时配置,增加了“仅保存”按钮 - 修复了内核1.19.8更新后gist同步失效的问题(#780)
- 悬浮窗改为纯矩形,修复 windows 下兼容性问题带来的白边 - 部分遗漏的多国语言翻译
- MacOS 下启动Error: EACCES: permission denied
- MacOS 系统代理 bypass 不生效
- MacOS 系统代理开启时 500 报错
## 1.8.2 ## 1.7.5
**本次更新主要集中在重大内核更新和依赖升级后所产生的 bug 修复解决了自1.7版以后首次安装无法启动的问题,推荐更新**
### 新功能 (Feat) ### 新功能 (Feat)
- 重构 域名嗅探 卡片模块,改为“覆写”逻辑,当开关打开后,使用 嗅探覆写 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置 - 增加组延迟测试时的动画
- 订阅/覆写卡片可右键呼出菜单 - 订阅卡片可右键点击
- MacOS 下“轻触(tap)”触控板可进行开关操作(之前必须“按下(click)”) -
### 修复 (Fix)
- 1.7.4引入的内核启动错误
- 无法手动设置内核权限
- 完善 系统代理socket 重建和检测机制
## 1.7.4
### 新功能 (Feat)
- Mihomo 内核升级 v1.19.10
- 改进 socket创建机制防止 MacOS 下系统代理开启无法找到 socket 文件的问题
- mihomo-party-helper增加更多日志以方便调试
- 改进 MacOS 下签名和公正流程
- 增加 MacOS 下 plist 权限设置
- 改进安装流程
-
### 修复 (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: target:
- pkg - pkg
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
hardenedRuntime: true
gatekeeperAssess: false
extendInfo: extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera. - NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false notarize: true
artifactName: ${name}-macos-${version}-${arch}.${ext} artifactName: ${name}-macos-${version}-${arch}.${ext}
pkg: pkg:
allowAnywhere: false allowAnywhere: false
@ -56,9 +54,8 @@ pkg:
file: build/background.png file: build/background.png
linux: linux:
desktop: desktop:
entry: Name: Mihomo Party
Name: Mihomo Party MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
target: target:
- deb - deb
- rpm - rpm

View File

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

View File

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

9277
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: {}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

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 ======= */ /* ======= mihomo release ======= */
const MIHOMO_VERSION_URL = const MIHOMO_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt' '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}"`) throw new Error(`unsupported platform "${platform}-${arch}"`)
} }
if (!MIHOMO_SMART_MAP[`${platform}-${arch}`]) {
throw new Error(`unsupported platform "${platform}-${arch}"`)
}
/** /**
* core info * core info
*/ */
@ -157,23 +123,6 @@ function mihomo() {
downloadURL 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 * download sidecar and rename
*/ */
@ -322,6 +271,11 @@ const resolveSysproxy = () =>
file: 'sysproxy.exe', file: 'sysproxy.exe',
downloadURL: `https://github.com/mihomo-party-org/sysproxy/releases/download/${arch}/sysproxy.exe` downloadURL: `https://github.com/mihomo-party-org/sysproxy/releases/download/${arch}/sysproxy.exe`
}) })
const resolveRunner = () =>
resolveResource({
file: 'mihomo-party-run.exe',
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-run/releases/download/${arch}/mihomo-party-run.exe`
})
const resolveMonitor = async () => { const resolveMonitor = async () => {
const tempDir = path.join(TEMP_DIR, 'TrafficMonitor') const tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
@ -351,7 +305,7 @@ const resolve7zip = () =>
}) })
const resolveSubstore = () => const resolveSubstore = () =>
resolveResource({ resolveResource({
file: 'sub-store.bundle.cjs', file: 'sub-store.bundle.js',
downloadURL: downloadURL:
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js' 'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js'
}) })
@ -406,11 +360,6 @@ const tasks = [
func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())), func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
retry: 5 retry: 5
}, },
{
name: 'mihomo-smart',
func: () => getLatestSmartVersion().then(() => resolveSidecar(mihomoSmart())),
retry: 5
},
{ name: 'mmdb', func: resolveMmdb, retry: 5 }, { name: 'mmdb', func: resolveMmdb, retry: 5 },
{ name: 'metadb', func: resolveMetadb, retry: 5 }, { name: 'metadb', func: resolveMetadb, retry: 5 },
{ name: 'geosite', func: resolveGeosite, retry: 5 }, { name: 'geosite', func: resolveGeosite, retry: 5 },
@ -433,6 +382,12 @@ const tasks = [
retry: 5, retry: 5,
winOnly: true winOnly: true
}, },
{
name: 'runner',
func: resolveRunner,
retry: 5,
winOnly: true
},
{ {
name: 'monitor', name: 'monitor',
func: resolveMonitor, func: resolveMonitor,
@ -477,14 +432,7 @@ async function runTask() {
break break
} catch (err) { } catch (err) {
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message) console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message)
if (i === task.retry - 1) { if (i === task.retry - 1) throw err
if (task.optional) {
console.log(`[WARN]: Optional task::${task.name} failed, skipping...`)
break
} else {
throw err
}
}
} }
} }
return runTask() return runTask()

View File

@ -1,32 +0,0 @@
const fs = require('fs')
const path = require('path')
function restoreVersion() {
const backupPath = path.join(__dirname, '..', 'package.json.bak')
const packagePath = path.join(__dirname, '..', 'package.json')
if (fs.existsSync(backupPath)) {
try {
const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'))
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
// 恢复版本号
packageJson.version = backup.version
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2))
// 删除备份文件
fs.unlinkSync(backupPath)
console.log(`版本号已恢复: ${backup.version}`)
} catch (error) {
console.error('恢复版本号时出错:', error)
}
}
}
// 如果是直接运行此脚本,则执行版本恢复
if (require.main === module) {
restoreVersion()
}
module.exports = { restoreVersion }

View File

@ -24,7 +24,5 @@ changelog += '\n#### Linux\n\n'
changelog += `- DEB[64位](${downloadUrl}/mihomo-party-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/mihomo-party-linux-${version}-arm64.deb)\n\n` changelog += `- DEB[64位](${downloadUrl}/mihomo-party-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/mihomo-party-linux-${version}-arm64.deb)\n\n`
changelog += `- RPM[64位](${downloadUrl}/mihomo-party-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/mihomo-party-linux-${version}-aarch64.rpm)` changelog += `- RPM[64位](${downloadUrl}/mihomo-party-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/mihomo-party-linux-${version}-aarch64.rpm)`
changelog += '\n\n### 机场推荐:\n- 高性能海外机场,稳定首选:[https://狗狗加速.com](https://party.dginv.click/#/register?code=ARdo0mXx)'
writeFileSync('latest.yml', yaml.stringify(latest)) writeFileSync('latest.yml', yaml.stringify(latest))
writeFileSync('changelog.md', changelog) writeFileSync('changelog.md', changelog)

View File

@ -1,47 +0,0 @@
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
function getGitCommitHash() {
try {
return execSync('git rev-parse --short=7 HEAD').toString().trim()
} catch (error) {
console.warn('无法获取 Git commit hash使用默认值')
return 'unknown'
}
}
function processVersion() {
const packagePath = path.join(__dirname, '..', 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
// 备份原始版本号
const originalVersion = packageJson.version
fs.writeFileSync(
path.join(__dirname, '..', 'package.json.bak'),
JSON.stringify({ version: originalVersion }, null, 2)
)
// 检查版本号是否以 -dev 结尾
if (originalVersion.endsWith('-dev')) {
const commitHash = getGitCommitHash()
const newVersion = originalVersion.replace('-dev', `-${commitHash}-dev`)
// 更新 package.json
packageJson.version = newVersion
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2))
console.log(`版本号已更新: ${originalVersion} -> ${newVersion}`)
return newVersion
}
console.log(`版本号未修改: ${originalVersion}`)
return originalVersion
}
// 如果是直接运行此脚本,则执行版本处理
if (require.main === module) {
processVersion()
}
module.exports = { processVersion, getGitCommitHash }

View File

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

View File

@ -27,9 +27,3 @@ export {
setOverride, setOverride,
updateOverrideItem updateOverrideItem
} from './override' } 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> { export async function changeCurrentProfile(id: string): Promise<void> {
const config = await getProfileConfig() const config = await getProfileConfig()
const current = config.current const current = config.current
if (current === id) {
return
}
config.current = id config.current = id
await setProfileConfig(config) await setProfileConfig(config)
try { try {
await restartCore() await restartCore()
} catch (e) { } catch (e) {
// 如果重启失败,恢复原来的配置
config.current = current config.current = current
await setProfileConfig(config)
throw e throw e
} finally {
await setProfileConfig(config)
} }
} }
@ -205,10 +199,7 @@ export async function setProfileStr(id: string, content: string): Promise<void>
export async function getProfile(id: string | undefined): Promise<IMihomoConfig> { export async function getProfile(id: string | undefined): Promise<IMihomoConfig> {
const profile = await getProfileStr(id) const profile = await getProfileStr(id)
let result = yaml.parse(profile, { merge: true }) || {}
// 替换 防止错误使用科学记数法解析
const patchedProfile = profile.replace(/(\w+:\s*)(\d+E\d+)(\s|$)/gi, '$1"$2"$3')
let result = yaml.parse(patchedProfile, { merge: true }) || {}
if (typeof result !== 'object') result = {} if (typeof result !== 'object') result = {}
return result return result
} }
@ -226,7 +217,7 @@ function parseFilename(str: string): string {
// subscription-userinfo: upload=1234; download=2234; total=1024000; expire=2218532293 // subscription-userinfo: upload=1234; download=2234; total=1024000; expire=2218532293
function parseSubinfo(str: string): ISubscriptionUserInfo { function parseSubinfo(str: string): ISubscriptionUserInfo {
const parts = str.split(/\s*;\s*/) const parts = str.split('; ')
const obj = {} as ISubscriptionUserInfo const obj = {} as ISubscriptionUserInfo
parts.forEach((part) => { parts.forEach((part) => {
const [key, value] = part.split('=') const [key, value] = part.split('=')

View File

@ -1,284 +0,0 @@
import { getAppConfig } from './app'
import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override'
import { overrideLogger } from '../utils/logger'
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) {
await overrideLogger.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) {
await overrideLogger.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> { export async function generateProfile(): Promise<void> {
const { current } = await getProfileConfig() 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)) const currentProfile = await overrideProfile(current, await getProfile(current))
let controledMihomoConfig = await getControledMihomoConfig() const controledMihomoConfig = await getControledMihomoConfig()
// 根据开关状态过滤控制配置
controledMihomoConfig = { ...controledMihomoConfig }
if (!controlDns) {
delete controledMihomoConfig.dns
delete controledMihomoConfig.hosts
}
if (!controlSniff) {
delete controledMihomoConfig.sniffer
}
if (!useNameserverPolicy) {
delete controledMihomoConfig?.dns?.['nameserver-policy']
}
const profile = deepMerge(currentProfile, controledMihomoConfig) const profile = deepMerge(currentProfile, controledMihomoConfig)
// 确保可以拿到基础日志信息 // 确保可以拿到基础日志信息
// 使用 debug 可以调试内核相关问题 `debug/pprof` // 使用 debug 可以调试内核相关问题 `debug/pprof`
@ -50,16 +36,7 @@ export async function generateProfile(): Promise<void> {
profile['log-level'] = 'info' profile['log-level'] = 'info'
} }
runtimeConfig = profile runtimeConfig = profile
runtimeConfigStr = yaml.stringify(profile)
// 先正常生成 YAML 字符串
let yamlStr = yaml.stringify(profile)
// 还原科学记数法的引号
yamlStr = yamlStr.replace(
/(\w+:\s*)"(\d+E\d+)"(\s|$)/gi,
'$1$2$3'
)
runtimeConfigStr = yamlStr
if (diffWorkDir) { if (diffWorkDir) {
await prepareProfileWorkDir(current) await prepareProfileWorkDir(current)
} }

View File

@ -1,7 +1,7 @@
import { ChildProcess, exec, execFile, spawn } from 'child_process' import { ChildProcess, exec, execFile, spawn } from 'child_process'
import { import {
dataDir, dataDir,
coreLogPath, logPath,
mihomoCoreDir, mihomoCoreDir,
mihomoCorePath, mihomoCorePath,
mihomoProfileWorkDir, mihomoProfileWorkDir,
@ -15,10 +15,9 @@ import {
getControledMihomoConfig, getControledMihomoConfig,
getProfileConfig, getProfileConfig,
patchAppConfig, patchAppConfig,
patchControledMihomoConfig, patchControledMihomoConfig
manageSmartOverride
} from '../config' } from '../config'
import { app, ipcMain, net } from 'electron' import { app, dialog, ipcMain, net } from 'electron'
import { import {
startMihomoTraffic, startMihomoTraffic,
startMihomoConnections, startMihomoConnections,
@ -39,16 +38,14 @@ import os from 'os'
import { createWriteStream, existsSync } from 'fs' import { createWriteStream, existsSync } from 'fs'
import { uploadRuntimeConfig } from '../resolve/gistApi' import { uploadRuntimeConfig } from '../resolve/gistApi'
import { startMonitor } from '../resolve/trafficMonitor' import { startMonitor } from '../resolve/trafficMonitor'
import { safeShowErrorBox } from '../utils/init'
import i18next from '../../shared/i18n' import i18next from '../../shared/i18n'
import { managerLogger } from '../utils/logger'
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => { chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
try { try {
await stopCore(true) await stopCore(true)
await startCore() await startCore()
} catch (e) { } catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
} }
}) })
@ -86,10 +83,6 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
const { current } = await getProfileConfig() const { current } = await getProfileConfig()
const { tun } = await getControledMihomoConfig() const { tun } = await getControledMihomoConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
// 管理 Smart 内核覆写配置
await manageSmartOverride()
await generateProfile() await generateProfile()
await checkProfile() await checkProfile()
await stopCore() await stopCore()
@ -97,12 +90,13 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
try { try {
await setPublicDNS() await setPublicDNS()
} catch (error) { } catch (error) {
await managerLogger.error('set dns failed', error) await writeFile(logPath(), `[Manager]: set dns failed, ${error}`, {
flag: 'a'
})
} }
} }
// 内核日志输出到独立的 core-日期.log 文件 const stdout = createWriteStream(logPath(), { flags: 'a' })
const stdout = createWriteStream(coreLogPath(), { flags: 'a' }) const stderr = createWriteStream(logPath(), { flags: 'a' })
const stderr = createWriteStream(coreLogPath(), { flags: 'a' })
const env = { const env = {
DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector), DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector),
DISABLE_EMBED_CA: String(disableEmbedCA), DISABLE_EMBED_CA: String(disableEmbedCA),
@ -113,7 +107,7 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
corePath, corePath,
['-d', diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), ctlParam, mihomoIpcPath], ['-d', diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), ctlParam, mihomoIpcPath],
{ {
detached: true, detached: detached,
stdio: detached ? 'ignore' : undefined, stdio: detached ? 'ignore' : undefined,
env: env env: env
} }
@ -128,9 +122,11 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
}) })
} }
child.on('close', async (code, signal) => { child.on('close', async (code, signal) => {
await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`) await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, {
flag: 'a'
})
if (retry) { if (retry) {
await managerLogger.info('Try Restart Core') await writeFile(logPath(), `[Manager]: Try Restart Core\n`, { flag: 'a' })
retry-- retry--
await restartCore() await restartCore()
} else { } else {
@ -192,7 +188,9 @@ export async function stopCore(force = false): Promise<void> {
await recoverDNS() await recoverDNS()
} }
} catch (error) { } catch (error) {
await managerLogger.error('recover dns failed', error) await writeFile(logPath(), `[Manager]: recover dns failed, ${error}`, {
flag: 'a'
})
} }
if (child) { if (child) {
@ -209,21 +207,18 @@ export async function restartCore(): Promise<void> {
try { try {
await startCore() await startCore()
} catch (e) { } catch (e) {
// 记录错误到日志而不是显示阻塞对话框 dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
await managerLogger.error('restart core failed', e)
// 重新抛出错误,让调用者处理
throw e
} }
} }
export async function keepCoreAlive(): Promise<void> { export async function keepCoreAlive(): Promise<void> {
try { try {
if (!child) await startCore(true) await startCore(true)
if (child && child.pid) { if (child && child.pid) {
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
} }
} catch (e) { } catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
} }
} }
@ -254,69 +249,29 @@ async function checkProfile(): Promise<void> {
mihomoTestDir() mihomoTestDir()
], { env }) ], { env })
} catch (error) { } catch (error) {
await managerLogger.error('Profile check failed', error)
if (error instanceof Error && 'stdout' in error) { if (error instanceof Error && 'stdout' in error) {
const { stdout, stderr } = error as { stdout: string; stderr?: string } const { stdout } = error as { stdout: string }
await managerLogger.info('Profile check stdout', stdout)
await managerLogger.info('Profile check stderr', stderr)
const errorLines = stdout const errorLines = stdout
.split('\n') .split('\n')
.filter((line) => line.includes('level=error') || line.includes('error')) .filter((line) => line.includes('level=error'))
.map((line) => { .map((line) => line.split('level=error')[1])
if (line.includes('level=error')) { throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`)
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')}`)
}
} else { } else {
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}: ${error}`) throw error
} }
} }
} }
export async function checkTunPermissions(): Promise<boolean> { export async function manualGrantCorePermition(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
try {
if (process.platform === 'win32') {
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
}
export async function grantTunPermissions(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig() const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
const execPromise = promisify(exec) const execPromise = promisify(exec)
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}` const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
const command = `do shell script "${shell}" with administrator privileges` const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`) await execPromise(`osascript -e '${command}'`)
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
await execFilePromise('pkexec', [ await execFilePromise('pkexec', [
'bash', 'bash',
@ -324,358 +279,6 @@ export async function grantTunPermissions(): Promise<void> {
`chown root:root "${corePath}" && chmod +sx "${corePath}"` `chown root:root "${corePath}" && chmod +sx "${corePath}"`
]) ])
} }
if (process.platform === 'win32') {
throw new Error('Windows platform requires running as administrator')
}
}
export async function checkAdminPrivileges(): Promise<boolean> {
if (process.platform !== 'win32') {
return true
}
const execPromise = promisify(exec)
try {
// 首先尝试 fltmc 命令检测管理员权限
await execPromise('fltmc')
await managerLogger.info('Admin privileges confirmed via fltmc')
return true
} catch (fltmcError) {
await managerLogger.info('fltmc failed, trying net session as fallback', fltmcError)
try {
// 如果 fltmc 失败,尝试 net session 命令作为备用检测方法
await execPromise('net session')
await managerLogger.info('Admin privileges confirmed via net session')
return true
} catch (netSessionError) {
await managerLogger.info('Both fltmc and net session failed, no admin privileges', netSessionError)
return false
}
}
}
// TUN 权限确认框
export async function showTunPermissionDialog(): Promise<boolean> {
const { dialog } = await import('electron')
const i18next = await import('i18next')
await managerLogger.info('Preparing TUN permission dialog...')
await managerLogger.info(`i18next available: ${typeof i18next.t === 'function'}`)
const title = i18next.t('tun.permissions.title') || '需要管理员权限'
const message = i18next.t('tun.permissions.message') || '启用TUN模式需要管理员权限是否现在重启应用获取权限'
const confirmText = i18next.t('common.confirm') || '确认'
const cancelText = i18next.t('common.cancel') || '取消'
await managerLogger.info(`Dialog texts - Title: "${title}", Message: "${message}", Confirm: "${confirmText}", Cancel: "${cancelText}"`)
const choice = dialog.showMessageBoxSync({
type: 'warning',
title: title,
message: message,
buttons: [confirmText, cancelText],
defaultId: 0,
cancelId: 1
})
await managerLogger.info(`TUN permission dialog choice: ${choice}`)
return choice === 0
}
// 错误显示框
export async function showErrorDialog(title: string, message: string): Promise<void> {
const { dialog } = await import('electron')
const i18next = await import('i18next')
const okText = i18next.t('common.confirm') || '确认'
dialog.showMessageBoxSync({
type: 'error',
title: title,
message: message,
buttons: [okText],
defaultId: 0
})
}
export async function restartAsAdmin(forTun: boolean = true): Promise<void> {
if (process.platform !== 'win32') {
throw new Error('This function is only available on Windows')
}
const exePath = process.execPath
const args = process.argv.slice(1)
const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args
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"`
}
await managerLogger.info('Restarting as administrator with command', command)
// 执行PowerShell命令
exec(command, { windowsHide: true }, async (error, _stdout, stderr) => {
if (error) {
await managerLogger.error('PowerShell execution error', error)
await managerLogger.error('stderr', stderr)
} else {
await managerLogger.info('PowerShell command executed successfully')
}
})
await new Promise(resolve => setTimeout(resolve, 1500))
const { app } = await import('electron')
app.quit()
} catch (error) {
await managerLogger.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
}
// 检测高权限内核
export async function checkHighPrivilegeCore(): Promise<boolean> {
try {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
await managerLogger.info(`Checking high privilege core: ${corePath}`)
if (process.platform === 'win32') {
const { existsSync } = await import('fs')
if (!existsSync(corePath)) {
await managerLogger.info('Core file does not exist')
return false
}
const hasHighPrivilegeProcess = await checkHighPrivilegeMihomoProcess()
if (hasHighPrivilegeProcess) {
await managerLogger.info('Found high privilege mihomo process running')
return true
}
const isAdmin = await checkAdminPrivileges()
await managerLogger.info(`Current process admin privileges: ${isAdmin}`)
return isAdmin
}
if (process.platform === 'darwin' || process.platform === 'linux') {
await managerLogger.info('Non-Windows platform, skipping high privilege core check')
return false
}
} catch (error) {
await managerLogger.error('Failed to check high privilege core', error)
return false
}
return false
}
async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
try {
if (process.platform === 'win32') {
const execPromise = promisify(exec)
const mihomoExecutables = ['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe']
for (const executable of mihomoExecutables) {
try {
const { stdout } = await execPromise(`tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`)
const lines = stdout.split('\n').filter(line => line.includes(executable))
if (lines.length > 0) {
await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
for (const line of lines) {
const parts = line.split(',')
if (parts.length >= 2) {
const pid = parts[1].replace(/"/g, '').trim()
try {
const { stdout: processInfo } = await execPromise(`powershell -Command "Get-Process -Id ${pid} | Select-Object Name,Id,Path,CommandLine | ConvertTo-Json"`)
const processJson = JSON.parse(processInfo)
await managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`)
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
return true
}
} catch (error) {
await managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`)
}
}
}
}
} catch (error) {
await managerLogger.error(`Failed to check ${executable} processes`, error)
}
}
}
if (process.platform === 'darwin' || process.platform === 'linux') {
const execPromise = promisify(exec)
try {
const mihomoExecutables = ['mihomo', 'mihomo-alpha', 'mihomo-smart']
let foundProcesses = false
for (const executable of mihomoExecutables) {
try {
const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`)
const lines = stdout.split('\n').filter(line => line.trim() && line.includes(executable))
if (lines.length > 0) {
foundProcesses = true
await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 1) {
const user = parts[0]
await managerLogger.info(`${executable} process running as user: ${user}`)
if (user === 'root') {
return true
}
}
}
}
} catch (error) {
}
}
if (!foundProcesses) {
await managerLogger.info('No mihomo processes found running')
}
} catch (error) {
await managerLogger.error('Failed to check mihomo processes on Unix', error)
}
}
} catch (error) {
await managerLogger.error('Failed to check high privilege mihomo process', error)
}
return false
}
// TUN模式获取权限
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')) {
await managerLogger.info('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 } })
const { checkAutoRun, enableAutoRun } = await import('../sys/autoRun')
const autoRunEnabled = await checkAutoRun()
if (autoRunEnabled) {
await enableAutoRun()
}
await restartCore()
await managerLogger.info('TUN mode auto-enabled after admin restart')
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
} else {
await managerLogger.warn('Admin restart detected but no admin privileges found')
}
}
} catch (error) {
await managerLogger.error('Failed to auto-enable TUN after admin restart', error)
}
} else {
// 检查TUN配置与权限的匹配但不自动开启 TUN
await validateTunPermissionsOnStartup()
}
}
export async function validateTunPermissionsOnStartup(): Promise<void> {
try {
const { tun } = await getControledMihomoConfig()
if (!tun?.enable) {
return
}
const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) {
await managerLogger.warn(
'TUN is enabled but insufficient permissions detected, prompting user...'
)
const confirmed = await showTunPermissionDialog()
if (confirmed) {
await restartAsAdmin()
return
}
await managerLogger.warn('User declined admin restart, auto-disabling TUN...')
await patchControledMihomoConfig({ tun: { enable: false } })
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
await managerLogger.info('TUN auto-disabled due to insufficient permissions')
} else {
await managerLogger.info('TUN permissions validated successfully')
}
} catch (error) {
await managerLogger.error('Failed to validate TUN permissions on startup', error)
}
}
export async function manualGrantCorePermition(): Promise<void> {
return grantTunPermissions()
} }
export async function getDefaultDevice(): Promise<string> { export async function getDefaultDevice(): Promise<string> {

View File

@ -168,23 +168,6 @@ export const mihomoUpgrade = async (): Promise<void> => {
return await instance.post('/upgrade') 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> => { export const startMihomoTraffic = async (): Promise<void> => {
await mihomoTraffic() await mihomoTraffic()
} }
@ -364,27 +347,3 @@ const mihomoConnections = async (): Promise<void> => {
} }
} }
} }
export async function SysProxyStatus(): Promise<boolean> {
const appConfig = await getAppConfig()
return appConfig.sysProxy.enable
}
export const TunStatus = async (): Promise<boolean> => {
const config = await getControledMihomoConfig()
return config?.tun?.enable === true
}
export async function getTrayIconStatus(): Promise<'white' | 'blue' | 'green' | 'red'> {
const [sysProxyEnabled, tunEnabled] = await Promise.all([SysProxyStatus(), TunStatus()])
if (sysProxyEnabled && tunEnabled) {
return 'red' // 系统代理 + TUN 同时启用(警告状态)
} else if (sysProxyEnabled) {
return 'blue' // 仅系统代理启用
} else if (tunEnabled) {
return 'green' // 仅 TUN 启用
} else {
return 'white' // 全关
}
}

View File

@ -1,37 +1,22 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
import { Cron } from 'croner'
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {} const intervalPool: Record<string, NodeJS.Timeout> = {}
export async function initProfileUpdater(): Promise<void> { export async function initProfileUpdater(): Promise<void> {
const { items, current } = await getProfileConfig() const { items, current } = await getProfileConfig()
const currentItem = await getCurrentProfileItem() const currentItem = await getCurrentProfileItem()
for (const item of items.filter((i) => i.id !== current)) { for (const item of items.filter((i) => i.id !== current)) {
if (item.type === 'remote' && item.interval) { if (item.type === 'remote' && item.interval) {
if (typeof item.interval === 'number') { intervalPool[item.id] = setTimeout(
// 数字间隔使用setInterval async () => {
intervalPool[item.id] = setInterval(
async () => {
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
},
item.interval * 60 * 1000
)
} else if (typeof item.interval === 'string') {
// 字符串间隔使用Cron
intervalPool[item.id] = new Cron(item.interval, async () => {
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch (e) { } catch (e) {
/* ignore */ /* ignore */
} }
}) },
} item.interval * 60 * 1000
)
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch (e) { } catch (e) {
@ -39,40 +24,17 @@ export async function initProfileUpdater(): Promise<void> {
} }
} }
} }
if (currentItem?.type === 'remote' && currentItem.interval) { if (currentItem?.type === 'remote' && currentItem.interval) {
if (typeof currentItem.interval === 'number') { intervalPool[currentItem.id] = setTimeout(
intervalPool[currentItem.id] = setInterval( async () => {
async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
},
currentItem.interval * 60 * 1000
)
setTimeout(
async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
},
currentItem.interval * 60 * 1000 + 10000 // +10s
)
} else if (typeof currentItem.interval === 'string') {
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
try { try {
await addProfileItem(currentItem) await addProfileItem(currentItem)
} catch (e) { } catch (e) {
/* ignore */ /* ignore */
} }
}) },
} currentItem.interval * 60 * 1000 + 10000 // +10s
)
try { try {
await addProfileItem(currentItem) await addProfileItem(currentItem)
} catch (e) { } catch (e) {
@ -84,32 +46,17 @@ export async function initProfileUpdater(): Promise<void> {
export async function addProfileUpdater(item: IProfileItem): Promise<void> { export async function addProfileUpdater(item: IProfileItem): Promise<void> {
if (item.type === 'remote' && item.interval) { if (item.type === 'remote' && item.interval) {
if (intervalPool[item.id]) { if (intervalPool[item.id]) {
if (intervalPool[item.id] instanceof Cron) { clearTimeout(intervalPool[item.id])
(intervalPool[item.id] as Cron).stop()
} else {
clearInterval(intervalPool[item.id] as NodeJS.Timeout)
}
} }
intervalPool[item.id] = setTimeout(
if (typeof item.interval === 'number') { async () => {
intervalPool[item.id] = setInterval(
async () => {
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
},
item.interval * 60 * 1000
)
} else if (typeof item.interval === 'string') {
intervalPool[item.id] = new Cron(item.interval, async () => {
try { try {
await addProfileItem(item) await addProfileItem(item)
} catch (e) { } catch (e) {
/* ignore */ /* ignore */
} }
}) },
} item.interval * 60 * 1000
)
} }
} }

View File

@ -3,43 +3,26 @@ import { registerIpcMainHandlers } from './utils/ipc'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron' import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
import { addProfileItem, getAppConfig, patchAppConfig } from './config' import { addProfileItem, getAppConfig, patchAppConfig } from './config'
import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun, checkHighPrivilegeCore, restartAsAdmin } from './core/manager' import { quitWithoutCore, startCore, stopCore } from './core/manager'
import { triggerSysProxy } from './sys/sysproxy' import { triggerSysProxy } from './sys/sysproxy'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray' import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
import { init, initBasic } from './utils/init' import { init } from './utils/init'
import { join } from 'path' import { join } from 'path'
import { initShortcut } from './resolve/shortcut' import { initShortcut } from './resolve/shortcut'
import { spawn, exec } from 'child_process' import { execSync, spawn, exec } from 'child_process'
import { createElevateTask } from './sys/misc'
import { promisify } from 'util' import { promisify } from 'util'
import { stat } from 'fs/promises' import { stat } from 'fs/promises'
import { initProfileUpdater } from './core/profileUpdater' import { initProfileUpdater } from './core/profileUpdater'
import { existsSync } from 'fs' import { existsSync, writeFileSync } from 'fs'
import { exePath } from './utils/dirs' import { exePath, taskDir } from './utils/dirs'
import path from 'path'
import { startMonitor } from './resolve/trafficMonitor' import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow' import { showFloatingWindow } from './resolve/floatingWindow'
import iconv from 'iconv-lite'
import { initI18n } from '../shared/i18n' import { initI18n } from '../shared/i18n'
import i18next from 'i18next' import i18next from 'i18next'
import { logger } from './utils/logger'
// 错误处理
function showSafeErrorBox(titleKey: string, message: string): void {
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> { async function fixUserDataPermissions(): Promise<void> {
if (process.platform !== 'darwin') return if (process.platform !== 'darwin') return
@ -67,6 +50,39 @@ async function fixUserDataPermissions(): Promise<void> {
let quitTimeout: NodeJS.Timeout | null = null let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null export let mainWindow: BrowserWindow | null = null
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> { async function initApp(): Promise<void> {
await fixUserDataPermissions() await fixUserDataPermissions()
@ -113,59 +129,7 @@ if (process.platform === 'win32' && !exePath().startsWith('C')) {
app.commandLine.appendSwitch('in-process-gpu') app.commandLine.appendSwitch('in-process-gpu')
} }
// 运行内核检测 const initPromise = init()
async function checkHighPrivilegeCoreEarly(): Promise<void> {
if (process.platform !== 'win32') {
return
}
try {
await initBasic()
const { checkAdminPrivileges } = await import('./core/manager')
const isCurrentAppAdmin = await checkAdminPrivileges()
if (isCurrentAppAdmin) {
console.log('Current app is running as administrator, skipping privilege check')
return
}
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
if (hasHighPrivilegeCore) {
try {
const appConfig = await getAppConfig()
const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
await initI18n({ lng: language })
} catch {
await initI18n({ lng: 'zh-CN' })
}
const choice = dialog.showMessageBoxSync({
type: 'warning',
title: i18next.t('core.highPrivilege.title'),
message: i18next.t('core.highPrivilege.message'),
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
defaultId: 0,
cancelId: 1
})
if (choice === 0) {
try {
// Windows 平台重启应用获取管理员权限
await restartAsAdmin(false)
process.exit(0)
} catch (error) {
showSafeErrorBox('common.error.adminRequired', `${error}`)
process.exit(1)
}
} else {
process.exit(0)
}
}
} catch (e) {
console.error('Failed to check high privilege core:', e)
}
}
app.on('second-instance', async (_event, commandline) => { app.on('second-instance', async (_event, commandline) => {
showMainWindow() showMainWindow()
@ -206,11 +170,7 @@ app.whenReady().then(async () => {
// Set app user model id for windows // Set app user model id for windows
electronApp.setAppUserModelId('party.mihomo.app') electronApp.setAppUserModelId('party.mihomo.app')
await checkHighPrivilegeCoreEarly()
try { try {
await init()
const appConfig = await getAppConfig() const appConfig = await getAppConfig()
// 如果配置中没有语言设置,则使用系统语言 // 如果配置中没有语言设置,则使用系统语言
if (!appConfig.language) { if (!appConfig.language) {
@ -219,20 +179,18 @@ app.whenReady().then(async () => {
appConfig.language = systemLanguage appConfig.language = systemLanguage
} }
await initI18n({ lng: appConfig.language }) await initI18n({ lng: appConfig.language })
await initPromise
} catch (e) { } catch (e) {
showSafeErrorBox('common.error.initFailed', `${e}`) dialog.showErrorBox(i18next.t('common.error.initFailed'), `${e}`)
app.quit() app.quit()
} }
try { try {
const [startPromise] = await startCore() const [startPromise] = await startCore()
startPromise.then(async () => { startPromise.then(async () => {
await initProfileUpdater() await initProfileUpdater()
// 上次是否为了开启 TUN 而重启
await checkAdminRestartForTun()
}) })
} catch (e) { } catch (e) {
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`) dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`)
} }
try { try {
await startMonitor() await startMonitor()
@ -250,11 +208,7 @@ app.whenReady().then(async () => {
registerIpcMainHandlers() registerIpcMainHandlers()
await createWindow() await createWindow()
if (showFloating) { if (showFloating) {
try { showFloatingWindow()
await showFloatingWindow()
} catch (error) {
await logger.error('Failed to create floating window on startup', error)
}
} }
if (!disableTray) { if (!disableTray) {
await createTray() await createTray()
@ -288,7 +242,7 @@ async function handleDeepLink(url: string): Promise<void> {
new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show() new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show()
break break
} catch (e) { } catch (e) {
showSafeErrorBox('profiles.error.importFailed', `${url}\n${e}`) dialog.showErrorBox(i18next.t('profiles.error.importFailed'), `${url}\n${e}`)
} }
} }
} }

View File

@ -26,31 +26,13 @@ export async function checkUpdate(): Promise<IAppVersion | undefined> {
) )
const latest = yaml.parse(res.data, { merge: true }) as IAppVersion const latest = yaml.parse(res.data, { merge: true }) as IAppVersion
const currentVersion = app.getVersion() const currentVersion = app.getVersion()
if (compareVersions(latest.version, currentVersion) > 0) { if (latest.version !== currentVersion) {
return latest return latest
} else { } else {
return undefined return undefined
} }
} }
// 1:新 -1:旧 0:相同
function compareVersions(a: string, b: string): number {
const parsePart = (part: string) => {
const numPart = part.split('-')[0]
const num = parseInt(numPart, 10)
return isNaN(num) ? 0 : num
}
const v1 = a.replace(/^v/, '').split('.').map(parsePart)
const v2 = b.replace(/^v/, '').split('.').map(parsePart)
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
const num1 = v1[i] || 0
const num2 = v2[i] || 0
if (num1 > num2) return 1
if (num1 < num2) return -1
}
return 0
}
export async function downloadAndInstallUpdate(version: string): Promise<void> { export async function downloadAndInstallUpdate(version: string): Promise<void> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const baseUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}/` const baseUrl = `https://github.com/mihomo-party-org/mihomo-party/releases/download/v${version}/`

View File

@ -12,7 +12,6 @@ import {
subStoreDir, subStoreDir,
themesDir themesDir
} from '../utils/dirs' } from '../utils/dirs'
import { systemLogger } from '../utils/logger'
export async function webdavBackup(): Promise<boolean> { export async function webdavBackup(): Promise<boolean> {
const { createClient } = await import('webdav/dist/node/index.js') const { createClient } = await import('webdav/dist/node/index.js')
@ -76,7 +75,7 @@ export async function webdavBackup(): Promise<boolean> {
} }
} }
} catch (error) { } catch (error) {
await systemLogger.error('Failed to clean up old backup files', error) console.error('Failed to clean up old backup files:', error)
} }
} }

View File

@ -5,112 +5,62 @@ import { join } from 'path'
import { getAppConfig, patchAppConfig } from '../config' import { getAppConfig, patchAppConfig } from '../config'
import { applyTheme } from './theme' import { applyTheme } from './theme'
import { buildContextMenu, showTrayIcon } from './tray' import { buildContextMenu, showTrayIcon } from './tray'
import { floatingWindowLogger } from '../utils/logger'
export let floatingWindow: BrowserWindow | null = null export let floatingWindow: BrowserWindow | null = null
function logError(message: string, error?: any): void {
floatingWindowLogger.log(`FloatingWindow Error: ${message}`, error).catch(() => {})
}
async function createFloatingWindow(): Promise<void> { async function createFloatingWindow(): Promise<void> {
try { const floatingWindowState = windowStateKeeper({
const floatingWindowState = windowStateKeeper({ file: 'floating-window-state.json' }) file: 'floating-window-state.json'
const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig() })
const { customTheme = 'default.css' } = await getAppConfig()
const safeMode = process.env.FLOATING_SAFE_MODE === 'true' floatingWindow = new BrowserWindow({
const useCompatMode = floatingWindowCompatMode || width: 120,
process.env.FLOATING_COMPAT_MODE === 'true' || height: 42,
safeMode x: floatingWindowState.x,
y: floatingWindowState.y,
const windowOptions: Electron.BrowserWindowConstructorOptions = { show: false,
width: 120, frame: false,
height: 42, alwaysOnTop: true,
x: floatingWindowState.x, resizable: false,
y: floatingWindowState.y, transparent: true,
show: false, skipTaskbar: true,
frame: safeMode, minimizable: false,
alwaysOnTop: !safeMode, maximizable: false,
resizable: safeMode, fullscreenable: false,
transparent: !safeMode && !useCompatMode, closable: false,
skipTaskbar: !safeMode, webPreferences: {
minimizable: safeMode, preload: join(__dirname, '../preload/index.js'),
maximizable: safeMode, spellcheck: false,
fullscreenable: false, sandbox: false
closable: safeMode,
backgroundColor: safeMode ? '#ffffff' : (useCompatMode ? '#f0f0f0' : '#00000000'),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
spellcheck: false,
sandbox: false,
nodeIntegration: false,
contextIsolation: true
}
} }
})
if (process.platform === 'win32') { floatingWindowState.manage(floatingWindow)
windowOptions.hasShadow = !safeMode floatingWindow.on('ready-to-show', () => {
windowOptions.webPreferences!.offscreen = false applyTheme(customTheme)
floatingWindow?.show()
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
})
floatingWindow.on('moved', () => {
if (floatingWindow) floatingWindowState.saveState(floatingWindow)
})
ipcMain.on('updateFloatingWindow', () => {
if (floatingWindow) {
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
floatingWindow?.webContents.send('appConfigUpdated')
} }
})
floatingWindow = new BrowserWindow(windowOptions) if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
floatingWindowState.manage(floatingWindow) floatingWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/floating.html`)
} else {
// 事件监听器 floatingWindow.loadFile(join(__dirname, '../renderer/floating.html'))
floatingWindow.webContents.on('render-process-gone', (_, details) => {
logError('Render process gone', details.reason)
floatingWindow = null
})
floatingWindow.on('ready-to-show', () => {
applyTheme(customTheme)
floatingWindow?.show()
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
})
floatingWindow.on('moved', () => {
floatingWindow && floatingWindowState.saveState(floatingWindow)
})
// IPC 监听器
ipcMain.on('updateFloatingWindow', () => {
if (floatingWindow) {
floatingWindow.webContents.send('controledMihomoConfigUpdated')
floatingWindow.webContents.send('appConfigUpdated')
}
})
// 加载页面
const url = is.dev && process.env['ELECTRON_RENDERER_URL']
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
: join(__dirname, '../renderer/floating.html')
is.dev ? await floatingWindow.loadURL(url) : await floatingWindow.loadFile(url)
} catch (error) {
logError('Failed to create floating window', error)
floatingWindow = null
throw error
} }
} }
export async function showFloatingWindow(): Promise<void> { export async function showFloatingWindow(): Promise<void> {
try { if (floatingWindow) {
if (floatingWindow && !floatingWindow.isDestroyed()) { floatingWindow.show()
floatingWindow.show() } else {
} else { createFloatingWindow()
await createFloatingWindow()
}
} catch (error) {
logError('Failed to show floating window', error)
// 如果已经是兼容模式还是崩溃,自动禁用悬浮窗
const { floatingWindowCompatMode = true } = await getAppConfig()
if (floatingWindowCompatMode) {
await patchAppConfig({ showFloatingWindow: false })
} else {
await patchAppConfig({ floatingWindowCompatMode: true })
}
throw error
} }
} }

View File

@ -11,7 +11,6 @@ import { nativeImage } from 'electron'
import express from 'express' import express from 'express'
import axios from 'axios' import axios from 'axios'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import { systemLogger } from '../utils/logger'
export let pacPort: number export let pacPort: number
export let subStorePort: number export let subStorePort: number
@ -119,7 +118,7 @@ export async function startSubStoreBackendServer(): Promise<void> {
SUB_STORE_MMDB_COUNTRY_PATH: path.join(mihomoWorkDir(), 'country.mmdb'), SUB_STORE_MMDB_COUNTRY_PATH: path.join(mihomoWorkDir(), 'country.mmdb'),
SUB_STORE_MMDB_ASN_PATH: path.join(mihomoWorkDir(), 'ASN.mmdb') SUB_STORE_MMDB_ASN_PATH: path.join(mihomoWorkDir(), 'ASN.mmdb')
} }
subStoreBackendWorker = new Worker(path.join(mihomoWorkDir(), 'sub-store.bundle.cjs'), { subStoreBackendWorker = new Worker(path.join(mihomoWorkDir(), 'sub-store.bundle.js'), {
env: useProxyInSubStore env: useProxyInSubStore
? { ? {
...env, ...env,
@ -143,7 +142,7 @@ export async function stopSubStoreBackendServer(): Promise<void> {
export async function downloadSubStore(): Promise<void> { export async function downloadSubStore(): Promise<void> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const frontendDir = path.join(mihomoWorkDir(), 'sub-store-frontend') const frontendDir = path.join(mihomoWorkDir(), 'sub-store-frontend')
const backendPath = path.join(mihomoWorkDir(), 'sub-store.bundle.cjs') const backendPath = path.join(mihomoWorkDir(), 'sub-store.bundle.js')
const tempDir = path.join(dataDir(), 'temp') const tempDir = path.join(dataDir(), 'temp')
try { try {
@ -154,7 +153,7 @@ export async function downloadSubStore(): Promise<void> {
mkdirSync(tempDir, { recursive: true }) mkdirSync(tempDir, { recursive: true })
// 下载后端文件 // 下载后端文件
const tempBackendPath = path.join(tempDir, 'sub-store.bundle.cjs') const tempBackendPath = path.join(tempDir, 'sub-store.bundle.js')
const backendRes = await axios.get( const backendRes = await axios.get(
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js', 'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js',
{ {
@ -169,6 +168,7 @@ export async function downloadSubStore(): Promise<void> {
) )
await writeFile(tempBackendPath, Buffer.from(backendRes.data)) await writeFile(tempBackendPath, Buffer.from(backendRes.data))
// 下载前端文件 // 下载前端文件
const tempFrontendDir = path.join(tempDir, 'dist')
const frontendRes = await axios.get( const frontendRes = await axios.get(
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip', 'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
{ {
@ -192,7 +192,7 @@ export async function downloadSubStore(): Promise<void> {
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true }) await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
await rm(tempDir, { recursive: true }) await rm(tempDir, { recursive: true })
} catch (error) { } catch (error) {
await systemLogger.error('substore.downloadFailed', error) console.error('substore.downloadFailed:', error)
throw error throw error
} }
} }

View File

@ -7,30 +7,29 @@ import {
patchControledMihomoConfig patchControledMihomoConfig
} from '../config' } from '../config'
import icoIcon from '../../../resources/icon.ico?asset' import icoIcon from '../../../resources/icon.ico?asset'
import icoIconBlue from '../../../resources/icon_blue.ico?asset'
import icoIconRed from '../../../resources/icon_red.ico?asset'
import icoIconGreen from '../../../resources/icon_green.ico?asset'
import pngIcon from '../../../resources/icon.png?asset' import pngIcon from '../../../resources/icon.png?asset'
import pngIconBlue from '../../../resources/icon_blue.png?asset' import templateIcon from '../../../resources/iconTemplate.png?asset'
import pngIconRed from '../../../resources/icon_red.png?asset' import {
import pngIconGreen from '../../../resources/icon_green.png?asset' mihomoChangeProxy,
import { mihomoChangeProxy, mihomoCloseAllConnections, mihomoGroups, patchMihomoConfig, getTrayIconStatus } from '../core/mihomoApi' mihomoCloseAllConnections,
mihomoGroups,
patchMihomoConfig
} from '../core/mihomoApi'
import { mainWindow, showMainWindow, triggerMainWindow } from '..' import { mainWindow, showMainWindow, triggerMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron' import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs' import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager' import { quitWithoutCore, restartCore } from '../core/manager'
import { floatingWindow, triggerFloatingWindow } from './floatingWindow' import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
import { t } from 'i18next' import { t } from 'i18next'
import { trayLogger } from '../utils/logger'
export let tray: Tray | null = null export let tray: Tray | null = null
export const buildContextMenu = async (): Promise<Menu> => { export const buildContextMenu = async (): Promise<Menu> => {
// 添加调试日志 // 添加调试日志
await trayLogger.debug('Current translation for tray.showWindow', t('tray.showWindow')) console.log('Current translation for tray.showWindow:', t('tray.showWindow'))
await trayLogger.debug('Current translation for tray.hideFloatingWindow', t('tray.hideFloatingWindow')) console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow'))
await trayLogger.debug('Current translation for tray.showFloatingWindow', t('tray.showFloatingWindow')) console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow'))
const { mode, tun } = await getControledMihomoConfig() const { mode, tun } = await getControledMihomoConfig()
const { const {
@ -120,7 +119,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
mainWindow?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
mainWindow?.webContents.send('groupsUpdated') mainWindow?.webContents.send('groupsUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
await updateTrayIcon()
} }
}, },
{ {
@ -135,7 +133,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
mainWindow?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
mainWindow?.webContents.send('groupsUpdated') mainWindow?.webContents.send('groupsUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
await updateTrayIcon()
} }
}, },
{ {
@ -150,7 +147,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
mainWindow?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
mainWindow?.webContents.send('groupsUpdated') mainWindow?.webContents.send('groupsUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
await updateTrayIcon()
} }
}, },
{ type: 'separator' }, { type: 'separator' },
@ -170,7 +166,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
// ignore // ignore
} finally { } finally {
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
await updateTrayIcon()
} }
} }
}, },
@ -183,35 +178,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
const enable = item.checked const enable = item.checked
try { try {
if (enable) { if (enable) {
// 检查权限
try {
const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) {
if (process.platform === 'win32') {
try {
await restartAsAdmin()
} catch (error) {
await trayLogger.error('Failed to restart as admin from tray', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
} else {
try {
await requestTunPermissions()
} catch (error) {
await trayLogger.error('Failed to grant TUN permissions from tray', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
}
}
} catch (error) {
await trayLogger.warn('Permission check failed in tray', error)
}
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
} else { } else {
await patchControledMihomoConfig({ tun: { enable } }) await patchControledMihomoConfig({ tun: { enable } })
@ -223,7 +189,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
// ignore // ignore
} finally { } finally {
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
await updateTrayIcon()
} }
} }
}, },
@ -242,7 +207,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
await changeCurrentProfile(item.id) await changeCurrentProfile(item.id)
mainWindow?.webContents.send('profileConfigUpdated') mainWindow?.webContents.send('profileConfigUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
await updateTrayIcon()
} }
} }
}) })
@ -334,8 +298,7 @@ export async function createTray(): Promise<void> {
tray.setContextMenu(menu) tray.setContextMenu(menu)
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const iconPaths = getIconPaths() const icon = nativeImage.createFromPath(templateIcon).resize({ height: 16 })
const icon = nativeImage.createFromPath(iconPaths.white).resize({ height: 16 })
icon.setTemplateImage(true) icon.setTemplateImage(true)
tray = new Tray(icon) tray = new Tray(icon)
} }
@ -344,9 +307,6 @@ export async function createTray(): Promise<void> {
} }
tray?.setToolTip('Mihomo Party') tray?.setToolTip('Mihomo Party')
tray?.setIgnoreDoubleClickEvents(true) tray?.setIgnoreDoubleClickEvents(true)
await updateTrayIcon()
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
if (!useDockIcon) { if (!useDockIcon) {
hideDockIcon() hideDockIcon()
@ -429,53 +389,13 @@ export async function closeTrayIcon(): Promise<void> {
} }
export async function showDockIcon(): Promise<void> { export async function showDockIcon(): Promise<void> {
if (process.platform === 'darwin' && app.dock && !app.dock.isVisible()) { if (process.platform === 'darwin' && !app.dock.isVisible()) {
await app.dock.show() await app.dock.show()
} }
} }
export async function hideDockIcon(): Promise<void> { export async function hideDockIcon(): Promise<void> {
if (process.platform === 'darwin' && app.dock && app.dock.isVisible()) { if (process.platform === 'darwin' && app.dock.isVisible()) {
app.dock.hide() app.dock.hide()
} }
} }
const getIconPaths = () => {
if (process.platform === 'win32') {
return {
white: icoIcon,
blue: icoIconBlue,
green: icoIconGreen,
red: icoIconRed
}
} else {
return {
white: pngIcon,
blue: pngIconBlue,
green: pngIconGreen,
red: pngIconRed
}
}
}
export async function updateTrayIcon(): Promise<void> {
if (!tray) return
const status = await getTrayIconStatus()
const iconPaths = getIconPaths()
const iconPath = iconPaths[status]
try {
if (process.platform === 'darwin') {
const icon = nativeImage.createFromPath(iconPath).resize({ height: 16 })
icon.setTemplateImage(true)
tray.setImage(icon)
} else if (process.platform === 'win32') {
tray.setImage(iconPath)
} else if (process.platform === 'linux') {
tray.setImage(iconPath)
}
} catch (error) {
console.error('更新托盘图标失败:', error)
}
}

View File

@ -1,16 +1,13 @@
import { exePath, homeDir } from '../utils/dirs' import { exePath, homeDir, taskDir } from '../utils/dirs'
import { tmpdir } from 'os'
import { mkdir, readFile, rm, writeFile } from 'fs/promises' import { mkdir, readFile, rm, writeFile } from 'fs/promises'
import { exec } from 'child_process' import { exec } from 'child_process'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import { promisify } from 'util' import { promisify } from 'util'
import path from 'path' import path from 'path'
import { managerLogger } from '../utils/logger'
const appName = 'mihomo-party' const appName = 'mihomo-party'
function getTaskXml(asAdmin: boolean): string { function getTaskXml(): string {
const runLevel = asAdmin ? 'HighestAvailable' : 'LeastPrivilege'
return `<?xml version="1.0" encoding="UTF-16"?> return `<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers> <Triggers>
@ -22,7 +19,7 @@ function getTaskXml(asAdmin: boolean): string {
<Principals> <Principals>
<Principal id="Author"> <Principal id="Author">
<LogonType>InteractiveToken</LogonType> <LogonType>InteractiveToken</LogonType>
<RunLevel>${runLevel}</RunLevel> <RunLevel>HighestAvailable</RunLevel>
</Principal> </Principal>
</Principals> </Principals>
<Settings> <Settings>
@ -46,7 +43,8 @@ function getTaskXml(asAdmin: boolean): string {
</Settings> </Settings>
<Actions Context="Author"> <Actions Context="Author">
<Exec> <Exec>
<Command>"${exePath()}"</Command> <Command>"${path.join(taskDir(), `mihomo-party-run.exe`)}"</Command>
<Arguments>"${exePath()}"</Arguments>
</Exec> </Exec>
</Actions> </Actions>
</Task> </Task>
@ -83,22 +81,11 @@ export async function checkAutoRun(): Promise<boolean> {
export async function enableAutoRun(): Promise<void> { export async function enableAutoRun(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
const taskFilePath = path.join(tmpdir(), `${appName}.xml`) const taskFilePath = path.join(taskDir(), `${appName}.xml`)
const { checkAdminPrivileges } = await import('../core/manager') await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml()}`, 'utf-16le'))
const isAdmin = await checkAdminPrivileges() await execPromise(
await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml(isAdmin)}`, 'utf-16le')) `%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`
if (isAdmin) { )
await execPromise(`%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`)
} else {
try {
await execPromise(
`powershell -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
)
}
catch (e) {
await managerLogger.info('Maybe the user rejected the UAC dialog?')
}
}
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
@ -134,17 +121,7 @@ Categories=Utility;
export async function disableAutoRun(): Promise<void> { export async function disableAutoRun(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
const { checkAdminPrivileges } = await import('../core/manager') await execPromise(`%SystemRoot%\\System32\\schtasks.exe /delete /tn "${appName}" /f`)
const isAdmin = await checkAdminPrivileges()
if (isAdmin) {
await execPromise(`%SystemRoot%\\System32\\schtasks.exe /delete /tn "${appName}" /f`)
} else {
try {
await execPromise(`powershell -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"`)
} catch (e) {
await managerLogger.info('Maybe the user rejected the UAC dialog?')
}
}
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const execPromise = promisify(exec) const execPromise = promisify(exec)

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

View File

@ -8,7 +8,6 @@ import { resourcesFilesDir } from '../utils/dirs'
import { net } from 'electron' import { net } from 'electron'
import axios from 'axios' import axios from 'axios'
import fs from 'fs' import fs from 'fs'
import { proxyLogger } from '../utils/logger'
let defaultBypass: string[] let defaultBypass: string[]
let triggerSysProxyTimer: NodeJS.Timeout | null = null let triggerSysProxyTimer: NodeJS.Timeout | null = null
@ -168,7 +167,7 @@ async function requestSocketRecreation(): Promise<void> {
const { promisify } = require('util') const { promisify } = require('util')
const execPromise = promisify(exec) const execPromise = promisify(exec)
// Use osascript with administrator privileges (same pattern as grantTunPermissions) // Use osascript with administrator privileges (same pattern as manualGrantCorePermition)
const shell = `pkill -USR1 -f party.mihomo.helper` const shell = `pkill -USR1 -f party.mihomo.helper`
const command = `do shell script "${shell}" with administrator privileges` const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`) await execPromise(`osascript -e '${command}'`)
@ -176,7 +175,7 @@ async function requestSocketRecreation(): Promise<void> {
// Wait a bit for socket recreation // Wait a bit for socket recreation
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) { } catch (error) {
await proxyLogger.error('Failed to send signal to helper', error) console.log('Failed to send signal to helper:', error)
throw error throw error
} }
} }
@ -198,16 +197,16 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
(error as Error).message?.includes('connect ECONNREFUSED') || (error as Error).message?.includes('connect ECONNREFUSED') ||
(error as Error).message?.includes('ENOENT'))) { (error as Error).message?.includes('ENOENT'))) {
await proxyLogger.info(`Helper request failed (attempt ${attempt + 1}), checking socket file...`) console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
if (!isSocketFileExists()) { if (!isSocketFileExists()) {
await proxyLogger.info('Socket file missing, requesting recreation...') console.log('Socket file missing, requesting recreation...')
try { try {
await requestSocketRecreation() await requestSocketRecreation()
await proxyLogger.info('Socket recreation requested, retrying...') console.log('Socket recreation requested, retrying...')
continue continue
} catch (signalError) { } catch (signalError) {
await proxyLogger.warn('Failed to request socket recreation', signalError) console.log('Failed to request socket recreation:', signalError)
} }
} }
} }

View File

@ -69,10 +69,6 @@ export function mihomoCoreDir(): string {
export function mihomoCorePath(core: string): string { export function mihomoCorePath(core: string): string {
const isWin = process.platform === 'win32' 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' : ''}`) return path.join(mihomoCoreDir(), `${core}${isWin ? '.exe' : ''}`)
} }
@ -134,27 +130,12 @@ export function logDir(): string {
export function logPath(): string { export function logPath(): string {
const date = new Date() const date = new Date()
const year = date.getFullYear() const name = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const name = `mihomo-party-${year}-${month}-${day}`
return path.join(logDir(), `${name}.log`) return path.join(logDir(), `${name}.log`)
} }
export function substoreLogPath(): string { export function substoreLogPath(): string {
const date = new Date() const date = new Date()
const year = date.getFullYear() const name = `sub-store-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const name = `sub-store-${year}-${month}-${day}`
return path.join(logDir(), `${name}.log`)
}
export function coreLogPath(): string {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const name = `core-${year}-${month}-${day}`
return path.join(logDir(), `${name}.log`) return path.join(logDir(), `${name}.log`)
} }

View File

@ -39,26 +39,8 @@ import {
patchAppConfig, patchAppConfig,
patchControledMihomoConfig patchControledMihomoConfig
} from '../config' } from '../config'
import { app, dialog } from 'electron' import { app } from 'electron'
import { startSSIDCheck } from '../sys/ssid' import { startSSIDCheck } from '../sys/ssid'
import i18next from '../../shared/i18n'
import { initLogger } from './logger'
// 安全错误处理
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> { async function fixDataDirPermissions(): Promise<void> {
if (process.platform !== 'darwin') return if (process.platform !== 'darwin') return
@ -83,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> { async function initDirs(): Promise<void> {
await fixDataDirPermissions() await fixDataDirPermissions()
// 按依赖顺序创建目录 if (!existsSync(dataDir())) {
const dirsToCreate = [ await mkdir(dataDir())
dataDir(), }
themesDir(), if (!existsSync(themesDir())) {
profilesDir(), await mkdir(themesDir())
overrideDir(), }
mihomoWorkDir(), if (!existsSync(profilesDir())) {
logDir(), await mkdir(profilesDir())
mihomoTestDir(), }
subStoreDir() if (!existsSync(overrideDir())) {
] await mkdir(overrideDir())
}
for (const dir of dirsToCreate) { if (!existsSync(mihomoWorkDir())) {
try { await mkdir(mihomoWorkDir())
if (!existsSync(dir)) { }
await mkdir(dir, { recursive: true }) if (!existsSync(logDir())) {
} await mkdir(logDir())
} catch (error) { }
await initLogger.error(`Failed to create directory ${dir}`, error) if (!existsSync(mihomoTestDir())) {
throw new Error(`Failed to create directory ${dir}: ${error}`) await mkdir(mihomoTestDir())
} }
if (!existsSync(subStoreDir())) {
await mkdir(subStoreDir())
} }
} }
async function initConfig(): Promise<void> { async function initConfig(): Promise<void> {
const configs = [ if (!existsSync(appConfigPath())) {
{ path: appConfigPath(), content: defaultConfig, name: 'app config' }, await writeFile(appConfigPath(), yaml.stringify(defaultConfig))
{ path: profileConfigPath(), content: defaultProfileConfig, name: 'profile config' }, }
{ path: overrideConfigPath(), content: defaultOverrideConfig, name: 'override config' }, if (!existsSync(profileConfigPath())) {
{ path: profilePath('default'), content: defaultProfile, name: 'default profile' }, await writeFile(profileConfigPath(), yaml.stringify(defaultProfileConfig))
{ path: controledMihomoConfigPath(), content: defaultControledMihomoConfig, name: 'mihomo config' } }
] if (!existsSync(overrideConfigPath())) {
await writeFile(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
for (const config of configs) { }
try { if (!existsSync(profilePath('default'))) {
if (!existsSync(config.path)) { await writeFile(profilePath('default'), yaml.stringify(defaultProfile))
await writeFile(config.path, yaml.stringify(config.content)) }
} if (!existsSync(controledMihomoConfigPath())) {
} catch (error) { await writeFile(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
await initLogger.error(`Failed to create ${config.name} at ${config.path}`, error)
throw new Error(`Failed to create ${config.name}: ${error}`)
}
} }
} }
@ -148,44 +117,20 @@ async function initFiles(): Promise<void> {
const targetPath = path.join(mihomoWorkDir(), file) const targetPath = path.join(mihomoWorkDir(), file)
const testTargetPath = path.join(mihomoTestDir(), file) const testTargetPath = path.join(mihomoTestDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file) const sourcePath = path.join(resourcesFilesDir(), file)
if (!existsSync(targetPath) && existsSync(sourcePath)) {
try { await cp(sourcePath, targetPath, { recursive: true })
// 检查是否需要复制 }
if (existsSync(sourcePath)) { if (!existsSync(testTargetPath) && existsSync(sourcePath)) {
const shouldCopyToWork = !existsSync(targetPath) || await isSourceNewer(sourcePath, targetPath) await cp(sourcePath, testTargetPath, { recursive: true })
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) {
await initLogger.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(mihomoWorkDir())) {
await mkdir(mihomoWorkDir(), { recursive: true })
}
if (!existsSync(mihomoTestDir())) {
await mkdir(mihomoTestDir(), { recursive: true })
}
await Promise.all([ await Promise.all([
copy('country.mmdb'), copy('country.mmdb'),
copy('geoip.metadb'), copy('geoip.metadb'),
copy('geoip.dat'), copy('geoip.dat'),
copy('geosite.dat'), copy('geosite.dat'),
copy('ASN.mmdb'), copy('ASN.mmdb'),
copy('sub-store.bundle.cjs'), copy('sub-store.bundle.js'),
copy('sub-store-frontend') copy('sub-store-frontend')
]) ])
} }
@ -250,8 +195,7 @@ async function migration(): Promise<void> {
authentication, authentication,
'bind-address': bindAddress, 'bind-address': bindAddress,
'lan-allowed-ips': lanAllowedIps, 'lan-allowed-ips': lanAllowedIps,
'lan-disallowed-ips': lanDisallowedIps, 'lan-disallowed-ips': lanDisallowedIps
tun
} = await getControledMihomoConfig() } = await getControledMihomoConfig()
// add substore sider card // add substore sider card
if (useSubStore && !siderOrder.includes('substore')) { if (useSubStore && !siderOrder.includes('substore')) {
@ -277,16 +221,6 @@ async function migration(): Promise<void> {
if (!lanDisallowedIps) { if (!lanDisallowedIps) {
await patchControledMihomoConfig({ 'lan-disallowed-ips': [] }) await patchControledMihomoConfig({ 'lan-disallowed-ips': [] })
} }
// default tun device
if (!tun?.device || (process.platform === 'darwin' && tun.device === 'Mihomo')) {
const defaultDevice = process.platform === 'darwin' ? 'utun1500' : 'Mihomo'
await patchControledMihomoConfig({
tun: {
...tun,
device: defaultDevice
}
})
}
// remove custom app theme // remove custom app theme
if (!['system', 'light', 'dark'].includes(appTheme)) { if (!['system', 'light', 'dark'].includes(appTheme)) {
await patchAppConfig({ appTheme: 'system' }) await patchAppConfig({ appTheme: 'system' })
@ -329,17 +263,12 @@ function initDeeplink(): void {
} }
} }
// 基础初始化 export async function init(): Promise<void> {
export async function initBasic(): Promise<void> {
await initDirs() await initDirs()
await initConfig() await initConfig()
await migration() await migration()
await initFiles() await initFiles()
await cleanup() await cleanup()
}
export async function init(): Promise<void> {
await initBasic()
await startSubStoreFrontendServer() await startSubStoreFrontendServer()
await startSubStoreBackendServer() await startSubStoreBackendServer()
const { sysProxy } = await getAppConfig() const { sysProxy } = await getAppConfig()

View File

@ -16,9 +16,7 @@ import {
mihomoUpgrade, mihomoUpgrade,
mihomoUpgradeGeo, mihomoUpgradeGeo,
mihomoVersion, mihomoVersion,
patchMihomoConfig, patchMihomoConfig
mihomoSmartGroupWeights,
mihomoSmartFlushCache
} from '../core/mihomoApi' } from '../core/mihomoApi'
import { checkAutoRun, disableAutoRun, enableAutoRun } from '../sys/autoRun' import { checkAutoRun, disableAutoRun, enableAutoRun } from '../sys/autoRun'
import { import {
@ -56,20 +54,7 @@ import {
subStoreFrontendPort, subStoreFrontendPort,
subStorePort subStorePort
} from '../resolve/server' } from '../resolve/server'
import { import { manualGrantCorePermition, quitWithoutCore, restartCore } from '../core/manager'
quitWithoutCore,
restartCore,
checkTunPermissions,
grantTunPermissions,
manualGrantCorePermition,
checkAdminPrivileges,
restartAsAdmin,
checkMihomoCorePermissions,
requestTunPermissions,
checkHighPrivilegeCore,
showTunPermissionDialog,
showErrorDialog
} from '../core/manager'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
import { import {
@ -84,7 +69,7 @@ import {
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory' import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup' import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup'
import { getInterfaces } from '../sys/interface' import { getInterfaces } from '../sys/interface'
import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon } from '../resolve/tray' import { closeTrayIcon, copyEnv, showTrayIcon } from '../resolve/tray'
import { registerShortcut } from '../resolve/shortcut' import { registerShortcut } from '../resolve/shortcut'
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..' import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
import { import {
@ -156,13 +141,6 @@ export function registerIpcMainHandlers(): void {
ipcErrorWrapper(mihomoGroupDelay)(group, url) ipcErrorWrapper(mihomoGroupDelay)(group, url)
) )
ipcMain.handle('patchMihomoConfig', (_e, patch) => ipcErrorWrapper(patchMihomoConfig)(patch)) 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('checkAutoRun', ipcErrorWrapper(checkAutoRun))
ipcMain.handle('enableAutoRun', ipcErrorWrapper(enableAutoRun)) ipcMain.handle('enableAutoRun', ipcErrorWrapper(enableAutoRun))
ipcMain.handle('disableAutoRun', ipcErrorWrapper(disableAutoRun)) ipcMain.handle('disableAutoRun', ipcErrorWrapper(disableAutoRun))
@ -199,16 +177,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached)) ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable)) ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)()) ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)())
ipcMain.handle('checkAdminPrivileges', () => ipcErrorWrapper(checkAdminPrivileges)())
ipcMain.handle('restartAsAdmin', () => ipcErrorWrapper(restartAsAdmin)())
ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)())
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)())
ipcMain.handle('showTunPermissionDialog', () => ipcErrorWrapper(showTunPermissionDialog)())
ipcMain.handle('showErrorDialog', (_, title: string, message: string) => ipcErrorWrapper(showErrorDialog)(title, message))
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr)) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
@ -259,7 +227,6 @@ export function registerIpcMainHandlers(): void {
}) })
ipcMain.handle('showTrayIcon', () => ipcErrorWrapper(showTrayIcon)()) ipcMain.handle('showTrayIcon', () => ipcErrorWrapper(showTrayIcon)())
ipcMain.handle('closeTrayIcon', () => ipcErrorWrapper(closeTrayIcon)()) ipcMain.handle('closeTrayIcon', () => ipcErrorWrapper(closeTrayIcon)())
ipcMain.handle('updateTrayIcon', () => ipcErrorWrapper(updateTrayIcon)())
ipcMain.handle('showMainWindow', showMainWindow) ipcMain.handle('showMainWindow', showMainWindow)
ipcMain.handle('closeMainWindow', closeMainWindow) ipcMain.handle('closeMainWindow', closeMainWindow)
ipcMain.handle('triggerMainWindow', triggerMainWindow) ipcMain.handle('triggerMainWindow', triggerMainWindow)
@ -284,18 +251,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('alert', (_e, msg) => { ipcMain.handle('alert', (_e, msg) => {
dialog.showErrorBox('Mihomo Party', 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('resetAppConfig', resetAppConfig)
ipcMain.handle('relaunchApp', () => { ipcMain.handle('relaunchApp', () => {
app.relaunch() app.relaunch()

View File

@ -1,108 +0,0 @@
import { writeFile } from 'fs/promises'
import { logPath } from './dirs'
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
class Logger {
private moduleName: string
constructor(moduleName: string) {
this.moduleName = moduleName
}
private formatTimestamp(): string {
return new Date().toISOString()
}
private formatLogMessage(level: LogLevel, message: string, error?: any): string {
const timestamp = this.formatTimestamp()
const errorStr = error ? `: ${error}` : ''
return `[${timestamp}] [${level.toUpperCase()}] [${this.moduleName}] ${message}${errorStr}\n`
}
private async writeToFile(level: LogLevel, message: string, error?: any): Promise<void> {
try {
const appLogPath = logPath()
const logMessage = this.formatLogMessage(level, message, error)
await writeFile(appLogPath, logMessage, { flag: 'a' })
} catch (logError) {
// 如果写入日志文件失败,仍然输出到控制台
console.error(`[Logger] Failed to write to log file:`, logError)
console.error(`[Logger] Original message: [${level.toUpperCase()}] [${this.moduleName}] ${message}`, error)
}
}
private logToConsole(level: LogLevel, message: string, error?: any): void {
const prefix = `[${this.moduleName}] ${message}`
switch (level) {
case 'debug':
console.debug(prefix, error || '')
break
case 'info':
console.log(prefix, error || '')
break
case 'warn':
console.warn(prefix, error || '')
break
case 'error':
console.error(prefix, error || '')
break
}
}
async debug(message: string, error?: any): Promise<void> {
await this.writeToFile('debug', message, error)
this.logToConsole('debug', message, error)
}
async info(message: string, error?: any): Promise<void> {
await this.writeToFile('info', message, error)
this.logToConsole('info', message, error)
}
async warn(message: string, error?: any): Promise<void> {
await this.writeToFile('warn', message, error)
this.logToConsole('warn', message, error)
}
async error(message: string, error?: any): Promise<void> {
await this.writeToFile('error', message, error)
this.logToConsole('error', message, error)
}
// 兼容原有的 logFloatingWindow 函数签名
async log(message: string, error?: any): Promise<void> {
if (error) {
await this.error(message, error)
} else {
await this.info(message)
}
}
}
// 创建不同模块的日志实例
export const createLogger = (moduleName: string): Logger => {
return new Logger(moduleName)
}
// 统一的应用日志实例 - 所有模块共享同一个日志文件
export const appLogger = createLogger('app')
// 为了保持向后兼容性,创建各模块的日志实例(都指向同一个应用日志)
export const floatingWindowLogger = createLogger('floating-window')
export const coreLogger = createLogger('mihomo-core')
export const apiLogger = createLogger('mihomo-api')
export const configLogger = createLogger('config')
export const systemLogger = createLogger('system')
export const trafficLogger = createLogger('traffic-monitor')
export const trayLogger = createLogger('tray')
export const initLogger = createLogger('init')
export const ipcLogger = createLogger('ipc')
export const proxyLogger = createLogger('sysproxy')
export const managerLogger = createLogger('manager')
export const factoryLogger = createLogger('factory')
export const overrideLogger = createLogger('override')
// 默认日志实例
export const logger = appLogger

View File

@ -1,10 +1,5 @@
export const defaultConfig: IAppConfig = { export const defaultConfig: IAppConfig = {
core: 'mihomo', core: 'mihomo',
enableSmartCore: true,
enableSmartOverride: true,
smartCoreUseLightGBM: false,
smartCoreCollectData: false,
smartCoreStrategy: 'sticky-sessions',
silentStart: false, silentStart: false,
appTheme: 'system', appTheme: 'system',
useWindowFrame: false, useWindowFrame: false,
@ -21,11 +16,6 @@ export const defaultConfig: IAppConfig = {
useNameserverPolicy: false, useNameserverPolicy: false,
controlDns: true, controlDns: true,
controlSniff: true, controlSniff: true,
floatingWindowCompatMode: true,
disableLoopbackDetector: false,
disableEmbedCA: false,
disableSystemCA: false,
skipSafePathCheck: false,
nameserverPolicy: {}, nameserverPolicy: {},
siderOrder: [ siderOrder: [
'sysproxy', 'sysproxy',
@ -67,7 +57,7 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
'skip-auth-prefixes': ['127.0.0.1/32'], 'skip-auth-prefixes': ['127.0.0.1/32'],
tun: { tun: {
enable: false, enable: false,
device: process.platform === 'darwin' ? 'utun1500' : 'Mihomo', device: 'Mihomo',
stack: 'mixed', stack: 'mixed',
'auto-route': true, 'auto-route': true,
'auto-redirect': false, 'auto-redirect': false,
@ -84,7 +74,6 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
'fake-ip-filter': ['*', '+.lan', '+.local', 'time.*.com', 'ntp.*.com', '+.market.xiaomi.com'], 'fake-ip-filter': ['*', '+.lan', '+.local', 'time.*.com', 'ntp.*.com', '+.market.xiaomi.com'],
'use-hosts': false, 'use-hosts': false,
'use-system-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'], 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'], 'proxy-server-nameserver': ['https://120.53.53.53/dns-query', 'https://223.5.5.5/dns-query'],
'direct-nameserver': [] 'direct-nameserver': []

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,6 @@
@import 'tailwindcss'; @tailwind base;
@plugin './hero.ts'; @tailwind components;
@tailwind utilities;
@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;
}
@font-face { @font-face {
font-family: 'Noto Color Emoji'; font-family: 'Noto Color Emoji';
@ -81,8 +75,6 @@
*:focus { *:focus {
outline: none; outline: none;
outline-color: transparent;
outline-width: 0;
} }
.flag-emoji { .flag-emoji {

View File

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

View File

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

View File

@ -20,7 +20,6 @@ import { restartCore, addProfileUpdater } from '@renderer/utils/ipc'
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
import { FaPlus } from 'react-icons/fa6' import { FaPlus } from 'react-icons/fa6'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { isValidCron } from 'cron-validator';
interface Props { interface Props {
item: IProfileItem item: IProfileItem
@ -101,57 +100,15 @@ const EditInfoModal: React.FC<Props> = (props) => {
/> />
</SettingItem> </SettingItem>
<SettingItem title={t('profiles.editInfo.interval')}> <SettingItem title={t('profiles.editInfo.interval')}>
<div className="flex flex-col gap-2"> <Input
<Input size="sm"
size="sm" type="number"
type="text" className={cn(inputWidth)}
className={cn( value={values.interval?.toString() ?? ''}
inputWidth, onValueChange={(v) => {
// 不合法 setValues({ ...values, interval: parseInt(v) })
typeof values.interval === 'string' && }}
!/^\d+$/.test(values.interval) && />
!isValidCron(values.interval, { seconds: false }) &&
'border-red-500'
)}
value={values.interval?.toString() ?? ''}
onValueChange={(v) => {
// 输入限制
if (/^[\d\s*\-,\/]*$/.test(v)) {
// 纯数字
if (/^\d+$/.test(v)) {
setValues({ ...values, interval: parseInt(v, 10) || 0 });
return;
}
// 非纯数字
try {
setValues({ ...values, interval: v });
} catch (e) {
// ignore
}
}
}}
placeholder="例如30 或 '0 * * * *'"
/>
{/* 动态提示信息 */}
<div className="text-xs" style={{
color: typeof values.interval === 'string' &&
!/^\d+$/.test(values.interval) &&
!isValidCron(values.interval, { seconds: false })
? '#ef4444'
: '#6b7280'
}}>
{typeof values.interval === 'number' ? (
'以分钟为单位的定时间隔'
) : /^\d+$/.test(values.interval?.toString() || '') ? (
'以分钟为单位的定时间隔'
) : isValidCron(values.interval?.toString() || '', { seconds: false }) ? (
'有效的Cron表达式'
) : (
'请输入数字或合法的Cron表达式0 * * * *'
)}
</div>
</div>
</SettingItem> </SettingItem>
<SettingItem title={t('profiles.editInfo.fixedInterval')}> <SettingItem title={t('profiles.editInfo.fixedInterval')}>
<Switch <Switch

View File

@ -14,7 +14,7 @@ import {
import { calcPercent, calcTraffic } from '@renderer/utils/calc' import { calcPercent, calcTraffic } from '@renderer/utils/calc'
import { IoMdMore, IoMdRefresh } from 'react-icons/io' import { IoMdMore, IoMdRefresh } from 'react-icons/io'
import dayjs from '@renderer/utils/dayjs' 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 EditFileModal from './edit-file-modal'
import EditInfoModal from './edit-info-modal' import EditInfoModal from './edit-info-modal'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
@ -72,8 +72,7 @@ const ProfileItem: React.FC<Props> = (props) => {
id: info.id id: info.id
}) })
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const [isActuallyDragging, setIsActuallyDragging] = useState(false) const [disableSelect, setDisableSelect] = useState(false)
const [clickStartPos, setClickStartPos] = useState<{ x: number; y: number } | null>(null)
const menuItems: MenuItem[] = useMemo(() => { const menuItems: MenuItem[] = useMemo(() => {
const list = [ const list = [
@ -151,46 +150,19 @@ const ProfileItem: React.FC<Props> = (props) => {
setDropdownOpen(true) setDropdownOpen(true)
} }
const handleMouseDown = (e: React.MouseEvent) => { useEffect(() => {
if (e.button === 0) { if (isDragging) {
setClickStartPos({ x: e.clientX, y: e.clientY }) setTimeout(() => {
setIsActuallyDragging(false) 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 ( return (
<div <div
@ -214,19 +186,18 @@ const ProfileItem: React.FC<Props> = (props) => {
<Card <Card
as="div" as="div"
fullWidth fullWidth
isPressable={false} isPressable
onPress={() => {
if (disableSelect) return
setSelecting(true)
onPress().finally(() => {
setSelecting(false)
})
}}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''} cursor-pointer`} className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`}
> >
<div <div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full">
ref={setNodeRef}
{...attributes}
{...listeners}
className="w-full h-full"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
<CardBody className="pb-1"> <CardBody className="pb-1">
<div className="flex justify-between h-[32px]"> <div className="flex justify-between h-[32px]">
<h3 <h3
@ -263,12 +234,7 @@ const ProfileItem: React.FC<Props> = (props) => {
onOpenChange={setDropdownOpen} onOpenChange={setDropdownOpen}
> >
<DropdownTrigger> <DropdownTrigger>
<Button <Button isIconOnly size="sm" variant="light" color="default">
isIconOnly
size="sm"
variant="light"
color="default"
>
<IoMdMore <IoMdMore
color="default" color="default"
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`} 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)} onPress={() => onSelect(group.name, proxy.name)}
isPressable isPressable
fullWidth fullWidth
shadow="xs" shadow="sm"
className={`${ className={`${
fixed fixed
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary' ? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'

View File

@ -48,7 +48,6 @@ const GeneralConfig: React.FC = () => {
disableTray = false, disableTray = false,
showFloatingWindow: showFloating = false, showFloatingWindow: showFloating = false,
spinFloatingIcon = true, spinFloatingIcon = true,
floatingWindowCompatMode = true,
useWindowFrame = false, useWindowFrame = false,
autoQuitWithoutCore = false, autoQuitWithoutCore = false,
autoQuitWithoutCoreDelay = 60, autoQuitWithoutCoreDelay = 60,
@ -104,14 +103,6 @@ const GeneralConfig: React.FC = () => {
isSelected={enable} isSelected={enable}
onValueChange={async (v) => { onValueChange={async (v) => {
try { try {
// 检查管理员权限
const hasAdminPrivileges = await window.electron.ipcRenderer.invoke('checkAdminPrivileges')
if (!hasAdminPrivileges) {
const notification = new Notification(t('settings.autoStart.permissions'))
notification.close()
}
if (v) { if (v) {
await enableAutoRun() await enableAutoRun()
} else { } else {
@ -164,25 +155,19 @@ const GeneralConfig: React.FC = () => {
</SettingItem> </SettingItem>
{autoQuitWithoutCore && ( {autoQuitWithoutCore && (
<SettingItem title={t('settings.autoQuitWithoutCoreDelay')} divider> <SettingItem title={t('settings.autoQuitWithoutCoreDelay')} divider>
<div className="flex items-center gap-2"> <Input
<Input size="sm"
size="sm" className="w-[100px]"
className="w-[100px]" type="number"
type="number" endContent={t('common.seconds')}
value={autoQuitWithoutCoreDelay.toString()} value={autoQuitWithoutCoreDelay.toString()}
onValueChange={async (v: string) => { onValueChange={async (v: string) => {
let num = parseInt(v) let num = parseInt(v)
await patchAppConfig({ autoQuitWithoutCoreDelay: num }) if (isNaN(num)) num = 5
}} if (num < 5) num = 5
onBlur={async (e) => { await patchAppConfig({ autoQuitWithoutCoreDelay: num })
let num = parseInt(e.target.value) }}
if (isNaN(num)) num = 5 />
if (num < 5) num = 5
await patchAppConfig({ autoQuitWithoutCoreDelay: num })
}}
/>
<span className="text-default-500">{t('common.seconds')}</span>
</div>
</SettingItem> </SettingItem>
)} )}
<SettingItem <SettingItem
@ -251,43 +236,22 @@ const GeneralConfig: React.FC = () => {
}} }}
/> />
</SettingItem> </SettingItem>
<SettingItem <SettingItem title={t('settings.disableTray')} divider>
title={t('settings.floatingWindowCompatMode')} <Switch
divider size="sm"
> isSelected={disableTray}
<div className="flex items-center gap-2"> onValueChange={async (v) => {
<Switch await patchAppConfig({ disableTray: v })
size="sm" if (v) {
isSelected={floatingWindowCompatMode} closeTrayIcon()
onValueChange={async (v) => { } else {
await patchAppConfig({ floatingWindowCompatMode: v }) showTrayIcon()
closeFloatingWindow() }
setTimeout(() => { }}
showFloatingWindow() />
}, 100)
}}
/>
<Tooltip content={t('settings.floatingWindowCompatModeTooltip')}>
<IoIosHelpCircle className="text-default-500 cursor-help" />
</Tooltip>
</div>
</SettingItem> </SettingItem>
</> </>
)} )}
<SettingItem title={t('settings.disableTray')} divider>
<Switch
size="sm"
isSelected={disableTray}
onValueChange={async (v) => {
await patchAppConfig({ disableTray: v })
if (v) {
closeTrayIcon()
} else {
showTrayIcon()
}
}}
/>
</SettingItem>
{platform !== 'linux' && ( {platform !== 'linux' && (
<> <>
<SettingItem title={t('settings.proxyInTray')} divider> <SettingItem title={t('settings.proxyInTray')} divider>

View File

@ -4,7 +4,7 @@ import SettingItem from '../base/base-setting-item'
import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react' import { Button, Input, Select, SelectItem, Switch, Tooltip } from '@heroui/react'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import debounce from '@renderer/utils/debounce' 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 { MdDeleteForever } from 'react-icons/md'
import { BiCopy } from 'react-icons/bi' import { BiCopy } from 'react-icons/bi'
import { IoIosHelpCircle } from 'react-icons/io' import { IoIosHelpCircle } from 'react-icons/io'
@ -16,6 +16,8 @@ const MihomoConfig: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { const {
diffWorkDir = false, diffWorkDir = false,
controlDns = true,
controlSniff = true,
delayTestConcurrency, delayTestConcurrency,
delayTestTimeout, delayTestTimeout,
githubToken = '', githubToken = '',
@ -191,8 +193,36 @@ const MihomoConfig: React.FC = () => {
}} }}
/> />
</SettingItem> </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> <SettingItem title={t('mihomo.autoCloseConnection')} divider>
<Switch <Switch
size="sm" 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 { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc' 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 { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { IoLink } from 'react-icons/io5' import { IoLink } from 'react-icons/io5'
import { useTheme } from 'next-themes'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import { Line } from 'react-chartjs-2' import { Area, AreaChart, ResponsiveContainer } from 'recharts'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Filler,
ChartOptions,
ScriptableContext
} from 'chart.js'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
// 注册 Chart.js 组件
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler)
let currentUpload: number | undefined = undefined let currentUpload: number | undefined = undefined
let currentDownload: number | undefined = undefined let currentDownload: number | undefined = undefined
let hasShowTraffic = false let hasShowTraffic = false
@ -34,9 +21,10 @@ interface Props {
iconOnly?: boolean iconOnly?: boolean
} }
const ConnCard: React.FC<Props> = (props) => { const ConnCard: React.FC<Props> = (props) => {
const { theme = 'system', systemTheme = 'dark' } = useTheme()
const { iconOnly } = props const { iconOnly } = props
const { appConfig } = useAppConfig() const { appConfig } = useAppConfig()
const { showTraffic = false, connectionCardStatus = 'col-span-2' } = appConfig || {} const { showTraffic = false, connectionCardStatus = 'col-span-2', customTheme } = appConfig || {}
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const match = location.pathname.includes('/connections') const match = location.pathname.includes('/connections')
@ -55,69 +43,25 @@ const ConnCard: React.FC<Props> = (props) => {
id: 'connection' id: 'connection'
}) })
const [series, setSeries] = useState(Array(10).fill(0)) const [series, setSeries] = useState(Array(10).fill(0))
const [chartColor, setChartColor] = useState('rgba(255,255,255)')
// Chart.js 配置 useEffect(() => {
const chartData = useMemo(() => { setChartColor(
return { match
labels: Array(10).fill(''), ? `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-primary-foreground')})`
datasets: [ : `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-foreground')})`
{ )
data: series, }, [theme, systemTheme, match])
fill: true,
backgroundColor: (context: ScriptableContext<'line'>) => {
const chart = context.chart
const { ctx, chartArea } = chart
if (!chartArea) {
return 'transparent'
}
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom) useEffect(() => {
setTimeout(() => {
// 颜色处理 setChartColor(
const isMatch = location.pathname.includes('/connections') match
const baseColor = isMatch ? '6, 182, 212' : '161, 161, 170' // primary vs foreground 的近似 RGB 值 ? `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-primary-foreground')})`
: `hsla(${getComputedStyle(document.documentElement).getPropertyValue('--heroui-foreground')})`
gradient.addColorStop(0, `rgba(${baseColor}, 0.8)`) )
gradient.addColorStop(1, `rgba(${baseColor}, 0)`) }, 200)
return gradient }, [customTheme])
},
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
}
}
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
useEffect(() => { useEffect(() => {
@ -224,9 +168,30 @@ const ConnCard: React.FC<Props> = (props) => {
</h3> </h3>
</CardFooter> </CardFooter>
</Card> </Card>
<div className="w-full h-full absolute top-0 left-0 pointer-events-none overflow-hidden rounded-[14px]"> <ResponsiveContainer
<Line data={chartData} options={chartOptions} /> height="100%"
</div> 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 <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 BorderSwitch from '@renderer/components/base/border-swtich'
import { LuServer } from 'react-icons/lu' import { LuServer } from 'react-icons/lu'
import { useLocation, useNavigate } from 'react-router-dom' 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 { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -15,13 +15,15 @@ interface Props {
} }
const DNSCard: React.FC<Props> = (props) => { const DNSCard: React.FC<Props> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig } = useAppConfig()
const { iconOnly } = props const { iconOnly } = props
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {} const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const match = location.pathname.includes('/dns') const match = location.pathname.includes('/dns')
const { patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { dns, tun } = controledMihomoConfig || {}
const { enable = true } = dns || {}
const { const {
attributes, attributes,
listeners, listeners,
@ -33,19 +35,14 @@ const DNSCard: React.FC<Props> = (props) => {
id: 'dns' id: 'dns'
}) })
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (controlDns: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
try { await patchControledMihomoConfig({ dns: { enable } })
await patchAppConfig({ controlDns }) await patchMihomoConfig({ dns: { enable } })
await patchControledMihomoConfig({})
await restartCore()
} catch (e) {
alert(e)
}
} }
if (iconOnly) { if (iconOnly) {
return ( return (
<div className={`${dnsCardStatus} flex justify-center`}> <div className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} flex justify-center`}>
<Tooltip content={t('sider.cards.dns')} placement="right"> <Tooltip content={t('sider.cards.dns')} placement="right">
<Button <Button
size="sm" size="sm"
@ -71,7 +68,7 @@ const DNSCard: React.FC<Props> = (props) => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className={`${dnsCardStatus} dns-card`} className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} dns-card`}
> >
<Card <Card
fullWidth fullWidth
@ -93,9 +90,9 @@ const DNSCard: React.FC<Props> = (props) => {
/> />
</Button> </Button>
<BorderSwitch <BorderSwitch
isShowBorder={match && controlDns} isShowBorder={match && enable}
isSelected={controlDns} isSelected={enable}
isDisabled={false} isDisabled={tun?.enable}
onValueChange={onChange} onValueChange={onChange}
/> />
</div> </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 BorderSwitch from '@renderer/components/base/border-swtich'
import { RiScan2Fill } from 'react-icons/ri' import { RiScan2Fill } from 'react-icons/ri'
import { useLocation, useNavigate } from 'react-router-dom' 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 { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
@ -15,13 +15,15 @@ interface Props {
} }
const SniffCard: React.FC<Props> = (props) => { const SniffCard: React.FC<Props> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig } = useAppConfig()
const { iconOnly } = props const { iconOnly } = props
const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {} const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {}
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const match = location.pathname.includes('/sniffer') const match = location.pathname.includes('/sniffer')
const { patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { sniffer } = controledMihomoConfig || {}
const { enable } = sniffer || {}
const { const {
attributes, attributes,
listeners, listeners,
@ -33,19 +35,14 @@ const SniffCard: React.FC<Props> = (props) => {
id: 'sniff' id: 'sniff'
}) })
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (controlSniff: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
try { await patchControledMihomoConfig({ sniffer: { enable } })
await patchAppConfig({ controlSniff }) await patchMihomoConfig({ sniffer: { enable } })
await patchControledMihomoConfig({})
await restartCore()
} catch (e) {
alert(e)
}
} }
if (iconOnly) { if (iconOnly) {
return ( return (
<div className={`${sniffCardStatus} flex justify-center`}> <div className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''} flex justify-center`}>
<Tooltip content={t('sider.cards.sniff')} placement="right"> <Tooltip content={t('sider.cards.sniff')} placement="right">
<Button <Button
size="sm" size="sm"
@ -71,7 +68,7 @@ const SniffCard: React.FC<Props> = (props) => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className={`${sniffCardStatus} sniff-card`} className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''} sniff-card`}
> >
<Card <Card
fullWidth fullWidth
@ -94,8 +91,8 @@ const SniffCard: React.FC<Props> = (props) => {
/> />
</Button> </Button>
<BorderSwitch <BorderSwitch
isShowBorder={match && controlSniff} isShowBorder={match && enable}
isSelected={controlSniff} isSelected={enable}
onValueChange={onChange} onValueChange={onChange}
/> />
</div> </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 BorderSwitch from '@renderer/components/base/border-swtich'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import { triggerSysProxy, updateTrayIcon } from '@renderer/utils/ipc' import { triggerSysProxy } from '@renderer/utils/ipc'
import { AiOutlineGlobal } from 'react-icons/ai' import { AiOutlineGlobal } from 'react-icons/ai'
import React from 'react' import React from 'react'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
@ -43,7 +43,6 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
window.electron.ipcRenderer.send('updateFloatingWindow') window.electron.ipcRenderer.send('updateFloatingWindow')
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
await updateTrayIcon()
} catch (e) { } catch (e) {
await patchAppConfig({ sysProxy: { enable: previousState } }) await patchAppConfig({ sysProxy: { enable: previousState } })
alert(e) alert(e)

View File

@ -3,7 +3,7 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import BorderSwitch from '@renderer/components/base/border-swtich' import BorderSwitch from '@renderer/components/base/border-swtich'
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb' import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore, updateTrayIcon } from '@renderer/utils/ipc' import { restartCore } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import React from 'react' import React from 'react'
@ -38,53 +38,13 @@ const TunSwitcher: React.FC<Props> = (props) => {
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (enable: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
if (enable) { if (enable) {
try {
// 检查内核权限
const hasPermissions = await window.electron.ipcRenderer.invoke('checkMihomoCorePermissions')
if (!hasPermissions) {
if (window.electron.process.platform === 'win32') {
const confirmed = await window.electron.ipcRenderer.invoke('showTunPermissionDialog')
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)
await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error))
return
}
} else {
return
}
} else {
// macOS/Linux下尝试自动获取权限
try {
await window.electron.ipcRenderer.invoke('requestTunPermissions')
} catch (error) {
console.warn('Permission grant failed:', error)
await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error))
return
}
}
}
} catch (error) {
console.warn('Permission check failed:', error)
}
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
if (enable && appConfig?.silentStart) {
await window.electron.ipcRenderer.invoke('enableAutoRun')
}
} else { } else {
await patchControledMihomoConfig({ tun: { enable } }) await patchControledMihomoConfig({ tun: { enable } })
} }
await restartCore() await restartCore()
window.electron.ipcRenderer.send('updateFloatingWindow') window.electron.ipcRenderer.send('updateFloatingWindow')
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
await updateTrayIcon()
} }
if (iconOnly) { if (iconOnly) {

View File

@ -71,34 +71,13 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
} }
const changeCurrentProfile = async (id: string): Promise<void> => { const changeCurrentProfile = async (id: string): Promise<void> => {
if (profileConfig?.current === id) {
return
}
// 乐观更新:立即更新 UI 状态,提供即时反馈
if (profileConfig) {
const optimisticUpdate = { ...profileConfig, current: id }
mutateProfileConfig(optimisticUpdate, false)
}
try { try {
// 异步执行后台切换,不阻塞 UI await change(id)
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()
}
})
} catch (e) { } catch (e) {
alert(`切换 Profile 失败: ${e}`) alert(e)
} finally {
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('updateTrayMenu')
} }
} }

View File

@ -36,8 +36,6 @@
"common.error.shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}", "common.error.shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}",
"common.error.adminRequired": "Please run with administrator privileges for first launch", "common.error.adminRequired": "Please run with administrator privileges for first launch",
"common.error.initFailed": "Application initialization failed", "common.error.initFailed": "Application initialization failed",
"core.highPrivilege.title": "High Privilege Core Detected",
"core.highPrivilege.message": "A high-privilege core is detected. The application needs to restart in administrator mode to match permissions. Restart now?",
"common.updater.versionReady": "v{{version}} Version Ready", "common.updater.versionReady": "v{{version}} Version Ready",
"common.updater.goToDownload": "Go to Download", "common.updater.goToDownload": "Go to Download",
"common.updater.update": "Update", "common.updater.update": "Update",
@ -48,7 +46,6 @@
"settings.darkMode": "Dark Mode", "settings.darkMode": "Dark Mode",
"settings.lightMode": "Light Mode", "settings.lightMode": "Light Mode",
"settings.autoStart": "Auto Start", "settings.autoStart": "Auto Start",
"settings.autoStart.permissions": "Configuring scheduled tasks with administrator privileges, please click 'Yes' in the UAC dialog...",
"settings.autoCheckUpdate": "Auto Check Update", "settings.autoCheckUpdate": "Auto Check Update",
"settings.silentStart": "Silent Start", "settings.silentStart": "Silent Start",
"settings.autoQuitWithoutCore": "Auto Enable Light Mode", "settings.autoQuitWithoutCore": "Auto Enable Light Mode",
@ -57,8 +54,6 @@
"settings.envType": "Environment Variable Type", "settings.envType": "Environment Variable Type",
"settings.showFloatingWindow": "Show Floating Window", "settings.showFloatingWindow": "Show Floating Window",
"settings.spinFloatingIcon": "Spin Floating Icon Based on Network Speed", "settings.spinFloatingIcon": "Spin Floating Icon Based on Network Speed",
"settings.floatingWindowCompatMode": "Floating Window Compatibility Mode (Recommended)",
"settings.floatingWindowCompatModeTooltip": "Disables transparency effects to prevent crashes on some Windows systems. Recommended to keep enabled for stability",
"settings.disableTray": "Disable Tray Icon", "settings.disableTray": "Disable Tray Icon",
"settings.proxyInTray": "Show Proxy Info in Tray Menu", "settings.proxyInTray": "Show Proxy Info in Tray Menu",
"settings.showTraffic_windows": "Show Network Speed in Taskbar", "settings.showTraffic_windows": "Show Network Speed in Taskbar",
@ -103,6 +98,7 @@
"mihomo.cpuPriority.low": "Low", "mihomo.cpuPriority.low": "Low",
"mihomo.workDir.title": "Separate Work Directory for Different Subscriptions", "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.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.controlSniff": "Control Domain Sniffing",
"mihomo.autoCloseConnection": "Auto Close Connection", "mihomo.autoCloseConnection": "Auto Close Connection",
"mihomo.pauseSSID.title": "Direct Connection for Specific WiFi SSIDs", "mihomo.pauseSSID.title": "Direct Connection for Specific WiFi SSIDs",
@ -118,15 +114,6 @@
"mihomo.selectCoreVersion": "Select Core Version", "mihomo.selectCoreVersion": "Select Core Version",
"mihomo.stableVersion": "Stable", "mihomo.stableVersion": "Stable",
"mihomo.alphaVersion": "Alpha", "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.mixedPort": "Mixed Port",
"mihomo.confirm": "Confirm", "mihomo.confirm": "Confirm",
"mihomo.socksPort": "Socks Port", "mihomo.socksPort": "Socks Port",
@ -220,8 +207,8 @@
"sider.cards.override": "Override", "sider.cards.override": "Override",
"sider.cards.connections": "Connections", "sider.cards.connections": "Connections",
"sider.cards.core": "Core Settings", "sider.cards.core": "Core Settings",
"sider.cards.dns": "DNS Override", "sider.cards.dns": "DNS",
"sider.cards.sniff": "Sniff OVRD", "sider.cards.sniff": "Sniffing",
"sider.cards.logs": "Logs", "sider.cards.logs": "Logs",
"sider.cards.substore": "Sub-Store", "sider.cards.substore": "Sub-Store",
"sider.cards.config": "Runtime Config", "sider.cards.config": "Runtime Config",
@ -274,7 +261,6 @@
"proxies.search.placeholder": "Search Proxies", "proxies.search.placeholder": "Search Proxies",
"proxies.locate": "Locate Current Proxy", "proxies.locate": "Locate Current Proxy",
"sniffer.title": "Domain Sniffing Settings", "sniffer.title": "Domain Sniffing Settings",
"sniffer.enable": "Enable Domain Sniffing",
"sniffer.parsePureIP": "Sniff Unmapped IP Addresses", "sniffer.parsePureIP": "Sniff Unmapped IP Addresses",
"sniffer.forceDNSMapping": "Sniff Real IP Mappings", "sniffer.forceDNSMapping": "Sniff Real IP Mappings",
"sniffer.overrideDestination": "Override Connection Address", "sniffer.overrideDestination": "Override Connection Address",
@ -290,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "Example: 1.1.1.1/32", "sniffer.skipDstAddress.placeholder": "Example: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "Skip Source Address Sniffing", "sniffer.skipSrcAddress.title": "Skip Source Address Sniffing",
"sniffer.skipSrcAddress.placeholder": "Example: 192.168.1.1/24", "sniffer.skipSrcAddress.placeholder": "Example: 192.168.1.1/24",
"sniffer.saveOnly": "Save Only",
"sysproxy.title": "System Proxy", "sysproxy.title": "System Proxy",
"sysproxy.host.title": "Proxy Host", "sysproxy.host.title": "Proxy Host",
"sysproxy.host.placeholder": "Default 127.0.0.1, do not modify unless necessary", "sysproxy.host.placeholder": "Default 127.0.0.1, do not modify unless necessary",
@ -320,12 +305,8 @@
"tun.excludeAddress.placeholder": "Example: 172.20.0.0/16", "tun.excludeAddress.placeholder": "Example: 172.20.0.0/16",
"tun.notifications.coreAuthSuccess": "Core Authorization Successful", "tun.notifications.coreAuthSuccess": "Core Authorization Successful",
"tun.notifications.firewallResetSuccess": "Firewall Reset Successful", "tun.notifications.firewallResetSuccess": "Firewall Reset Successful",
"tun.permissions.title": "Administrator Privileges Required", "tun.error.tunPermissionDenied": "TUN interface start failed, please try to manually grant core permissions",
"tun.permissions.message": "TUN mode requires administrator privileges. Restart the application now to get permissions?",
"tun.permissions.failed": "Permission authorization failed",
"tun.permissions.restarting": "Restarting application with administrator privileges, please click 'Yes' in the UAC dialog...",
"dns.title": "DNS Settings", "dns.title": "DNS Settings",
"dns.enable": "Enable DNS",
"dns.enhancedMode.title": "Domain Mapping Mode", "dns.enhancedMode.title": "Domain Mapping Mode",
"dns.enhancedMode.fakeIp": "Fake IP", "dns.enhancedMode.fakeIp": "Fake IP",
"dns.enhancedMode.redirHost": "Real IP", "dns.enhancedMode.redirHost": "Real IP",
@ -352,7 +333,6 @@
"dns.customHosts.list": "Hosts List", "dns.customHosts.list": "Hosts List",
"dns.customHosts.domainPlaceholder": "Domain", "dns.customHosts.domainPlaceholder": "Domain",
"dns.customHosts.valuePlaceholder": "Domain or IP", "dns.customHosts.valuePlaceholder": "Domain or IP",
"dns.saveOnly": "Save Only",
"profiles.title": "Profile Management", "profiles.title": "Profile Management",
"profiles.updateAll": "Update All Profiles", "profiles.updateAll": "Update All Profiles",
"profiles.useProxy": "Proxy", "profiles.useProxy": "Proxy",
@ -374,7 +354,7 @@
"profiles.editInfo.name": "Name", "profiles.editInfo.name": "Name",
"profiles.editInfo.url": "Subscription URL", "profiles.editInfo.url": "Subscription URL",
"profiles.editInfo.useProxy": "Use Proxy to Update", "profiles.editInfo.useProxy": "Use Proxy to Update",
"profiles.editInfo.interval": "Upd. Interval", "profiles.editInfo.interval": "Upd. Interval (min)",
"profiles.editInfo.fixedInterval": "Fixed Update Interval", "profiles.editInfo.fixedInterval": "Fixed Update Interval",
"profiles.editInfo.override.title": "Override", "profiles.editInfo.override.title": "Override",
"profiles.editInfo.override.global": "Global", "profiles.editInfo.override.global": "Global",
@ -523,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.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.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.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.title": "DNS",
"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.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.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." "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

@ -36,8 +36,6 @@
"common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}", "common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}",
"common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید", "common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید",
"common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد", "common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد",
"core.highPrivilege.title": "هسته با سطح دسترسی بالا شناسایی شد",
"core.highPrivilege.message": "هسته‌ای با سطح دسترسی بالا شناسایی شد. برنامه باید در حالت مدیر سیستم برای تطابق سطح دسترسی‌ها دوباره راه‌اندازی شود. آیا می‌خواهید اکنون راه‌اندازی مجدد شود؟",
"common.updater.versionReady": "نسخه v{{version}} آماده است", "common.updater.versionReady": "نسخه v{{version}} آماده است",
"common.updater.goToDownload": "دانلود", "common.updater.goToDownload": "دانلود",
"common.updater.update": "به‌روزرسانی", "common.updater.update": "به‌روزرسانی",
@ -103,6 +101,7 @@
"mihomo.cpuPriority.low": "پایین", "mihomo.cpuPriority.low": "پایین",
"mihomo.workDir.title": "استفاده از پوشه کاری مجزا برای اشتراک‌های مختلف", "mihomo.workDir.title": "استفاده از پوشه کاری مجزا برای اشتراک‌های مختلف",
"mihomo.workDir.tooltip": "برای جلوگیری از تداخل گروه‌های پراکسی با نام یکسان در اشتراک‌های مختلف", "mihomo.workDir.tooltip": "برای جلوگیری از تداخل گروه‌های پراکسی با نام یکسان در اشتراک‌های مختلف",
"mihomo.controlDns": "کنترل تنظیمات DNS",
"mihomo.controlSniff": "کنترل تشخیص دامنه", "mihomo.controlSniff": "کنترل تشخیص دامنه",
"mihomo.autoCloseConnection": "بستن خودکار اتصال", "mihomo.autoCloseConnection": "بستن خودکار اتصال",
"mihomo.pauseSSID.title": "اتصال مستقیم برای SSIDهای خاص", "mihomo.pauseSSID.title": "اتصال مستقیم برای SSIDهای خاص",
@ -208,8 +207,8 @@
"sider.cards.override": "جایگزینی", "sider.cards.override": "جایگزینی",
"sider.cards.connections": "اتصالات", "sider.cards.connections": "اتصالات",
"sider.cards.core": "تنظیمات هسته", "sider.cards.core": "تنظیمات هسته",
"sider.cards.dns": "بازنویسی دی‌ان‌اس", "sider.cards.dns": "DNS",
"sider.cards.sniff": "لغو بو کشیدن", "sider.cards.sniff": "تشخیص دامنه",
"sider.cards.logs": "گزارش‌ها", "sider.cards.logs": "گزارش‌ها",
"sider.cards.substore": "ساب استور", "sider.cards.substore": "ساب استور",
"sider.cards.config": "پیکربندی اجرا", "sider.cards.config": "پیکربندی اجرا",
@ -262,7 +261,6 @@
"proxies.search.placeholder": "جستجوی پراکسی‌ها", "proxies.search.placeholder": "جستجوی پراکسی‌ها",
"proxies.locate": "یافتن پراکسی فعلی", "proxies.locate": "یافتن پراکسی فعلی",
"sniffer.title": "تنظیمات تشخیص دامنه", "sniffer.title": "تنظیمات تشخیص دامنه",
"sniffer.enable": "فعال کردن قابلیت شنود دامنه",
"sniffer.parsePureIP": "تشخیص آدرس‌های IP بدون نگاشت", "sniffer.parsePureIP": "تشخیص آدرس‌های IP بدون نگاشت",
"sniffer.forceDNSMapping": "تشخیص نگاشت‌های IP واقعی", "sniffer.forceDNSMapping": "تشخیص نگاشت‌های IP واقعی",
"sniffer.overrideDestination": "جایگزینی آدرس اتصال", "sniffer.overrideDestination": "جایگزینی آدرس اتصال",
@ -278,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "مثال: 1.1.1.1/32", "sniffer.skipDstAddress.placeholder": "مثال: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "رد کردن تشخیص آدرس مبدا", "sniffer.skipSrcAddress.title": "رد کردن تشخیص آدرس مبدا",
"sniffer.skipSrcAddress.placeholder": "مثال: 192.168.1.1/24", "sniffer.skipSrcAddress.placeholder": "مثال: 192.168.1.1/24",
"sniffer.saveOnly": "فقط ذخیره",
"sysproxy.title": "پراکسی سیستم", "sysproxy.title": "پراکسی سیستم",
"sysproxy.host.title": "میزبان پراکسی", "sysproxy.host.title": "میزبان پراکسی",
"sysproxy.host.placeholder": "پیش‌فرض 127.0.0.1، در صورت عدم نیاز تغییر ندهید", "sysproxy.host.placeholder": "پیش‌فرض 127.0.0.1، در صورت عدم نیاز تغییر ندهید",
@ -310,7 +307,6 @@
"tun.notifications.firewallResetSuccess": "بازنشانی دیوار آتش با موفقیت انجام شد", "tun.notifications.firewallResetSuccess": "بازنشانی دیوار آتش با موفقیت انجام شد",
"tun.error.tunPermissionDenied": "راه‌اندازی رابط TUN با شکست مواجه شد، لطفا به صورت دستی به هسته مجوز دهید", "tun.error.tunPermissionDenied": "راه‌اندازی رابط TUN با شکست مواجه شد، لطفا به صورت دستی به هسته مجوز دهید",
"dns.title": "تنظیمات DNS", "dns.title": "تنظیمات DNS",
"dns.enable": "فعال‌سازی DNS",
"dns.enhancedMode.title": "حالت نگاشت دامنه", "dns.enhancedMode.title": "حالت نگاشت دامنه",
"dns.enhancedMode.fakeIp": "IP جعلی", "dns.enhancedMode.fakeIp": "IP جعلی",
"dns.enhancedMode.redirHost": "IP واقعی", "dns.enhancedMode.redirHost": "IP واقعی",
@ -337,7 +333,6 @@
"dns.customHosts.list": "لیست Hosts", "dns.customHosts.list": "لیست Hosts",
"dns.customHosts.domainPlaceholder": "دامنه", "dns.customHosts.domainPlaceholder": "دامنه",
"dns.customHosts.valuePlaceholder": "دامنه یا IP", "dns.customHosts.valuePlaceholder": "دامنه یا IP",
"dns.saveOnly": "فقط ذخیره",
"profiles.title": "مدیریت پروفایل", "profiles.title": "مدیریت پروفایل",
"profiles.updateAll": "به‌روزرسانی همه پروفایل‌ها", "profiles.updateAll": "به‌روزرسانی همه پروفایل‌ها",
"profiles.useProxy": "پراکسی", "profiles.useProxy": "پراکسی",
@ -364,7 +359,7 @@
"profiles.editInfo.name": "نام", "profiles.editInfo.name": "نام",
"profiles.editInfo.url": "آدرس اشتراک", "profiles.editInfo.url": "آدرس اشتراک",
"profiles.editInfo.useProxy": "استفاده از پراکسی برای به‌روزرسانی", "profiles.editInfo.useProxy": "استفاده از پراکسی برای به‌روزرسانی",
"profiles.editInfo.interval": "فاصله به‌روزرسانی", "profiles.editInfo.interval": "فاصله به‌روزرسانی (دقیقه)",
"profiles.editInfo.fixedInterval": "فاصله به‌روزرسانی ثابت", "profiles.editInfo.fixedInterval": "فاصله به‌روزرسانی ثابت",
"profiles.editInfo.override.title": "جایگزینی", "profiles.editInfo.override.title": "جایگزینی",
"profiles.editInfo.override.global": "جهانی", "profiles.editInfo.override.global": "جهانی",
@ -509,7 +504,7 @@
"guide.override.title": "Override", "guide.override.title": "Override",
"guide.override.description": "میهومو پارتی قابلیت‌های قدرتمند جایگزینی را برای سفارشی‌سازی پیکربندی‌های اشتراک وارد شده، مانند افزودن قوانین و سفارشی‌سازی گروه‌های پراکسی ارائه می‌دهد. می‌توانید مستقیماً فایل‌های جایگزینی نوشته شده توسط دیگران را وارد کنید یا فایل‌های خود را بنویسید. <b>فراموش نکنید که فایل جایگزینی را روی اشتراکی که می‌خواهید جایگزین کنید فعال کنید</b>. برای نحو فایل جایگزینی، لطفاً به <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">مستندات رسمی</a> مراجعه کنید.", "guide.override.description": "میهومو پارتی قابلیت‌های قدرتمند جایگزینی را برای سفارشی‌سازی پیکربندی‌های اشتراک وارد شده، مانند افزودن قوانین و سفارشی‌سازی گروه‌های پراکسی ارائه می‌دهد. می‌توانید مستقیماً فایل‌های جایگزینی نوشته شده توسط دیگران را وارد کنید یا فایل‌های خود را بنویسید. <b>فراموش نکنید که فایل جایگزینی را روی اشتراکی که می‌خواهید جایگزین کنید فعال کنید</b>. برای نحو فایل جایگزینی، لطفاً به <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">مستندات رسمی</a> مراجعه کنید.",
"guide.dns.title": "DNS", "guide.dns.title": "DNS",
"guide.dns.description": "نرم‌افزار به‌طور پیش‌فرض از تنظیمات DNS برنامه برای بازنویسی پیکربندی اشتراک استفاده می‌کند. اگر نیاز به استفاده از تنظیمات DNS موجود در پیکربندی اشتراک دارید، لطفاً این قابلیت را غیرفعال کنید. همین موضوع برای شناسایی دامنه نیز صدق می‌کند.", "guide.dns.description": "نرم‌افزار به طور پیش‌فرض کنترل تنظیمات DNS هسته را در دست می‌گیرد. اگر نیاز دارید از تنظیمات DNS پیکربندی اشتراک خود استفاده کنید، می‌توانید 'کنترل تنظیمات DNS' را در تنظیمات برنامه غیرفعال کنید. همین مورد برای تشخیص دامنه نیز صدق می‌کند.",
"guide.end.title": "پایان آموزش", "guide.end.title": "پایان آموزش",
"guide.end.description": "اکنون که با استفاده اساسی از نرم‌افزار آشنا شدید، اشتراک خود را وارد کنید و از آن استفاده کنید. لذت ببرید!\nهمچنین می‌توانید برای آخرین اخبار به <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">گروه تلگرام</a> ما بپیوندید." "guide.end.description": "اکنون که با استفاده اساسی از نرم‌افزار آشنا شدید، اشتراک خود را وارد کنید و از آن استفاده کنید. لذت ببرید!\nهمچنین می‌توانید برای آخرین اخبار به <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">گروه تلگرام</a> ما بپیوندید."
} }

View File

@ -36,9 +36,7 @@
"common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}", "common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}",
"common.error.adminRequired": "Для первого запуска требуются права администратора", "common.error.adminRequired": "Для первого запуска требуются права администратора",
"common.error.initFailed": "Не удалось инициализировать приложение", "common.error.initFailed": "Не удалось инициализировать приложение",
"core.highPrivilege.title": "High Privilege Core Detected", "common.updater.versionReady": "Версия v{{version}} готова",
"core.highPrivilege.message": "Обнаружено ядро с повышенными привилегиями. Приложение необходимо перезапустить в режиме администратора для согласования прав. Перезапустить сейчас?",
"common.updater.versionReady": "Обнаружено ядро с повышенными привилегиями",
"common.updater.goToDownload": "Перейти к загрузке", "common.updater.goToDownload": "Перейти к загрузке",
"common.updater.update": "Обновить", "common.updater.update": "Обновить",
"settings.general": "Общие настройки", "settings.general": "Общие настройки",
@ -103,6 +101,7 @@
"mihomo.cpuPriority.low": "Низкий", "mihomo.cpuPriority.low": "Низкий",
"mihomo.workDir.title": "Отдельные рабочие каталоги для разных подписок", "mihomo.workDir.title": "Отдельные рабочие каталоги для разных подписок",
"mihomo.workDir.tooltip": "Включите для избежания конфликтов при наличии групп прокси с одинаковыми именами в разных подписках", "mihomo.workDir.tooltip": "Включите для избежания конфликтов при наличии групп прокси с одинаковыми именами в разных подписках",
"mihomo.controlDns": "Управление настройками DNS",
"mihomo.controlSniff": "Управление сниффингом доменов", "mihomo.controlSniff": "Управление сниффингом доменов",
"mihomo.autoCloseConnection": "Автозакрытие соединений", "mihomo.autoCloseConnection": "Автозакрытие соединений",
"mihomo.pauseSSID.title": "Прямое подключение для определённых WiFi SSID", "mihomo.pauseSSID.title": "Прямое подключение для определённых WiFi SSID",
@ -208,8 +207,8 @@
"sider.cards.override": "Переопределения", "sider.cards.override": "Переопределения",
"sider.cards.connections": "Подключения", "sider.cards.connections": "Подключения",
"sider.cards.core": "Настройки ядра", "sider.cards.core": "Настройки ядра",
"sider.cards.dns": "Переопределение DNS", "sider.cards.dns": "DNS",
"sider.cards.sniff": "переопределение сниффинга", "sider.cards.sniff": "Анализ трафика",
"sider.cards.logs": "Журналы", "sider.cards.logs": "Журналы",
"sider.cards.substore": "Sub-Store", "sider.cards.substore": "Sub-Store",
"sider.cards.config": "Конфигурация", "sider.cards.config": "Конфигурация",
@ -262,7 +261,6 @@
"proxies.search.placeholder": "Поиск прокси", "proxies.search.placeholder": "Поиск прокси",
"proxies.locate": "Найти текущий прокси", "proxies.locate": "Найти текущий прокси",
"sniffer.title": "Настройки анализа доменов", "sniffer.title": "Настройки анализа доменов",
"sniffer.enable": "Включить анализ домена",
"sniffer.parsePureIP": "Анализировать немаппированные IP-адреса", "sniffer.parsePureIP": "Анализировать немаппированные IP-адреса",
"sniffer.forceDNSMapping": "Анализировать реальные IP-маппинги", "sniffer.forceDNSMapping": "Анализировать реальные IP-маппинги",
"sniffer.overrideDestination": "Переопределить адрес подключения", "sniffer.overrideDestination": "Переопределить адрес подключения",
@ -278,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "Пример: 1.1.1.1/32", "sniffer.skipDstAddress.placeholder": "Пример: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "Пропустить анализ исходных адресов", "sniffer.skipSrcAddress.title": "Пропустить анализ исходных адресов",
"sniffer.skipSrcAddress.placeholder": "Пример: 192.168.1.1/24", "sniffer.skipSrcAddress.placeholder": "Пример: 192.168.1.1/24",
"sniffer.saveOnly": "Только сохранить",
"sysproxy.title": "Системный прокси", "sysproxy.title": "Системный прокси",
"sysproxy.host.title": "Хост прокси", "sysproxy.host.title": "Хост прокси",
"sysproxy.host.placeholder": "По умолчанию 127.0.0.1, не изменяйте без необходимости", "sysproxy.host.placeholder": "По умолчанию 127.0.0.1, не изменяйте без необходимости",
@ -310,7 +307,6 @@
"tun.notifications.firewallResetSuccess": "Брандмауэр успешно сброшен", "tun.notifications.firewallResetSuccess": "Брандмауэр успешно сброшен",
"tun.error.tunPermissionDenied": "Ошибка запуска TUN, попробуйте вручную предоставить разрешения ядру", "tun.error.tunPermissionDenied": "Ошибка запуска TUN, попробуйте вручную предоставить разрешения ядру",
"dns.title": "Настройки DNS", "dns.title": "Настройки DNS",
"dns.enable": "Включить DNS",
"dns.enhancedMode.title": "Режим маппинга доменов", "dns.enhancedMode.title": "Режим маппинга доменов",
"dns.enhancedMode.fakeIp": "Фиктивный IP", "dns.enhancedMode.fakeIp": "Фиктивный IP",
"dns.enhancedMode.redirHost": "Реальный IP", "dns.enhancedMode.redirHost": "Реальный IP",
@ -337,7 +333,6 @@
"dns.customHosts.list": "Список Hosts", "dns.customHosts.list": "Список Hosts",
"dns.customHosts.domainPlaceholder": "Домен", "dns.customHosts.domainPlaceholder": "Домен",
"dns.customHosts.valuePlaceholder": "Домен или IP", "dns.customHosts.valuePlaceholder": "Домен или IP",
"dns.saveOnly": "Только сохранить",
"profiles.title": "Управление профилями", "profiles.title": "Управление профилями",
"profiles.updateAll": "Обновить все профили", "profiles.updateAll": "Обновить все профили",
"profiles.useProxy": "Прокси", "profiles.useProxy": "Прокси",
@ -364,7 +359,7 @@
"profiles.editInfo.name": "Имя", "profiles.editInfo.name": "Имя",
"profiles.editInfo.url": "URL подписки", "profiles.editInfo.url": "URL подписки",
"profiles.editInfo.useProxy": "Использовать прокси для обновления", "profiles.editInfo.useProxy": "Использовать прокси для обновления",
"profiles.editInfo.interval": "Интервал обн.", "profiles.editInfo.interval": "Интервал обн. (мин)",
"profiles.editInfo.fixedInterval": "Фиксированный интервал обновления", "profiles.editInfo.fixedInterval": "Фиксированный интервал обновления",
"profiles.editInfo.override.title": "Переопределение", "profiles.editInfo.override.title": "Переопределение",
"profiles.editInfo.override.global": "Глобальный", "profiles.editInfo.override.global": "Глобальный",
@ -509,7 +504,7 @@
"guide.override.title": "Переопределение", "guide.override.title": "Переопределение",
"guide.override.description": "Mihomo Party предоставляет мощную функцию переопределения для настройки импортированных конфигураций подписки, таких как добавление правил и настройка групп прокси. Вы можете напрямую импортировать файлы переопределения, написанные другими, или написать свои собственные. <b>Не забудьте включить файл переопределения для подписки, которую вы хотите переопределить</b>. Синтаксис файла переопределения см. в <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">официальной документации</a>.", "guide.override.description": "Mihomo Party предоставляет мощную функцию переопределения для настройки импортированных конфигураций подписки, таких как добавление правил и настройка групп прокси. Вы можете напрямую импортировать файлы переопределения, написанные другими, или написать свои собственные. <b>Не забудьте включить файл переопределения для подписки, которую вы хотите переопределить</b>. Синтаксис файла переопределения см. в <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">официальной документации</a>.",
"guide.dns.title": "DNS", "guide.dns.title": "DNS",
"guide.dns.description": рограммное обеспечение по умолчанию использует настройки DNS приложения для переопределения конфигурации подписки. Если вам нужно использовать настройки DNS из конфигурации подписки, пожалуйста, отключите эту функцию. То же самое относится к определению домена.", "guide.dns.description": о умолчанию программа контролирует настройки DNS ядра. Если вам нужно использовать настройки DNS из конфигурации подписки, вы можете отключить 'Контроль настроек DNS' в настройках приложения. То же самое относится к сниффингу доменов.",
"guide.end.title": "Руководство завершено", "guide.end.title": "Руководство завершено",
"guide.end.description": "Теперь, когда вы понимаете основы использования программы, импортируйте свою подписку и начните использовать ее. Приятного использования!\nВы также можете присоединиться к нашей официальной <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">группе Telegram</a> для получения последних новостей." "guide.end.description": "Теперь, когда вы понимаете основы использования программы, импортируйте свою подписку и начните использовать ее. Приятного использования!\nВы также можете присоединиться к нашей официальной <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">группе Telegram</a> для получения последних новостей."
} }

View File

@ -36,8 +36,6 @@
"common.error.shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}", "common.error.shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}",
"common.error.adminRequired": "首次启动请以管理员权限运行", "common.error.adminRequired": "首次启动请以管理员权限运行",
"common.error.initFailed": "应用初始化失败", "common.error.initFailed": "应用初始化失败",
"core.highPrivilege.title": "检测到高权限内核",
"core.highPrivilege.message": "检测到运行中的高权限内核,需以管理员模式重启应用以匹配权限,确定重启?",
"common.updater.versionReady": "v{{version}} 版本就绪", "common.updater.versionReady": "v{{version}} 版本就绪",
"common.updater.goToDownload": "前往下载", "common.updater.goToDownload": "前往下载",
"common.updater.update": "更新", "common.updater.update": "更新",
@ -48,7 +46,6 @@
"settings.darkMode": "深色模式", "settings.darkMode": "深色模式",
"settings.lightMode": "浅色模式", "settings.lightMode": "浅色模式",
"settings.autoStart": "开机自启", "settings.autoStart": "开机自启",
"settings.autoStart.permissions": "正在以管理员权限配置计划任务请在UAC对话框中点击'是'...",
"settings.autoCheckUpdate": "自动检查更新", "settings.autoCheckUpdate": "自动检查更新",
"settings.silentStart": "静默启动", "settings.silentStart": "静默启动",
"settings.autoQuitWithoutCore": "自动进入轻量模式", "settings.autoQuitWithoutCore": "自动进入轻量模式",
@ -57,8 +54,6 @@
"settings.envType": "环境变量类型", "settings.envType": "环境变量类型",
"settings.showFloatingWindow": "显示悬浮窗", "settings.showFloatingWindow": "显示悬浮窗",
"settings.spinFloatingIcon": "根据网速旋转悬浮窗图标", "settings.spinFloatingIcon": "根据网速旋转悬浮窗图标",
"settings.floatingWindowCompatMode": "悬浮窗兼容模式(推荐开启)",
"settings.floatingWindowCompatModeTooltip": "禁用透明效果以避免在某些 Windows 系统上崩溃,建议保持开启以确保稳定性",
"settings.disableTray": "禁用托盘图标", "settings.disableTray": "禁用托盘图标",
"settings.proxyInTray": "在托盘菜单显示代理信息", "settings.proxyInTray": "在托盘菜单显示代理信息",
"settings.showTraffic_windows": "在任务栏显示网速", "settings.showTraffic_windows": "在任务栏显示网速",
@ -106,6 +101,7 @@
"mihomo.cpuPriority.low": "低", "mihomo.cpuPriority.low": "低",
"mihomo.workDir.title": "不同订阅使用独立工作目录", "mihomo.workDir.title": "不同订阅使用独立工作目录",
"mihomo.workDir.tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突", "mihomo.workDir.tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突",
"mihomo.controlDns": "控制 DNS 设置",
"mihomo.controlSniff": "控制域名嗅探", "mihomo.controlSniff": "控制域名嗅探",
"mihomo.autoCloseConnection": "自动关闭连接", "mihomo.autoCloseConnection": "自动关闭连接",
"mihomo.pauseSSID.title": "指定 WiFi SSID 直连", "mihomo.pauseSSID.title": "指定 WiFi SSID 直连",
@ -118,15 +114,6 @@
"mihomo.selectCoreVersion": "选择内核版本", "mihomo.selectCoreVersion": "选择内核版本",
"mihomo.stableVersion": "稳定版", "mihomo.stableVersion": "稳定版",
"mihomo.alphaVersion": "预览版", "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.mixedPort": "混合端口",
"mihomo.confirm": "确认", "mihomo.confirm": "确认",
"mihomo.socksPort": "Socks 端口", "mihomo.socksPort": "Socks 端口",
@ -220,8 +207,8 @@
"sider.cards.override": "覆写", "sider.cards.override": "覆写",
"sider.cards.connections": "连接", "sider.cards.connections": "连接",
"sider.cards.core": "内核设置", "sider.cards.core": "内核设置",
"sider.cards.dns": "DNS覆写", "sider.cards.dns": "DNS",
"sider.cards.sniff": "嗅探覆写", "sider.cards.sniff": "域名嗅探",
"sider.cards.logs": "日志", "sider.cards.logs": "日志",
"sider.cards.substore": "Sub-Store", "sider.cards.substore": "Sub-Store",
"sider.cards.config": "运行时配置", "sider.cards.config": "运行时配置",
@ -274,7 +261,6 @@
"proxies.search.placeholder": "搜索节点", "proxies.search.placeholder": "搜索节点",
"proxies.locate": "定位到当前节点", "proxies.locate": "定位到当前节点",
"sniffer.title": "域名嗅探设置", "sniffer.title": "域名嗅探设置",
"sniffer.enable": "启用域名嗅探",
"sniffer.parsePureIP": "对未映射 IP 地址嗅探", "sniffer.parsePureIP": "对未映射 IP 地址嗅探",
"sniffer.forceDNSMapping": "对真实 IP 映射嗅探", "sniffer.forceDNSMapping": "对真实 IP 映射嗅探",
"sniffer.overrideDestination": "覆盖连接地址", "sniffer.overrideDestination": "覆盖连接地址",
@ -290,7 +276,6 @@
"sniffer.skipDstAddress.placeholder": "例1.1.1.1/32", "sniffer.skipDstAddress.placeholder": "例1.1.1.1/32",
"sniffer.skipSrcAddress.title": "跳过来源地址嗅探", "sniffer.skipSrcAddress.title": "跳过来源地址嗅探",
"sniffer.skipSrcAddress.placeholder": "例192.168.1.1/24", "sniffer.skipSrcAddress.placeholder": "例192.168.1.1/24",
"sniffer.saveOnly": "仅保存",
"sysproxy.title": "系统代理", "sysproxy.title": "系统代理",
"sysproxy.host.title": "代理主机", "sysproxy.host.title": "代理主机",
"sysproxy.host.placeholder": "默认 127.0.0.1 若无特殊需求请勿修改", "sysproxy.host.placeholder": "默认 127.0.0.1 若无特殊需求请勿修改",
@ -320,12 +305,8 @@
"tun.excludeAddress.placeholder": "例: 172.20.0.0/16", "tun.excludeAddress.placeholder": "例: 172.20.0.0/16",
"tun.notifications.coreAuthSuccess": "内核授权成功", "tun.notifications.coreAuthSuccess": "内核授权成功",
"tun.notifications.firewallResetSuccess": "防火墙重设成功", "tun.notifications.firewallResetSuccess": "防火墙重设成功",
"tun.permissions.title": "需要管理员权限", "tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限",
"tun.permissions.message": "启用TUN模式需要管理员权限是否现在重启应用获取权限",
"tun.permissions.failed": "权限授权失败",
"tun.permissions.restarting": "正在以管理员权限重启应用请在UAC对话框中点击'是'...",
"dns.title": "DNS 设置", "dns.title": "DNS 设置",
"dns.enable": "启用 DNS",
"dns.enhancedMode.title": "域名映射模式", "dns.enhancedMode.title": "域名映射模式",
"dns.enhancedMode.fakeIp": "虚假 IP", "dns.enhancedMode.fakeIp": "虚假 IP",
"dns.enhancedMode.redirHost": "真实 IP", "dns.enhancedMode.redirHost": "真实 IP",
@ -352,7 +333,6 @@
"dns.customHosts.list": "Hosts 列表", "dns.customHosts.list": "Hosts 列表",
"dns.customHosts.domainPlaceholder": "域名", "dns.customHosts.domainPlaceholder": "域名",
"dns.customHosts.valuePlaceholder": "域名或 IP", "dns.customHosts.valuePlaceholder": "域名或 IP",
"dns.saveOnly": "仅保存",
"profiles.title": "订阅管理", "profiles.title": "订阅管理",
"profiles.updateAll": "更新全部订阅", "profiles.updateAll": "更新全部订阅",
"profiles.useProxy": "代理", "profiles.useProxy": "代理",
@ -379,7 +359,7 @@
"profiles.editInfo.name": "名称", "profiles.editInfo.name": "名称",
"profiles.editInfo.url": "订阅地址", "profiles.editInfo.url": "订阅地址",
"profiles.editInfo.useProxy": "使用代理更新", "profiles.editInfo.useProxy": "使用代理更新",
"profiles.editInfo.interval": "更新间隔", "profiles.editInfo.interval": "更新间隔(分钟)",
"profiles.editInfo.fixedInterval": "固定更新间隔", "profiles.editInfo.fixedInterval": "固定更新间隔",
"profiles.editInfo.override.title": "覆写", "profiles.editInfo.override.title": "覆写",
"profiles.editInfo.override.global": "全局", "profiles.editInfo.override.global": "全局",
@ -524,7 +504,7 @@
"guide.override.title": "覆写", "guide.override.title": "覆写",
"guide.override.description": "Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">官方文档</a>", "guide.override.description": "Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">官方文档</a>",
"guide.dns.title": "DNS", "guide.dns.title": "DNS",
"guide.dns.description": "软件默认使用应用的 DNS 设置覆盖订阅配置,如果您需要使用订阅配置中的 DNS 设置,请关闭此功能,域名嗅探同理", "guide.dns.description": "软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭\"接管 DNS 设置\",域名嗅探同理",
"guide.end.title": "教程结束", "guide.end.title": "教程结束",
"guide.end.description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群组</a> 获取最新资讯" "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> </div>
<Divider /> <Divider />
</div> </div>
<div className="h-[calc(100vh-100px)] mt-px"> <div className="h-[calc(100vh-100px)] mt-[1px]">
<Virtuoso <Virtuoso
data={filteredConnections} data={filteredConnections}
itemContent={(i, connection) => ( 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 SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useAppConfig } from '@renderer/hooks/use-app-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 React, { Key, ReactNode, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -13,10 +13,9 @@ const DNS: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { nameserverPolicy, useNameserverPolicy, controlDns = true } = appConfig || {} const { nameserverPolicy, useNameserverPolicy } = appConfig || {}
const { dns, hosts } = controledMihomoConfig || {} const { dns, hosts } = controledMihomoConfig || {}
const { const {
enable = true,
ipv6 = false, ipv6 = false,
'fake-ip-range': fakeIPRange = '198.18.0.1/16', 'fake-ip-range': fakeIPRange = '198.18.0.1/16',
'fake-ip-filter': fakeIPFilter = [ 'fake-ip-filter': fakeIPFilter = [
@ -41,7 +40,6 @@ const DNS: React.FC = () => {
} = dns || {} } = dns || {}
const [changed, setChanged] = useState(false) const [changed, setChanged] = useState(false)
const [values, originSetValues] = useState({ const [values, originSetValues] = useState({
enable,
ipv6, ipv6,
useHosts, useHosts,
enhancedMode, enhancedMode,
@ -128,10 +126,7 @@ const DNS: React.FC = () => {
try { try {
setChanged(false) setChanged(false)
await patchControledMihomoConfig(patch) await patchControledMihomoConfig(patch)
if (controlDns) { await restartCore()
await patchMihomoConfig(patch)
await restartCore()
}
} catch (e) { } catch (e) {
alert(e) alert(e)
} }
@ -148,7 +143,6 @@ const DNS: React.FC = () => {
color="primary" color="primary"
onPress={() => { onPress={() => {
const dnsConfig = { const dnsConfig = {
enable: values.enable,
ipv6: values.ipv6, ipv6: values.ipv6,
'fake-ip-range': values.fakeIPRange, 'fake-ip-range': values.fakeIPRange,
'fake-ip-filter': values.fakeIPFilter, 'fake-ip-filter': values.fakeIPFilter,
@ -177,21 +171,12 @@ const DNS: React.FC = () => {
onSave(result) onSave(result)
}} }}
> >
{controlDns ? t('common.save') : t('dns.saveOnly')} {t('common.save')}
</Button> </Button>
) )
} }
> >
<SettingCard> <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> <SettingItem title={t('dns.enhancedMode.title')} divider>
<Tabs <Tabs
size="sm" size="sm"
@ -279,7 +264,7 @@ const DNS: React.FC = () => {
{[...values.nameserverPolicy, { domain: '', value: '' }].map( {[...values.nameserverPolicy, { domain: '', value: '' }].map(
({ domain, value }, index) => ( ({ domain, value }, index) => (
<div key={index} className="flex mb-2"> <div key={index} className="flex mb-2">
<div className="flex-4"> <div className="flex-[4]">
<Input <Input
size="sm" size="sm"
fullWidth fullWidth
@ -296,7 +281,7 @@ const DNS: React.FC = () => {
/> />
</div> </div>
<span className="mx-2">:</span> <span className="mx-2">:</span>
<div className="flex-6 flex"> <div className="flex-[6] flex">
<Input <Input
size="sm" size="sm"
fullWidth fullWidth
@ -347,7 +332,7 @@ const DNS: React.FC = () => {
<h3 className="mb-2">{t('dns.customHosts.list')}</h3> <h3 className="mb-2">{t('dns.customHosts.list')}</h3>
{[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => ( {[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => (
<div key={index} className="flex mb-2"> <div key={index} className="flex mb-2">
<div className="flex-4"> <div className="flex-[4]">
<Input <Input
size="sm" size="sm"
fullWidth fullWidth
@ -364,7 +349,7 @@ const DNS: React.FC = () => {
/> />
</div> </div>
<span className="mx-2">:</span> <span className="mx-2">:</span>
<div className="flex-6 flex"> <div className="flex-[6] flex">
<Input <Input
size="sm" size="sm"
fullWidth fullWidth

View File

@ -109,7 +109,7 @@ const Logs: React.FC = () => {
</div> </div>
<Divider /> <Divider />
</div> </div>
<div className="h-[calc(100vh-100px)] mt-px"> <div className="h-[calc(100vh-100px)] mt-[1px]">
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
data={filteredLogs} 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 BasePage from '@renderer/components/base/base-page'
import SettingCard from '@renderer/components/base/base-setting-card' import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
@ -6,14 +6,13 @@ import { useAppConfig } from '@renderer/hooks/use-app-config'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import { FaNetworkWired } from 'react-icons/fa' 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 PubSub from 'pubsub-js'
import { import {
mihomoUpgrade, mihomoUpgrade,
restartCore, restartCore,
startSubStoreBackendServer, startSubStoreBackendServer,
triggerSysProxy, triggerSysProxy
showDetailedError
} from '@renderer/utils/ipc' } from '@renderer/utils/ipc'
import React, { useState } from 'react' import React, { useState } from 'react'
import InterfaceModal from '@renderer/components/mihomo/interface-modal' import InterfaceModal from '@renderer/components/mihomo/interface-modal'
@ -22,8 +21,7 @@ import { useTranslation } from 'react-i18next'
const CoreMap = { const CoreMap = {
mihomo: 'mihomo.stableVersion', mihomo: 'mihomo.stableVersion',
'mihomo-alpha': 'mihomo.alphaVersion', 'mihomo-alpha': 'mihomo.alphaVersion'
'mihomo-smart': 'mihomo.smartVersion'
} }
const Mihomo: React.FC = () => { const Mihomo: React.FC = () => {
@ -31,11 +29,6 @@ const Mihomo: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { const {
core = 'mihomo', core = 'mihomo',
enableSmartCore = true,
enableSmartOverride = true,
smartCoreUseLightGBM = false,
smartCoreCollectData = false,
smartCoreStrategy = 'sticky-sessions',
maxLogDays = 7, maxLogDays = 7,
sysProxy, sysProxy,
disableLoopbackDetector, disableLoopbackDetector,
@ -89,14 +82,7 @@ const Mihomo: React.FC = () => {
await patchAppConfig({ [key]: value }) await patchAppConfig({ [key]: value })
await restartCore() await restartCore()
} catch (e) { } catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e) alert(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)
}
} finally { } finally {
PubSub.publish('mihomo-core-changed') PubSub.publish('mihomo-core-changed')
} }
@ -106,188 +92,59 @@ const Mihomo: React.FC = () => {
<> <>
{lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />} {lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />}
<BasePage title={t('mihomo.title')}> <BasePage title={t('mihomo.title')}>
{/* Smart 内核设置 */}
<SettingCard> <SettingCard>
<div className={`rounded-md border p-2 transition-all duration-200 ${ <SettingItem
enableSmartCore title={t('mihomo.coreVersion')}
? 'border-blue-300 bg-blue-50/30 dark:border-blue-700 dark:bg-blue-950/20' actions={
: 'border-gray-300 bg-gray-50/30 dark:border-gray-600 dark:bg-gray-800/20' <Button
}`}>
<SettingItem
title={t('mihomo.enableSmartCore')}
divider
>
<Switch
size="sm" size="sm"
isSelected={enableSmartCore} isIconOnly
color={enableSmartCore ? 'primary' : 'default'} title={t('mihomo.upgradeCore')}
onValueChange={async (v) => { variant="light"
await patchAppConfig({ enableSmartCore: v }) isLoading={upgrading}
if (v && core !== 'mihomo-smart') { onPress={async () => {
await handleConfigChangeWithRestart('core', 'mihomo-smart') try {
} else if (!v && core === 'mihomo-smart') { setUpgrading(true)
await handleConfigChangeWithRestart('core', 'mihomo') 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 <IoMdCloudDownload className="text-lg" />
size="sm" </Button>
isSelected={enableSmartOverride} }
color="primary" divider
onValueChange={async (v) => { >
await patchAppConfig({ enableSmartOverride: v }) <Select
await restartCore() classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
}} className="w-[100px]"
/> size="sm"
</SettingItem> aria-label={t('mihomo.selectCoreVersion')}
)} selectedKeys={new Set([core])}
disallowEmptySelection={true}
<SettingItem onSelectionChange={async (v) => {
title={t('mihomo.coreVersion')} handleConfigChangeWithRestart('core', v.currentKey as 'mihomo' | 'mihomo-alpha')
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'}
> >
<Select <SelectItem key="mihomo">{t(CoreMap['mihomo'])}</SelectItem>
classNames={{ <SelectItem key="mihomo-alpha">{t(CoreMap['mihomo-alpha'])}</SelectItem>
trigger: enableSmartCore </Select>
? 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50' </SettingItem>
: '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>
<SettingItem title={t('mihomo.mixedPort')} divider> <SettingItem title={t('mihomo.mixedPort')} divider>
<div className="flex"> <div className="flex">
{mixedPortInput !== mixedPort && ( {mixedPortInput !== mixedPort && (
@ -646,7 +503,7 @@ const Mihomo: React.FC = () => {
const [user, pass] = auth.split(':') const [user, pass] = auth.split(':')
return ( return (
<div key={index} className="flex mb-2"> <div key={index} className="flex mb-2">
<div className="flex-4"> <div className="flex-[4]">
<Input <Input
size="sm" size="sm"
fullWidth fullWidth
@ -666,7 +523,7 @@ const Mihomo: React.FC = () => {
/> />
</div> </div>
<span className="mx-2">:</span> <span className="mx-2">:</span>
<div className="flex-6 flex"> <div className="flex-[6] flex">
<Input <Input
size="sm" size="sm"
fullWidth fullWidth

View File

@ -234,7 +234,7 @@ const Profiles: React.FC = () => {
endContent={ endContent={
<> <>
<Button <Button
size="md" size="sm"
isIconOnly isIconOnly
variant="light" variant="light"
onPress={() => { onPress={() => {
@ -242,7 +242,6 @@ const Profiles: React.FC = () => {
setUrl(text) setUrl(text)
}) })
}} }}
className="mr-2"
> >
<MdContentPaste className="text-lg" /> <MdContentPaste className="text-lg" />
</Button> </Button>

View File

@ -38,7 +38,7 @@ const Rules: React.FC = () => {
</div> </div>
<Divider /> <Divider />
</div> </div>
<div className="h-[calc(100vh-100px)] mt-px"> <div className="h-[calc(100vh-100px)] mt-[1px]">
<Virtuoso <Virtuoso
data={filteredRules} data={filteredRules}
itemContent={(i, rule) => ( 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 SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { restartCore } from '@renderer/utils/ipc'
import { restartCore, patchMihomoConfig } from '@renderer/utils/ipc'
import React, { ReactNode, useState } from 'react' import React, { ReactNode, useState } from 'react'
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -12,11 +11,8 @@ import { useTranslation } from 'react-i18next'
const Sniffer: React.FC = () => { const Sniffer: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { appConfig } = useAppConfig()
const { controlSniff = true } = appConfig || {}
const { sniffer } = controledMihomoConfig || {} const { sniffer } = controledMihomoConfig || {}
const { const {
enable = true,
'parse-pure-ip': parsePureIP = true, 'parse-pure-ip': parsePureIP = true,
'force-dns-mapping': forceDNSMapping = true, 'force-dns-mapping': forceDNSMapping = true,
'override-destination': overrideDestination = false, 'override-destination': overrideDestination = false,
@ -45,7 +41,6 @@ const Sniffer: React.FC = () => {
} = sniffer || {} } = sniffer || {}
const [changed, setChanged] = useState(false) const [changed, setChanged] = useState(false)
const [values, originSetValues] = useState({ const [values, originSetValues] = useState({
enable,
parsePureIP, parsePureIP,
forceDNSMapping, forceDNSMapping,
overrideDestination, overrideDestination,
@ -64,11 +59,7 @@ const Sniffer: React.FC = () => {
try { try {
setChanged(false) setChanged(false)
await patchControledMihomoConfig(patch) await patchControledMihomoConfig(patch)
await restartCore()
if (controlSniff) {
await patchMihomoConfig(patch)
await restartCore()
}
} catch (e) { } catch (e) {
alert(e) alert(e)
} }
@ -139,34 +130,22 @@ const Sniffer: React.FC = () => {
onPress={() => onPress={() =>
onSave({ onSave({
sniffer: { sniffer: {
enable: values.enable,
'parse-pure-ip': values.parsePureIP, 'parse-pure-ip': values.parsePureIP,
'force-dns-mapping': values.forceDNSMapping, 'force-dns-mapping': values.forceDNSMapping,
'override-destination': values.overrideDestination, 'override-destination': values.overrideDestination,
sniff: values.sniff, sniff: values.sniff,
'skip-domain': values.skipDomain, 'skip-domain': values.skipDomain,
'force-domain': values.forceDomain, 'force-domain': values.forceDomain
'skip-dst-address': values.skipDstAddress,
'skip-src-address': values.skipSrcAddress
} }
}) })
} }
> >
{controlSniff ? t('common.save') : t('sniffer.saveOnly')} {t('common.save')}
</Button> </Button>
) )
} }
> >
<SettingCard> <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> <SettingItem title={t('sniffer.overrideDestination')} divider>
<Switch <Switch
size="sm" 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 SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item' import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { grantTunPermissions, restartCore, setupFirewall } from '@renderer/utils/ipc' import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import React, { Key, useState } from 'react' import React, { Key, useState } from 'react'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -18,7 +18,7 @@ const Tun: React.FC = () => {
const { tun } = controledMihomoConfig || {} const { tun } = controledMihomoConfig || {}
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { const {
device = platform === 'darwin' ? 'utun1500' : 'Mihomo', device = 'Mihomo',
stack = 'mixed', stack = 'mixed',
'auto-route': autoRoute = true, 'auto-route': autoRoute = true,
'auto-redirect': autoRedirect = false, 'auto-redirect': autoRedirect = false,
@ -129,7 +129,7 @@ const Tun: React.FC = () => {
color="primary" color="primary"
onPress={async () => { onPress={async () => {
try { try {
await grantTunPermissions() await manualGrantCorePermition()
new Notification(t('tun.notifications.coreAuthSuccess')) new Notification(t('tun.notifications.coreAuthSuccess'))
await restartCore() await restartCore()
} catch (e) { } catch (e) {
@ -165,17 +165,18 @@ const Tun: React.FC = () => {
<Tab key="system" title="System" /> <Tab key="system" title="System" />
</Tabs> </Tabs>
</SettingItem> </SettingItem>
<SettingItem title={t('tun.device.title')} divider> {platform !== 'darwin' && (
<Input <SettingItem title={t('tun.device.title')} divider>
size="sm" <Input
className="w-[100px]" size="sm"
value={values.device} className="w-[100px]"
placeholder={platform === 'darwin' ? 'utun1500' : 'Mihomo'} value={values.device}
onValueChange={(v) => { onValueChange={(v) => {
setValues({ ...values, device: v }) setValues({ ...values, device: v })
}} }}
/> />
</SettingItem> </SettingItem>
)}
<SettingItem title={t('tun.strictRoute')} divider> <SettingItem title={t('tun.strictRoute')} divider>
<Switch <Switch

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)) 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> { export async function patchMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch))
} }
@ -227,40 +211,10 @@ export async function triggerSysProxy(enable: boolean): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable))
} }
export async function checkTunPermissions(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkTunPermissions'))
}
export async function grantTunPermissions(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('grantTunPermissions'))
}
export async function manualGrantCorePermition(): Promise<void> { export async function manualGrantCorePermition(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
} }
export async function checkHighPrivilegeCore(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkHighPrivilegeCore'))
}
export async function checkAdminPrivileges(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkAdminPrivileges'))
}
export async function restartAsAdmin(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartAsAdmin'))
}
export async function showTunPermissionDialog(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showTunPermissionDialog'))
}
export async function showErrorDialog(title: string, message: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showErrorDialog', title, message))
}
export async function getFilePath(ext: string[]): Promise<string[] | undefined> { export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext))
} }
@ -402,10 +356,6 @@ export async function closeTrayIcon(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('closeTrayIcon')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('closeTrayIcon'))
} }
export async function updateTrayIcon(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateTrayIcon'))
}
export async function showMainWindow(): Promise<void> { export async function showMainWindow(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showMainWindow')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showMainWindow'))
} }

10
src/shared/types.d.ts vendored
View File

@ -216,12 +216,7 @@ interface ISysProxyConfig {
} }
interface IAppConfig { interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha' | 'mihomo-smart' core: 'mihomo' | 'mihomo-alpha'
enableSmartCore: boolean
enableSmartOverride: boolean
smartCoreUseLightGBM: boolean
smartCoreCollectData: boolean
smartCoreStrategy: 'sticky-sessions' | 'round-robin'
disableLoopbackDetector: boolean disableLoopbackDetector: boolean
disableEmbedCA: boolean disableEmbedCA: boolean
disableSystemCA: boolean disableSystemCA: boolean
@ -236,7 +231,6 @@ interface IAppConfig {
spinFloatingIcon?: boolean spinFloatingIcon?: boolean
disableTray?: boolean disableTray?: boolean
showFloatingWindow?: boolean showFloatingWindow?: boolean
floatingWindowCompatMode?: boolean
connectionCardStatus?: CardStatus connectionCardStatus?: CardStatus
dnsCardStatus?: CardStatus dnsCardStatus?: CardStatus
logCardStatus?: CardStatus logCardStatus?: CardStatus
@ -461,7 +455,7 @@ interface IProfileItem {
name: string name: string
url?: string // remote url?: string // remote
file?: string // local file?: string // local
interval?: number | string interval?: number
home?: string home?: string
updated?: number updated?: number
override?: string[] override?: string[]

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/**/*"], "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"types": ["electron-vite/node"], "types": ["electron-vite/node"]
"moduleResolution": "bundler"
} }
} }