mihomo-party/src/main/core/factory.ts

167 lines
4.7 KiB
TypeScript

import {
getControledMihomoConfig,
getProfileConfig,
getProfile,
getProfileItem,
getOverride,
getOverrideItem,
getOverrideConfig,
getAppConfig
} from '../config'
import {
mihomoProfileWorkDir,
mihomoWorkConfigPath,
mihomoWorkDir,
overridePath
} from '../utils/dirs'
import yaml from 'yaml'
import { copyFile, mkdir, writeFile } from 'fs/promises'
import { deepMerge } from '../utils/merge'
import vm from 'vm'
import { existsSync, writeFileSync } from 'fs'
import path from 'path'
let runtimeConfigStr: string
let runtimeConfig: IMihomoConfig
export async function generateProfile(): Promise<void> {
const { current } = await getProfileConfig()
const { diffWorkDir = false, controlDns = true, controlSniff = true, useNameserverPolicy } = await getAppConfig()
const currentProfile = await overrideProfile(current, await getProfile(current))
let controledMihomoConfig = await getControledMihomoConfig()
// 根据开关状态过滤控制配置
controledMihomoConfig = { ...controledMihomoConfig }
if (!controlDns) {
delete controledMihomoConfig.dns
delete controledMihomoConfig.hosts
}
if (!controlSniff) {
delete controledMihomoConfig.sniffer
}
if (!useNameserverPolicy) {
delete controledMihomoConfig?.dns?.['nameserver-policy']
}
const profile = deepMerge(currentProfile, controledMihomoConfig)
// 确保可以拿到基础日志信息
// 使用 debug 可以调试内核相关问题 `debug/pprof`
if (['info', 'debug'].includes(profile['log-level']) === false) {
profile['log-level'] = 'info'
}
runtimeConfig = profile
// 先正常生成 YAML 字符串
let yamlStr = yaml.stringify(profile)
// 还原科学记数法的引号
yamlStr = yamlStr.replace(
/(\w+:\s*)"(\d+E\d+)"(\s|$)/gi,
'$1$2$3'
)
runtimeConfigStr = yamlStr
if (diffWorkDir) {
await prepareProfileWorkDir(current)
}
await writeFile(
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
runtimeConfigStr
)
}
async function prepareProfileWorkDir(current: string | undefined): Promise<void> {
if (!existsSync(mihomoProfileWorkDir(current))) {
await mkdir(mihomoProfileWorkDir(current), { recursive: true })
}
const copy = async (file: string): Promise<void> => {
const targetPath = path.join(mihomoProfileWorkDir(current), file)
const sourcePath = path.join(mihomoWorkDir(), file)
if (!existsSync(targetPath) && existsSync(sourcePath)) {
await copyFile(sourcePath, targetPath)
}
}
await Promise.all([
copy('country.mmdb'),
copy('geoip.metadb'),
copy('geoip.dat'),
copy('geosite.dat'),
copy('ASN.mmdb')
])
}
async function overrideProfile(
current: string | undefined,
profile: IMihomoConfig
): Promise<IMihomoConfig> {
const { items = [] } = (await getOverrideConfig()) || {}
const globalOverride = items.filter((item) => item.global).map((item) => item.id)
const { override = [] } = (await getProfileItem(current)) || {}
for (const ov of new Set(globalOverride.concat(override))) {
const item = await getOverrideItem(ov)
const content = await getOverride(ov, item?.ext || 'js')
switch (item?.ext) {
case 'js':
profile = runOverrideScript(profile, content, item)
break
case 'yaml': {
let patch = yaml.parse(content, { merge: true }) || {}
if (typeof patch !== 'object') patch = {}
profile = deepMerge(profile, patch)
break
}
}
}
return profile
}
function runOverrideScript(
profile: IMihomoConfig,
script: string,
item: IOverrideItem
): IMihomoConfig {
const log = (type: string, data: string, flag = 'a'): void => {
writeFileSync(overridePath(item.id, 'log'), `[${type}] ${data}\n`, {
encoding: 'utf-8',
flag
})
}
try {
const ctx = {
console: Object.freeze({
log(data: never) {
log('log', JSON.stringify(data))
},
info(data: never) {
log('info', JSON.stringify(data))
},
error(data: never) {
log('error', JSON.stringify(data))
},
debug(data: never) {
log('debug', JSON.stringify(data))
}
})
}
vm.createContext(ctx)
const code = `${script} main(${JSON.stringify(profile)})`
log('info', '开始执行脚本', 'w')
const newProfile = vm.runInContext(code, ctx)
if (typeof newProfile !== 'object') {
throw new Error('脚本返回值必须是对象')
}
log('info', '脚本执行成功')
return newProfile
} catch (e) {
log('exception', `脚本执行失败:${e}`)
return profile
}
}
export async function getRuntimeConfigStr(): Promise<string> {
return runtimeConfigStr
}
export async function getRuntimeConfig(): Promise<IMihomoConfig> {
return runtimeConfig
}