Skip to content

Commit 7785c22

Browse files
author
pompurin404
committed
support profile override script
1 parent d434352 commit 7785c22

17 files changed

+817
-19
lines changed

src/main/config/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,14 @@ export {
1414
changeCurrentProfile,
1515
updateProfileItem
1616
} from './profile'
17+
export {
18+
getOverrideConfig,
19+
setOverrideConfig,
20+
getOverrideItem,
21+
addOverrideItem,
22+
removeOverrideItem,
23+
createOverride,
24+
getOverride,
25+
setOverride,
26+
updateOverrideItem
27+
} from './override'

src/main/config/override.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { overrideConfigPath, overridePath } from '../utils/dirs'
2+
import yaml from 'yaml'
3+
import fs from 'fs'
4+
import { dialog } from 'electron'
5+
import axios from 'axios'
6+
import { getControledMihomoConfig } from './controledMihomo'
7+
8+
let overrideConfig: IOverrideConfig // override.yaml
9+
10+
export function getOverrideConfig(force = false): IOverrideConfig {
11+
if (force || !overrideConfig) {
12+
overrideConfig = yaml.parse(fs.readFileSync(overrideConfigPath(), 'utf-8'))
13+
}
14+
return overrideConfig
15+
}
16+
17+
export function setOverrideConfig(config: IOverrideConfig): void {
18+
overrideConfig = config
19+
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
20+
}
21+
22+
export function getOverrideItem(id: string): IOverrideItem | undefined {
23+
return overrideConfig.items.find((item) => item.id === id)
24+
}
25+
export function updateOverrideItem(item: IOverrideItem): void {
26+
const index = overrideConfig.items.findIndex((i) => i.id === item.id)
27+
overrideConfig.items[index] = item
28+
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
29+
}
30+
31+
export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> {
32+
const newItem = await createOverride(item)
33+
if (overrideConfig.items.find((i) => i.id === newItem.id)) {
34+
updateOverrideItem(newItem)
35+
} else {
36+
overrideConfig.items.push(newItem)
37+
}
38+
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
39+
}
40+
41+
export function removeOverrideItem(id: string): void {
42+
overrideConfig.items = overrideConfig.items?.filter((item) => item.id !== id)
43+
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
44+
fs.rmSync(overridePath(id))
45+
}
46+
47+
export async function createOverride(item: Partial<IOverrideItem>): Promise<IOverrideItem> {
48+
const id = item.id || new Date().getTime().toString(16)
49+
const newItem = {
50+
id,
51+
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
52+
type: item.type,
53+
url: item.url,
54+
updated: new Date().getTime()
55+
} as IOverrideItem
56+
switch (newItem.type) {
57+
case 'remote': {
58+
if (!item.url) {
59+
dialog.showErrorBox(
60+
'URL is required for remote script',
61+
'URL is required for remote script'
62+
)
63+
throw new Error('URL is required for remote script')
64+
}
65+
try {
66+
const res = await axios.get(item.url, {
67+
proxy: {
68+
protocol: 'http',
69+
host: '127.0.0.1',
70+
port: getControledMihomoConfig()['mixed-port'] || 7890
71+
},
72+
responseType: 'text'
73+
})
74+
const data = res.data
75+
setOverride(id, data)
76+
} catch (e) {
77+
dialog.showErrorBox('Failed to fetch remote script', `${e}\nurl: ${item.url}`)
78+
throw new Error(`Failed to fetch remote script ${e}`)
79+
}
80+
break
81+
}
82+
case 'local': {
83+
if (!item.file) {
84+
dialog.showErrorBox(
85+
'File is required for local script',
86+
'File is required for local script'
87+
)
88+
throw new Error('File is required for local script')
89+
}
90+
const data = item.file
91+
setOverride(id, data)
92+
break
93+
}
94+
}
95+
96+
return newItem
97+
}
98+
99+
export function getOverride(id: string): string {
100+
if (!fs.existsSync(overridePath(id))) {
101+
return `function main(config){ return config }`
102+
}
103+
return fs.readFileSync(overridePath(id), 'utf-8')
104+
}
105+
106+
export function setOverride(id: string, content: string): void {
107+
fs.writeFileSync(overridePath(id), content, 'utf-8')
108+
}

