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
6 Commits
0e0b337a8b
...
9963f67433
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9963f67433 | ||
|
|
0b65eb490f | ||
|
|
bff3aedf86 | ||
|
|
2c638f56c0 | ||
|
|
38389e0c3c | ||
|
|
5d10c45206 |
BIN
extra/sidecar/sysproxy.win32-x64-msvc.node
Normal file
BIN
extra/sidecar/sysproxy.win32-x64-msvc.node
Normal file
Binary file not shown.
@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@mihomo-party/sysproxy": "^2.0.8",
|
"sysproxy-rs": "file:src/native/sysproxy",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
|
|||||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@ -11,9 +11,6 @@ importers:
|
|||||||
'@electron-toolkit/utils':
|
'@electron-toolkit/utils':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0(electron@37.10.0)
|
version: 4.0.0(electron@37.10.0)
|
||||||
'@mihomo-party/sysproxy':
|
|
||||||
specifier: ^2.0.8
|
|
||||||
version: 2.0.8
|
|
||||||
adm-zip:
|
adm-zip:
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
@ -38,6 +35,9 @@ importers:
|
|||||||
iconv-lite:
|
iconv-lite:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
|
sysproxy-rs:
|
||||||
|
specifier: file:src/native/sysproxy
|
||||||
|
version: file:src/native/sysproxy
|
||||||
webdav:
|
webdav:
|
||||||
specifier: ^5.8.0
|
specifier: ^5.8.0
|
||||||
version: 5.8.0
|
version: 5.8.0
|
||||||
@ -1405,54 +1405,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-darwin-arm64@2.0.8':
|
|
||||||
resolution: {integrity: sha512-4bSqsjEkmtXzgr8zrSUiNmOdlfRDnkFoXICfJqH7ZlM+4L6n74zrm6perFP0NHPpn/oZO97QXGxIJUQSNhFDrw==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-darwin-x64@2.0.8':
|
|
||||||
resolution: {integrity: sha512-sIDzG7yyQZu+DKQ8X1MeYubdEqXSDjzYjVi+5rVZG/jfLlucS9QZNNiXyoTTDUD5cGRcqv1gYNRynd2Csewesg==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-linux-arm64-gnu@2.0.8':
|
|
||||||
resolution: {integrity: sha512-weKk+KcB4lghEj3z15x9FSyla3PT3uLIEU4l4LE4RqhzxgkbJmOt7Wu+ofx4/1k8g8OwwGXIucNgYsV0qpnZQg==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-linux-x64-gnu@2.0.8':
|
|
||||||
resolution: {integrity: sha512-wLt63mztsnZoGFUKxzizRRRd5qAtINg+tB2zdhOnr+0E9TaKLGxZnhYm+Nk8tAB1EBvqjmTWsJG9MDHikh2agg==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-win32-arm64-msvc@2.0.8':
|
|
||||||
resolution: {integrity: sha512-+Mxkw8d3rD6sbFZjjZ18kfx1/WrWXOVlpKd8k3Gdf4LUg7nW8vr64Eaxvjxwcw9AQ1Bu61SHEtvNZfu7woCc1w==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-win32-ia32-msvc@2.0.8':
|
|
||||||
resolution: {integrity: sha512-xOVagbGu21MGzEMidpgMoQGRHY0V1EFdDKb+ZhPPnoIbFhECBRR9fQK7lENV6e7S41ppNbg1Rja4J94VpXdvZA==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-win32-x64-msvc@2.0.8':
|
|
||||||
resolution: {integrity: sha512-AIgCFoExX36BgXN8sQyf0G99wrObFO0LGBzEFs9OsS2cg8bPkpt63XkAkNcGVxlqJD4WLzsS1GhbsL3qzo64DQ==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy@2.0.8':
|
|
||||||
resolution: {integrity: sha512-tCnkDL4UjbUPvFvFubswmWxz56f+gTsYDpv1ULke1YDEZN7aTSREgC3K+Ge7JjZj2jUZYU1lEYuhFgeFz6+W6w==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
|
|
||||||
'@npmcli/fs@2.1.2':
|
'@npmcli/fs@2.1.2':
|
||||||
resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==}
|
resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==}
|
||||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||||
@ -4746,6 +4698,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
sysproxy-rs@file:src/native/sysproxy:
|
||||||
|
resolution: {directory: src/native/sysproxy, type: directory}
|
||||||
|
|
||||||
tailwind-merge@3.4.0:
|
tailwind-merge@3.4.0:
|
||||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||||
|
|
||||||
@ -6769,37 +6724,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-darwin-arm64@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-darwin-x64@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-linux-arm64-gnu@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-linux-x64-gnu@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-win32-arm64-msvc@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-win32-ia32-msvc@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy-win32-x64-msvc@2.0.8':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mihomo-party/sysproxy@2.0.8':
|
|
||||||
optionalDependencies:
|
|
||||||
'@mihomo-party/sysproxy-darwin-arm64': 2.0.8
|
|
||||||
'@mihomo-party/sysproxy-darwin-x64': 2.0.8
|
|
||||||
'@mihomo-party/sysproxy-linux-arm64-gnu': 2.0.8
|
|
||||||
'@mihomo-party/sysproxy-linux-x64-gnu': 2.0.8
|
|
||||||
'@mihomo-party/sysproxy-win32-arm64-msvc': 2.0.8
|
|
||||||
'@mihomo-party/sysproxy-win32-ia32-msvc': 2.0.8
|
|
||||||
'@mihomo-party/sysproxy-win32-x64-msvc': 2.0.8
|
|
||||||
|
|
||||||
'@npmcli/fs@2.1.2':
|
'@npmcli/fs@2.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@gar/promisify': 1.1.3
|
'@gar/promisify': 1.1.3
|
||||||
@ -10935,6 +10859,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/core': 0.2.9
|
'@pkgr/core': 0.2.9
|
||||||
|
|
||||||
|
sysproxy-rs@file:src/native/sysproxy: {}
|
||||||
|
|
||||||
tailwind-merge@3.4.0: {}
|
tailwind-merge@3.4.0: {}
|
||||||
|
|
||||||
tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18):
|
tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18):
|
||||||
|
|||||||
@ -55,37 +55,42 @@ async function getWebDAVClient(): Promise<WebDAVContext> {
|
|||||||
return { client, webdavDir, webdavMaxBackups }
|
return { client, webdavDir, webdavMaxBackups }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function webdavBackup(): Promise<boolean> {
|
function createBackupZip(): AdmZip {
|
||||||
const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient()
|
|
||||||
const zip = new AdmZip()
|
const zip = new AdmZip()
|
||||||
|
|
||||||
if (existsSync(appConfigPath())) {
|
const files = [
|
||||||
zip.addLocalFile(appConfigPath())
|
appConfigPath(),
|
||||||
|
controledMihomoConfigPath(),
|
||||||
|
profileConfigPath(),
|
||||||
|
overrideConfigPath()
|
||||||
|
]
|
||||||
|
|
||||||
|
const folders = [
|
||||||
|
{ path: themesDir(), name: 'themes' },
|
||||||
|
{ path: profilesDir(), name: 'profiles' },
|
||||||
|
{ path: overrideDir(), name: 'override' },
|
||||||
|
{ path: rulesDir(), name: 'rules' },
|
||||||
|
{ path: subStoreDir(), name: 'substore' }
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (existsSync(file)) {
|
||||||
|
zip.addLocalFile(file)
|
||||||
}
|
}
|
||||||
if (existsSync(controledMihomoConfigPath())) {
|
|
||||||
zip.addLocalFile(controledMihomoConfigPath())
|
|
||||||
}
|
}
|
||||||
if (existsSync(profileConfigPath())) {
|
|
||||||
zip.addLocalFile(profileConfigPath())
|
for (const { path, name } of folders) {
|
||||||
|
if (existsSync(path)) {
|
||||||
|
zip.addLocalFolder(path, name)
|
||||||
}
|
}
|
||||||
if (existsSync(overrideConfigPath())) {
|
|
||||||
zip.addLocalFile(overrideConfigPath())
|
|
||||||
}
|
|
||||||
if (existsSync(themesDir())) {
|
|
||||||
zip.addLocalFolder(themesDir(), 'themes')
|
|
||||||
}
|
|
||||||
if (existsSync(profilesDir())) {
|
|
||||||
zip.addLocalFolder(profilesDir(), 'profiles')
|
|
||||||
}
|
|
||||||
if (existsSync(overrideDir())) {
|
|
||||||
zip.addLocalFolder(overrideDir(), 'override')
|
|
||||||
}
|
|
||||||
if (existsSync(rulesDir())) {
|
|
||||||
zip.addLocalFolder(rulesDir(), 'rules')
|
|
||||||
}
|
|
||||||
if (existsSync(subStoreDir())) {
|
|
||||||
zip.addLocalFolder(subStoreDir(), 'substore')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return zip
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function webdavBackup(): Promise<boolean> {
|
||||||
|
const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient()
|
||||||
|
const zip = createBackupZip()
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
||||||
|
|
||||||
@ -214,34 +219,7 @@ export async function reinitScheduler(): Promise<void> {
|
|||||||
* 导出本地备份
|
* 导出本地备份
|
||||||
*/
|
*/
|
||||||
export async function exportLocalBackup(): Promise<boolean> {
|
export async function exportLocalBackup(): Promise<boolean> {
|
||||||
const zip = new AdmZip()
|
const zip = createBackupZip()
|
||||||
if (existsSync(appConfigPath())) {
|
|
||||||
zip.addLocalFile(appConfigPath())
|
|
||||||
}
|
|
||||||
if (existsSync(controledMihomoConfigPath())) {
|
|
||||||
zip.addLocalFile(controledMihomoConfigPath())
|
|
||||||
}
|
|
||||||
if (existsSync(profileConfigPath())) {
|
|
||||||
zip.addLocalFile(profileConfigPath())
|
|
||||||
}
|
|
||||||
if (existsSync(overrideConfigPath())) {
|
|
||||||
zip.addLocalFile(overrideConfigPath())
|
|
||||||
}
|
|
||||||
if (existsSync(themesDir())) {
|
|
||||||
zip.addLocalFolder(themesDir(), 'themes')
|
|
||||||
}
|
|
||||||
if (existsSync(profilesDir())) {
|
|
||||||
zip.addLocalFolder(profilesDir(), 'profiles')
|
|
||||||
}
|
|
||||||
if (existsSync(overrideDir())) {
|
|
||||||
zip.addLocalFolder(overrideDir(), 'override')
|
|
||||||
}
|
|
||||||
if (existsSync(subStoreDir())) {
|
|
||||||
zip.addLocalFolder(subStoreDir(), 'substore')
|
|
||||||
}
|
|
||||||
if (existsSync(rulesDir())) {
|
|
||||||
zip.addLocalFolder(rulesDir(), 'rules')
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { triggerAutoProxy, triggerManualProxy } from '@mihomo-party/sysproxy'
|
import { triggerAutoProxy, triggerManualProxy } from 'sysproxy-rs'
|
||||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||||
import { pacPort, startPacServer, stopPacServer } from '../resolve/server'
|
import { pacPort, startPacServer, stopPacServer } from '../resolve/server'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { exec, execFile } from 'child_process'
|
import { exec } from 'child_process'
|
||||||
import path from 'path'
|
|
||||||
import { resourcesFilesDir } from '../utils/dirs'
|
|
||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
@ -76,87 +74,51 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
const { sysProxy } = await getAppConfig()
|
const { sysProxy } = await getAppConfig()
|
||||||
const { mode, host, bypass = defaultBypass } = sysProxy
|
const { mode, host, bypass = defaultBypass } = sysProxy
|
||||||
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
|
||||||
const execFilePromise = promisify(execFile)
|
const proxyHost = host || '127.0.0.1'
|
||||||
switch (mode || 'manual') {
|
|
||||||
case 'auto': {
|
if (process.platform === 'darwin') {
|
||||||
if (process.platform === 'win32') {
|
// macOS 需要 helper 提权
|
||||||
try {
|
if (mode === 'auto') {
|
||||||
await execFilePromise(path.join(resourcesFilesDir(), 'sysproxy.exe'), [
|
|
||||||
'pac',
|
|
||||||
`http://${host || '127.0.0.1'}:${pacPort}/pac`
|
|
||||||
])
|
|
||||||
} catch {
|
|
||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
|
||||||
}
|
|
||||||
} else if (process.platform === 'darwin') {
|
|
||||||
await helperRequest(() =>
|
await helperRequest(() =>
|
||||||
axios.post(
|
axios.post(
|
||||||
'http://localhost/pac',
|
'http://localhost/pac',
|
||||||
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
|
{ url: `http://${proxyHost}:${pacPort}/pac` },
|
||||||
{
|
{ socketPath: helperSocketPath }
|
||||||
socketPath: helperSocketPath
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'manual': {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
try {
|
|
||||||
await execFilePromise(path.join(resourcesFilesDir(), 'sysproxy.exe'), [
|
|
||||||
'global',
|
|
||||||
`${host || '127.0.0.1'}:${port}`,
|
|
||||||
bypass.join(';')
|
|
||||||
])
|
|
||||||
} catch {
|
|
||||||
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
|
||||||
}
|
|
||||||
} else if (process.platform === 'darwin') {
|
|
||||||
await helperRequest(() =>
|
await helperRequest(() =>
|
||||||
axios.post(
|
axios.post(
|
||||||
'http://localhost/global',
|
'http://localhost/global',
|
||||||
{ host: host || '127.0.0.1', port: port.toString(), bypass: bypass.join(',') },
|
{ host: proxyHost, port: port.toString(), bypass: bypass.join(',') },
|
||||||
{
|
{ socketPath: helperSocketPath }
|
||||||
socketPath: helperSocketPath
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
// Windows / Linux 直接使用 sysproxy-rs
|
||||||
}
|
if (mode === 'auto') {
|
||||||
break
|
triggerAutoProxy(true, `http://${proxyHost}:${pacPort}/pac`)
|
||||||
|
} else {
|
||||||
|
triggerManualProxy(true, proxyHost, port, bypass.join(','))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disableSysProxy(): Promise<void> {
|
async function disableSysProxy(): Promise<void> {
|
||||||
await stopPacServer()
|
await stopPacServer()
|
||||||
const execFilePromise = promisify(execFile)
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'darwin') {
|
||||||
try {
|
|
||||||
await execFilePromise(path.join(resourcesFilesDir(), 'sysproxy.exe'), ['set', '1'])
|
|
||||||
} catch {
|
|
||||||
triggerAutoProxy(false, '')
|
|
||||||
triggerManualProxy(false, '', 0, '')
|
|
||||||
}
|
|
||||||
} else if (process.platform === 'darwin') {
|
|
||||||
await helperRequest(() =>
|
await helperRequest(() =>
|
||||||
axios.get('http://localhost/off', {
|
axios.get('http://localhost/off', { socketPath: helperSocketPath })
|
||||||
socketPath: helperSocketPath
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// Windows / Linux 直接使用 sysproxy-rs
|
||||||
triggerAutoProxy(false, '')
|
triggerAutoProxy(false, '')
|
||||||
triggerManualProxy(false, '', 0, '')
|
triggerManualProxy(false, '', 0, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if socket file exists
|
|
||||||
function isSocketFileExists(): boolean {
|
function isSocketFileExists(): boolean {
|
||||||
try {
|
try {
|
||||||
return fs.existsSync(helperSocketPath)
|
return fs.existsSync(helperSocketPath)
|
||||||
@ -165,7 +127,6 @@ function isSocketFileExists(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if helper process is running (no admin privileges needed)
|
|
||||||
async function isHelperRunning(): Promise<boolean> {
|
async function isHelperRunning(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
@ -176,7 +137,6 @@ async function isHelperRunning(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start or restart helper service via launchctl
|
|
||||||
async function startHelperService(): Promise<void> {
|
async function startHelperService(): Promise<void> {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
const shell = `launchctl kickstart -k system/party.mihomo.helper`
|
const shell = `launchctl kickstart -k system/party.mihomo.helper`
|
||||||
@ -185,7 +145,6 @@ async function startHelperService(): Promise<void> {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
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 {
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
@ -199,7 +158,6 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = 2): Promise<unknown> {
|
||||||
let lastError: Error | null = null
|
let lastError: Error | null = null
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,6 @@ import { initLogger } from './logger'
|
|||||||
|
|
||||||
let isInitBasicCompleted = false
|
let isInitBasicCompleted = false
|
||||||
|
|
||||||
// 安全错误处理
|
|
||||||
export function safeShowErrorBox(titleKey: string, message: string): void {
|
export function safeShowErrorBox(titleKey: string, message: string): void {
|
||||||
let title: string
|
let title: string
|
||||||
try {
|
try {
|
||||||
@ -84,12 +83,9 @@ async function fixDataDirPermissions(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 比较修改 geodata 文件修改时间
|
|
||||||
async function isSourceNewer(sourcePath: string, targetPath: string): Promise<boolean> {
|
async function isSourceNewer(sourcePath: string, targetPath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const sourceStats = await stat(sourcePath)
|
const [sourceStats, targetStats] = await Promise.all([stat(sourcePath), stat(targetPath)])
|
||||||
const targetStats = await stat(targetPath)
|
|
||||||
|
|
||||||
return sourceStats.mtime > targetStats.mtime
|
return sourceStats.mtime > targetStats.mtime
|
||||||
} catch {
|
} catch {
|
||||||
return true
|
return true
|
||||||
@ -99,7 +95,6 @@ async function isSourceNewer(sourcePath: string, targetPath: string): Promise<bo
|
|||||||
async function initDirs(): Promise<void> {
|
async function initDirs(): Promise<void> {
|
||||||
await fixDataDirPermissions()
|
await fixDataDirPermissions()
|
||||||
|
|
||||||
// 按依赖顺序创建目录
|
|
||||||
const dirsToCreate = [
|
const dirsToCreate = [
|
||||||
dataDir(),
|
dataDir(),
|
||||||
themesDir(),
|
themesDir(),
|
||||||
@ -112,16 +107,13 @@ async function initDirs(): Promise<void> {
|
|||||||
subStoreDir()
|
subStoreDir()
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const dir of dirsToCreate) {
|
await Promise.all(
|
||||||
try {
|
dirsToCreate.map(async (dir) => {
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
await mkdir(dir, { recursive: true })
|
await mkdir(dir, { recursive: true })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})
|
||||||
await initLogger.error(`Failed to create directory ${dir}`, error)
|
)
|
||||||
throw new Error(`Failed to create directory ${dir}: ${error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initConfig(): Promise<void> {
|
async function initConfig(): Promise<void> {
|
||||||
@ -137,97 +129,120 @@ async function initConfig(): Promise<void> {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of configs) {
|
await Promise.all(
|
||||||
try {
|
configs.map(async (config) => {
|
||||||
if (!existsSync(config.path)) {
|
if (!existsSync(config.path)) {
|
||||||
await writeFile(config.path, stringify(config.content))
|
await writeFile(config.path, stringify(config.content))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})
|
||||||
await initLogger.error(`Failed to create ${config.name} at ${config.path}`, error)
|
)
|
||||||
throw new Error(`Failed to create ${config.name}: ${error}`)
|
}
|
||||||
|
|
||||||
|
async function killOldMihomoProcesses(): Promise<void> {
|
||||||
|
if (process.platform !== 'win32') return
|
||||||
|
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
try {
|
||||||
|
const { stdout } = await execPromise(
|
||||||
|
'powershell -NoProfile -Command "Get-Process | Where-Object {$_.ProcessName -like \'*mihomo*\'} | Select-Object Id | ConvertTo-Json"',
|
||||||
|
{ encoding: 'utf8' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!stdout.trim()) return
|
||||||
|
|
||||||
|
const processes = JSON.parse(stdout)
|
||||||
|
const processArray = Array.isArray(processes) ? processes : [processes]
|
||||||
|
|
||||||
|
for (const proc of processArray) {
|
||||||
|
const pid = proc.Id
|
||||||
|
if (pid && pid !== process.pid) {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGTERM')
|
||||||
|
await initLogger.info(`Terminated old mihomo process ${pid}`)
|
||||||
|
} catch {
|
||||||
|
// 进程可能退出
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initFiles(): Promise<void> {
|
async function initFiles(): Promise<void> {
|
||||||
const copy = async (file: string): Promise<void> => {
|
await killOldMihomoProcesses()
|
||||||
const targetPath = path.join(mihomoWorkDir(), file)
|
|
||||||
const testTargetPath = path.join(mihomoTestDir(), file)
|
|
||||||
const sourcePath = path.join(resourcesFilesDir(), file)
|
|
||||||
|
|
||||||
try {
|
const copyFile = async (file: string): Promise<void> => {
|
||||||
// 检查是否需要复制
|
const sourcePath = path.join(resourcesFilesDir(), file)
|
||||||
if (existsSync(sourcePath)) {
|
if (!existsSync(sourcePath)) return
|
||||||
const shouldCopyToWork =
|
|
||||||
!existsSync(targetPath) || (await isSourceNewer(sourcePath, targetPath))
|
const targets = [
|
||||||
if (shouldCopyToWork) {
|
path.join(mihomoWorkDir(), file),
|
||||||
|
path.join(mihomoTestDir(), file)
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
targets.map(async (targetPath) => {
|
||||||
|
const shouldCopy = !existsSync(targetPath) || (await isSourceNewer(sourcePath, targetPath))
|
||||||
|
if (shouldCopy) {
|
||||||
await cp(sourcePath, targetPath, { recursive: true, force: true })
|
await cp(sourcePath, targetPath, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
if (existsSync(sourcePath)) {
|
)
|
||||||
const shouldCopyToTest =
|
|
||||||
!existsSync(testTargetPath) || (await isSourceNewer(sourcePath, testTargetPath))
|
|
||||||
if (shouldCopyToTest) {
|
|
||||||
await cp(sourcePath, testTargetPath, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await initLogger.error(`Failed to copy ${file}`, error)
|
|
||||||
if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) {
|
|
||||||
throw new Error(`Failed to copy critical file ${file}: ${error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保工作目录存在
|
const files = [
|
||||||
if (!existsSync(mihomoWorkDir())) {
|
'country.mmdb',
|
||||||
await mkdir(mihomoWorkDir(), { recursive: true })
|
'geoip.metadb',
|
||||||
}
|
'geoip.dat',
|
||||||
if (!existsSync(mihomoTestDir())) {
|
'geosite.dat',
|
||||||
await mkdir(mihomoTestDir(), { recursive: true })
|
'ASN.mmdb',
|
||||||
}
|
'sub-store.bundle.cjs',
|
||||||
|
'sub-store-frontend'
|
||||||
|
]
|
||||||
|
|
||||||
await Promise.all([
|
const criticalFiles = ['country.mmdb', 'geoip.dat', 'geosite.dat']
|
||||||
copy('country.mmdb'),
|
|
||||||
copy('geoip.metadb'),
|
const results = await Promise.allSettled(files.map(copyFile))
|
||||||
copy('geoip.dat'),
|
|
||||||
copy('geosite.dat'),
|
for (let i = 0; i < results.length; i++) {
|
||||||
copy('ASN.mmdb'),
|
const result = results[i]
|
||||||
copy('sub-store.bundle.cjs'),
|
if (result.status === 'rejected') {
|
||||||
copy('sub-store-frontend')
|
const file = files[i]
|
||||||
])
|
await initLogger.error(`Failed to copy ${file}`, result.reason)
|
||||||
|
if (criticalFiles.includes(file)) {
|
||||||
|
throw new Error(`Failed to copy critical file ${file}: ${result.reason}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanup(): Promise<void> {
|
async function cleanup(): Promise<void> {
|
||||||
// update cache
|
const [dataFiles, logFiles] = await Promise.all([readdir(dataDir()), readdir(logDir())])
|
||||||
const files = await readdir(dataDir())
|
|
||||||
for (const file of files) {
|
// 清理更新缓存
|
||||||
if (file.endsWith('.exe') || file.endsWith('.pkg') || file.endsWith('.7z')) {
|
const cacheExtensions = ['.exe', '.pkg', '.7z']
|
||||||
try {
|
const cacheCleanup = dataFiles
|
||||||
await rm(path.join(dataDir(), file))
|
.filter((file) => cacheExtensions.some((ext) => file.endsWith(ext)))
|
||||||
} catch {
|
.map((file) => rm(path.join(dataDir(), file)).catch(() => {}))
|
||||||
// ignore
|
|
||||||
}
|
// 清理过期日志
|
||||||
}
|
|
||||||
}
|
|
||||||
// logs
|
|
||||||
const { maxLogDays = 7 } = await getAppConfig()
|
const { maxLogDays = 7 } = await getAppConfig()
|
||||||
const logs = await readdir(logDir())
|
const maxAge = maxLogDays * 24 * 60 * 60 * 1000
|
||||||
const datePattern = /^\d{4}-\d{2}-\d{2}/
|
const datePattern = /^\d{4}-\d{2}-\d{2}/
|
||||||
for (const log of logs) {
|
|
||||||
|
const logCleanup = logFiles
|
||||||
|
.filter((log) => {
|
||||||
const match = log.match(datePattern)
|
const match = log.match(datePattern)
|
||||||
if (!match) continue
|
if (!match) return false
|
||||||
const date = new Date(match[0])
|
const date = new Date(match[0])
|
||||||
if (isNaN(date.getTime())) continue
|
return !isNaN(date.getTime()) && Date.now() - date.getTime() > maxAge
|
||||||
const diff = Date.now() - date.getTime()
|
})
|
||||||
if (diff > maxLogDays * 24 * 60 * 60 * 1000) {
|
.map((log) => rm(path.join(logDir(), log)).catch(() => {}))
|
||||||
try {
|
|
||||||
await rm(path.join(logDir(), log))
|
await Promise.all([...cacheCleanup, ...logCleanup])
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateSubStoreFiles(): Promise<void> {
|
async function migrateSubStoreFiles(): Promise<void> {
|
||||||
@ -243,111 +258,97 @@ async function migrateSubStoreFiles(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migration(): Promise<void> {
|
// 迁移:添加 substore 到侧边栏
|
||||||
const {
|
async function migrateSiderOrder(): Promise<void> {
|
||||||
siderOrder = [
|
const { siderOrder = [], useSubStore = true } = await getAppConfig()
|
||||||
'sysproxy',
|
|
||||||
'tun',
|
|
||||||
'profile',
|
|
||||||
'proxy',
|
|
||||||
'rule',
|
|
||||||
'resource',
|
|
||||||
'override',
|
|
||||||
'connection',
|
|
||||||
'mihomo',
|
|
||||||
'dns',
|
|
||||||
'sniff',
|
|
||||||
'log',
|
|
||||||
'substore'
|
|
||||||
],
|
|
||||||
appTheme = 'system',
|
|
||||||
envType = [process.platform === 'win32' ? 'powershell' : 'bash'],
|
|
||||||
useSubStore = true,
|
|
||||||
showFloatingWindow = false,
|
|
||||||
disableTray = false,
|
|
||||||
encryptedPassword
|
|
||||||
} = await getAppConfig()
|
|
||||||
const {
|
|
||||||
'external-controller-pipe': externalControllerPipe,
|
|
||||||
'external-controller-unix': externalControllerUnix,
|
|
||||||
'external-controller': externalController,
|
|
||||||
'skip-auth-prefixes': skipAuthPrefixes,
|
|
||||||
authentication,
|
|
||||||
'bind-address': bindAddress,
|
|
||||||
'lan-allowed-ips': lanAllowedIps,
|
|
||||||
'lan-disallowed-ips': lanDisallowedIps,
|
|
||||||
tun
|
|
||||||
} = await getControledMihomoConfig()
|
|
||||||
// add substore sider card
|
|
||||||
if (useSubStore && !siderOrder.includes('substore')) {
|
if (useSubStore && !siderOrder.includes('substore')) {
|
||||||
await patchAppConfig({ siderOrder: [...siderOrder, 'substore'] })
|
await patchAppConfig({ siderOrder: [...siderOrder, 'substore'] })
|
||||||
}
|
}
|
||||||
// add default skip auth prefix
|
}
|
||||||
if (!skipAuthPrefixes) {
|
|
||||||
await patchControledMihomoConfig({ 'skip-auth-prefixes': ['127.0.0.1/32', '::1/128'] })
|
// 迁移:修复 appTheme
|
||||||
} else if (skipAuthPrefixes.length >= 1 && skipAuthPrefixes[0] === '127.0.0.1/32') {
|
async function migrateAppTheme(): Promise<void> {
|
||||||
const filteredPrefixes = skipAuthPrefixes.filter((ip) => ip !== '::1/128')
|
const { appTheme = 'system' } = await getAppConfig()
|
||||||
const newPrefixes = [filteredPrefixes[0], '::1/128', ...filteredPrefixes.slice(1)]
|
|
||||||
if (JSON.stringify(newPrefixes) !== JSON.stringify(skipAuthPrefixes)) {
|
|
||||||
await patchControledMihomoConfig({ 'skip-auth-prefixes': newPrefixes })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// add default authentication
|
|
||||||
if (!authentication) {
|
|
||||||
await patchControledMihomoConfig({ authentication: [] })
|
|
||||||
}
|
|
||||||
// add default bind address
|
|
||||||
if (!bindAddress) {
|
|
||||||
await patchControledMihomoConfig({ 'bind-address': '*' })
|
|
||||||
}
|
|
||||||
// add default lan allowed ips
|
|
||||||
if (!lanAllowedIps) {
|
|
||||||
await patchControledMihomoConfig({ 'lan-allowed-ips': ['0.0.0.0/0', '::/0'] })
|
|
||||||
}
|
|
||||||
// add default lan disallowed ips
|
|
||||||
if (!lanDisallowedIps) {
|
|
||||||
await patchControledMihomoConfig({ 'lan-disallowed-ips': [] })
|
|
||||||
}
|
|
||||||
// default tun device
|
|
||||||
if (!tun?.device || (process.platform === 'darwin' && tun.device === 'Mihomo')) {
|
|
||||||
const defaultDevice = process.platform === 'darwin' ? 'utun1500' : 'Mihomo'
|
|
||||||
await patchControledMihomoConfig({
|
|
||||||
tun: {
|
|
||||||
...tun,
|
|
||||||
device: defaultDevice
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// remove custom app theme
|
|
||||||
if (!['system', 'light', 'dark'].includes(appTheme)) {
|
if (!['system', 'light', 'dark'].includes(appTheme)) {
|
||||||
await patchAppConfig({ appTheme: 'system' })
|
await patchAppConfig({ appTheme: 'system' })
|
||||||
}
|
}
|
||||||
// change env type
|
}
|
||||||
|
|
||||||
|
// 迁移:envType 字符串转数组
|
||||||
|
async function migrateEnvType(): Promise<void> {
|
||||||
|
const { envType } = await getAppConfig()
|
||||||
if (typeof envType === 'string') {
|
if (typeof envType === 'string') {
|
||||||
await patchAppConfig({ envType: [envType] })
|
await patchAppConfig({ envType: [envType] })
|
||||||
}
|
}
|
||||||
// use unix socket
|
}
|
||||||
if (externalControllerUnix) {
|
|
||||||
await patchControledMihomoConfig({ 'external-controller-unix': undefined })
|
// 迁移:禁用托盘时必须显示悬浮窗
|
||||||
}
|
async function migrateTraySettings(): Promise<void> {
|
||||||
// use named pipe
|
const { showFloatingWindow = false, disableTray = false } = await getAppConfig()
|
||||||
if (externalControllerPipe) {
|
|
||||||
await patchControledMihomoConfig({
|
|
||||||
'external-controller-pipe': undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (externalController === undefined) {
|
|
||||||
await patchControledMihomoConfig({ 'external-controller': '' })
|
|
||||||
}
|
|
||||||
if (!showFloatingWindow && disableTray) {
|
if (!showFloatingWindow && disableTray) {
|
||||||
await patchAppConfig({ disableTray: false })
|
await patchAppConfig({ disableTray: false })
|
||||||
}
|
}
|
||||||
// remove password
|
}
|
||||||
|
|
||||||
|
// 迁移:移除加密密码
|
||||||
|
async function migrateRemovePassword(): Promise<void> {
|
||||||
|
const { encryptedPassword } = await getAppConfig()
|
||||||
if (encryptedPassword) {
|
if (encryptedPassword) {
|
||||||
await patchAppConfig({ encryptedPassword: undefined })
|
await patchAppConfig({ encryptedPassword: undefined })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 迁移:mihomo 配置默认值
|
||||||
|
async function migrateMihomoConfig(): Promise<void> {
|
||||||
|
const config = await getControledMihomoConfig()
|
||||||
|
const patches: Partial<IMihomoConfig> = {}
|
||||||
|
|
||||||
|
// skip-auth-prefixes
|
||||||
|
if (!config['skip-auth-prefixes']) {
|
||||||
|
patches['skip-auth-prefixes'] = ['127.0.0.1/32', '::1/128']
|
||||||
|
} else if (
|
||||||
|
config['skip-auth-prefixes'].length >= 1 &&
|
||||||
|
config['skip-auth-prefixes'][0] === '127.0.0.1/32' &&
|
||||||
|
!config['skip-auth-prefixes'].includes('::1/128')
|
||||||
|
) {
|
||||||
|
patches['skip-auth-prefixes'] = ['127.0.0.1/32', '::1/128', ...config['skip-auth-prefixes'].slice(1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他默认值
|
||||||
|
if (!config.authentication) patches.authentication = []
|
||||||
|
if (!config['bind-address']) patches['bind-address'] = '*'
|
||||||
|
if (!config['lan-allowed-ips']) patches['lan-allowed-ips'] = ['0.0.0.0/0', '::/0']
|
||||||
|
if (!config['lan-disallowed-ips']) patches['lan-disallowed-ips'] = []
|
||||||
|
|
||||||
|
// tun device
|
||||||
|
if (!config.tun?.device || (process.platform === 'darwin' && config.tun.device === 'Mihomo')) {
|
||||||
|
patches.tun = {
|
||||||
|
...config.tun,
|
||||||
|
device: process.platform === 'darwin' ? 'utun1500' : 'Mihomo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除废弃配置
|
||||||
|
if (config['external-controller-unix']) patches['external-controller-unix'] = undefined
|
||||||
|
if (config['external-controller-pipe']) patches['external-controller-pipe'] = undefined
|
||||||
|
if (config['external-controller'] === undefined) patches['external-controller'] = ''
|
||||||
|
|
||||||
|
if (Object.keys(patches).length > 0) {
|
||||||
|
await patchControledMihomoConfig(patches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migration(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
migrateSiderOrder(),
|
||||||
|
migrateAppTheme(),
|
||||||
|
migrateEnvType(),
|
||||||
|
migrateTraySettings(),
|
||||||
|
migrateRemovePassword(),
|
||||||
|
migrateMihomoConfig()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
function initDeeplink(): void {
|
function initDeeplink(): void {
|
||||||
if (process.defaultApp) {
|
if (process.defaultApp) {
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
@ -360,11 +361,8 @@ function initDeeplink(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基础初始化
|
|
||||||
export async function initBasic(): Promise<void> {
|
export async function initBasic(): Promise<void> {
|
||||||
if (isInitBasicCompleted) {
|
if (isInitBasicCompleted) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await initDirs()
|
await initDirs()
|
||||||
await initConfig()
|
await initConfig()
|
||||||
@ -379,6 +377,7 @@ export async function initBasic(): Promise<void> {
|
|||||||
export async function init(): Promise<void> {
|
export async function init(): Promise<void> {
|
||||||
await startSubStoreFrontendServer()
|
await startSubStoreFrontendServer()
|
||||||
await startSubStoreBackendServer()
|
await startSubStoreBackendServer()
|
||||||
|
|
||||||
const { sysProxy } = await getAppConfig()
|
const { sysProxy } = await getAppConfig()
|
||||||
try {
|
try {
|
||||||
if (sysProxy.enable) {
|
if (sysProxy.enable) {
|
||||||
@ -388,7 +387,7 @@ export async function init(): Promise<void> {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
await startSSIDCheck()
|
|
||||||
|
|
||||||
|
await startSSIDCheck()
|
||||||
initDeeplink()
|
initDeeplink()
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/native/sysproxy/index.d.ts
vendored
Normal file
24
src/native/sysproxy/index.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export interface SysproxyInfo {
|
||||||
|
enable: boolean
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
bypass: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoproxyInfo {
|
||||||
|
enable: boolean
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerManualProxy(
|
||||||
|
enable: boolean,
|
||||||
|
host: string,
|
||||||
|
port: number,
|
||||||
|
bypass: string
|
||||||
|
): void
|
||||||
|
|
||||||
|
export function triggerAutoProxy(enable: boolean, url: string): void
|
||||||
|
|
||||||
|
export function getSystemProxy(): SysproxyInfo
|
||||||
|
|
||||||
|
export function getAutoProxy(): AutoproxyInfo
|
||||||
75
src/native/sysproxy/index.js
Normal file
75
src/native/sysproxy/index.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const { existsSync, readFileSync } = require('fs')
|
||||||
|
const { join } = require('path')
|
||||||
|
|
||||||
|
const { platform, arch } = process
|
||||||
|
|
||||||
|
let nativeBinding = null
|
||||||
|
let loadError = null
|
||||||
|
|
||||||
|
function isMusl() {
|
||||||
|
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||||
|
try {
|
||||||
|
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
||||||
|
return readFileSync(lddPath, 'utf8').includes('musl')
|
||||||
|
} catch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { glibcVersionRuntime } = process.report.getReport().header
|
||||||
|
return !glibcVersionRuntime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBindingName() {
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
if (arch === 'x64') return 'sysproxy.win32-x64-msvc.node'
|
||||||
|
if (arch === 'arm64') return 'sysproxy.win32-arm64-msvc.node'
|
||||||
|
break
|
||||||
|
case 'darwin':
|
||||||
|
if (arch === 'x64') return 'sysproxy.darwin-x64.node'
|
||||||
|
if (arch === 'arm64') return 'sysproxy.darwin-arm64.node'
|
||||||
|
break
|
||||||
|
case 'linux':
|
||||||
|
if (isMusl()) {
|
||||||
|
if (arch === 'x64') return 'sysproxy.linux-x64-musl.node'
|
||||||
|
if (arch === 'arm64') return 'sysproxy.linux-arm64-musl.node'
|
||||||
|
} else {
|
||||||
|
if (arch === 'x64') return 'sysproxy.linux-x64-gnu.node'
|
||||||
|
if (arch === 'arm64') return 'sysproxy.linux-arm64-gnu.node'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBinding() {
|
||||||
|
const bindingName = getBindingName()
|
||||||
|
|
||||||
|
// 查找项目根目录的 extra/sidecar
|
||||||
|
let currentDir = __dirname
|
||||||
|
while (currentDir !== require('path').dirname(currentDir)) {
|
||||||
|
const sidecarPath = join(currentDir, 'extra', 'sidecar', bindingName)
|
||||||
|
if (existsSync(sidecarPath)) {
|
||||||
|
try {
|
||||||
|
nativeBinding = require(sidecarPath)
|
||||||
|
return nativeBinding
|
||||||
|
} catch (e) {
|
||||||
|
loadError = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentDir = require('path').dirname(currentDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadError) {
|
||||||
|
throw loadError
|
||||||
|
}
|
||||||
|
throw new Error(`Native binding not found: ${bindingName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const binding = loadBinding()
|
||||||
|
|
||||||
|
module.exports.triggerManualProxy = binding.triggerManualProxy
|
||||||
|
module.exports.triggerAutoProxy = binding.triggerAutoProxy
|
||||||
|
module.exports.getSystemProxy = binding.getSystemProxy
|
||||||
|
module.exports.getAutoProxy = binding.getAutoProxy
|
||||||
8
src/native/sysproxy/package.json
Normal file
8
src/native/sysproxy/package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "sysproxy-rs",
|
||||||
|
"version": "0.4.0",
|
||||||
|
"description": "System proxy library for Node.js",
|
||||||
|
"main": "index.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
72
src/renderer/src/hooks/create-config-context.tsx
Normal file
72
src/renderer/src/hooks/create-config-context.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { createContext, useContext, ReactNode, useCallback, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { showError } from '@renderer/utils/error-display'
|
||||||
|
import useSWR, { KeyedMutator } from 'swr'
|
||||||
|
|
||||||
|
interface ConfigContextValue<T> {
|
||||||
|
config: T | undefined
|
||||||
|
mutate: KeyedMutator<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateConfigContextOptions<T> {
|
||||||
|
swrKey: string
|
||||||
|
fetcher: () => Promise<T>
|
||||||
|
ipcEvent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConfigContext<T>(options: CreateConfigContextOptions<T>) {
|
||||||
|
const { swrKey, fetcher, ipcEvent } = options
|
||||||
|
const Context = createContext<ConfigContextValue<T> | undefined>(undefined)
|
||||||
|
|
||||||
|
const Provider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const { data: config, mutate } = useSWR(swrKey, fetcher)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (): void => {
|
||||||
|
mutate()
|
||||||
|
}
|
||||||
|
window.electron.ipcRenderer.on(ipcEvent, handler)
|
||||||
|
return () => {
|
||||||
|
window.electron.ipcRenderer.removeListener(ipcEvent, handler)
|
||||||
|
}
|
||||||
|
}, [mutate])
|
||||||
|
|
||||||
|
return <Context.Provider value={{ config, mutate }}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useConfig = (): ConfigContextValue<T> => {
|
||||||
|
const context = useContext(Context)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`useConfig must be used within Provider`)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
return { Provider, useConfig, Context }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionOptions {
|
||||||
|
errorKey: string
|
||||||
|
updateTray?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfigAction<T>(
|
||||||
|
mutate: KeyedMutator<T>,
|
||||||
|
action: () => Promise<void>,
|
||||||
|
options: ActionOptions
|
||||||
|
): () => Promise<void> {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await action()
|
||||||
|
} catch (e) {
|
||||||
|
await showError(e, t(options.errorKey))
|
||||||
|
} finally {
|
||||||
|
mutate()
|
||||||
|
if (options.updateTray) {
|
||||||
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mutate, action, t, options.errorKey, options.updateTray])
|
||||||
|
}
|
||||||
@ -1,51 +1,60 @@
|
|||||||
import React, { createContext, useContext, ReactNode } from 'react'
|
import React, { ReactNode, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { showError } from '@renderer/utils/error-display'
|
import { showError } from '@renderer/utils/error-display'
|
||||||
import useSWR from 'swr'
|
import { createConfigContext } from './create-config-context'
|
||||||
import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
|
import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
|
||||||
|
|
||||||
|
const { Provider, useConfig } = createConfigContext<IAppConfig>({
|
||||||
|
swrKey: 'getAppConfig',
|
||||||
|
fetcher: getAppConfig,
|
||||||
|
ipcEvent: 'appConfigUpdated'
|
||||||
|
})
|
||||||
|
|
||||||
interface AppConfigContextType {
|
interface AppConfigContextType {
|
||||||
appConfig: IAppConfig | undefined
|
appConfig: IAppConfig | undefined
|
||||||
mutateAppConfig: () => void
|
mutateAppConfig: () => void
|
||||||
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppConfigContext = createContext<AppConfigContextType | undefined>(undefined)
|
|
||||||
|
|
||||||
export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const { t } = useTranslation()
|
return (
|
||||||
const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig())
|
<Provider>
|
||||||
|
<AppConfigContextWrapper>{children}</AppConfigContextWrapper>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const patchAppConfig = async (value: Partial<IAppConfig>): Promise<void> => {
|
const AppConfigContextWrapper: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const { config, mutate } = useConfig()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const patchAppConfig = useCallback(
|
||||||
|
async (value: Partial<IAppConfig>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await patch(value)
|
await patch(value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await showError(e, t('common.error.updateAppConfigFailed'))
|
await showError(e, t('common.error.updateAppConfigFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
mutateAppConfig()
|
mutate()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[mutate, t]
|
||||||
React.useEffect(() => {
|
)
|
||||||
const handler = (): void => {
|
|
||||||
mutateAppConfig()
|
|
||||||
}
|
|
||||||
window.electron.ipcRenderer.on('appConfigUpdated', handler)
|
|
||||||
return (): void => {
|
|
||||||
window.electron.ipcRenderer.removeListener('appConfigUpdated', handler)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppConfigContext.Provider value={{ appConfig, mutateAppConfig, patchAppConfig }}>
|
<AppConfigContext.Provider
|
||||||
|
value={{ appConfig: config, mutateAppConfig: mutate, patchAppConfig }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AppConfigContext.Provider>
|
</AppConfigContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AppConfigContext = React.createContext<AppConfigContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const useAppConfig = (): AppConfigContextType => {
|
export const useAppConfig = (): AppConfigContextType => {
|
||||||
const context = useContext(AppConfigContext)
|
const context = React.useContext(AppConfigContext)
|
||||||
if (context === undefined) {
|
if (!context) {
|
||||||
throw new Error('useAppConfig must be used within an AppConfigProvider')
|
throw new Error('useAppConfig must be used within an AppConfigProvider')
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { createContext, useContext, ReactNode } from 'react'
|
import React, { ReactNode, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { showError } from '@renderer/utils/error-display'
|
import { showError } from '@renderer/utils/error-display'
|
||||||
import useSWR from 'swr'
|
import { createConfigContext } from './create-config-context'
|
||||||
import {
|
import {
|
||||||
getOverrideConfig,
|
getOverrideConfig,
|
||||||
setOverrideConfig as set,
|
setOverrideConfig as set,
|
||||||
@ -10,6 +10,12 @@ import {
|
|||||||
updateOverrideItem as update
|
updateOverrideItem as update
|
||||||
} from '@renderer/utils/ipc'
|
} from '@renderer/utils/ipc'
|
||||||
|
|
||||||
|
const { Provider, useConfig } = createConfigContext<IOverrideConfig>({
|
||||||
|
swrKey: 'getOverrideConfig',
|
||||||
|
fetcher: getOverrideConfig,
|
||||||
|
ipcEvent: 'overrideConfigUpdated'
|
||||||
|
})
|
||||||
|
|
||||||
interface OverrideConfigContextType {
|
interface OverrideConfigContextType {
|
||||||
overrideConfig: IOverrideConfig | undefined
|
overrideConfig: IOverrideConfig | undefined
|
||||||
setOverrideConfig: (config: IOverrideConfig) => Promise<void>
|
setOverrideConfig: (config: IOverrideConfig) => Promise<void>
|
||||||
@ -19,60 +25,59 @@ interface OverrideConfigContextType {
|
|||||||
removeOverrideItem: (id: string) => Promise<void>
|
removeOverrideItem: (id: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const OverrideConfigContext = createContext<OverrideConfigContextType | undefined>(undefined)
|
const OverrideConfigContext = React.createContext<OverrideConfigContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Provider>
|
||||||
|
<OverrideConfigContextWrapper>{children}</OverrideConfigContextWrapper>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OverrideConfigContextWrapper: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const { config, mutate } = useConfig()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: overrideConfig, mutate: mutateOverrideConfig } = useSWR('getOverrideConfig', () =>
|
|
||||||
getOverrideConfig()
|
const withErrorHandling = useCallback(
|
||||||
|
(action: () => Promise<void>, errorKey: string) => async () => {
|
||||||
|
try {
|
||||||
|
await action()
|
||||||
|
} catch (e) {
|
||||||
|
await showError(e, t(errorKey))
|
||||||
|
} finally {
|
||||||
|
mutate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mutate, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const setOverrideConfig = async (config: IOverrideConfig): Promise<void> => {
|
const setOverrideConfig = useCallback(
|
||||||
try {
|
(cfg: IOverrideConfig) => withErrorHandling(() => set(cfg), 'common.error.saveOverrideConfigFailed')(),
|
||||||
await set(config)
|
[withErrorHandling]
|
||||||
} catch (e) {
|
)
|
||||||
await showError(e, t('common.error.saveOverrideConfigFailed'))
|
|
||||||
} finally {
|
|
||||||
mutateOverrideConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addOverrideItem = async (item: Partial<IOverrideItem>): Promise<void> => {
|
const addOverrideItem = useCallback(
|
||||||
try {
|
(item: Partial<IOverrideItem>) => withErrorHandling(() => add(item), 'common.error.addOverrideFailed')(),
|
||||||
await add(item)
|
[withErrorHandling]
|
||||||
} catch (e) {
|
)
|
||||||
await showError(e, t('common.error.addOverrideFailed'))
|
|
||||||
} finally {
|
|
||||||
mutateOverrideConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeOverrideItem = async (id: string): Promise<void> => {
|
const removeOverrideItem = useCallback(
|
||||||
try {
|
(id: string) => withErrorHandling(() => remove(id), 'common.error.deleteOverrideFailed')(),
|
||||||
await remove(id)
|
[withErrorHandling]
|
||||||
} catch (e) {
|
)
|
||||||
await showError(e, t('common.error.deleteOverrideFailed'))
|
|
||||||
} finally {
|
|
||||||
mutateOverrideConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateOverrideItem = async (item: IOverrideItem): Promise<void> => {
|
const updateOverrideItem = useCallback(
|
||||||
try {
|
(item: IOverrideItem) => withErrorHandling(() => update(item), 'common.error.updateOverrideFailed')(),
|
||||||
await update(item)
|
[withErrorHandling]
|
||||||
} catch (e) {
|
)
|
||||||
await showError(e, t('common.error.updateOverrideFailed'))
|
|
||||||
} finally {
|
|
||||||
mutateOverrideConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverrideConfigContext.Provider
|
<OverrideConfigContext.Provider
|
||||||
value={{
|
value={{
|
||||||
overrideConfig,
|
overrideConfig: config,
|
||||||
setOverrideConfig,
|
setOverrideConfig,
|
||||||
mutateOverrideConfig,
|
mutateOverrideConfig: mutate,
|
||||||
addOverrideItem,
|
addOverrideItem,
|
||||||
removeOverrideItem,
|
removeOverrideItem,
|
||||||
updateOverrideItem
|
updateOverrideItem
|
||||||
@ -84,8 +89,8 @@ export const OverrideConfigProvider: React.FC<{ children: ReactNode }> = ({ chil
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useOverrideConfig = (): OverrideConfigContextType => {
|
export const useOverrideConfig = (): OverrideConfigContextType => {
|
||||||
const context = useContext(OverrideConfigContext)
|
const context = React.useContext(OverrideConfigContext)
|
||||||
if (context === undefined) {
|
if (!context) {
|
||||||
throw new Error('useOverrideConfig must be used within an OverrideConfigProvider')
|
throw new Error('useOverrideConfig must be used within an OverrideConfigProvider')
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { createContext, ReactNode, useContext } from 'react'
|
import React, { ReactNode, useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { showError } from '@renderer/utils/error-display'
|
import { showError } from '@renderer/utils/error-display'
|
||||||
import useSWR from 'swr'
|
import { createConfigContext } from './create-config-context'
|
||||||
import {
|
import {
|
||||||
addProfileItem as add,
|
addProfileItem as add,
|
||||||
changeCurrentProfile as change,
|
changeCurrentProfile as change,
|
||||||
@ -11,6 +11,12 @@ import {
|
|||||||
updateProfileItem as update
|
updateProfileItem as update
|
||||||
} from '@renderer/utils/ipc'
|
} from '@renderer/utils/ipc'
|
||||||
|
|
||||||
|
const { Provider, useConfig } = createConfigContext<IProfileConfig>({
|
||||||
|
swrKey: 'getProfileConfig',
|
||||||
|
fetcher: getProfileConfig,
|
||||||
|
ipcEvent: 'profileConfigUpdated'
|
||||||
|
})
|
||||||
|
|
||||||
interface ProfileConfigContextType {
|
interface ProfileConfigContextType {
|
||||||
profileConfig: IProfileConfig | undefined
|
profileConfig: IProfileConfig | undefined
|
||||||
setProfileConfig: (config: IProfileConfig) => Promise<void>
|
setProfileConfig: (config: IProfileConfig) => Promise<void>
|
||||||
@ -21,80 +27,64 @@ interface ProfileConfigContextType {
|
|||||||
changeCurrentProfile: (id: string) => Promise<void>
|
changeCurrentProfile: (id: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileConfigContext = createContext<ProfileConfigContextType | undefined>(undefined)
|
const ProfileConfigContext = React.createContext<ProfileConfigContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const { t } = useTranslation()
|
return (
|
||||||
const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
|
<Provider>
|
||||||
getProfileConfig()
|
<ProfileConfigContextWrapper>{children}</ProfileConfigContextWrapper>
|
||||||
|
</Provider>
|
||||||
)
|
)
|
||||||
const targetProfileId = React.useRef<string | null>(null)
|
}
|
||||||
const pendingTask = React.useRef<Promise<void> | null>(null)
|
|
||||||
|
|
||||||
const setProfileConfig = async (config: IProfileConfig): Promise<void> => {
|
const ProfileConfigContextWrapper: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const { config, mutate } = useConfig()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const targetProfileId = useRef<string | null>(null)
|
||||||
|
const pendingTask = useRef<Promise<void> | null>(null)
|
||||||
|
|
||||||
|
const withErrorHandling = useCallback(
|
||||||
|
(action: () => Promise<void>, errorKey: string, updateTray = true) =>
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
await set(config)
|
await action()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await showError(e, t('common.error.saveProfileConfigFailed'))
|
await showError(e, t(errorKey))
|
||||||
} finally {
|
} finally {
|
||||||
mutateProfileConfig()
|
mutate()
|
||||||
|
if (updateTray) {
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[mutate, t]
|
||||||
|
)
|
||||||
|
|
||||||
const addProfileItem = async (item: Partial<IProfileItem>): Promise<void> => {
|
const setProfileConfig = useCallback(
|
||||||
try {
|
(cfg: IProfileConfig) =>
|
||||||
await add(item)
|
withErrorHandling(() => set(cfg), 'common.error.saveProfileConfigFailed')(),
|
||||||
} catch (e) {
|
[withErrorHandling]
|
||||||
await showError(e, t('common.error.addProfileFailed'))
|
)
|
||||||
} finally {
|
|
||||||
mutateProfileConfig()
|
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeProfileItem = async (id: string): Promise<void> => {
|
const addProfileItem = useCallback(
|
||||||
try {
|
(item: Partial<IProfileItem>) =>
|
||||||
await remove(id)
|
withErrorHandling(() => add(item), 'common.error.addProfileFailed')(),
|
||||||
} catch (e) {
|
[withErrorHandling]
|
||||||
await showError(e, t('common.error.deleteProfileFailed'))
|
)
|
||||||
} finally {
|
|
||||||
mutateProfileConfig()
|
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateProfileItem = async (item: IProfileItem): Promise<void> => {
|
const removeProfileItem = useCallback(
|
||||||
try {
|
(id: string) => withErrorHandling(() => remove(id), 'common.error.deleteProfileFailed')(),
|
||||||
await update(item)
|
[withErrorHandling]
|
||||||
} catch (e) {
|
)
|
||||||
await showError(e, t('common.error.updateProfileFailed'))
|
|
||||||
} finally {
|
|
||||||
mutateProfileConfig()
|
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeCurrentProfile = async (id: string): Promise<void> => {
|
const updateProfileItem = useCallback(
|
||||||
if (targetProfileId.current === id) {
|
(item: IProfileItem) =>
|
||||||
return
|
withErrorHandling(() => update(item), 'common.error.updateProfileFailed')(),
|
||||||
}
|
[withErrorHandling]
|
||||||
|
)
|
||||||
|
|
||||||
// 立即更新 UI 状态和托盘菜单,提供即时反馈
|
const processChange = useCallback(async () => {
|
||||||
if (profileConfig) {
|
if (pendingTask.current) return
|
||||||
const optimisticUpdate = { ...profileConfig, current: id }
|
|
||||||
mutateProfileConfig(optimisticUpdate, false)
|
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
|
||||||
}
|
|
||||||
|
|
||||||
targetProfileId.current = id
|
|
||||||
await processChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
const processChange = async () => {
|
|
||||||
if (pendingTask.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
while (targetProfileId.current) {
|
while (targetProfileId.current) {
|
||||||
const targetId = targetProfileId.current
|
const targetId = targetProfileId.current
|
||||||
@ -102,41 +92,48 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
|||||||
|
|
||||||
pendingTask.current = change(targetId)
|
pendingTask.current = change(targetId)
|
||||||
try {
|
try {
|
||||||
// 异步执行后台切换,不阻塞 UI
|
|
||||||
await pendingTask.current
|
await pendingTask.current
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMsg = (e as { message?: string })?.message || String(e)
|
const errorMsg = (e as { message?: string })?.message || String(e)
|
||||||
// 处理 IPC 超时错误
|
|
||||||
if (errorMsg.includes('reply was never sent')) {
|
if (errorMsg.includes('reply was never sent')) {
|
||||||
setTimeout(() => mutateProfileConfig(), 1000)
|
setTimeout(() => mutate(), 1000)
|
||||||
} else {
|
} else {
|
||||||
await showError(errorMsg, t('common.error.switchProfileFailed'))
|
await showError(errorMsg, t('common.error.switchProfileFailed'))
|
||||||
mutateProfileConfig()
|
mutate()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
pendingTask.current = null
|
pendingTask.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [mutate, t])
|
||||||
|
|
||||||
|
const changeCurrentProfile = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
if (targetProfileId.current === id) return
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
mutate({ ...config, current: id }, false)
|
||||||
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetProfileId.current = id
|
||||||
|
await processChange()
|
||||||
|
},
|
||||||
|
[config, mutate, processChange]
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handler = (): void => {
|
return () => {
|
||||||
mutateProfileConfig()
|
|
||||||
}
|
|
||||||
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
|
|
||||||
return (): void => {
|
|
||||||
// 清理待处理任务,防止内存泄漏
|
|
||||||
targetProfileId.current = null
|
targetProfileId.current = null
|
||||||
window.electron.ipcRenderer.removeListener('profileConfigUpdated', handler)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfileConfigContext.Provider
|
<ProfileConfigContext.Provider
|
||||||
value={{
|
value={{
|
||||||
profileConfig,
|
profileConfig: config,
|
||||||
setProfileConfig,
|
setProfileConfig,
|
||||||
mutateProfileConfig,
|
mutateProfileConfig: mutate,
|
||||||
addProfileItem,
|
addProfileItem,
|
||||||
removeProfileItem,
|
removeProfileItem,
|
||||||
updateProfileItem,
|
updateProfileItem,
|
||||||
@ -149,8 +146,8 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useProfileConfig = (): ProfileConfigContextType => {
|
export const useProfileConfig = (): ProfileConfigContextType => {
|
||||||
const context = useContext(ProfileConfigContext)
|
const context = React.useContext(ProfileConfigContext)
|
||||||
if (context === undefined) {
|
if (!context) {
|
||||||
throw new Error('useProfileConfig must be used within a ProfileConfigProvider')
|
throw new Error('useProfileConfig must be used within a ProfileConfigProvider')
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user