support pac mode

This commit is contained in:
pompurin404 2024-08-02 19:07:39 +08:00
parent c4dcead397
commit cb1d8c6141
No known key found for this signature in database
16 changed files with 410 additions and 15 deletions

View File

@ -21,12 +21,14 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@mihomo-party/sysproxy": "^1.0.1",
"@nextui-org/react": "^2.4.6",
"axios": "^1.7.2",
"electron-updater": "^6.2.1",
"framer-motion": "^11.3.19",
"next-themes": "^0.3.0",
"react-icons": "^5.2.1",
"react-monaco-editor": "^0.55.0",
"react-router-dom": "^6.25.1",
"swr": "^2.2.5",
"ws": "^8.18.0",

View File

@ -14,6 +14,9 @@ importers:
'@electron-toolkit/utils':
specifier: ^3.0.0
version: 3.0.0(electron@31.3.1)
'@mihomo-party/sysproxy':
specifier: ^1.0.1
version: 1.0.1
'@nextui-org/react':
specifier: ^2.4.6
version: 2.4.6(@types/react@18.3.3)(framer-motion@11.3.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.7)
@ -32,6 +35,9 @@ importers:
react-icons:
specifier: ^5.2.1
version: 5.2.1(react@18.3.1)
react-monaco-editor:
specifier: ^0.55.0
version: 0.55.0(@types/react@18.3.3)(monaco-editor@0.44.0)(react@18.3.1)
react-router-dom:
specifier: ^6.25.1
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -522,6 +528,52 @@ packages:
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
engines: {node: '>= 10.0.0'}
'@mihomo-party/sysproxy-darwin-arm64@1.0.1':
resolution: {integrity: sha512-VNrJLIXpgQBesNw9Ng3IgHK8L9JS4hHIXe/cn3ODHjq2kDXar1Q1qcXAHpi8ZRiw6Q1H5pewRl+EBb+ZCAHInQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@mihomo-party/sysproxy-darwin-x64@1.0.1':
resolution: {integrity: sha512-OdkcgjXscnos4hQ6YmFtxyTJZde0QjNJpgyCW826JYyjPzWPTtjftCnLWFVnSIU6U0jAfW+iSF8B1qmj3+db+w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@mihomo-party/sysproxy-linux-arm64-gnu@1.0.1':
resolution: {integrity: sha512-NHzw7lI4oqANuYoU+QmwmEwntYDakkgYBZ1xf8JF48eIKoff48Nw0c3+lF57U0NNVy+s1xdPlZwmPABHe9fmhg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@mihomo-party/sysproxy-linux-x64-gnu@1.0.1':
resolution: {integrity: sha512-BYKTHGMxxxCMjaeUV5VH/Ikq1QMC0vJrQ9dX/LXnYN2++wL/bz8cKs3K2FgzUN2rczDj/7yQGmlhLqUgEQ6Wqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@mihomo-party/sysproxy-win32-arm64-msvc@1.0.1':
resolution: {integrity: sha512-d467s+fpXYUafJ1rEiqeCpbEC3iL25+3SXIReTg3264irq9H439+S/BBqENAoenq30UTp58r8f2Cr93LcsqLSQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@mihomo-party/sysproxy-win32-ia32-msvc@1.0.1':
resolution: {integrity: sha512-ZXtNYV+egkb0DdQrAfsEwUH25S4PRyuh9ziSW9TuqQW9d+La8SOMxPut9jbMmyFnKnbvXtcibqXxKpxDHPspoQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@mihomo-party/sysproxy-win32-x64-msvc@1.0.1':
resolution: {integrity: sha512-lJR8sIN3ciFdKs5A2oy+nKhiFMseDxPiP1tFDhFYwnzgqTCkZBTadH6km+TMP1VWseYT1QG/sDG6U2jVz126bg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@mihomo-party/sysproxy@1.0.1':
resolution: {integrity: sha512-VLuplU5WEsexJJZWcHut8zO/l8o1vZFpyisMhL1PwAaYwsTEOt4NGtkXrTB1HkNTvmsdhAFPk7EU7XTbs3QPmA==}
engines: {node: '>= 10'}
'@nextui-org/accordion@2.0.38':
resolution: {integrity: sha512-kFCZU1VaKkUI295Fg3NxuQR2+kZ5vTH4ftIs0oByrOs0+l14dVQGFOd9ZV402fHNykZJt7Sk6oWjTp4Qwl83JA==}
peerDependencies:
@ -3258,6 +3310,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
monaco-editor@0.44.0:
resolution: {integrity: sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@ -3554,6 +3609,13 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-monaco-editor@0.55.0:
resolution: {integrity: sha512-GdEP0Q3Rn1dczfKEEyY08Nes5plWwIYU4sWRBQO0+jsQWQsKMHKCC6+hPRwR7G/4aA3V/iU9jSmWPzVJYMVFSQ==}
peerDependencies:
'@types/react': '>=16 <= 18'
monaco-editor: ^0.44.0
react: '>=16 <= 18'
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@ -4674,6 +4736,37 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mihomo-party/sysproxy-darwin-arm64@1.0.1':
optional: true
'@mihomo-party/sysproxy-darwin-x64@1.0.1':
optional: true
'@mihomo-party/sysproxy-linux-arm64-gnu@1.0.1':
optional: true
'@mihomo-party/sysproxy-linux-x64-gnu@1.0.1':
optional: true
'@mihomo-party/sysproxy-win32-arm64-msvc@1.0.1':
optional: true
'@mihomo-party/sysproxy-win32-ia32-msvc@1.0.1':
optional: true
'@mihomo-party/sysproxy-win32-x64-msvc@1.0.1':
optional: true
'@mihomo-party/sysproxy@1.0.1':
optionalDependencies:
'@mihomo-party/sysproxy-darwin-arm64': 1.0.1
'@mihomo-party/sysproxy-darwin-x64': 1.0.1
'@mihomo-party/sysproxy-linux-arm64-gnu': 1.0.1
'@mihomo-party/sysproxy-linux-x64-gnu': 1.0.1
'@mihomo-party/sysproxy-win32-arm64-msvc': 1.0.1
'@mihomo-party/sysproxy-win32-ia32-msvc': 1.0.1
'@mihomo-party/sysproxy-win32-x64-msvc': 1.0.1
'@nextui-org/accordion@2.0.38(@nextui-org/system@2.2.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.7))(framer-motion@11.3.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@nextui-org/theme@2.2.9(tailwindcss@3.4.7))(framer-motion@11.3.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@nextui-org/aria-utils': 2.0.24(@nextui-org/theme@2.2.9(tailwindcss@3.4.7))(framer-motion@11.3.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -8574,6 +8667,8 @@ snapshots:
mkdirp@3.0.1: {}
monaco-editor@0.44.0: {}
ms@2.1.2: {}
ms@2.1.3: {}
@ -8847,6 +8942,13 @@ snapshots:
react-is@16.13.1: {}
react-monaco-editor@0.55.0(@types/react@18.3.3)(monaco-editor@0.44.0)(react@18.3.1):
dependencies:
'@types/react': 18.3.3
monaco-editor: 0.44.0
prop-types: 15.8.1
react: 18.3.1
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):

View File

@ -12,6 +12,11 @@ export function getAppConfig(force = false): IAppConfig {
}
export function setAppConfig(patch: Partial<IAppConfig>): void {
if (patch.sysProxy) {
const oldSysProxy = appConfig.sysProxy || {}
const newSysProxy = Object.assign(oldSysProxy, patch.sysProxy)
patch.sysProxy = newSysProxy
}
appConfig = Object.assign(appConfig, patch)
fs.writeFileSync(appConfigPath(), yaml.stringify(appConfig))
}

View File

@ -68,10 +68,16 @@ const buildContextMenu = (): Menu => {
checked: getAppConfig().sysProxy?.enable ?? false,
click: (item): void => {
const enable = item.checked
setAppConfig({ sysProxy: { enable } })
triggerSysProxy(enable)
window?.webContents.send('appConfigUpdated')
updateTrayMenu()
try {
triggerSysProxy(enable)
setAppConfig({ sysProxy: { enable } })
window?.webContents.send('appConfigUpdated')
} catch (e) {
setAppConfig({ sysProxy: { enable: !enable } })
console.error(e)
} finally {
updateTrayMenu()
}
}
},
{

View File

@ -2,6 +2,7 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { registerIpcMainHandlers } from './utils/cmds'
import { app, shell, BrowserWindow } from 'electron'
import { stopCore, startCore } from './core/manager'
import { triggerSysProxy } from './resolve/sysproxy'
import icon from '../../resources/icon.png?asset'
import { mihomoTraffic } from './core/mihomoApi'
import { createTray } from './core/tray'
@ -35,6 +36,7 @@ if (!gotTheLock) {
app.on('before-quit', () => {
stopCore()
triggerSysProxy(false)
app.exit()
})
@ -86,6 +88,10 @@ function createWindow(): void {
}
})
window.on('resize', () => {
window?.webContents.send('resize')
})
window.on('close', (event) => {
event.preventDefault()
window?.hide()

View File

@ -18,6 +18,9 @@ import {
import yaml from 'yaml'
import fs from 'fs'
import path from 'path'
import { startPacServer } from './server'
import { triggerSysProxy } from './sysproxy'
import { getAppConfig } from '../config'
function initDirs(): void {
if (!fs.existsSync(dataDir)) {
@ -64,4 +67,7 @@ export function init(): void {
initDirs()
initConfig()
initFiles()
startPacServer().then(() => {
triggerSysProxy(getAppConfig().sysProxy.enable)
})
}

View File

@ -0,0 +1,49 @@
import { getAppConfig, getControledMihomoConfig } from '../config'
import http from 'http'
import net from 'net'
export let pacPort: number
const defaultPacScript = `
function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}
`
function findAvailablePort(startPort: number): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer()
server.unref()
server.on('error', (err) => {
if (startPort <= 65535) {
resolve(findAvailablePort(startPort + 1))
} else {
reject(err)
}
})
server.listen(startPort, () => {
// 端口可用
server.close(() => {
resolve(startPort)
})
})
})
}
export async function startPacServer(): Promise<void> {
pacPort = await findAvailablePort(10000)
const server = http
.createServer((_req, res) => {
const {
sysProxy: { pacScript }
} = getAppConfig()
const { 'mixed-port': port = 7890 } = getControledMihomoConfig()
let script = pacScript || defaultPacScript
script = script.replaceAll('%mixed-port%', port.toString())
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })
res.end(script)
})
.listen(pacPort)
server.unref()
}

View File

@ -1,7 +1,49 @@
import { getControledMihomoConfig } from '../config'
import { triggerAutoProxy, triggerManualProxy } from '@mihomo-party/sysproxy'
import { getAppConfig, getControledMihomoConfig } from '../config'
import { pacPort } from './server'
let defaultBypass: string[]
if (process.platform === 'linux')
defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
if (process.platform === 'darwin')
defaultBypass = [
'127.0.0.1',
'192.168.0.0/16',
'10.0.0.0/8',
'172.16.0.0/12',
'localhost',
'*.local',
'*.crashlytics.com',
'<local>'
]
if (process.platform === 'win32')
defaultBypass = [
'localhost',
'127.*',
'192.168.*',
'10.*',
'172.16.*',
'172.17.*',
'172.18.*',
'172.19.*',
'172.20.*',
'172.21.*',
'172.22.*',
'172.23.*',
'172.24.*',
'172.25.*',
'172.26.*',
'172.27.*',
'172.28.*',
'172.29.*',
'172.30.*',
'172.31.*',
'<local>'
]
export function triggerSysProxy(enable: boolean): void {
if (enable) {
disableSysProxy()
enableSysProxy()
} else {
disableSysProxy()
@ -9,9 +51,29 @@ export function triggerSysProxy(enable: boolean): void {
}
export function enableSysProxy(): void {
console.log('enableSysProxy', getControledMihomoConfig()['mixed-port'])
const { sysProxy } = getAppConfig()
const { mode, host, bypass = defaultBypass } = sysProxy
const { 'mixed-port': port = 7890 } = getControledMihomoConfig()
switch (mode || 'manual') {
case 'auto': {
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
break
}
case 'manual': {
triggerManualProxy(
true,
host || '127.0.0.1',
port,
bypass.join(process.platform === 'win32' ? ';' : ',')
)
break
}
}
}
export function disableSysProxy(): void {
console.log('disableSysProxy')
triggerAutoProxy(false, '')
triggerManualProxy(false, '', 0, '')
}

View File

@ -15,8 +15,12 @@ const SysproxySwitcher: React.FC = () => {
const { enable } = sysProxy || {}
const onChange = async (enable: boolean): Promise<void> => {
await patchAppConfig({ sysProxy: { enable } })
await triggerSysProxy(enable)
try {
await triggerSysProxy(enable)
await patchAppConfig({ sysProxy: { enable } })
} catch (e) {
console.log(e)
}
}
return (

View File

@ -15,7 +15,6 @@ const TunSwitcher: React.FC = () => {
const { tun } = controledMihomoConfig || {}
const { enable } = tun || {}
console.log('controledMihomoConfig', controledMihomoConfig)
const onChange = async (enable: boolean): Promise<void> => {
await patchControledMihomoConfig({ tun: { enable } })
await patchMihomoConfig({ tun: { enable } })

View File

@ -0,0 +1,63 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
import React, { useState } from 'react'
import MonacoEditor, { monaco } from 'react-monaco-editor'
import { useTheme } from 'next-themes'
interface Props {
script: string
onCancel: () => void
onConfirm: (script: string) => void
}
const PacEditorViewer: React.FC<Props> = (props) => {
const { script, onCancel, onConfirm } = props
const [currData, setCurrData] = useState(script)
const { theme } = useTheme()
const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor): void => {
window.electron.ipcRenderer.on('resize', () => {
editor.layout()
})
}
const editorWillUnmount = (editor: monaco.editor.IStandaloneCodeEditor): void => {
window.electron.ipcRenderer.removeAllListeners('resize')
editor.dispose()
}
return (
<Modal size="5xl" hideCloseButton isOpen={true} scrollBehavior="inside">
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex">PAC脚本</ModalHeader>
<ModalBody className="h-full">
<MonacoEditor
height="100%"
language="javascript"
value={currData}
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
options={{
minimap: {
enabled: false
},
mouseWheelZoom: true,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"`,
fontLigatures: true, // 连字符
smoothScrolling: true // 平滑滚动
}}
editorDidMount={editorDidMount}
editorWillUnmount={editorWillUnmount}
onChange={(value) => setCurrData(value)}
/>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onCancel}>
</Button>
<Button color="primary" onPress={() => onConfirm(currData)}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default PacEditorViewer

View File

@ -1,7 +1,7 @@
import { Button, Switch } from '@nextui-org/react'
import BasePage from '@renderer/components/base/base-page'
import SettingCard from '@renderer/components/settings/setting-card'
import SettingItem from '@renderer/components/settings/setting-item'
import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
import { useAppConfig } from '@renderer/hooks/use-config'
import { checkAutoRun, enableAutoRun, disableAutoRun } from '@renderer/utils/ipc'
import { IoLogoGithub } from 'react-icons/io5'

View File

@ -1,5 +1,95 @@
import { Button, Input, Tab, Tabs } from '@nextui-org/react'
import BasePage from '@renderer/components/base/base-page'
import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
import PacEditorViewer from '@renderer/components/sysproxy/pac-editor-modal'
import { useAppConfig } from '@renderer/hooks/use-config'
import { triggerSysProxy } from '@renderer/utils/ipc'
import { Key, useState } from 'react'
import React from 'react'
const defaultPacScript = `
function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}
`
const Sysproxy: React.FC = () => {
return <div>Sysproxy</div>
const { appConfig, patchAppConfig } = useAppConfig()
const { sysProxy } = appConfig || { sysProxy: { enable: false } }
const [values, setValues] = useState<ISysProxyConfig>(sysProxy)
const [openPacEditor, setOpenPacEditor] = useState(false)
const onSave = async (): Promise<void> => {
// check valid TODO
await patchAppConfig({ sysProxy: values })
try {
await triggerSysProxy(true)
await patchAppConfig({ sysProxy: { enable: true } })
} catch (e) {
await patchAppConfig({ sysProxy: { enable: false } })
console.error(e)
}
}
return (
<BasePage
title="系统代理设置"
header={
<Button size="sm" color="primary" onPress={onSave}>
</Button>
}
>
{openPacEditor && (
<PacEditorViewer
script={values.pacScript || defaultPacScript}
onCancel={() => setOpenPacEditor(false)}
onConfirm={(script: string) => {
setValues({ ...values, pacScript: script })
setOpenPacEditor(false)
}}
/>
)}
<SettingCard>
<SettingItem title="代理主机" divider>
<Input
size="sm"
className="w-[50%]"
value={values.host}
placeholder="默认127.0.0.1若无特殊需求请勿修改"
onValueChange={(v) => {
setValues({ ...values, host: v })
}}
/>
</SettingItem>
<SettingItem title="代理模式" divider>
<Tabs
size="sm"
color="primary"
selectedKey={values.mode}
onSelectionChange={(key: Key) => setValues({ ...values, mode: key as SysProxyMode })}
>
<Tab
className={`select-none ${values.mode === 'manual' ? 'font-bold' : ''}`}
key="manual"
title="手动"
/>
<Tab
className={`select-none ${values.mode === 'auto' ? 'font-bold' : ''}`}
key="auto"
title="PAC"
/>
</Tabs>
</SettingItem>
<SettingItem title="代理模式">
<Button size="sm" onPress={() => setOpenPacEditor(true)} variant="bordered">
PAC脚本
</Button>
</SettingItem>
</SettingCard>
</BasePage>
)
}
export default Sysproxy

View File

@ -1,6 +1,6 @@
type OutboundMode = 'rule' | 'global' | 'direct'
type LogLevel = 'info' | 'debug' | 'warn' | 'error' | 'silent'
type SysProxyMode = 'auto' | 'manual'
interface IMihomoVersion {
version: string
meta: boolean
@ -65,7 +65,8 @@ interface IMihomoConnectionDetail {
interface ISysProxyConfig {
enable: boolean
mode?: 'auto' | 'manual'
host?: string
mode?: SysProxyMode
bypass?: string[]
pacScript?: string
}