src/main/resolve/factory.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { getControledMihomoConfig, getProfileConfig, getProfile } from '../config'
1+
import {
2+
getControledMihomoConfig,
3+
getProfileConfig,
4+
getProfile,
5+
getProfileItem,
6+
getOverride
7+
} from '../config'
28
import { mihomoWorkConfigPath } from '../utils/dirs'
39
import yaml from 'yaml'
410
import fs from 'fs'
511

612
export function generateProfile(): void {
713
const current = getProfileConfig().current
8-
const currentProfile = getProfile(current)
14+
const currentProfile = overrideProfile(current, getProfile(current))
915
const controledMihomoConfig = getControledMihomoConfig()
1016
const { tun: profileTun = {} } = currentProfile
1117
const { tun: controledTun } = controledMihomoConfig
@@ -22,3 +28,25 @@ export function generateProfile(): void {
2228
profile.sniffer = sniffer
2329
fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile))
2430
}
31+
32+
function overrideProfile(current: string | undefined, profile: IMihomoConfig): IMihomoConfig {
33+
const overrideScriptList = getProfileItem(current).override || []
34+
for (const override of overrideScriptList) {
35+
const script = getOverride(override)
36+
profile = runOverrideScript(profile, script)
37+
}
38+
return profile
39+
}
40+
41+
function runOverrideScript(profile: IMihomoConfig, script: string): IMihomoConfig {
42+
try {
43+
const func = eval(`${script} main`)
44+
const newProfile = func(profile)
45+
if (typeof newProfile !== 'object') {
46+
throw new Error('Override script must return an object')
47+
}
48+
return newProfile
49+
} catch (e) {
50+
return profile
51+
}
52+
}

src/main/resolve/init.ts

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
logDir,
66
mihomoTestDir,
77
mihomoWorkDir,
8+
overrideConfigPath,
9+
overrideDir,
810
profileConfigPath,
911
profilePath,
1012
profilesDir,
@@ -13,6 +15,7 @@ import {
1315
import {
1416
defaultConfig,
1517
defaultControledMihomoConfig,
18+
defaultOverrideConfig,
1619
defaultProfile,
1720
defaultProfileConfig
1821
} from '../utils/template'
@@ -31,6 +34,9 @@ function initDirs(): void {
3134
if (!fs.existsSync(profilesDir())) {
3235
fs.mkdirSync(profilesDir())
3336
}
37+
if (!fs.existsSync(overrideDir())) {
38+
fs.mkdirSync(overrideDir())
39+
}
3440
if (!fs.existsSync(mihomoWorkDir())) {
3541
fs.mkdirSync(mihomoWorkDir())
3642
}
@@ -49,6 +55,9 @@ function initConfig(): void {
4955
if (!fs.existsSync(profileConfigPath())) {
5056
fs.writeFileSync(profileConfigPath(), yaml.stringify(defaultProfileConfig))
5157
}
58+
if (!fs.existsSync(overrideConfigPath())) {
59+
fs.writeFileSync(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
60+
}
5261
if (!fs.existsSync(profilePath('default'))) {
5362
fs.writeFileSync(profilePath('default'), yaml.stringify(defaultProfile))
5463
}

src/main/utils/dirs.ts

+12
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ export function profilePath(id: string): string {
5353
return path.join(profilesDir(), `${id}.yaml`)
5454
}
5555

56+
export function overrideDir(): string {
57+
return path.join(dataDir, 'override')
58+
}
59+
60+
export function overrideConfigPath(): string {
61+
return path.join(dataDir, 'override.yaml')
62+
}
63+
64+
export function overridePath(id: string): string {
65+
return path.join(overrideDir(), `${id}.js`)
66+
}
67+
5668
export function mihomoWorkDir(): string {
5769
return path.join(dataDir, 'work')
5870
}

src/main/utils/ipc.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ import {
3434
getProfileStr,
3535
setProfileStr,
3636
updateProfileItem,
37-
setProfileConfig
37+
setProfileConfig,
38+
getOverrideConfig,
39+
setOverrideConfig,
40+
getOverrideItem,
41+
addOverrideItem,
42+
removeOverrideItem,
43+
getOverride,
44+
setOverride,
45+
updateOverrideItem
3846
} from '../config'
3947
import { isEncryptionAvailable, restartCore } from '../core/manager'
4048
import { triggerSysProxy } from '../resolve/sysproxy'
@@ -81,11 +89,19 @@ export function registerIpcMainHandlers(): void {
8189
ipcMain.handle('changeCurrentProfile', (_e, id) => changeCurrentProfile(id))
8290
ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item))
8391
ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id))
92+
ipcMain.handle('getOverrideConfig', (_e, force) => getOverrideConfig(force))
93+
ipcMain.handle('setOverrideConfig', (_e, config) => setOverrideConfig(config))
94+
ipcMain.handle('getOverrideItem', (_e, id) => getOverrideItem(id))
95+
ipcMain.handle('addOverrideItem', (_e, item) => addOverrideItem(item))
96+
ipcMain.handle('removeOverrideItem', (_e, id) => removeOverrideItem(id))
97+
ipcMain.handle('updateOverrideItem', (_e, item) => updateOverrideItem(item))
98+
ipcMain.handle('getOverride', (_e, id) => getOverride(id))
99+
ipcMain.handle('setOverride', (_e, id, str) => setOverride(id, str))
84100
ipcMain.handle('restartCore', restartCore)
85101
ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable))
86102
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
87103
ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str))
88-
ipcMain.handle('getFilePath', getFilePath)
104+
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
89105
ipcMain.handle('readTextFile', (_e, filePath) => readTextFile(filePath))
90106
ipcMain.handle('getRuntimeConfigStr', getRuntimeConfigStr)
91107
ipcMain.handle('getRuntimeConfig', getRuntimeConfig)
@@ -97,10 +113,10 @@ export function registerIpcMainHandlers(): void {
97113
ipcMain.handle('quitApp', () => app.quit())
98114
}
99115

