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

254 lines
7.3 KiB
TypeScript

import {
getControledMihomoConfig,
getProfileConfig,
getProfile,
getProfileItem,
getOverride,
getOverrideItem,
getOverrideConfig,
getAppConfig
} from '../config'
import {
mihomoProfileWorkDir,
mihomoWorkConfigPath,
mihomoWorkDir,
overridePath,
rulePath
} from '../utils/dirs'
import { parse, stringify } from '../utils/yaml'
import { copyFile, mkdir, writeFile, readFile } from 'fs/promises'
import { deepMerge } from '../utils/merge'
import vm from 'vm'
import { existsSync, writeFileSync } from 'fs'
import path from 'path'
import { createLogger } from '../utils/logger'
const factoryLogger = createLogger('Factory')
let runtimeConfigStr: string = ''
let runtimeConfig: IMihomoConfig = {} as IMihomoConfig
// 辅助函数:处理带偏移量的规则
function processRulesWithOffset(ruleStrings: string[], currentRules: string[], isAppend = false) {
const normalRules: string[] = []
const rules = [...currentRules]
ruleStrings.forEach((ruleStr) => {
const parts = ruleStr.split(',')
const firstPartIsNumber =
!isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3
if (firstPartIsNumber) {
const offset = parseInt(parts[0])
const rule = parts.slice(1).join(',')
if (isAppend) {
// 后置规则的插入位置计算
const insertPosition = Math.max(0, rules.length - Math.min(offset, rules.length))
rules.splice(insertPosition, 0, rule)
} else {
// 前置规则的插入位置计算
const insertPosition = Math.min(offset, rules.length)
rules.splice(insertPosition, 0, rule)
}
} else {
normalRules.push(ruleStr)
}
})
return { normalRules, insertRules: rules }
}
export async function generateProfile(): Promise<string | undefined> {
// 读取最新的配置
const { current } = await getProfileConfig(true)
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']
}
// 应用规则文件
try {
const ruleFilePath = rulePath(current || 'default')
if (existsSync(ruleFilePath)) {
const ruleFileContent = await readFile(ruleFilePath, 'utf-8')
const ruleData = parse(ruleFileContent) as {
prepend?: string[]
append?: string[]
delete?: string[]
} | null
if (ruleData && typeof ruleData === 'object') {
// 确保 rules 数组存在
if (!currentProfile.rules) {
currentProfile.rules = [] as unknown as []
}
let rules = [...currentProfile.rules] as unknown as string[]
// 处理前置规则
if (ruleData.prepend?.length) {
const { normalRules: prependRules, insertRules } = processRulesWithOffset(
ruleData.prepend,
rules
)
rules = [...prependRules, ...insertRules]
}
// 处理后置规则
if (ruleData.append?.length) {
const { normalRules: appendRules, insertRules } = processRulesWithOffset(
ruleData.append,
rules,
true
)
rules = [...insertRules, ...appendRules]
}
// 处理删除规则
if (ruleData.delete?.length) {
const deleteSet = new Set(ruleData.delete)
rules = rules.filter((rule) => {
const ruleStr = Array.isArray(rule) ? rule.join(',') : rule
return !deleteSet.has(ruleStr)
})
}
currentProfile.rules = rules as unknown as []
}
}
} catch (error) {
factoryLogger.error('Failed to read or apply rule file', error)
}
const profile = deepMerge(currentProfile, controledMihomoConfig)
// 确保可以拿到基础日志信息
// 使用 debug 可以调试内核相关问题 `debug/pprof`
if (['info', 'debug'].includes(profile['log-level']) === false) {
profile['log-level'] = 'info'
}
runtimeConfig = profile
runtimeConfigStr = stringify(profile)
if (diffWorkDir) {
await prepareProfileWorkDir(current)
}
await writeFile(
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
runtimeConfigStr
)
return current
}
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 = parse(content) || {}
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
}