mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 19:50:28 +08:00
Compare commits
11 Commits
2467306903
...
0198630e57
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0198630e57 | ||
|
|
3d6d545a93 | ||
|
|
fbde5c3f09 | ||
|
|
75f2522a99 | ||
|
|
a4ab3cb448 | ||
|
|
818b546817 | ||
|
|
b60c01bb4c | ||
|
|
ae91194a74 | ||
|
|
38d9e8b81b | ||
|
|
dacb77f414 | ||
|
|
a159974142 |
74
.github/workflows/build.yml
vendored
74
.github/workflows/build.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
||||
# Delete each asset with detailed logging
|
||||
echo "$ALL_ASSETS" | jq -r '.[].id' | while read asset_id; do
|
||||
if [ ! -z "$asset_id" ]; then
|
||||
echo "🗑️ Deleting asset ID: $asset_id"
|
||||
echo "🗑️ Deleting asset ID: $asset_id"
|
||||
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id")
|
||||
@ -102,9 +102,14 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
@ -136,7 +141,7 @@ jobs:
|
||||
run: pnpm copy-legacy
|
||||
- name: Upload Artifacts
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
path: |
|
||||
@ -178,9 +183,14 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
@ -214,7 +224,7 @@ jobs:
|
||||
run: pnpm copy-legacy
|
||||
- name: Upload Artifacts
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Win7 ${{ matrix.arch }}
|
||||
path: |
|
||||
@ -256,9 +266,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
@ -285,7 +300,7 @@ jobs:
|
||||
run: pnpm copy-legacy
|
||||
- name: Upload Artifacts
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Linux ${{ matrix.arch }}
|
||||
path: |
|
||||
@ -327,9 +342,14 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
@ -357,7 +377,7 @@ jobs:
|
||||
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
||||
pnpm build:mac --${{ matrix.arch }}
|
||||
- name: Setup temporary installer signing keychain
|
||||
uses: apple-actions/import-codesign-certs@v3
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
||||
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
||||
@ -381,7 +401,7 @@ jobs:
|
||||
run: pnpm copy-legacy
|
||||
- name: Upload Artifacts
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: MacOS ${{ matrix.arch }}
|
||||
path: |
|
||||
@ -420,9 +440,14 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
npm_config_arch: ${{ matrix.arch }}
|
||||
@ -452,7 +477,7 @@ jobs:
|
||||
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
||||
pnpm build:mac --${{ matrix.arch }}
|
||||
- name: Setup temporary installer signing keychain
|
||||
uses: apple-actions/import-codesign-certs@v3
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
||||
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
||||
@ -476,7 +501,7 @@ jobs:
|
||||
run: pnpm copy-legacy
|
||||
- name: Upload Artifacts
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Catalina ${{ matrix.arch }}
|
||||
path: |
|
||||
@ -509,9 +534,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
run: npm install -g pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
- name: Update Version for Dev Build
|
||||
@ -566,7 +596,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Update Version
|
||||
run: |
|
||||
sed -i "s/pkgver=.*/pkgver=$(echo ${{ github.ref }} | tr -d 'refs/tags/v')/" aur/${{ matrix.pkgname }}/PKGBUILD
|
||||
@ -601,7 +631,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: update version
|
||||
|
||||
1
.npmrc
1
.npmrc
@ -1,3 +1,4 @@
|
||||
shamefully-hoist=true
|
||||
virtual-store-dir-max-length=80
|
||||
public-hoist-pattern[]=*@heroui/*
|
||||
only-built-dependencies="['electron', 'esbuild', 'meta-json-schema']"
|
||||
|
||||
@ -72,5 +72,8 @@ deb:
|
||||
rpm:
|
||||
afterInstall: 'build/linux/postinst'
|
||||
afterRemove: 'build/linux/postuninst'
|
||||
fpm:
|
||||
- '--rpm-rpmbuild-define'
|
||||
- '_build_id_links none'
|
||||
npmRebuild: true
|
||||
publish: []
|
||||
|
||||
@ -40,7 +40,10 @@ module.exports = [
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 0,
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn'
|
||||
}
|
||||
|
||||
75
package.json
75
package.json
@ -10,7 +10,7 @@
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"prepare:dev": "node scripts/update-version.mjs && node scripts/prepare.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
@ -23,33 +23,26 @@
|
||||
"dev": "electron-vite dev",
|
||||
"postinstall": "electron-builder install-app-deps && node node_modules/electron/install.js",
|
||||
"build:win": "electron-vite build && electron-builder --publish never --win",
|
||||
"build:win:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --win",
|
||||
"build:win:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --win",
|
||||
"build:mac": "electron-vite build && electron-builder --publish never --mac",
|
||||
"build:mac:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --mac",
|
||||
"build:mac:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --mac",
|
||||
"build:linux": "electron-vite build && electron-builder --publish never --linux",
|
||||
"build:linux:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --linux"
|
||||
"build:linux:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --linux"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@heroui/react": "^2.8.5",
|
||||
"@mihomo-party/sysproxy": "^2.0.8",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.13.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"croner": "^9.1.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"express": "^5.1.0",
|
||||
"i18next": "^25.6.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-i18next": "^15.7.4",
|
||||
"express": "^5.2.1",
|
||||
"i18next": "^25.7.3",
|
||||
"iconv-lite": "^0.7.1",
|
||||
"webdav": "^5.8.0",
|
||||
"ws": "^8.18.3",
|
||||
"yaml": "^2.8.1"
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@ -57,49 +50,55 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@heroui/react": "^2.8.7",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/express": "^5.0.5",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/pubsub-js": "^1.8.6",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"cron-validator": "^1.4.0",
|
||||
"driver.js": "^1.3.6",
|
||||
"electron": "^37.10.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"driver.js": "^1.4.0",
|
||||
"electron": "^39.2.7",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-vite": "^4.0.1",
|
||||
"electron-vite": "^5.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"eslint": "9.32.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"form-data": "^4.0.4",
|
||||
"framer-motion": "12.23.12",
|
||||
"form-data": "^4.0.5",
|
||||
"framer-motion": "12.23.26",
|
||||
"lodash": "^4.17.21",
|
||||
"meta-json-schema": "^1.19.16",
|
||||
"meta-json-schema": "^1.19.17",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"pubsub-js": "^1.9.5",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react": "^19.2.3",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-monaco-editor": "^0.59.0",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"swr": "^2.3.6",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"react-router-dom": "^7.11.0",
|
||||
"react-virtuoso": "^4.18.0",
|
||||
"swr": "^2.3.8",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tar": "^7.5.2",
|
||||
"tsx": "^4.20.6",
|
||||
"tsx": "^4.21.0",
|
||||
"types-pac": "^1.0.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.22.0"
|
||||
|
||||
5100
pnpm-lock.yaml
generated
5100
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import { deepMerge } from '../utils/merge'
|
||||
import { defaultConfig } from '../utils/template'
|
||||
|
||||
let appConfig: IAppConfig // config.yaml
|
||||
let appConfigWriteQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
export async function getAppConfig(force = false): Promise<IAppConfig> {
|
||||
if (force || !appConfig) {
|
||||
@ -22,9 +23,12 @@ export async function getAppConfig(force = false): Promise<IAppConfig> {
|
||||
}
|
||||
|
||||
export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
|
||||
if (patch.nameserverPolicy) {
|
||||
appConfig.nameserverPolicy = patch.nameserverPolicy
|
||||
}
|
||||
appConfig = deepMerge(appConfig, patch)
|
||||
await writeFile(appConfigPath(), stringify(appConfig))
|
||||
appConfigWriteQueue = appConfigWriteQueue.then(async () => {
|
||||
if (patch.nameserverPolicy) {
|
||||
appConfig.nameserverPolicy = patch.nameserverPolicy
|
||||
}
|
||||
appConfig = deepMerge(appConfig, patch)
|
||||
await writeFile(appConfigPath(), stringify(appConfig))
|
||||
})
|
||||
await appConfigWriteQueue
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import * as chromeRequest from '../utils/chromeRequest'
|
||||
import { parse, stringify } from '../utils/yaml'
|
||||
|
||||
let overrideConfig: IOverrideConfig // override.yaml
|
||||
let overrideConfigWriteQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||
if (force || !overrideConfig) {
|
||||
@ -17,8 +18,11 @@ export async function getOverrideConfig(force = false): Promise<IOverrideConfig>
|
||||
}
|
||||
|
||||
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
||||
overrideConfig = config
|
||||
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
|
||||
overrideConfigWriteQueue = overrideConfigWriteQueue.then(async () => {
|
||||
overrideConfig = config
|
||||
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
|
||||
})
|
||||
await overrideConfigWriteQueue
|
||||
}
|
||||
|
||||
export async function getOverrideItem(id: string | undefined): Promise<IOverrideItem | undefined> {
|
||||
|
||||
@ -15,8 +15,8 @@ import { mihomoUpgradeConfig } from '../core/mihomoApi'
|
||||
|
||||
import i18next from 'i18next'
|
||||
|
||||
let profileConfig: IProfileConfig // profile.yaml
|
||||
// 最终选中订阅ID
|
||||
let profileConfig: IProfileConfig
|
||||
let profileConfigWriteQueue: Promise<void> = Promise.resolve()
|
||||
let targetProfileId: string | null = null
|
||||
|
||||
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
||||
@ -25,12 +25,31 @@ export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
||||
profileConfig = parse(data) || { items: [] }
|
||||
}
|
||||
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
|
||||
return profileConfig
|
||||
return structuredClone(profileConfig)
|
||||
}
|
||||
|
||||
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
|
||||
profileConfig = config
|
||||
await writeFile(profileConfigPath(), stringify(config), 'utf-8')
|
||||
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
|
||||
profileConfig = config
|
||||
await writeFile(profileConfigPath(), stringify(config), 'utf-8')
|
||||
})
|
||||
await profileConfigWriteQueue
|
||||
}
|
||||
|
||||
export async function updateProfileConfig(
|
||||
updater: (config: IProfileConfig) => IProfileConfig | Promise<IProfileConfig>
|
||||
): Promise<IProfileConfig> {
|
||||
let result: IProfileConfig
|
||||
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
|
||||
const data = await readFile(profileConfigPath(), 'utf-8')
|
||||
profileConfig = parse(data) || { items: [] }
|
||||
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
|
||||
profileConfig = await updater(structuredClone(profileConfig))
|
||||
result = profileConfig
|
||||
await writeFile(profileConfigPath(), stringify(profileConfig), 'utf-8')
|
||||
})
|
||||
await profileConfigWriteQueue
|
||||
return structuredClone(result!)
|
||||
}
|
||||
|
||||
export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
|
||||
@ -41,8 +60,7 @@ export async function getProfileItem(id: string | undefined): Promise<IProfileIt
|
||||
}
|
||||
|
||||
export async function changeCurrentProfile(id: string): Promise<void> {
|
||||
const config = await getProfileConfig()
|
||||
const current = config.current
|
||||
const { current } = await getProfileConfig()
|
||||
|
||||
if (current === id && targetProfileId !== id) {
|
||||
return
|
||||
@ -50,13 +68,12 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
||||
|
||||
targetProfileId = id
|
||||
|
||||
config.current = id
|
||||
const configSavePromise = setProfileConfig(config)
|
||||
|
||||
try {
|
||||
await configSavePromise
|
||||
await updateProfileConfig((config) => {
|
||||
config.current = id
|
||||
return config
|
||||
})
|
||||
|
||||
// 检查订阅切换是否中断
|
||||
if (targetProfileId !== id) {
|
||||
return
|
||||
}
|
||||
@ -66,8 +83,10 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (targetProfileId === id) {
|
||||
config.current = current
|
||||
await setProfileConfig(config)
|
||||
await updateProfileConfig((config) => {
|
||||
config.current = current
|
||||
return config
|
||||
})
|
||||
targetProfileId = null
|
||||
throw e
|
||||
}
|
||||
@ -75,47 +94,51 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
||||
const config = await getProfileConfig()
|
||||
const index = config.items.findIndex((i) => i.id === item.id)
|
||||
if (index === -1) {
|
||||
throw new Error('Profile not found')
|
||||
}
|
||||
config.items[index] = item
|
||||
await setProfileConfig(config)
|
||||
await updateProfileConfig((config) => {
|
||||
const index = config.items.findIndex((i) => i.id === item.id)
|
||||
if (index === -1) {
|
||||
throw new Error('Profile not found')
|
||||
}
|
||||
config.items[index] = item
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
|
||||
const newItem = await createProfile(item)
|
||||
const config = await getProfileConfig()
|
||||
if (await getProfileItem(newItem.id)) {
|
||||
await updateProfileItem(newItem)
|
||||
} else {
|
||||
config.items.push(newItem)
|
||||
}
|
||||
await setProfileConfig(config)
|
||||
let shouldChangeCurrent = false
|
||||
await updateProfileConfig((config) => {
|
||||
const existingIndex = config.items.findIndex((i) => i.id === newItem.id)
|
||||
if (existingIndex !== -1) {
|
||||
config.items[existingIndex] = newItem
|
||||
} else {
|
||||
config.items.push(newItem)
|
||||
}
|
||||
if (!config.current) {
|
||||
shouldChangeCurrent = true
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
if (!config.current) {
|
||||
if (shouldChangeCurrent) {
|
||||
await changeCurrentProfile(newItem.id)
|
||||
}
|
||||
await addProfileUpdater(newItem)
|
||||
}
|
||||
|
||||
export async function removeProfileItem(id: string): Promise<void> {
|
||||
// 先清理自动更新定时器,防止已删除的订阅重新出现
|
||||
await removeProfileUpdater(id)
|
||||
|
||||
const config = await getProfileConfig()
|
||||
config.items = config.items?.filter((item) => item.id !== id)
|
||||
let shouldRestart = false
|
||||
if (config.current === id) {
|
||||
shouldRestart = true
|
||||
if (config.items.length > 0) {
|
||||
config.current = config.items[0].id
|
||||
} else {
|
||||
config.current = undefined
|
||||
await updateProfileConfig((config) => {
|
||||
config.items = config.items?.filter((item) => item.id !== id)
|
||||
if (config.current === id) {
|
||||
shouldRestart = true
|
||||
config.current = config.items.length > 0 ? config.items[0].id : undefined
|
||||
}
|
||||
}
|
||||
await setProfileConfig(config)
|
||||
return config
|
||||
})
|
||||
|
||||
if (existsSync(profilePath(id))) {
|
||||
await rm(profilePath(id))
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ function processRulesWithOffset(ruleStrings: string[], currentRules: string[], i
|
||||
return { normalRules, insertRules: rules }
|
||||
}
|
||||
|
||||
export async function generateProfile(): Promise<void> {
|
||||
export async function generateProfile(): Promise<string | undefined> {
|
||||
// 读取最新的配置
|
||||
const { current } = await getProfileConfig(true)
|
||||
const {
|
||||
@ -150,6 +150,7 @@ export async function generateProfile(): Promise<void> {
|
||||
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
|
||||
runtimeConfigStr
|
||||
)
|
||||
return current
|
||||
}
|
||||
|
||||
async function prepareProfileWorkDir(current: string | undefined): Promise<void> {
|
||||
|
||||
@ -13,7 +13,6 @@ import { generateProfile } from './factory'
|
||||
import {
|
||||
getAppConfig,
|
||||
getControledMihomoConfig,
|
||||
getProfileConfig,
|
||||
patchAppConfig,
|
||||
patchControledMihomoConfig,
|
||||
manageSmartOverride
|
||||
@ -130,15 +129,15 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
||||
await rm(path.join(dataDir(), 'core.pid'))
|
||||
}
|
||||
}
|
||||
const { current } = await getProfileConfig(true)
|
||||
const { tun } = await getControledMihomoConfig()
|
||||
const corePath = mihomoCorePath(core)
|
||||
|
||||
// 管理 Smart 内核覆写配置
|
||||
await manageSmartOverride()
|
||||
|
||||
await generateProfile()
|
||||
await checkProfile()
|
||||
// generateProfile 返回实际使用的 current,确保内核工作目录与配置文件一致
|
||||
const current = await generateProfile()
|
||||
await checkProfile(current)
|
||||
await stopCore()
|
||||
|
||||
await cleanupSocketFile()
|
||||
@ -453,9 +452,8 @@ export async function quitWithoutCore(): Promise<void> {
|
||||
app.exit()
|
||||
}
|
||||
|
||||
async function checkProfile(): Promise<void> {
|
||||
async function checkProfile(current: string | undefined): Promise<void> {
|
||||
const { core = 'mihomo', diffWorkDir = false } = await getAppConfig()
|
||||
const { current } = await getProfileConfig()
|
||||
const corePath = mihomoCorePath(core)
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
@ -562,7 +560,7 @@ async function waitForCoreReady(): Promise<void> {
|
||||
await axios.get('/')
|
||||
await managerLogger.info(`Core ready after ${i + 1} attempts (${(i + 1) * retryInterval}ms)`)
|
||||
return
|
||||
} catch (error) {
|
||||
} catch {
|
||||
if (i === 0) {
|
||||
await managerLogger.info('Waiting for core to be ready...')
|
||||
}
|
||||
@ -791,7 +789,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
||||
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
await managerLogger.info(
|
||||
`Cannot get info for process ${pid}, might be high privilege`
|
||||
)
|
||||
@ -835,7 +833,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,8 @@ let logsRetry = 10
|
||||
let mihomoConnectionsWs: WebSocket | null = null
|
||||
let connectionsRetry = 10
|
||||
|
||||
const MAX_RETRY = 10
|
||||
|
||||
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
|
||||
const dynamicIpcPath = getMihomoIpcPath()
|
||||
|
||||
@ -233,6 +235,7 @@ export const mihomoSmartFlushCache = async (configName?: string): Promise<void>
|
||||
}
|
||||
|
||||
export const startMihomoTraffic = async (): Promise<void> => {
|
||||
trafficRetry = MAX_RETRY
|
||||
await mihomoTraffic()
|
||||
}
|
||||
|
||||
@ -258,7 +261,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
|
||||
const data = e.data as string
|
||||
const json = JSON.parse(data) as IMihomoTrafficInfo
|
||||
trafficRetry = 10
|
||||
trafficRetry = MAX_RETRY
|
||||
try {
|
||||
mainWindow?.webContents.send('mihomoTraffic', json)
|
||||
if (process.platform !== 'linux') {
|
||||
@ -292,6 +295,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
export const startMihomoMemory = async (): Promise<void> => {
|
||||
memoryRetry = MAX_RETRY
|
||||
await mihomoMemory()
|
||||
}
|
||||
|
||||
@ -314,7 +318,7 @@ const mihomoMemory = async (): Promise<void> => {
|
||||
|
||||
mihomoMemoryWs.onmessage = (e): void => {
|
||||
const data = e.data as string
|
||||
memoryRetry = 10
|
||||
memoryRetry = MAX_RETRY
|
||||
try {
|
||||
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
|
||||
} catch {
|
||||
@ -338,6 +342,7 @@ const mihomoMemory = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
export const startMihomoLogs = async (): Promise<void> => {
|
||||
logsRetry = MAX_RETRY
|
||||
await mihomoLogs()
|
||||
}
|
||||
|
||||
@ -362,7 +367,7 @@ const mihomoLogs = async (): Promise<void> => {
|
||||
|
||||
mihomoLogsWs.onmessage = (e): void => {
|
||||
const data = e.data as string
|
||||
logsRetry = 10
|
||||
logsRetry = MAX_RETRY
|
||||
try {
|
||||
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
|
||||
} catch {
|
||||
@ -386,6 +391,7 @@ const mihomoLogs = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
export const startMihomoConnections = async (): Promise<void> => {
|
||||
connectionsRetry = MAX_RETRY
|
||||
await mihomoConnections()
|
||||
}
|
||||
|
||||
@ -408,7 +414,7 @@ const mihomoConnections = async (): Promise<void> => {
|
||||
|
||||
mihomoConnectionsWs.onmessage = (e): void => {
|
||||
const data = e.data as string
|
||||
connectionsRetry = 10
|
||||
connectionsRetry = MAX_RETRY
|
||||
try {
|
||||
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
||||
} catch {
|
||||
|
||||
@ -1,33 +1,40 @@
|
||||
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
|
||||
import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config'
|
||||
import { Cron } from 'croner'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
|
||||
|
||||
async function updateProfile(id: string): Promise<void> {
|
||||
const item = await getProfileItem(id)
|
||||
if (item && item.type === 'remote') {
|
||||
await addProfileItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
export async function initProfileUpdater(): Promise<void> {
|
||||
const { items, current } = await getProfileConfig()
|
||||
const currentItem = await getCurrentProfileItem()
|
||||
|
||||
for (const item of items.filter((i) => i.id !== current)) {
|
||||
if (item.type === 'remote' && item.autoUpdate && item.interval) {
|
||||
const itemId = item.id
|
||||
if (typeof item.interval === 'number') {
|
||||
// 数字间隔使用 setInterval
|
||||
intervalPool[item.id] = setInterval(
|
||||
intervalPool[itemId] = setInterval(
|
||||
async () => {
|
||||
try {
|
||||
await addProfileItem(item)
|
||||
await updateProfile(itemId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||
}
|
||||
},
|
||||
item.interval * 60 * 1000
|
||||
)
|
||||
} else if (typeof item.interval === 'string') {
|
||||
// 字符串间隔使用 Cron
|
||||
intervalPool[item.id] = new Cron(item.interval, async () => {
|
||||
intervalPool[itemId] = new Cron(item.interval, async () => {
|
||||
try {
|
||||
await addProfileItem(item)
|
||||
await updateProfile(itemId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -35,19 +42,20 @@ export async function initProfileUpdater(): Promise<void> {
|
||||
try {
|
||||
await addProfileItem(item)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to init profile ${item.name}:`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) {
|
||||
const currentId = currentItem.id
|
||||
if (typeof currentItem.interval === 'number') {
|
||||
intervalPool[currentItem.id] = setInterval(
|
||||
intervalPool[currentId] = setInterval(
|
||||
async () => {
|
||||
try {
|
||||
await addProfileItem(currentItem)
|
||||
await updateProfile(currentId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
||||
}
|
||||
},
|
||||
currentItem.interval * 60 * 1000
|
||||
@ -56,19 +64,19 @@ export async function initProfileUpdater(): Promise<void> {
|
||||
setTimeout(
|
||||
async () => {
|
||||
try {
|
||||
await addProfileItem(currentItem)
|
||||
await updateProfile(currentId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
||||
}
|
||||
},
|
||||
currentItem.interval * 60 * 1000 + 10000 // +10s
|
||||
currentItem.interval * 60 * 1000 + 10000
|
||||
)
|
||||
} else if (typeof currentItem.interval === 'string') {
|
||||
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
|
||||
intervalPool[currentId] = new Cron(currentItem.interval, async () => {
|
||||
try {
|
||||
await addProfileItem(currentItem)
|
||||
await updateProfile(currentId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -76,7 +84,7 @@ export async function initProfileUpdater(): Promise<void> {
|
||||
try {
|
||||
await addProfileItem(currentItem)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to init current profile:`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,23 +99,24 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const itemId = item.id
|
||||
if (typeof item.interval === 'number') {
|
||||
intervalPool[item.id] = setInterval(
|
||||
intervalPool[itemId] = setInterval(
|
||||
async () => {
|
||||
try {
|
||||
await addProfileItem(item)
|
||||
await updateProfile(itemId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||
}
|
||||
},
|
||||
item.interval * 60 * 1000
|
||||
)
|
||||
} else if (typeof item.interval === 'string') {
|
||||
intervalPool[item.id] = new Cron(item.interval, async () => {
|
||||
intervalPool[itemId] = new Cron(item.interval, async () => {
|
||||
try {
|
||||
await addProfileItem(item)
|
||||
await updateProfile(itemId)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
groupsMenu = groupItems
|
||||
groupsMenu.unshift({ type: 'separator' })
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
// 避免出错时无法创建托盘菜单
|
||||
}
|
||||
@ -206,7 +206,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
await patchAppConfig({ sysProxy: { enable } })
|
||||
mainWindow?.webContents.send('appConfigUpdated')
|
||||
floatingWindow?.webContents.send('appConfigUpdated')
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
@ -390,6 +390,8 @@ export async function createTray(): Promise<void> {
|
||||
if (!useDockIcon) {
|
||||
hideDockIcon()
|
||||
}
|
||||
// 移除旧监听器防止累积
|
||||
ipcMain.removeAllListeners('trayIconUpdate')
|
||||
ipcMain.on('trayIconUpdate', async (_, png: string) => {
|
||||
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
|
||||
image.setTemplateImage(true)
|
||||
@ -435,6 +437,8 @@ export async function createTray(): Promise<void> {
|
||||
triggerMainWindow()
|
||||
}
|
||||
})
|
||||
// 移除旧监听器防止累积
|
||||
ipcMain.removeAllListeners('updateTrayMenu')
|
||||
ipcMain.on('updateTrayMenu', async () => {
|
||||
await updateTrayMenu()
|
||||
})
|
||||
|
||||
@ -61,7 +61,7 @@ export async function checkAutoRun(): Promise<boolean> {
|
||||
`chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"`
|
||||
)
|
||||
return stdout.includes(appName)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -96,7 +96,7 @@ export async function enableAutoRun(): Promise<void> {
|
||||
await execPromise(
|
||||
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
|
||||
)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
||||
}
|
||||
}
|
||||
@ -144,7 +144,7 @@ export async function disableAutoRun(): Promise<void> {
|
||||
await execPromise(
|
||||
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"`
|
||||
)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,18 +160,33 @@ function isSocketFileExists(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to send signal to recreate socket
|
||||
// Check if helper process is running (no admin privileges needed)
|
||||
async function isHelperRunning(): Promise<boolean> {
|
||||
try {
|
||||
const execPromise = promisify(exec)
|
||||
const { stdout } = await execPromise('pgrep -f party.mihomo.helper')
|
||||
return stdout.trim().length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Start or restart helper service via launchctl
|
||||
async function startHelperService(): Promise<void> {
|
||||
const execPromise = promisify(exec)
|
||||
const shell = `launchctl kickstart -k system/party.mihomo.helper`
|
||||
const command = `do shell script "${shell}" with administrator privileges`
|
||||
await execPromise(`osascript -e '${command}'`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
}
|
||||
|
||||
// Send signal to recreate socket (only if process is running)
|
||||
async function requestSocketRecreation(): Promise<void> {
|
||||
try {
|
||||
// Send SIGUSR1 signal to helper process to recreate socket
|
||||
const execPromise = promisify(exec)
|
||||
|
||||
// Use osascript with administrator privileges (same pattern as grantTunPermissions)
|
||||
const shell = `pkill -USR1 -f party.mihomo.helper`
|
||||
const command = `do shell script "${shell}" with administrator privileges`
|
||||
await execPromise(`osascript -e '${command}'`)
|
||||
|
||||
// Wait a bit for socket recreation
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
} catch (error) {
|
||||
await proxyLogger.error('Failed to send signal to helper', error)
|
||||
@ -180,7 +195,7 @@ async function requestSocketRecreation(): Promise<void> {
|
||||
}
|
||||
|
||||
// Wrapper function for helper requests with auto-retry on socket issues
|
||||
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
|
||||
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2): Promise<unknown> {
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
@ -188,21 +203,34 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
||||
return await requestFn()
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
const errCode = (error as NodeJS.ErrnoException).code
|
||||
const errMsg = (error as Error).message || ''
|
||||
|
||||
// Check if it's a connection error and socket file doesn't exist
|
||||
if (
|
||||
attempt < maxRetries &&
|
||||
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
||||
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
||||
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||
(error as Error).message?.includes('ENOENT'))
|
||||
(errCode === 'ECONNREFUSED' ||
|
||||
errCode === 'ENOENT' ||
|
||||
errMsg.includes('connect ECONNREFUSED') ||
|
||||
errMsg.includes('ENOENT'))
|
||||
) {
|
||||
await proxyLogger.info(
|
||||
`Helper request failed (attempt ${attempt + 1}), checking socket file...`
|
||||
`Helper request failed (attempt ${attempt + 1}/${maxRetries + 1}), checking helper status...`
|
||||
)
|
||||
|
||||
if (!isSocketFileExists()) {
|
||||
await proxyLogger.info('Socket file missing, requesting recreation...')
|
||||
const helperRunning = await isHelperRunning()
|
||||
const socketExists = isSocketFileExists()
|
||||
|
||||
if (!helperRunning) {
|
||||
await proxyLogger.info('Helper process not running, starting service...')
|
||||
try {
|
||||
await startHelperService()
|
||||
await proxyLogger.info('Helper service started, retrying...')
|
||||
continue
|
||||
} catch (startError) {
|
||||
await proxyLogger.warn('Failed to start helper service', startError)
|
||||
}
|
||||
} else if (!socketExists) {
|
||||
await proxyLogger.info('Socket file missing but helper running, requesting recreation...')
|
||||
try {
|
||||
await requestSocketRecreation()
|
||||
await proxyLogger.info('Socket recreation requested, retrying...')
|
||||
@ -213,7 +241,6 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
||||
}
|
||||
}
|
||||
|
||||
// If not a connection error or we've exhausted retries, throw the error
|
||||
if (attempt === maxRetries) {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
18
src/preload/index.d.ts
vendored
18
src/preload/index.d.ts
vendored
@ -1,6 +1,22 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { webUtils } from 'electron'
|
||||
|
||||
type IpcListener = (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
||||
|
||||
interface SafeIpcRenderer {
|
||||
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
|
||||
send: (channel: string, ...args: unknown[]) => void
|
||||
on: (channel: string, listener: IpcListener) => void
|
||||
removeListener: (channel: string, listener: IpcListener) => void
|
||||
removeAllListeners: (channel: string) => void
|
||||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
ipcRenderer: SafeIpcRenderer
|
||||
process: {
|
||||
platform: NodeJS.Platform
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
|
||||
@ -1,13 +1,231 @@
|
||||
import { contextBridge, webUtils } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { contextBridge, ipcRenderer, webUtils } from 'electron'
|
||||
|
||||
// 允许的 invoke channels 白名单
|
||||
const validInvokeChannels = [
|
||||
// Mihomo API
|
||||
'mihomoVersion',
|
||||
'mihomoCloseConnection',
|
||||
'mihomoCloseAllConnections',
|
||||
'mihomoRules',
|
||||
'mihomoProxies',
|
||||
'mihomoGroups',
|
||||
'mihomoProxyProviders',
|
||||
'mihomoUpdateProxyProviders',
|
||||
'mihomoRuleProviders',
|
||||
'mihomoUpdateRuleProviders',
|
||||
'mihomoChangeProxy',
|
||||
'mihomoUnfixedProxy',
|
||||
'mihomoUpgradeGeo',
|
||||
'mihomoUpgrade',
|
||||
'mihomoUpgradeUI',
|
||||
'mihomoUpgradeConfig',
|
||||
'mihomoProxyDelay',
|
||||
'mihomoGroupDelay',
|
||||
'patchMihomoConfig',
|
||||
'mihomoSmartGroupWeights',
|
||||
'mihomoSmartFlushCache',
|
||||
// AutoRun
|
||||
'checkAutoRun',
|
||||
'enableAutoRun',
|
||||
'disableAutoRun',
|
||||
// Config
|
||||
'getAppConfig',
|
||||
'patchAppConfig',
|
||||
'getControledMihomoConfig',
|
||||
'patchControledMihomoConfig',
|
||||
'resetAppConfig',
|
||||
// Profile
|
||||
'getProfileConfig',
|
||||
'setProfileConfig',
|
||||
'getCurrentProfileItem',
|
||||
'getProfileItem',
|
||||
'getProfileStr',
|
||||
'setProfileStr',
|
||||
'addProfileItem',
|
||||
'removeProfileItem',
|
||||
'updateProfileItem',
|
||||
'changeCurrentProfile',
|
||||
'addProfileUpdater',
|
||||
'removeProfileUpdater',
|
||||
// Override
|
||||
'getOverrideConfig',
|
||||
'setOverrideConfig',
|
||||
'getOverrideItem',
|
||||
'addOverrideItem',
|
||||
'removeOverrideItem',
|
||||
'updateOverrideItem',
|
||||
'getOverride',
|
||||
'setOverride',
|
||||
// File
|
||||
'getFileStr',
|
||||
'setFileStr',
|
||||
'convertMrsRuleset',
|
||||
'getRuntimeConfig',
|
||||
'getRuntimeConfigStr',
|
||||
'getSmartOverrideContent',
|
||||
'getRuleStr',
|
||||
'setRuleStr',
|
||||
'getFilePath',
|
||||
'readTextFile',
|
||||
'openFile',
|
||||
// Core
|
||||
'restartCore',
|
||||
'startMonitor',
|
||||
'quitWithoutCore',
|
||||
// System
|
||||
'triggerSysProxy',
|
||||
'checkTunPermissions',
|
||||
'grantTunPermissions',
|
||||
'manualGrantCorePermition',
|
||||
'checkAdminPrivileges',
|
||||
'restartAsAdmin',
|
||||
'checkMihomoCorePermissions',
|
||||
'requestTunPermissions',
|
||||
'checkHighPrivilegeCore',
|
||||
'showTunPermissionDialog',
|
||||
'showErrorDialog',
|
||||
'openUWPTool',
|
||||
'setupFirewall',
|
||||
'getInterfaces',
|
||||
'setNativeTheme',
|
||||
'copyEnv',
|
||||
// Update
|
||||
'checkUpdate',
|
||||
'downloadAndInstallUpdate',
|
||||
'getVersion',
|
||||
'platform',
|
||||
'fetchMihomoTags',
|
||||
'installSpecificMihomoCore',
|
||||
'clearMihomoVersionCache',
|
||||
// Backup
|
||||
'webdavBackup',
|
||||
'webdavRestore',
|
||||
'listWebdavBackups',
|
||||
'webdavDelete',
|
||||
'reinitWebdavBackupScheduler',
|
||||
'exportLocalBackup',
|
||||
'importLocalBackup',
|
||||
// SubStore
|
||||
'startSubStoreFrontendServer',
|
||||
'stopSubStoreFrontendServer',
|
||||
'startSubStoreBackendServer',
|
||||
'stopSubStoreBackendServer',
|
||||
'downloadSubStore',
|
||||
'subStorePort',
|
||||
'subStoreFrontendPort',
|
||||
'subStoreSubs',
|
||||
'subStoreCollections',
|
||||
// Theme
|
||||
'resolveThemes',
|
||||
'fetchThemes',
|
||||
'importThemes',
|
||||
'readTheme',
|
||||
'writeTheme',
|
||||
'applyTheme',
|
||||
// Tray
|
||||
'showTrayIcon',
|
||||
'closeTrayIcon',
|
||||
'updateTrayIcon',
|
||||
'updateTrayIconImmediate',
|
||||
// Window
|
||||
'showMainWindow',
|
||||
'closeMainWindow',
|
||||
'triggerMainWindow',
|
||||
'showFloatingWindow',
|
||||
'closeFloatingWindow',
|
||||
'showContextMenu',
|
||||
'setTitleBarOverlay',
|
||||
'setAlwaysOnTop',
|
||||
'isAlwaysOnTop',
|
||||
'openDevTools',
|
||||
'createHeapSnapshot',
|
||||
'relaunchApp',
|
||||
'quitApp',
|
||||
// Shortcut
|
||||
'registerShortcut',
|
||||
// Misc
|
||||
'getGistUrl',
|
||||
'getImageDataURL',
|
||||
'changeLanguage'
|
||||
] as const
|
||||
|
||||
// 允许的 on/removeListener channels 白名单
|
||||
const validListenChannels = [
|
||||
'mihomoLogs',
|
||||
'mihomoConnections',
|
||||
'mihomoTraffic',
|
||||
'mihomoMemory',
|
||||
'appConfigUpdated',
|
||||
'controledMihomoConfigUpdated',
|
||||
'profileConfigUpdated',
|
||||
'groupsUpdated',
|
||||
'rulesUpdated'
|
||||
] as const
|
||||
|
||||
// 允许的 send channels 白名单
|
||||
const validSendChannels = [
|
||||
'updateTrayMenu',
|
||||
'updateFloatingWindow',
|
||||
'trayIconUpdate'
|
||||
] as const
|
||||
|
||||
type InvokeChannel = (typeof validInvokeChannels)[number]
|
||||
type ListenChannel = (typeof validListenChannels)[number]
|
||||
type SendChannel = (typeof validSendChannels)[number]
|
||||
|
||||
type IpcListener = (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
||||
const listenerMap = new Map<ListenChannel, Set<IpcListener>>()
|
||||
|
||||
// 安全的 IPC API,只暴露白名单内的 channels
|
||||
const electronAPI = {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: InvokeChannel, ...args: unknown[]): Promise<unknown> => {
|
||||
if (validInvokeChannels.includes(channel)) {
|
||||
return ipcRenderer.invoke(channel, ...args)
|
||||
}
|
||||
return Promise.reject(new Error(`Invalid invoke channel: ${channel}`))
|
||||
},
|
||||
send: (channel: SendChannel, ...args: unknown[]): void => {
|
||||
if (validSendChannels.includes(channel)) {
|
||||
ipcRenderer.send(channel, ...args)
|
||||
}
|
||||
},
|
||||
on: (channel: ListenChannel, listener: IpcListener): void => {
|
||||
if (validListenChannels.includes(channel)) {
|
||||
if (!listenerMap.has(channel)) {
|
||||
listenerMap.set(channel, new Set())
|
||||
}
|
||||
listenerMap.get(channel)!.add(listener)
|
||||
ipcRenderer.on(channel, listener)
|
||||
}
|
||||
},
|
||||
removeListener: (channel: ListenChannel, listener: IpcListener): void => {
|
||||
if (validListenChannels.includes(channel)) {
|
||||
listenerMap.get(channel)?.delete(listener)
|
||||
ipcRenderer.removeListener(channel, listener)
|
||||
}
|
||||
},
|
||||
removeAllListeners: (channel: ListenChannel): void => {
|
||||
if (validListenChannels.includes(channel)) {
|
||||
const listeners = listenerMap.get(channel)
|
||||
if (listeners) {
|
||||
listeners.forEach((listener) => {
|
||||
ipcRenderer.removeListener(channel, listener)
|
||||
})
|
||||
listeners.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
process: {
|
||||
platform: process.platform
|
||||
}
|
||||
}
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
webUtils: webUtils
|
||||
}
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
|
||||
@ -86,7 +86,7 @@ const App: React.FC = () => {
|
||||
options.color = window.getComputedStyle(document.documentElement).backgroundColor
|
||||
options.symbolColor = window.getComputedStyle(document.documentElement).color
|
||||
setTitleBarOverlay(options)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState, useCallback } from 'react'
|
||||
import MihomoIcon from './components/base/mihomo-icon'
|
||||
import { calcTraffic } from './utils/calc'
|
||||
import { showContextMenu, triggerMainWindow } from './utils/ipc'
|
||||
@ -48,16 +48,19 @@ const FloatingApp: React.FC = () => {
|
||||
}
|
||||
}, [spinSpeed, spinFloatingIcon])
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
|
||||
setUpload(info.up)
|
||||
setDownload(info.down)
|
||||
})
|
||||
return (): void => {
|
||||
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
||||
}
|
||||
const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
|
||||
const info = args[0] as IMihomoTrafficInfo
|
||||
setUpload(info.up)
|
||||
setDownload(info.down)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
||||
return (): void => {
|
||||
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
|
||||
}
|
||||
}, [handleTraffic])
|
||||
|
||||
return (
|
||||
<div className="app-drag h-screen w-screen overflow-hidden">
|
||||
<div className="floating-bg border border-divider flex bg-content1 h-full w-full">
|
||||
|
||||
@ -32,7 +32,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
// @ts-ignore windowControlsOverlay
|
||||
const windowControlsOverlay = window.navigator.windowControlsOverlay
|
||||
setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width)
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +149,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
// 非纯数字
|
||||
try {
|
||||
setValues({ ...values, interval: v })
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ const Viewer: React.FC<Props> = (props) => {
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setCurrData(fileContent)
|
||||
}
|
||||
} finally {
|
||||
|
||||
@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
|
||||
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import React, { useEffect, useState, useMemo } from 'react'
|
||||
import React, { useEffect, useState, useMemo, useRef, useCallback } from 'react'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { IoLink } from 'react-icons/io5'
|
||||
@ -25,11 +25,6 @@ import { useTranslation } from 'react-i18next'
|
||||
// 注册 Chart.js 组件
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler)
|
||||
|
||||
let currentUpload: number | undefined = undefined
|
||||
let currentDownload: number | undefined = undefined
|
||||
let hasShowTraffic = false
|
||||
let drawing = false
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
@ -61,6 +56,12 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
})
|
||||
const [series, setSeries] = useState(Array(10).fill(0))
|
||||
|
||||
// 使用 useRef 替代模块级变量
|
||||
const currentUploadRef = useRef<number | undefined>(undefined)
|
||||
const currentDownloadRef = useRef<number | undefined>(undefined)
|
||||
const hasShowTrafficRef = useRef(false)
|
||||
const drawingRef = useRef(false)
|
||||
|
||||
// Chart.js 配置
|
||||
const chartData = useMemo(() => {
|
||||
return {
|
||||
@ -125,35 +126,45 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
|
||||
|
||||
// 使用 useCallback 创建稳定的 handler 引用
|
||||
const handleTraffic = useCallback(
|
||||
async (_e: unknown, ...args: unknown[]) => {
|
||||
const info = args[0] as IMihomoTrafficInfo
|
||||
setUpload(info.up)
|
||||
setDownload(info.down)
|
||||
const data = series
|
||||
data.shift()
|
||||
data.push(info.up + info.down)
|
||||
setSeries([...data])
|
||||
setSeries((prev) => {
|
||||
const data = [...prev]
|
||||
data.shift()
|
||||
data.push(info.up + info.down)
|
||||
return data
|
||||
})
|
||||
if (platform === 'darwin' && showTraffic) {
|
||||
if (drawing) return
|
||||
drawing = true
|
||||
if (drawingRef.current) return
|
||||
drawingRef.current = true
|
||||
try {
|
||||
await drawSvg(info.up, info.down)
|
||||
hasShowTraffic = true
|
||||
await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef)
|
||||
hasShowTrafficRef.current = true
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
drawing = false
|
||||
drawingRef.current = false
|
||||
}
|
||||
} else {
|
||||
if (!hasShowTraffic) return
|
||||
if (!hasShowTrafficRef.current) return
|
||||
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
||||
hasShowTraffic = false
|
||||
hasShowTrafficRef.current = false
|
||||
}
|
||||
})
|
||||
},
|
||||
[showTraffic]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
||||
return (): void => {
|
||||
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
||||
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
|
||||
}
|
||||
}, [showTraffic])
|
||||
}, [handleTraffic])
|
||||
|
||||
if (iconOnly) {
|
||||
return (
|
||||
@ -273,10 +284,15 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
|
||||
export default ConnCard
|
||||
|
||||
const drawSvg = async (upload: number, download: number): Promise<void> => {
|
||||
if (upload === currentUpload && download === currentDownload) return
|
||||
currentUpload = upload
|
||||
currentDownload = download
|
||||
const drawSvg = async (
|
||||
upload: number,
|
||||
download: number,
|
||||
currentUploadRef: React.RefObject<number | undefined>,
|
||||
currentDownloadRef: React.RefObject<number | undefined>
|
||||
): Promise<void> => {
|
||||
if (upload === currentUploadRef.current && download === currentDownloadRef.current) return
|
||||
currentUploadRef.current = upload
|
||||
currentDownloadRef.current = download
|
||||
const svg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 36"><image height="36" width="36" href="${trayIconBase64}"/><text x="140" y="15" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(upload)}/s</text><text x="140" y="34" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(download)}/s</text></svg>`
|
||||
const image = await loadImage(svg)
|
||||
window.electron.ipcRenderer.send('trayIconUpdate', image)
|
||||
|
||||
@ -43,7 +43,8 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
|
||||
const token = PubSub.subscribe('mihomo-core-changed', () => {
|
||||
mutate()
|
||||
})
|
||||
window.electron.ipcRenderer.on('mihomoMemory', (_e, info: IMihomoMemoryInfo) => {
|
||||
window.electron.ipcRenderer.on('mihomoMemory', (_e, ...args) => {
|
||||
const info = args[0] as IMihomoMemoryInfo
|
||||
setMem(info.inuse)
|
||||
})
|
||||
return (): void => {
|
||||
|
||||
@ -170,7 +170,8 @@ const Connections: React.FC = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (_e: unknown, info: IMihomoConnectionsInfo): void => {
|
||||
const handler = (_e: unknown, ...args: unknown[]): void => {
|
||||
const info = args[0] as IMihomoConnectionsInfo
|
||||
setConnectionsInfo(info)
|
||||
|
||||
if (!info.connections) return
|
||||
|
||||
@ -26,7 +26,8 @@ const cachedLogs: {
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => {
|
||||
window.electron.ipcRenderer.on('mihomoLogs', (_e, ...args) => {
|
||||
const log = args[0] as IMihomoLogInfo
|
||||
log.time = new Date().toLocaleString()
|
||||
cachedLogs.log.push(log)
|
||||
if (cachedLogs.log.length >= 500) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { TitleBarOverlayOptions } from 'electron'
|
||||
|
||||
function checkIpcError<T>(response: T | { invokeError: unknown }): T {
|
||||
function checkIpcError<T>(response: unknown): T {
|
||||
if (response && typeof response === 'object' && 'invokeError' in response) {
|
||||
throw response.invokeError
|
||||
throw (response as { invokeError: unknown }).invokeError
|
||||
}
|
||||
return response as T
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user