100-
function getFilePath(): string[] | undefined {
116+
function getFilePath(ext: string[]): string[] | undefined {
101117
return dialog.showOpenDialogSync({
102118
title: '选择订阅文件',
103-
filters: [{ name: 'Yaml Files', extensions: ['yml', 'yaml'] }],
119+
filters: [{ name: 'Yaml Files', extensions: ext }],
104120
properties: ['openFile']
105121
})
106122
}

src/main/utils/template.ts

+4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export const defaultProfileConfig: IProfileConfig = {
9494
items: []
9595
}
9696

97+
export const defaultOverrideConfig: IOverrideConfig = {
98+
items: []
99+
}
100+
97101
export const defaultProfile: Partial<IMihomoConfig> = {
98102
proxies: [],
99103
'proxy-groups': [],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
2+
import React, { useEffect, useState } from 'react'
3+
import { BaseEditor } from '../base/base-editor'
4+
import { getOverride, setOverride } from '@renderer/utils/ipc'
5+
interface Props {
6+
id: string
7+
onClose: () => void
8+
}
9+
const EditFileModal: React.FC<Props> = (props) => {
10+
const { id, onClose } = props
11+
const [currData, setCurrData] = useState('')
12+
13+
const getContent = async (): Promise<void> => {
14+
setCurrData(await getOverride(id))
15+
}
16+
17+
useEffect(() => {
18+
getContent()
19+
}, [])
20+
21+
return (
22+
<Modal
23+
backdrop="blur"
24+
size="5xl"
25+
hideCloseButton
26+
isOpen={true}
27+
onOpenChange={onClose}
28+
scrollBehavior="inside"
29+
>
30+
<ModalContent className="h-full w-[calc(100%-100px)]">
31+
<ModalHeader className="flex">编辑覆写脚本</ModalHeader>
32+
<ModalBody className="h-full">
33+
<BaseEditor
34+
language="javascript"
35+
value={currData}
36+
onChange={(value) => setCurrData(value)}
37+
/>
38+
</ModalBody>
39+
<ModalFooter>
40+
<Button variant="light" onPress={onClose}>
41+
取消
42+
</Button>
43+
<Button
44+
color="primary"
45+
onPress={async () => {
46+
await setOverride(id, currData)
47+
onClose()
48+
}}
49+
>
50+
确认
51+
</Button>
52+
</ModalFooter>
53+
</ModalContent>
54+
</Modal>
55+
)
56+
}
57+
58+
export default EditFileModal

0 commit comments

Comments
 (0)