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
|
# 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,9 +102,14 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup pnpm
|
- 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
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -136,7 +141,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@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Windows ${{ matrix.arch }}
|
name: Windows ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -178,9 +183,14 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup pnpm
|
- 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
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -214,7 +224,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@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Win7 ${{ matrix.arch }}
|
name: Win7 ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -256,9 +266,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup pnpm
|
- 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
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -285,7 +300,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@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Linux ${{ matrix.arch }}
|
name: Linux ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -327,9 +342,14 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup pnpm
|
- 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
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -357,7 +377,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@v3
|
uses: apple-actions/import-codesign-certs@v6
|
||||||
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 }}
|
||||||
@ -381,7 +401,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@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: MacOS ${{ matrix.arch }}
|
name: MacOS ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -420,9 +440,14 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup pnpm
|
- 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
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
@ -452,7 +477,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@v3
|
uses: apple-actions/import-codesign-certs@v6
|
||||||
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 }}
|
||||||
@ -476,7 +501,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@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Catalina ${{ matrix.arch }}
|
name: Catalina ${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@ -509,9 +534,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup pnpm
|
- 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
|
- name: Install Dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
- name: Update Version for Dev Build
|
- name: Update Version for Dev Build
|
||||||
@ -566,7 +596,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@ -601,7 +631,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@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: update version
|
- name: update version
|
||||||
|
|||||||
3
.npmrc
3
.npmrc
@ -1,3 +1,4 @@
|
|||||||
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,5 +72,8 @@ 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,7 +40,10 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-unused-vars': 0,
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'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": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "pnpm run typecheck:node && pnpm 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,33 +23,26 @@
|
|||||||
"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": "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": "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": "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": {
|
"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",
|
||||||
"chart.js": "^4.5.1",
|
"chokidar": "^5.0.0",
|
||||||
"chokidar": "^4.0.3",
|
|
||||||
"croner": "^9.1.0",
|
"croner": "^9.1.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.19",
|
"express": "^5.2.1",
|
||||||
"express": "^5.1.0",
|
"i18next": "^25.7.3",
|
||||||
"i18next": "^25.6.2",
|
"iconv-lite": "^0.7.1",
|
||||||
"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.1"
|
"yaml": "^2.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@ -57,49 +50,55 @@
|
|||||||
"@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": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@heroui/react": "^2.8.7",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/express": "^5.0.5",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
"@types/pubsub-js": "^1.8.6",
|
"@types/pubsub-js": "^1.8.6",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.7",
|
||||||
"@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": "^4.7.0",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"cron-validator": "^1.4.0",
|
"cron-validator": "^1.4.0",
|
||||||
"driver.js": "^1.3.6",
|
"dayjs": "^1.11.19",
|
||||||
"electron": "^37.10.0",
|
"driver.js": "^1.4.0",
|
||||||
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "26.0.12",
|
"electron-builder": "26.0.12",
|
||||||
"electron-vite": "^4.0.1",
|
"electron-vite": "^5.0.0",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"eslint": "9.32.0",
|
"eslint": "9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.5",
|
||||||
"framer-motion": "12.23.12",
|
"framer-motion": "12.23.26",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"meta-json-schema": "^1.19.16",
|
"meta-json-schema": "^1.19.17",
|
||||||
"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.6.2",
|
"prettier": "^3.7.4",
|
||||||
"pubsub-js": "^1.9.5",
|
"pubsub-js": "^1.9.5",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.0",
|
"react-chartjs-2": "^5.3.1",
|
||||||
|
"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.9.6",
|
"react-router-dom": "^7.11.0",
|
||||||
"react-virtuoso": "^4.14.1",
|
"react-virtuoso": "^4.18.0",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.8",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.18",
|
||||||
"tar": "^7.5.2",
|
"tar": "^7.5.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.21.0",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.2",
|
"vite": "^7.3.0",
|
||||||
"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,6 +5,7 @@ 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) {
|
||||||
@ -22,9 +23,12 @@ 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> {
|
||||||
if (patch.nameserverPolicy) {
|
appConfigWriteQueue = appConfigWriteQueue.then(async () => {
|
||||||
appConfig.nameserverPolicy = patch.nameserverPolicy
|
if (patch.nameserverPolicy) {
|
||||||
}
|
appConfig.nameserverPolicy = patch.nameserverPolicy
|
||||||
appConfig = deepMerge(appConfig, patch)
|
}
|
||||||
await writeFile(appConfigPath(), stringify(appConfig))
|
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'
|
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) {
|
||||||
@ -17,8 +18,11 @@ export async function getOverrideConfig(force = false): Promise<IOverrideConfig>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
||||||
overrideConfig = config
|
overrideConfigWriteQueue = overrideConfigWriteQueue.then(async () => {
|
||||||
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
|
overrideConfig = config
|
||||||
|
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 // profile.yaml
|
let profileConfig: IProfileConfig
|
||||||
// 最终选中订阅ID
|
let profileConfigWriteQueue: Promise<void> = Promise.resolve()
|
||||||
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,12 +25,31 @@ 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 profileConfig
|
return structuredClone(profileConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
|
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
|
||||||
profileConfig = config
|
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
|
||||||
await writeFile(profileConfigPath(), stringify(config), 'utf-8')
|
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> {
|
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> {
|
export async function changeCurrentProfile(id: string): Promise<void> {
|
||||||
const config = await getProfileConfig()
|
const { current } = await getProfileConfig()
|
||||||
const current = config.current
|
|
||||||
|
|
||||||
if (current === id && targetProfileId !== id) {
|
if (current === id && targetProfileId !== id) {
|
||||||
return
|
return
|
||||||
@ -50,13 +68,12 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
|
|
||||||
targetProfileId = id
|
targetProfileId = id
|
||||||
|
|
||||||
config.current = id
|
|
||||||
const configSavePromise = setProfileConfig(config)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await configSavePromise
|
await updateProfileConfig((config) => {
|
||||||
|
config.current = id
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
// 检查订阅切换是否中断
|
|
||||||
if (targetProfileId !== id) {
|
if (targetProfileId !== id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -66,8 +83,10 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (targetProfileId === id) {
|
if (targetProfileId === id) {
|
||||||
config.current = current
|
await updateProfileConfig((config) => {
|
||||||
await setProfileConfig(config)
|
config.current = current
|
||||||
|
return config
|
||||||
|
})
|
||||||
targetProfileId = null
|
targetProfileId = null
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@ -75,47 +94,51 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
||||||
const config = await getProfileConfig()
|
await updateProfileConfig((config) => {
|
||||||
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
|
||||||
await setProfileConfig(config)
|
return 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)
|
||||||
const config = await getProfileConfig()
|
let shouldChangeCurrent = false
|
||||||
if (await getProfileItem(newItem.id)) {
|
await updateProfileConfig((config) => {
|
||||||
await updateProfileItem(newItem)
|
const existingIndex = config.items.findIndex((i) => i.id === newItem.id)
|
||||||
} else {
|
if (existingIndex !== -1) {
|
||||||
config.items.push(newItem)
|
config.items[existingIndex] = newItem
|
||||||
}
|
} else {
|
||||||
await setProfileConfig(config)
|
config.items.push(newItem)
|
||||||
|
}
|
||||||
|
if (!config.current) {
|
||||||
|
shouldChangeCurrent = true
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
if (!config.current) {
|
if (shouldChangeCurrent) {
|
||||||
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
|
||||||
if (config.current === id) {
|
await updateProfileConfig((config) => {
|
||||||
shouldRestart = true
|
config.items = config.items?.filter((item) => item.id !== id)
|
||||||
if (config.items.length > 0) {
|
if (config.current === id) {
|
||||||
config.current = config.items[0].id
|
shouldRestart = true
|
||||||
} else {
|
config.current = config.items.length > 0 ? config.items[0].id : undefined
|
||||||
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<void> {
|
export async function generateProfile(): Promise<string | undefined> {
|
||||||
// 读取最新的配置
|
// 读取最新的配置
|
||||||
const { current } = await getProfileConfig(true)
|
const { current } = await getProfileConfig(true)
|
||||||
const {
|
const {
|
||||||
@ -150,6 +150,7 @@ export async function generateProfile(): Promise<void> {
|
|||||||
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,7 +13,6 @@ import { generateProfile } from './factory'
|
|||||||
import {
|
import {
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
getProfileConfig,
|
|
||||||
patchAppConfig,
|
patchAppConfig,
|
||||||
patchControledMihomoConfig,
|
patchControledMihomoConfig,
|
||||||
manageSmartOverride
|
manageSmartOverride
|
||||||
@ -130,15 +129,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()
|
||||||
|
|
||||||
await generateProfile()
|
// generateProfile 返回实际使用的 current,确保内核工作目录与配置文件一致
|
||||||
await checkProfile()
|
const current = await generateProfile()
|
||||||
|
await checkProfile(current)
|
||||||
await stopCore()
|
await stopCore()
|
||||||
|
|
||||||
await cleanupSocketFile()
|
await cleanupSocketFile()
|
||||||
@ -453,9 +452,8 @@ export async function quitWithoutCore(): Promise<void> {
|
|||||||
app.exit()
|
app.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkProfile(): Promise<void> {
|
async function checkProfile(current: string | undefined): 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)
|
||||||
|
|
||||||
@ -562,7 +560,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 (error) {
|
} catch {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
await managerLogger.info('Waiting for core to be ready...')
|
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) {
|
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
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`
|
||||||
)
|
)
|
||||||
@ -835,7 +833,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ 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()
|
||||||
|
|
||||||
@ -233,6 +235,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +261,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 = 10
|
trafficRetry = MAX_RETRY
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoTraffic', json)
|
mainWindow?.webContents.send('mihomoTraffic', json)
|
||||||
if (process.platform !== 'linux') {
|
if (process.platform !== 'linux') {
|
||||||
@ -292,6 +295,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoMemory = async (): Promise<void> => {
|
export const startMihomoMemory = async (): Promise<void> => {
|
||||||
|
memoryRetry = MAX_RETRY
|
||||||
await mihomoMemory()
|
await mihomoMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +318,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 = 10
|
memoryRetry = MAX_RETRY
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
|
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
|
||||||
} catch {
|
} catch {
|
||||||
@ -338,6 +342,7 @@ const mihomoMemory = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoLogs = async (): Promise<void> => {
|
export const startMihomoLogs = async (): Promise<void> => {
|
||||||
|
logsRetry = MAX_RETRY
|
||||||
await mihomoLogs()
|
await mihomoLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,7 +367,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 = 10
|
logsRetry = MAX_RETRY
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
|
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
|
||||||
} catch {
|
} catch {
|
||||||
@ -386,6 +391,7 @@ const mihomoLogs = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const startMihomoConnections = async (): Promise<void> => {
|
export const startMihomoConnections = async (): Promise<void> => {
|
||||||
|
connectionsRetry = MAX_RETRY
|
||||||
await mihomoConnections()
|
await mihomoConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +414,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 = 10
|
connectionsRetry = MAX_RETRY
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -1,33 +1,40 @@
|
|||||||
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
|
import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } 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') {
|
||||||
// 数字间隔使用 setInterval
|
intervalPool[itemId] = setInterval(
|
||||||
intervalPool[item.id] = setInterval(
|
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await updateProfile(itemId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
item.interval * 60 * 1000
|
item.interval * 60 * 1000
|
||||||
)
|
)
|
||||||
} else if (typeof item.interval === 'string') {
|
} else if (typeof item.interval === 'string') {
|
||||||
// 字符串间隔使用 Cron
|
intervalPool[itemId] = new Cron(item.interval, async () => {
|
||||||
intervalPool[item.id] = new Cron(item.interval, async () => {
|
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await updateProfile(itemId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -35,19 +42,20 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
await logger.warn(`[ProfileUpdater] Failed to init profile ${item.name}:`, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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[currentItem.id] = setInterval(
|
intervalPool[currentId] = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await updateProfile(currentId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentItem.interval * 60 * 1000
|
currentItem.interval * 60 * 1000
|
||||||
@ -56,19 +64,19 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
setTimeout(
|
setTimeout(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await updateProfile(currentId)
|
||||||
} catch (e) {
|
} 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') {
|
} else if (typeof currentItem.interval === 'string') {
|
||||||
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
|
intervalPool[currentId] = new Cron(currentItem.interval, async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await updateProfile(currentId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -76,7 +84,7 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await addProfileItem(currentItem)
|
||||||
} catch (e) {
|
} 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') {
|
if (typeof item.interval === 'number') {
|
||||||
intervalPool[item.id] = setInterval(
|
intervalPool[itemId] = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await updateProfile(itemId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* ignore */
|
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
item.interval * 60 * 1000
|
item.interval * 60 * 1000
|
||||||
)
|
)
|
||||||
} else if (typeof item.interval === 'string') {
|
} else if (typeof item.interval === 'string') {
|
||||||
intervalPool[item.id] = new Cron(item.interval, async () => {
|
intervalPool[itemId] = new Cron(item.interval, async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await updateProfile(itemId)
|
||||||
} catch (e) {
|
} 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 = groupItems
|
||||||
groupsMenu.unshift({ type: 'separator' })
|
groupsMenu.unshift({ type: 'separator' })
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// 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 (e) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
@ -390,6 +390,8 @@ 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)
|
||||||
@ -435,6 +437,8 @@ 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 (e) {
|
} catch {
|
||||||
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 (e) {
|
} catch {
|
||||||
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 (e) {
|
} catch {
|
||||||
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
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> {
|
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)
|
||||||
@ -180,7 +195,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 = 1): Promise<unknown> {
|
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2): 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++) {
|
||||||
@ -188,21 +203,34 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
|||||||
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 &&
|
||||||
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
(errCode === 'ECONNREFUSED' ||
|
||||||
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
errCode === 'ENOENT' ||
|
||||||
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
errMsg.includes('connect ECONNREFUSED') ||
|
||||||
(error as Error).message?.includes('ENOENT'))
|
errMsg.includes('ENOENT'))
|
||||||
) {
|
) {
|
||||||
await proxyLogger.info(
|
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()) {
|
const helperRunning = await isHelperRunning()
|
||||||
await proxyLogger.info('Socket file missing, requesting recreation...')
|
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 {
|
try {
|
||||||
await requestSocketRecreation()
|
await requestSocketRecreation()
|
||||||
await proxyLogger.info('Socket recreation requested, retrying...')
|
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) {
|
if (attempt === maxRetries) {
|
||||||
throw lastError
|
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'
|
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,13 +1,231 @@
|
|||||||
import { contextBridge, webUtils } from 'electron'
|
import { contextBridge, ipcRenderer, 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 (e) {
|
} catch {
|
||||||
// ignore
|
// 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 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,16 +48,19 @@ const FloatingApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [spinSpeed, spinFloatingIcon])
|
}, [spinSpeed, spinFloatingIcon])
|
||||||
|
|
||||||
useEffect(() => {
|
const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
|
||||||
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
|
const info = args[0] as IMihomoTrafficInfo
|
||||||
setUpload(info.up)
|
setUpload(info.up)
|
||||||
setDownload(info.down)
|
setDownload(info.down)
|
||||||
})
|
|
||||||
return (): void => {
|
|
||||||
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
||||||
|
return (): void => {
|
||||||
|
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
|
||||||
|
}
|
||||||
|
}, [handleTraffic])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-drag h-screen w-screen overflow-hidden">
|
<div className="app-drag h-screen w-screen overflow-hidden">
|
||||||
<div className="floating-bg border border-divider flex bg-content1 h-full w-full">
|
<div className="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
|
// @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 (e) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,7 +149,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
// 非纯数字
|
// 非纯数字
|
||||||
try {
|
try {
|
||||||
setValues({ ...values, interval: v })
|
setValues({ ...values, interval: v })
|
||||||
} catch (e) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const Viewer: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
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 } from 'react'
|
import React, { useEffect, useState, useMemo, useRef, useCallback } 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,11 +25,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -61,6 +56,12 @@ 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 {
|
||||||
@ -125,35 +126,45 @@ 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(() => {
|
|
||||||
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)
|
setUpload(info.up)
|
||||||
setDownload(info.down)
|
setDownload(info.down)
|
||||||
const data = series
|
setSeries((prev) => {
|
||||||
data.shift()
|
const data = [...prev]
|
||||||
data.push(info.up + info.down)
|
data.shift()
|
||||||
setSeries([...data])
|
data.push(info.up + info.down)
|
||||||
|
return data
|
||||||
|
})
|
||||||
if (platform === 'darwin' && showTraffic) {
|
if (platform === 'darwin' && showTraffic) {
|
||||||
if (drawing) return
|
if (drawingRef.current) return
|
||||||
drawing = true
|
drawingRef.current = true
|
||||||
try {
|
try {
|
||||||
await drawSvg(info.up, info.down)
|
await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef)
|
||||||
hasShowTraffic = true
|
hasShowTrafficRef.current = true
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
drawing = false
|
drawingRef.current = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!hasShowTraffic) return
|
if (!hasShowTrafficRef.current) return
|
||||||
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
|
||||||
hasShowTraffic = false
|
hasShowTrafficRef.current = false
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
[showTraffic]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
||||||
return (): void => {
|
return (): void => {
|
||||||
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
|
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
|
||||||
}
|
}
|
||||||
}, [showTraffic])
|
}, [handleTraffic])
|
||||||
|
|
||||||
if (iconOnly) {
|
if (iconOnly) {
|
||||||
return (
|
return (
|
||||||
@ -273,10 +284,15 @@ const ConnCard: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
export default ConnCard
|
export default ConnCard
|
||||||
|
|
||||||
const drawSvg = async (upload: number, download: number): Promise<void> => {
|
const drawSvg = async (
|
||||||
if (upload === currentUpload && download === currentDownload) return
|
upload: number,
|
||||||
currentUpload = upload
|
download: number,
|
||||||
currentDownload = download
|
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 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,7 +43,8 @@ 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, info: IMihomoMemoryInfo) => {
|
window.electron.ipcRenderer.on('mihomoMemory', (_e, ...args) => {
|
||||||
|
const info = args[0] as IMihomoMemoryInfo
|
||||||
setMem(info.inuse)
|
setMem(info.inuse)
|
||||||
})
|
})
|
||||||
return (): void => {
|
return (): void => {
|
||||||
|
|||||||
@ -170,7 +170,8 @@ const Connections: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (_e: unknown, info: IMihomoConnectionsInfo): void => {
|
const handler = (_e: unknown, ...args: unknown[]): void => {
|
||||||
|
const info = args[0] as IMihomoConnectionsInfo
|
||||||
setConnectionsInfo(info)
|
setConnectionsInfo(info)
|
||||||
|
|
||||||
if (!info.connections) return
|
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()
|
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: T | { invokeError: unknown }): T {
|
function checkIpcError<T>(response: unknown): T {
|
||||||
if (response && typeof response === 'object' && 'invokeError' in response) {
|
if (response && typeof response === 'object' && 'invokeError' in response) {
|
||||||
throw response.invokeError
|
throw (response as { invokeError: unknown }).invokeError
|
||||||
}
|
}
|
||||||
return response as T
|
return response as T
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user