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 { 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 { if (!existsSync(mihomoProfileWorkDir(current))) { await mkdir(mihomoProfileWorkDir(current), { recursive: true }) } const copy = async (file: string): Promise => { 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 { 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 { return runtimeConfigStr } export async function getRuntimeConfig(): Promise { return runtimeConfig }