mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-11 04:00:32 +08:00
Compare commits
No commits in common. "0198630e57a4ffd0bc65ba6657928924f0cfcce2" and "24673069039eb55bc0174c95d0caaec28e902d24" have entirely different histories.
0198630e57
...
2467306903
74
.github/workflows/build.yml
vendored
74
.github/workflows/build.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
|||||||
# Delete each asset with detailed logging
|
# Delete each asset with detailed logging
|
||||||
echo "$ALL_ASSETS" | jq -r '.[].id' | while read asset_id; do
|
echo "$ALL_ASSETS" | jq -r '.[].id' | while read asset_id; do
|
||||||
if [ ! -z "$asset_id" ]; then
|
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 \
|
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id")
|
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id")
|
||||||
@ -102,14 +102,9 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
run: npm install -g pnpm
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -141,7 +136,7 @@ jobs:
|
|||||||
run: pnpm copy-legacy
|
run: pnpm copy-legacy
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Windows ${{ matrix.arch }}
|
name: Windows ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -183,14 +178,9 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
run: npm install -g pnpm
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -224,7 +214,7 @@ jobs:
|
|||||||
run: pnpm copy-legacy
|
run: pnpm copy-legacy
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Win7 ${{ matrix.arch }}
|
name: Win7 ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -266,14 +256,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
run: npm install -g pnpm
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -300,7 +285,7 @@ jobs:
|
|||||||
run: pnpm copy-legacy
|
run: pnpm copy-legacy
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Linux ${{ matrix.arch }}
|
name: Linux ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -342,14 +327,9 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
run: npm install -g pnpm
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -377,7 +357,7 @@ jobs:
|
|||||||
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
||||||
pnpm build:mac --${{ matrix.arch }}
|
pnpm build:mac --${{ matrix.arch }}
|
||||||
- name: Setup temporary installer signing keychain
|
- name: Setup temporary installer signing keychain
|
||||||
uses: apple-actions/import-codesign-certs@v6
|
uses: apple-actions/import-codesign-certs@v3
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
||||||
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
||||||
@ -401,7 +381,7 @@ jobs:
|
|||||||
run: pnpm copy-legacy
|
run: pnpm copy-legacy
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MacOS ${{ matrix.arch }}
|
name: MacOS ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -440,14 +420,9 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
run: npm install -g pnpm
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -477,7 +452,7 @@ jobs:
|
|||||||
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
|
||||||
pnpm build:mac --${{ matrix.arch }}
|
pnpm build:mac --${{ matrix.arch }}
|
||||||
- name: Setup temporary installer signing keychain
|
- name: Setup temporary installer signing keychain
|
||||||
uses: apple-actions/import-codesign-certs@v6
|
uses: apple-actions/import-codesign-certs@v3
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
|
||||||
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
|
||||||
@ -501,7 +476,7 @@ jobs:
|
|||||||
run: pnpm copy-legacy
|
run: pnpm copy-legacy
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Catalina ${{ matrix.arch }}
|
name: Catalina ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -534,14 +509,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
run: npm install -g pnpm
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
- name: Update Version for Dev Build
|
- name: Update Version for Dev Build
|
||||||
@ -596,7 +566,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
- name: Update Version
|
- name: Update Version
|
||||||
run: |
|
run: |
|
||||||
sed -i "s/pkgver=.*/pkgver=$(echo ${{ github.ref }} | tr -d 'refs/tags/v')/" aur/${{ matrix.pkgname }}/PKGBUILD
|
sed -i "s/pkgver=.*/pkgver=$(echo ${{ github.ref }} | tr -d 'refs/tags/v')/" aur/${{ matrix.pkgname }}/PKGBUILD
|
||||||
@ -631,7 +601,7 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/heads/')
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: update version
|
- name: update version
|
||||||
|
|||||||
1
.npmrc
1
.npmrc
@ -1,4 +1,3 @@
|
|||||||
shamefully-hoist=true
|
shamefully-hoist=true
|
||||||
virtual-store-dir-max-length=80
|
virtual-store-dir-max-length=80
|
||||||
public-hoist-pattern[]=*@heroui/*
|
public-hoist-pattern[]=*@heroui/*
|
||||||
only-built-dependencies="['electron', 'esbuild', 'meta-json-schema']"
|
|
||||||
|
|||||||
@ -72,8 +72,5 @@ deb:
|
|||||||
rpm:
|
rpm:
|
||||||
afterInstall: 'build/linux/postinst'
|
afterInstall: 'build/linux/postinst'
|
||||||
afterRemove: 'build/linux/postuninst'
|
afterRemove: 'build/linux/postuninst'
|
||||||
fpm:
|
|
||||||
- '--rpm-rpmbuild-define'
|
|
||||||
- '_build_id_links none'
|
|
||||||
npmRebuild: true
|
npmRebuild: true
|
||||||
publish: []
|
publish: []
|
||||||
|
|||||||
@ -40,10 +40,7 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': 0,
|
||||||
'warn',
|
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
|
||||||
],
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'warn'
|
'@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",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"prepare": "node scripts/prepare.mjs",
|
"prepare": "node scripts/prepare.mjs",
|
||||||
"prepare:dev": "node scripts/update-version.mjs && node scripts/prepare.mjs",
|
"prepare:dev": "node scripts/update-version.mjs && node scripts/prepare.mjs",
|
||||||
"updater": "node scripts/updater.mjs",
|
"updater": "node scripts/updater.mjs",
|
||||||
@ -23,26 +23,33 @@
|
|||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"postinstall": "electron-builder install-app-deps && node node_modules/electron/install.js",
|
"postinstall": "electron-builder install-app-deps && node node_modules/electron/install.js",
|
||||||
"build:win": "electron-vite build && electron-builder --publish never --win",
|
"build:win": "electron-vite build && electron-builder --publish never --win",
|
||||||
"build:win:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --win",
|
"build:win:dev": "npm run prepare:dev && 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:mac:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --mac",
|
"build:mac:dev": "npm run prepare:dev && 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",
|
||||||
"build:linux:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --linux"
|
"build:linux:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
"@heroui/react": "^2.8.5",
|
||||||
"@mihomo-party/sysproxy": "^2.0.8",
|
"@mihomo-party/sysproxy": "^2.0.8",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"chokidar": "^5.0.0",
|
"chart.js": "^4.5.1",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
"croner": "^9.1.0",
|
"croner": "^9.1.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"express": "^5.2.1",
|
"dayjs": "^1.11.19",
|
||||||
"i18next": "^25.7.3",
|
"express": "^5.1.0",
|
||||||
"iconv-lite": "^0.7.1",
|
"i18next": "^25.6.2",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"react-chartjs-2": "^5.3.1",
|
||||||
|
"react-i18next": "^15.7.4",
|
||||||
"webdav": "^5.8.0",
|
"webdav": "^5.8.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@ -50,55 +57,49 @@
|
|||||||
"@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": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@heroui/react": "^2.8.7",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/express": "^5.0.5",
|
||||||
"@types/express": "^5.0.6",
|
"@types/node": "^24.10.1",
|
||||||
"@types/node": "^25.0.3",
|
|
||||||
"@types/pubsub-js": "^1.8.6",
|
"@types/pubsub-js": "^1.8.6",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"chart.js": "^4.5.1",
|
|
||||||
"cron-validator": "^1.4.0",
|
"cron-validator": "^1.4.0",
|
||||||
"dayjs": "^1.11.19",
|
"driver.js": "^1.3.6",
|
||||||
"driver.js": "^1.4.0",
|
"electron": "^37.10.0",
|
||||||
"electron": "^39.2.7",
|
|
||||||
"electron-builder": "26.0.12",
|
"electron-builder": "26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^4.0.1",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.32.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.4",
|
||||||
"framer-motion": "12.23.26",
|
"framer-motion": "12.23.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"meta-json-schema": "^1.19.17",
|
"meta-json-schema": "^1.19.16",
|
||||||
"monaco-yaml": "^5.4.0",
|
"monaco-yaml": "^5.4.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.6.2",
|
||||||
"pubsub-js": "^1.9.5",
|
"pubsub-js": "^1.9.5",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.0",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-dom": "^19.2.3",
|
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-i18next": "^16.5.0",
|
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-monaco-editor": "^0.59.0",
|
"react-monaco-editor": "^0.59.0",
|
||||||
"react-router-dom": "^7.11.0",
|
"react-router-dom": "^7.9.6",
|
||||||
"react-virtuoso": "^4.18.0",
|
"react-virtuoso": "^4.14.1",
|
||||||
"swr": "^2.3.8",
|
"swr": "^2.3.6",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.17",
|
||||||
"tar": "^7.5.2",
|
"tar": "^7.5.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.20.6",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.2.2",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0"
|
"vite-plugin-monaco-editor": "^1.1.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.22.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,7 +5,6 @@ import { deepMerge } from '../utils/merge'
|
|||||||
import { defaultConfig } from '../utils/template'
|
import { defaultConfig } from '../utils/template'
|
||||||
|
|
||||||
let appConfig: IAppConfig // config.yaml
|
let appConfig: IAppConfig // config.yaml
|
||||||
let appConfigWriteQueue: Promise<void> = Promise.resolve()
|
|
||||||
|
|
||||||
export async function getAppConfig(force = false): Promise<IAppConfig> {
|
export async function getAppConfig(force = false): Promise<IAppConfig> {
|
||||||
if (force || !appConfig) {
|
if (force || !appConfig) {
|
||||||
@ -23,12 +22,9 @@ export async function getAppConfig(force = false): Promise<IAppConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
|
export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
|
||||||
appConfigWriteQueue = appConfigWriteQueue.then(async () => {
|
if (patch.nameserverPolicy) {
|
||||||
if (patch.nameserverPolicy) {
|
appConfig.nameserverPolicy = patch.nameserverPolicy
|
||||||
appConfig.nameserverPolicy = patch.nameserverPolicy
|
}
|
||||||
}
|
appConfig = deepMerge(appConfig, patch)
|
||||||
appConfig = deepMerge(appConfig, patch)
|
await writeFile(appConfigPath(), stringify(appConfig))
|
||||||
await writeFile(appConfigPath(), stringify(appConfig))
|
|
||||||
})
|
|
||||||
await appConfigWriteQueue
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import * as chromeRequest from '../utils/chromeRequest'
|
|||||||
import { parse, stringify } from '../utils/yaml'
|
import { parse, stringify } from '../utils/yaml'
|
||||||
|
|
||||||
let overrideConfig: IOverrideConfig // override.yaml
|
let overrideConfig: IOverrideConfig // override.yaml
|
||||||
let overrideConfigWriteQueue: Promise<void> = Promise.resolve()
|
|
||||||
|
|
||||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||||
if (force || !overrideConfig) {
|
if (force || !overrideConfig) {
|
||||||
@ -18,11 +17,8 @@ export async function getOverrideConfig(force = false): Promise<IOverrideConfig>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
||||||
overrideConfigWriteQueue = overrideConfigWriteQueue.then(async () => {
|
overrideConfig = config
|
||||||
overrideConfig = config
|
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
|
||||||
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
|
|
||||||
})
|
|
||||||
await overrideConfigWriteQueue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOverrideItem(id: string | undefined): Promise<IOverrideItem | undefined> {
|
export async function getOverrideItem(id: string | undefined): Promise<IOverrideItem | undefined> {
|
||||||
|
|||||||
@ -15,8 +15,8 @@ import { mihomoUpgradeConfig } from '../core/mihomoApi'
|
|||||||
|
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
|
|
||||||
let profileConfig: IProfileConfig
|
let profileConfig: IProfileConfig // profile.yaml
|
||||||
let profileConfigWriteQueue: Promise<void> = Promise.resolve()
|
// 最终选中订阅ID
|
||||||
let targetProfileId: string | null = null
|
let targetProfileId: string | null = null
|
||||||
|
|
||||||
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
||||||
@ -25,31 +25,12 @@ export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
|||||||
profileConfig = parse(data) || { items: [] }
|
profileConfig = parse(data) || { items: [] }
|
||||||
}
|
}
|
||||||
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
|
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
|
||||||
return structuredClone(profileConfig)
|
return profileConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
|
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
|
||||||
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
|
profileConfig = config
|
||||||
profileConfig = config
|
await writeFile(profileConfigPath(), stringify(config), 'utf-8')
|
||||||
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> {
|
export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
|
||||||
@ -60,7 +41,8 @@ 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 { current } = await getProfileConfig()
|
const config = await getProfileConfig()
|
||||||
|
const current = config.current
|
||||||
|
|
||||||
if (current === id && targetProfileId !== id) {
|
if (current === id && targetProfileId !== id) {
|
||||||
return
|
return
|
||||||
@ -68,12 +50,13 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
|
|
||||||
targetProfileId = id
|
targetProfileId = id
|
||||||
|
|
||||||
try {
|
config.current = id
|
||||||
await updateProfileConfig((config) => {
|
const configSavePromise = setProfileConfig(config)
|
||||||
config.current = id
|
|
||||||
return config
|
|
||||||
})
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await configSavePromise
|
||||||
|
|
||||||
|
// 检查订阅切换是否中断
|
||||||
if (targetProfileId !== id) {
|
if (targetProfileId !== id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -83,10 +66,8 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (targetProfileId === id) {
|
if (targetProfileId === id) {
|
||||||
await updateProfileConfig((config) => {
|
config.current = current
|
||||||
config.current = current
|
await setProfileConfig(config)
|
||||||
return config
|
|
||||||
})
|
|
||||||
targetProfileId = null
|
targetProfileId = null
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@ -94,51 +75,47 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
||||||
await updateProfileConfig((config) => {
|
const config = await getProfileConfig()
|
||||||
const index = config.items.findIndex((i) => i.id === item.id)
|
const index = config.items.findIndex((i) => i.id === item.id)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
throw new Error('Profile not found')
|
throw new Error('Profile not found')
|
||||||
}
|
}
|
||||||
config.items[index] = item
|
config.items[index] = item
|
||||||
return config
|
await setProfileConfig(config)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
|
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
|
||||||
const newItem = await createProfile(item)
|
const newItem = await createProfile(item)
|
||||||
let shouldChangeCurrent = false
|
const config = await getProfileConfig()
|
||||||
await updateProfileConfig((config) => {
|
if (await getProfileItem(newItem.id)) {
|
||||||
const existingIndex = config.items.findIndex((i) => i.id === newItem.id)
|
await updateProfileItem(newItem)
|
||||||
if (existingIndex !== -1) {
|
} else {
|
||||||
config.items[existingIndex] = newItem
|
config.items.push(newItem)
|
||||||
} else {
|
}
|
||||||
config.items.push(newItem)
|
await setProfileConfig(config)
|
||||||
}
|
|
||||||
if (!config.current) {
|
|
||||||
shouldChangeCurrent = true
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
})
|
|
||||||
|
|
||||||
if (shouldChangeCurrent) {
|
if (!config.current) {
|
||||||
await changeCurrentProfile(newItem.id)
|
await changeCurrentProfile(newItem.id)
|
||||||
}
|
}
|
||||||
await addProfileUpdater(newItem)
|
await addProfileUpdater(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeProfileItem(id: string): Promise<void> {
|
export async function removeProfileItem(id: string): Promise<void> {
|
||||||
|
// 先清理自动更新定时器,防止已删除的订阅重新出现
|
||||||
await removeProfileUpdater(id)
|
await removeProfileUpdater(id)
|
||||||
|
|
||||||
|
const config = await getProfileConfig()
|
||||||
|
config.items = config.items?.filter((item) => item.id !== id)
|
||||||
let shouldRestart = false
|
let shouldRestart = false
|
||||||
await updateProfileConfig((config) => {
|
if (config.current === id) {
|
||||||
config.items = config.items?.filter((item) => item.id !== id)
|
shouldRestart = true
|
||||||
if (config.current === id) {
|
if (config.items.length > 0) {
|
||||||
shouldRestart = true
|
config.current = config.items[0].id
|
||||||
config.current = config.items.length > 0 ? config.items[0].id : undefined
|
} else {
|
||||||
|
config.current = undefined
|
||||||
}
|
}
|
||||||
return config
|
}
|
||||||
})
|
await setProfileConfig(config)
|
||||||
|
|
||||||
if (existsSync(profilePath(id))) {
|
if (existsSync(profilePath(id))) {
|
||||||
await rm(profilePath(id))
|
await rm(profilePath(id))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ function processRulesWithOffset(ruleStrings: string[], currentRules: string[], i
|
|||||||
return { normalRules, insertRules: rules }
|
return { normalRules, insertRules: rules }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateProfile(): Promise<string | undefined> {
|
export async function generateProfile(): Promise<void> {
|
||||||
// 读取最新的配置
|
// 读取最新的配置
|
||||||
const { current } = await getProfileConfig(true)
|
const { current } = await getProfileConfig(true)
|
||||||
const {
|
const {
|
||||||
@ -150,7 +150,6 @@ export async function generateProfile(): Promise<string | undefined> {
|
|||||||
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
|
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
|
||||||
runtimeConfigStr
|
runtimeConfigStr
|
||||||
)
|
)
|
||||||
return current
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareProfileWorkDir(current: string | undefined): Promise<void> {
|
async function prepareProfileWorkDir(current: string | undefined): Promise<void> {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { generateProfile } from './factory'
|
|||||||
import {
|
import {
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
|
getProfileConfig,
|
||||||
patchAppConfig,
|
patchAppConfig,
|
||||||
patchControledMihomoConfig,
|
patchControledMihomoConfig,
|
||||||
manageSmartOverride
|
manageSmartOverride
|
||||||
@ -129,15 +130,15 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
await rm(path.join(dataDir(), 'core.pid'))
|
await rm(path.join(dataDir(), 'core.pid'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const { current } = await getProfileConfig(true)
|
||||||
const { tun } = await getControledMihomoConfig()
|
const { tun } = await getControledMihomoConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
|
|
||||||
// 管理 Smart 内核覆写配置
|
// 管理 Smart 内核覆写配置
|
||||||
await manageSmartOverride()
|
await manageSmartOverride()
|
||||||
|
|
||||||
// generateProfile 返回实际使用的 current,确保内核工作目录与配置文件一致
|
await generateProfile()
|
||||||
const current = await generateProfile()
|
await checkProfile()
|
||||||
await checkProfile(current)
|
|
||||||
await stopCore()
|
await stopCore()
|
||||||
|
|
||||||
await cleanupSocketFile()
|
await cleanupSocketFile()
|
||||||
@ -452,8 +453,9 @@ export async function quitWithoutCore(): Promise<void> {
|
|||||||
app.exit()
|
app.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkProfile(current: string | undefined): Promise<void> {
|
async function checkProfile(): Promise<void> {
|
||||||
const { core = 'mihomo', diffWorkDir = false } = await getAppConfig()
|
const { core = 'mihomo', diffWorkDir = false } = await getAppConfig()
|
||||||
|
const { current } = await getProfileConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
@ -560,7 +562,7 @@ async function waitForCoreReady(): Promise<void> {
|
|||||||
await axios.get('/')
|
await axios.get('/')
|
||||||
await managerLogger.info(`Core ready after ${i + 1} attempts (${(i + 1) * retryInterval}ms)`)
|
await managerLogger.info(`Core ready after ${i + 1} attempts (${(i + 1) * retryInterval}ms)`)
|
||||||
return
|
return
|
||||||
} catch {
|
} catch (error) {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
await managerLogger.info('Waiting for core to be ready...')
|
await managerLogger.info('Waiting for core to be ready...')
|
||||||
}
|
}
|
||||||
@ -789,7 +791,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
|
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
await managerLogger.info(
|
await managerLogger.info(
|
||||||
`Cannot get info for process ${pid}, might be high privilege`
|
`Cannot get info for process ${pid}, might be high privilege`
|
||||||
)
|
)
|
||||||
@ -833,7 +835,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,6 @@ let logsRetry = 10
|
|||||||
let mihomoConnectionsWs: WebSocket | null = null
|
let mihomoConnectionsWs: WebSocket | null = null
|
||||||
let connectionsRetry = 10
|
let connectionsRetry = 10
|
||||||
|
|
||||||
const MAX_RETRY = 10
|
|
||||||
|
|
||||||
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
|
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
|
||||||
const dynamicIpcPath = getMihomoIpcPath()
|
const dynamicIpcPath = getMihomoIpcPath()
|
||||||
|
|
||||||
@ -235,7 +233,6 @@ export const mihomoSmartFlushCache = async (configName?: string): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoTraffic = async (): Promise<void> => {
|
export const startMihomoTraffic = async (): Promise<void> => {
|
||||||
trafficRetry = MAX_RETRY
|
|
||||||
await mihomoTraffic()
|
await mihomoTraffic()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +258,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
|||||||
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
|
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
const json = JSON.parse(data) as IMihomoTrafficInfo
|
const json = JSON.parse(data) as IMihomoTrafficInfo
|
||||||
trafficRetry = MAX_RETRY
|
trafficRetry = 10
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoTraffic', json)
|
mainWindow?.webContents.send('mihomoTraffic', json)
|
||||||
if (process.platform !== 'linux') {
|
if (process.platform !== 'linux') {
|
||||||
@ -295,7 +292,6 @@ const mihomoTraffic = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoMemory = async (): Promise<void> => {
|
export const startMihomoMemory = async (): Promise<void> => {
|
||||||
memoryRetry = MAX_RETRY
|
|
||||||
await mihomoMemory()
|
await mihomoMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +314,7 @@ const mihomoMemory = async (): Promise<void> => {
|
|||||||
|
|
||||||
mihomoMemoryWs.onmessage = (e): void => {
|
mihomoMemoryWs.onmessage = (e): void => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
memoryRetry = MAX_RETRY
|
memoryRetry = 10
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
|
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
|
||||||
} catch {
|
} catch {
|
||||||
@ -342,7 +338,6 @@ const mihomoMemory = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoLogs = async (): Promise<void> => {
|
export const startMihomoLogs = async (): Promise<void> => {
|
||||||
logsRetry = MAX_RETRY
|
|
||||||
await mihomoLogs()
|
await mihomoLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,7 +362,7 @@ const mihomoLogs = async (): Promise<void> => {
|
|||||||
|
|
||||||
mihomoLogsWs.onmessage = (e): void => {
|
mihomoLogsWs.onmessage = (e): void => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
logsRetry = MAX_RETRY
|
logsRetry = 10
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
|
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
|
||||||
} catch {
|
} catch {
|
||||||
@ -391,7 +386,6 @@ const mihomoLogs = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoConnections = async (): Promise<void> => {
|
export const startMihomoConnections = async (): Promise<void> => {
|
||||||
connectionsRetry = MAX_RETRY
|
|
||||||
await mihomoConnections()
|
await mihomoConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +408,7 @@ const mihomoConnections = async (): Promise<void> => {
|
|||||||
|
|
||||||
mihomoConnectionsWs.onmessage = (e): void => {
|
mihomoConnectionsWs.onmessage = (e): void => {
|
||||||
const data = e.data as string
|
const data = e.data as string
|
||||||
connectionsRetry = MAX_RETRY
|
connectionsRetry = 10
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -1,40 +1,33 @@
|
|||||||
import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config'
|
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
|
||||||
import { Cron } from 'croner'
|
import { Cron } from 'croner'
|
||||||
import { logger } from '../utils/logger'
|
|
||||||
|
|
||||||
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
|
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> {
|
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.autoUpdate && item.interval) {
|
if (item.type === 'remote' && item.autoUpdate && item.interval) {
|
||||||
const itemId = item.id
|
|
||||||
if (typeof item.interval === 'number') {
|
if (typeof item.interval === 'number') {
|
||||||
intervalPool[itemId] = setInterval(
|
// 数字间隔使用 setInterval
|
||||||
|
intervalPool[item.id] = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(itemId)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
item.interval * 60 * 1000
|
item.interval * 60 * 1000
|
||||||
)
|
)
|
||||||
} else if (typeof item.interval === 'string') {
|
} else if (typeof item.interval === 'string') {
|
||||||
intervalPool[itemId] = new Cron(item.interval, async () => {
|
// 字符串间隔使用 Cron
|
||||||
|
intervalPool[item.id] = new Cron(item.interval, async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(itemId)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -42,20 +35,19 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to init profile ${item.name}:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) {
|
if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) {
|
||||||
const currentId = currentItem.id
|
|
||||||
if (typeof currentItem.interval === 'number') {
|
if (typeof currentItem.interval === 'number') {
|
||||||
intervalPool[currentId] = setInterval(
|
intervalPool[currentItem.id] = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(currentId)
|
await addProfileItem(currentItem)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentItem.interval * 60 * 1000
|
currentItem.interval * 60 * 1000
|
||||||
@ -64,19 +56,19 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
setTimeout(
|
setTimeout(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(currentId)
|
await addProfileItem(currentItem)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentItem.interval * 60 * 1000 + 10000
|
currentItem.interval * 60 * 1000 + 10000 // +10s
|
||||||
)
|
)
|
||||||
} else if (typeof currentItem.interval === 'string') {
|
} else if (typeof currentItem.interval === 'string') {
|
||||||
intervalPool[currentId] = new Cron(currentItem.interval, async () => {
|
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(currentId)
|
await addProfileItem(currentItem)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -84,7 +76,7 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await addProfileItem(currentItem)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to init current profile:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,24 +91,23 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemId = item.id
|
|
||||||
if (typeof item.interval === 'number') {
|
if (typeof item.interval === 'number') {
|
||||||
intervalPool[itemId] = setInterval(
|
intervalPool[item.id] = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(itemId)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
item.interval * 60 * 1000
|
item.interval * 60 * 1000
|
||||||
)
|
)
|
||||||
} else if (typeof item.interval === 'string') {
|
} else if (typeof item.interval === 'string') {
|
||||||
intervalPool[itemId] = new Cron(item.interval, async () => {
|
intervalPool[item.id] = new Cron(item.interval, async () => {
|
||||||
try {
|
try {
|
||||||
await updateProfile(itemId)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
/* ignore */
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
groupsMenu = groupItems
|
groupsMenu = groupItems
|
||||||
groupsMenu.unshift({ type: 'separator' })
|
groupsMenu.unshift({ type: 'separator' })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
// 避免出错时无法创建托盘菜单
|
// 避免出错时无法创建托盘菜单
|
||||||
}
|
}
|
||||||
@ -206,7 +206,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
floatingWindow?.webContents.send('appConfigUpdated')
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
@ -390,8 +390,6 @@ export async function createTray(): Promise<void> {
|
|||||||
if (!useDockIcon) {
|
if (!useDockIcon) {
|
||||||
hideDockIcon()
|
hideDockIcon()
|
||||||
}
|
}
|
||||||
// 移除旧监听器防止累积
|
|
||||||
ipcMain.removeAllListeners('trayIconUpdate')
|
|
||||||
ipcMain.on('trayIconUpdate', async (_, png: string) => {
|
ipcMain.on('trayIconUpdate', async (_, png: string) => {
|
||||||
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
|
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
|
||||||
image.setTemplateImage(true)
|
image.setTemplateImage(true)
|
||||||
@ -437,8 +435,6 @@ export async function createTray(): Promise<void> {
|
|||||||
triggerMainWindow()
|
triggerMainWindow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 移除旧监听器防止累积
|
|
||||||
ipcMain.removeAllListeners('updateTrayMenu')
|
|
||||||
ipcMain.on('updateTrayMenu', async () => {
|
ipcMain.on('updateTrayMenu', async () => {
|
||||||
await updateTrayMenu()
|
await updateTrayMenu()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export async function checkAutoRun(): Promise<boolean> {
|
|||||||
`chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"`
|
`chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"`
|
||||||
)
|
)
|
||||||
return stdout.includes(appName)
|
return stdout.includes(appName)
|
||||||
} catch {
|
} catch (e) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,7 +96,7 @@ export async function enableAutoRun(): Promise<void> {
|
|||||||
await execPromise(
|
await execPromise(
|
||||||
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
|
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
|
||||||
)
|
)
|
||||||
} catch {
|
} catch (e) {
|
||||||
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,7 +144,7 @@ export async function disableAutoRun(): Promise<void> {
|
|||||||
await execPromise(
|
await execPromise(
|
||||||
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"`
|
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"`
|
||||||
)
|
)
|
||||||
} catch {
|
} catch (e) {
|
||||||
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,33 +160,18 @@ function isSocketFileExists(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if helper process is running (no admin privileges needed)
|
// Helper function to send signal to recreate socket
|
||||||
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> {
|
async function requestSocketRecreation(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Send SIGUSR1 signal to helper process to recreate socket
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
|
// Use osascript with administrator privileges (same pattern as grantTunPermissions)
|
||||||
const shell = `pkill -USR1 -f party.mihomo.helper`
|
const shell = `pkill -USR1 -f party.mihomo.helper`
|
||||||
const command = `do shell script "${shell}" with administrator privileges`
|
const command = `do shell script "${shell}" with administrator privileges`
|
||||||
await execPromise(`osascript -e '${command}'`)
|
await execPromise(`osascript -e '${command}'`)
|
||||||
|
|
||||||
|
// 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)
|
await proxyLogger.error('Failed to send signal to helper', error)
|
||||||
@ -195,7 +180,7 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper function for helper requests with auto-retry on socket issues
|
// Wrapper function for helper requests with auto-retry on socket issues
|
||||||
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2): Promise<unknown> {
|
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
|
||||||
let lastError: Error | null = null
|
let lastError: Error | null = null
|
||||||
|
|
||||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
@ -203,34 +188,21 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2):
|
|||||||
return await requestFn()
|
return await requestFn()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error as 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 (
|
if (
|
||||||
attempt < maxRetries &&
|
attempt < maxRetries &&
|
||||||
(errCode === 'ECONNREFUSED' ||
|
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
||||||
errCode === 'ENOENT' ||
|
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
||||||
errMsg.includes('connect ECONNREFUSED') ||
|
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||||
errMsg.includes('ENOENT'))
|
(error as Error).message?.includes('ENOENT'))
|
||||||
) {
|
) {
|
||||||
await proxyLogger.info(
|
await proxyLogger.info(
|
||||||
`Helper request failed (attempt ${attempt + 1}/${maxRetries + 1}), checking helper status...`
|
`Helper request failed (attempt ${attempt + 1}), checking socket file...`
|
||||||
)
|
)
|
||||||
|
|
||||||
const helperRunning = await isHelperRunning()
|
if (!isSocketFileExists()) {
|
||||||
const socketExists = isSocketFileExists()
|
await proxyLogger.info('Socket file missing, requesting recreation...')
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
await requestSocketRecreation()
|
await requestSocketRecreation()
|
||||||
await proxyLogger.info('Socket recreation requested, retrying...')
|
await proxyLogger.info('Socket recreation requested, retrying...')
|
||||||
@ -241,6 +213,7 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not a connection error or we've exhausted retries, throw the error
|
||||||
if (attempt === maxRetries) {
|
if (attempt === maxRetries) {
|
||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/preload/index.d.ts
vendored
18
src/preload/index.d.ts
vendored
@ -1,22 +1,6 @@
|
|||||||
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
import { webUtils } from 'electron'
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronAPI
|
electron: ElectronAPI
|
||||||
|
|||||||
@ -1,231 +1,13 @@
|
|||||||
import { contextBridge, ipcRenderer, webUtils } from 'electron'
|
import { contextBridge, webUtils } from 'electron'
|
||||||
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
// 允许的 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 = {
|
const api = {
|
||||||
webUtils: webUtils
|
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) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||||
|
|||||||
@ -86,7 +86,7 @@ const App: React.FC = () => {
|
|||||||
options.color = window.getComputedStyle(document.documentElement).backgroundColor
|
options.color = window.getComputedStyle(document.documentElement).backgroundColor
|
||||||
options.symbolColor = window.getComputedStyle(document.documentElement).color
|
options.symbolColor = window.getComputedStyle(document.documentElement).color
|
||||||
setTitleBarOverlay(options)
|
setTitleBarOverlay(options)
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState, useCallback } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import MihomoIcon from './components/base/mihomo-icon'
|
import MihomoIcon from './components/base/mihomo-icon'
|
||||||
import { calcTraffic } from './utils/calc'
|
import { calcTraffic } from './utils/calc'
|
||||||
import { showContextMenu, triggerMainWindow } from './utils/ipc'
|
import { showContextMenu, triggerMainWindow } from './utils/ipc'
|
||||||
@ -48,18 +48,15 @@ const FloatingApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [spinSpeed, spinFloatingIcon])
|
}, [spinSpeed, spinFloatingIcon])
|
||||||
|
|
||||||
const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
|
|
||||||
const info = args[0] as IMihomoTrafficInfo
|
|
||||||
setUpload(info.up)
|
|
||||||
setDownload(info.down)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
|
||||||
|
setUpload(info.up)
|
||||||
|
setDownload(info.down)
|
||||||
|
})
|
||||||
return (): void => {
|
return (): void => {
|
||||||
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
|
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
||||||
}
|
}
|
||||||
}, [handleTraffic])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-drag h-screen w-screen overflow-hidden">
|
<div className="app-drag h-screen w-screen overflow-hidden">
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
|||||||
// @ts-ignore windowControlsOverlay
|
// @ts-ignore windowControlsOverlay
|
||||||
const windowControlsOverlay = window.navigator.windowControlsOverlay
|
const windowControlsOverlay = window.navigator.windowControlsOverlay
|
||||||
setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width)
|
setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width)
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,7 +149,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
// 非纯数字
|
// 非纯数字
|
||||||
try {
|
try {
|
||||||
setValues({ ...values, interval: v })
|
setValues({ ...values, interval: v })
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const Viewer: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
setCurrData(fileContent)
|
setCurrData(fileContent)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ 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, useRef, useCallback } from 'react'
|
import React, { useEffect, useState, useMemo } 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'
|
||||||
@ -25,6 +25,11 @@ import { useTranslation } from 'react-i18next'
|
|||||||
// 注册 Chart.js 组件
|
// 注册 Chart.js 组件
|
||||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler)
|
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 {
|
interface Props {
|
||||||
iconOnly?: boolean
|
iconOnly?: boolean
|
||||||
}
|
}
|
||||||
@ -56,12 +61,6 @@ const ConnCard: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
const [series, setSeries] = useState(Array(10).fill(0))
|
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 配置
|
// Chart.js 配置
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -126,45 +125,35 @@ const ConnCard: 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
|
||||||
|
useEffect(() => {
|
||||||
// 使用 useCallback 创建稳定的 handler 引用
|
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
|
||||||
const handleTraffic = useCallback(
|
|
||||||
async (_e: unknown, ...args: unknown[]) => {
|
|
||||||
const info = args[0] as IMihomoTrafficInfo
|
|
||||||
setUpload(info.up)
|
setUpload(info.up)
|
||||||
setDownload(info.down)
|
setDownload(info.down)
|
||||||
setSeries((prev) => {
|
const data = series
|
||||||
const data = [...prev]
|
data.shift()
|
||||||
data.shift()
|
data.push(info.up + info.down)
|
||||||
data.push(info.up + info.down)
|
setSeries([...data])
|
||||||
return data
|
|
||||||
})
|
|
||||||
if (platform === 'darwin' && showTraffic) {
|
if (platform === 'darwin' && showTraffic) {
|
||||||
if (drawingRef.current) return
|
if (drawing) return
|
||||||
drawingRef.current = true
|
drawing = true
|
||||||
try {
|
try {
|
||||||
await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef)
|
await drawSvg(info.up, info.down)
|
||||||
hasShowTrafficRef.current = true
|
hasShowTraffic = true
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
drawingRef.current = false
|
drawing = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!hasShowTrafficRef.current) return
|
if (!hasShowTraffic) return
|
||||||
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
||||||
hasShowTrafficRef.current = false
|
hasShowTraffic = false
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
[showTraffic]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
|
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
||||||
}
|
}
|
||||||
}, [handleTraffic])
|
}, [showTraffic])
|
||||||
|
|
||||||
if (iconOnly) {
|
if (iconOnly) {
|
||||||
return (
|
return (
|
||||||
@ -284,15 +273,10 @@ const ConnCard: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
export default ConnCard
|
export default ConnCard
|
||||||
|
|
||||||
const drawSvg = async (
|
const drawSvg = async (upload: number, download: number): Promise<void> => {
|
||||||
upload: number,
|
if (upload === currentUpload && download === currentDownload) return
|
||||||
download: number,
|
currentUpload = upload
|
||||||
currentUploadRef: React.RefObject<number | undefined>,
|
currentDownload = download
|
||||||
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 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)
|
const image = await loadImage(svg)
|
||||||
window.electron.ipcRenderer.send('trayIconUpdate', image)
|
window.electron.ipcRenderer.send('trayIconUpdate', image)
|
||||||
|
|||||||
@ -43,8 +43,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
|
|||||||
const token = PubSub.subscribe('mihomo-core-changed', () => {
|
const token = PubSub.subscribe('mihomo-core-changed', () => {
|
||||||
mutate()
|
mutate()
|
||||||
})
|
})
|
||||||
window.electron.ipcRenderer.on('mihomoMemory', (_e, ...args) => {
|
window.electron.ipcRenderer.on('mihomoMemory', (_e, info: IMihomoMemoryInfo) => {
|
||||||
const info = args[0] as IMihomoMemoryInfo
|
|
||||||
setMem(info.inuse)
|
setMem(info.inuse)
|
||||||
})
|
})
|
||||||
return (): void => {
|
return (): void => {
|
||||||
|
|||||||
@ -170,8 +170,7 @@ const Connections: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (_e: unknown, ...args: unknown[]): void => {
|
const handler = (_e: unknown, info: IMihomoConnectionsInfo): void => {
|
||||||
const info = args[0] as IMihomoConnectionsInfo
|
|
||||||
setConnectionsInfo(info)
|
setConnectionsInfo(info)
|
||||||
|
|
||||||
if (!info.connections) return
|
if (!info.connections) return
|
||||||
|
|||||||
@ -26,8 +26,7 @@ const cachedLogs: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('mihomoLogs', (_e, ...args) => {
|
window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => {
|
||||||
const log = args[0] as IMihomoLogInfo
|
|
||||||
log.time = new Date().toLocaleString()
|
log.time = new Date().toLocaleString()
|
||||||
cachedLogs.log.push(log)
|
cachedLogs.log.push(log)
|
||||||
if (cachedLogs.log.length >= 500) {
|
if (cachedLogs.log.length >= 500) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { TitleBarOverlayOptions } from 'electron'
|
import { TitleBarOverlayOptions } from 'electron'
|
||||||
|
|
||||||
function checkIpcError<T>(response: unknown): T {
|
function checkIpcError<T>(response: T | { invokeError: unknown }): T {
|
||||||
if (response && typeof response === 'object' && 'invokeError' in response) {
|
if (response && typeof response === 'object' && 'invokeError' in response) {
|
||||||
throw (response as { invokeError: unknown }).invokeError
|
throw response.invokeError
|
||||||
}
|
}
|
||||||
return response as T
|
return response as T
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user