/** * mihomo配置覆写脚本(全量版) * 作者:AIsouler * 源仓库:https://github.com/AIsouler/MyClash * 脚本链接:https://raw.githubusercontent.com/AIsouler/MyClash/main/Script/mihomoScript.js * 友情推荐,非常好用、省电且内存占用低的代理软件:https://github.com/appshubcc/Bettbox */ // --- 静态配置区域 --- // 适配 Bettbox 自定义配置参数 const Compatible_With_Bettbox = { ruleOptionsEnable: true }; /** * 自定义配置选项 * true = 启用 * false = 禁用 */ const ruleOptionsEnable = { // 基础策略组 手动选择: true, // 是否启用手动选择策略组 自动选择: true, // 是否启用自动选择策略组 负载均衡: true, // 是否启用负载均衡策略组 // 以下为分流策略配置 FCM: true, // GoogleFCM服务 YouTube: true, // YouTube视频平台 Google: true, // Google服务 AI: true, // 国外AI服务 Microsoft: true, // Microsoft服务 Apple: true, // Apple服务 Telegram: true, // Telegram通讯软件 Steam: true, // Steam游戏平台 TikTok: true, // TikTok视频平台 Twitter: true, // Twitter社交平台 Meta: true, // Meta服务 Line: true, // Line通讯软件 Netflix: true, // Netflix视频平台 Emby: true, // Emby媒体服务 PikPak: true, // PikPak网盘服务 Spotify: true, // Spotify音乐服务 Crypto: true, // 加密货币相关服务 PayPal: true, // PayPal支付服务 Patreon: true, // Patreon创作者赞助平台 EHentai: true, // E-Hentai网站 AdBlock: true, // 广告拦截 // 以下为非分流策略配置 极简模式: false, // 是否启用极简模式 生成地区自动选择组: true, // 是否生成地区自动选择策略组 隐藏地区手动选择组: false, // 是否隐藏地区手动选择策略组 生成倍率组: true, // 是否生成低倍率/高倍率策略组 分流组添加所有节点: false, // 是否为分流策略组添加所有节点 过滤低倍率节点: false, // 是否过滤低倍率节点 过滤高倍率节点: false, // 是否过滤高倍率节点 过滤非地区节点: true, // 是否过滤非地区节点 屏蔽国外QUIC: true, // 是否屏蔽国外QUIC流量 代理IPV4优先: false, // 是否将订阅节点统一为 IPv4 优先(与“代理IPV6优先”同时开启时不生效) 代理IPV6优先: false, // 是否将订阅节点统一为 IPv6 优先(与“代理IPV4优先”同时开启时不生效) }; // 定义前置规则 const prefixRules = [ // 私有网络直连 'RULE-SET,private,直连', // 国内直连 'RULE-SET,geolocation-cn,直连', 'RULE-SET,games_cn,直连', // 已包含 steam 下载域名 'RULE-SET,epicgames,直连', 'RULE-SET,nvidia_cn,直连', 'RULE-SET,apple_cn,直连', 'RULE-SET,microsoft_cn,直连', 'DOMAIN,fsend.cn,直连', 'DOMAIN,international-gfe.download.nvidia.com,直连', ]; // 此处添加自定义节点,填入下方[]内(可选,留空则不生成“自建节点”策略组) // 自定义节点不参与节点过滤与 hosts 改写;与订阅节点(标准化后)重名时自动添加“自建-”前缀 // 示例: // const customizeProxies = [ // { // name: '自建-日本-01', // type: 'vmess', // server: '5.6.7.8', // port: 443, // uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', // alterId: 0, // cipher: 'auto', // tls: true, // servername: 'example.com', // network: 'ws', // 'ws-opts': { // path: '/path', // headers: { Host: 'example.com' }, // }, // }, // ]; const customizeProxies = []; // 定义全局排除节点的正则表达式,用于排除非地区节点 const excludeFilter = /群|返利|循环|官网|客服|网站|网址|获取|订阅|流量|到期|机场|下次|版本|官址|备用|过期|已用|联系|邮箱|工单|贩卖|通知|倒卖|防止|国内|地址|频道|电报|无法|说明|提示|访问|支持|教程|关注|更新|作者|加入|超时|收藏|优惠|福利|邀请|好友|失联|选择|剩余|公益|发布|DIZTNA|通路|登录|禁止|定时|渠道|牢记|永久|余额|阁下|本站|刷新|导航|建议|重置|以下|过滤|⚠️|@|t\.me\/\+|\bexpire\b|\bhttps?:\/\/|\.com|\btraffic\b/iu; // 屏蔽国外QUIC const blockForeignQuic = [ 'AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((RULE-SET,cn_additional),(RULE-SET,cn_ip,no-resolve)))))),REJECT', ]; // 直连节点 const directProxies = [ { name: '🇨🇳 直连 | 双栈', type: 'direct', }, { name: '🇨🇳 直连 | IPv4优先', type: 'direct', 'ip-version': 'ipv4-prefer', }, { name: '🇨🇳 直连 | IPv6优先', type: 'direct', 'ip-version': 'ipv6-prefer', }, { name: '🇨🇳 直连 | 仅IPv4', type: 'direct', 'ip-version': 'ipv4', }, { name: '🇨🇳 直连 | 仅IPv6', type: 'direct', 'ip-version': 'ipv6', }, ]; // 图标 URL 公共前缀 const iconBaseUrl = 'https://cdn.jsdmirror.com/gh/AIsouler/MyClash@main/Icons/svg/'; // 规则集 URL 公共前缀 const ruleSetBaseUrl = 'https://cdn.jsdmirror.com/gh/appshubcc/bett-rules@meta/geo/'; // 定义地区策略组 const regionDefinitions = [ { name: '香港', flag: '🇭🇰', regex: /🇭🇰|香港|(? svc.name); // ---节点过滤、重命名及验证--- /** * 节点匹配缓存,避免重复执行正则 */ const regionMatchCache = new Map(); function getMatchedRegions(proxyName) { if (regionMatchCache.has(proxyName)) { return regionMatchCache.get(proxyName); } const regions = allRegionDefinitions.filter((region) => region.regex.test(proxyName)); regionMatchCache.set(proxyName, regions); return regions; } /** * 标准化节点名称:补全地区国旗、折叠多余空格,并预缓存匹配结果 */ const flagRegex = /[\u{1F1E6}-\u{1F1FF}]{2}/u; function normalizeProxyName(proxy) { const originalName = proxy.name; const flag = originalName.match(flagRegex)?.[0]; const nameWithoutFlag = (flag ? originalName.replace(flag, '') : originalName).replace(/\s+/g, ' ').trim(); const matchedRegions = getMatchedRegions(originalName); const regionFlag = flag || matchedRegions.find((region) => region.flag)?.flag; const normalizedName = regionFlag ? `${regionFlag} ${nameWithoutFlag}` : nameWithoutFlag; if (normalizedName !== originalName) { regionMatchCache.set(normalizedName, matchedRegions); } return normalizedName === originalName ? proxy : { ...proxy, name: normalizedName }; } /** * 修复 dialer-proxy 引用:目标被重命名则更新,被移除或不存在则删除引用 */ function fixDialerProxy(proxy, renameMap, normalizedProxyNames) { const target = proxy['dialer-proxy']; if (!target) return proxy; if (renameMap.has(target)) { return { ...proxy, 'dialer-proxy': renameMap.get(target) }; } if (normalizedProxyNames.has(target)) { return proxy; } const copy = { ...proxy }; delete copy['dialer-proxy']; return copy; } /** * 读取代理 IP 版本偏好:仅其中一个开关开启时返回对应偏好, * 同时开启或同时关闭时返回 null(不应用任何偏好,节点保持原样) */ function getIpVersionPreference() { const ipv4PreferEnabled = ruleOptionsEnable.代理IPV4优先; const ipv6PreferEnabled = ruleOptionsEnable.代理IPV6优先; if (ipv4PreferEnabled && !ipv6PreferEnabled) return 'ipv4-prefer'; if (ipv6PreferEnabled && !ipv4PreferEnabled) return 'ipv6-prefer'; return null; } /** * 过滤并标准化节点:剔除内置/信息节点、按配置过滤、去重、修复 dialer-proxy 引用,空列表时抛错 */ function filterAndNormalizeProxies(config) { regionMatchCache.clear(); const filterLowRateProxiesEnabled = ruleOptionsEnable.过滤低倍率节点; const filterHighRateProxiesEnabled = ruleOptionsEnable.过滤高倍率节点; const filterNonRegionProxiesEnabled = ruleOptionsEnable.过滤非地区节点; const lowRateRegex = filterLowRateProxiesEnabled ? rateRegionDefinitions.find((r) => r.name === lowRateRegionName)?.regex : null; const highRateRegex = filterHighRateProxiesEnabled ? rateRegionDefinitions.find((r) => r.name === highRateRegionName)?.regex : null; const originalProxies = config.proxies || []; const filteredRawProxies = originalProxies.filter((proxy) => { const type = String(proxy.type ?? '').toLowerCase(); if (type === 'direct' || type === 'reject' || type === 'rematch') return false; if (lowRateRegex?.test(proxy.name) || highRateRegex?.test(proxy.name)) return false; if (!filterNonRegionProxiesEnabled) return true; const isRegionProxy = getMatchedRegions(proxy.name).some((region) => regionDefinitions.includes(region)); return isRegionProxy || !excludeFilter.test(proxy.name); }); const renameMap = new Map(); const normalizedProxies = []; const uniqueNames = new Set(); for (const rawProxy of filteredRawProxies) { const normalized = normalizeProxyName(rawProxy); if (normalized.name !== rawProxy.name) { renameMap.set(rawProxy.name, normalized.name); } if (!uniqueNames.has(normalized.name)) { uniqueNames.add(normalized.name); normalizedProxies.push(normalized); } } const normalizedProxyNames = new Set(normalizedProxies.map((p) => p.name)); const filteredProxies = normalizedProxies.map((proxy) => fixDialerProxy(proxy, renameMap, normalizedProxyNames)); if (!filteredProxies.length) { throw new Error('配置文件中未找到任何代理节点,请使用机场提供的配置文件进行覆写'); } const ipVersionPreference = getIpVersionPreference(); if (ipVersionPreference) { return filteredProxies.map((proxy) => proxy['ip-version'] === ipVersionPreference ? proxy : { ...proxy, 'ip-version': ipVersionPreference }, ); } return filteredProxies; } // ---构建地区组和倍率组--- /** * 构建地区策略组,可附带自动选择组 */ function createRegionGroup(name, icon, proxies) { const generateRegionAutoSelectEnabled = ruleOptionsEnable.生成地区自动选择组; const hideManualSelectGroupEnabled = ruleOptionsEnable.隐藏地区手动选择组; if (generateRegionAutoSelectEnabled) { const urlTestName = `${name}-自动选择`; return [ { ...urlTestBaseOption, name: urlTestName, proxies, }, { ...selectBaseOption, name, icon, proxies: [...proxies, urlTestName], hidden: hideManualSelectGroupEnabled, }, ]; } return [ { ...selectBaseOption, name, icon, proxies, hidden: hideManualSelectGroupEnabled, }, ]; } /** * 将节点按地区/倍率归类,构建地区策略组、倍率策略组与“其他节点”组 */ function buildRegionGroups(filteredProxies, customProxies) { const generateRateGroupEnabled = ruleOptionsEnable.生成倍率组; const regionGroups = Object.fromEntries(allRegionDefinitions.map(({ name }) => [name, []])); const otherProxies = []; for (const proxy of [...filteredProxies, ...customProxies]) { const matchedRegions = getMatchedRegions(proxy.name); const isRegionProxy = matchedRegions.some((region) => regionDefinitions.includes(region)); for (const region of matchedRegions) { regionGroups[region.name].push(proxy.name); } if (!isRegionProxy) { otherProxies.push(proxy.name); } } const generatedRegionGroups = allRegionDefinitions .filter((r) => regionGroups[r.name].length > 0 && (generateRateGroupEnabled || !rateRegionDefinitions.includes(r))) .flatMap((r) => createRegionGroup(r.name, r.icon, regionGroups[r.name])); if (otherProxies.length > 0) { generatedRegionGroups.push(...createRegionGroup('其他节点', `${iconBaseUrl}WorldMap.svg`, otherProxies)); } return generatedRegionGroups; } // ---构建自定义节点组--- /** * 处理自定义节点:标准化名称、与订阅节点重名时添加“自建-”前缀、内部去重, * 并构建“自建节点”策略组。 * 自定义节点不参与订阅节点过滤,也不参与 hosts 改写及 DNS 域名处理。 */ function buildCustomizeGroups(filteredProxies, customizeList = customizeProxies) { if (!customizeList.length) { return { customProxies: [], customProxyNames: [], customGroup: null }; } const usedNames = new Set(filteredProxies.map((p) => p.name)); const customPrefix = '自建-'; const customProxies = []; for (const proxy of customizeList) { const normalized = normalizeProxyName(proxy); let name = normalized.name; while (usedNames.has(name)) { name = normalizeProxyName({ name: `${customPrefix}${name}` }).name.replace(`${customPrefix} `, customPrefix); } usedNames.add(name); customProxies.push(name === normalized.name ? normalized : { ...normalized, name }); } const customProxyNames = customProxies.map((p) => p.name); const customGroup = { ...selectBaseOption, name: '自建节点', proxies: customProxyNames, icon: `${iconBaseUrl}Server.svg`, }; return { customProxies, customProxyNames, customGroup, }; } // ---构建基础策略组和分流策略组--- /** * 构建基础/分流策略组/部分节点组、GLOBAL 组与规则集,并汇总分流规则 */ function buildFunctionalGroups(filteredProxies, generatedRegionGroups, customizeInfo) { const minimalModeEnabled = ruleOptionsEnable.极简模式; const blockForeignQuicEnabled = ruleOptionsEnable.屏蔽国外QUIC; const addAllNodesToServiceGroupsEnabled = ruleOptionsEnable.分流组添加所有节点; const hideManualSelectGroupEnabled = ruleOptionsEnable.隐藏地区手动选择组; const functionalGroups = []; const functionalRules = []; const finalRuleProviders = { ...baseRuleProviders }; if (!blockForeignQuicEnabled) { delete finalRuleProviders.cn_additional; } const { customProxyNames = [], customGroup = null } = customizeInfo || {}; const filteredProxyNames = filteredProxies.map((p) => p.name); const allProxiesNames = [...filteredProxyNames, ...customProxyNames]; const groupNamesOfSelect = generatedRegionGroups.filter((g) => g.type === 'select').map((g) => g.name); const baseGroupNames = baseGroups.filter((g) => ruleOptionsEnable[g.name]).map((g) => g.name); const customGroupNames = customGroup ? [customGroup.name] : []; if (minimalModeEnabled) { const defaultGroup = { ...selectBaseOption, name: '默认代理', proxies: allProxiesNames, icon: `${iconBaseUrl}Proxy.svg`, }; const finalRuleProviders = { ...baseRuleProviders }; if (!blockForeignQuicEnabled) delete finalRuleProviders.cn_additional; const directGroup = { ...selectBaseOption, name: '直连', proxies: [...directProxies.map((p) => p.name)], icon: `${iconBaseUrl}China.svg`, hidden: true, }; const globalGroup = { ...selectBaseOption, name: 'GLOBAL', proxies: ['默认代理', ...customGroupNames, '直连'], icon: `${iconBaseUrl}Global.svg`, }; return { globalGroup, functionalGroups: [defaultGroup], functionalRules: [], finalRuleProviders, directGroup, }; } functionalGroups.push({ ...selectBaseOption, name: '默认代理', proxies: [...groupNamesOfSelect, ...baseGroupNames, ...customGroupNames], icon: `${iconBaseUrl}Proxy.svg`, }); const orderedServiceConfigs = [ ...serviceConfigs.filter((svc) => svc.name === 'AdBlock'), ...serviceConfigs.filter((svc) => svc.name !== 'AdBlock'), ]; for (const svc of orderedServiceConfigs) { if (!ruleOptionsEnable[svc.name]) continue; functionalRules.push(...(svc.rules || [])); Object.assign(finalRuleProviders, svc.providers || {}); } for (const svc of serviceConfigs) { if (!ruleOptionsEnable[svc.name]) continue; let groupProxies = []; if (svc.includeAll) { groupProxies = [...allProxiesNames]; } else if (svc.reject) { groupProxies = ['REJECT', 'REJECT-DROP', 'PASS']; } else { groupProxies = !addAllNodesToServiceGroupsEnabled ? ['默认代理', ...customGroupNames, ...baseGroupNames, ...groupNamesOfSelect, ...(svc.direct ? ['直连'] : [])] : [ '默认代理', ...customGroupNames, ...baseGroupNames, ...groupNamesOfSelect, ...allProxiesNames, ...(svc.direct ? ['直连'] : []), ]; } functionalGroups.push({ ...svc.baseOption, name: svc.name, icon: svc.icon, proxies: groupProxies, ...(svc.defaultSelected !== undefined && { 'default-selected': svc.defaultSelected, }), }); } functionalGroups.push({ ...selectBaseOption, name: '漏网之鱼', proxies: ['默认代理', '直连', ...groupNamesOfSelect], icon: `${iconBaseUrl}Stack.svg`, }); const directGroup = { ...selectBaseOption, name: '直连', proxies: [...directProxies.map((p) => p.name)], icon: `${iconBaseUrl}China.svg`, hidden: hideManualSelectGroupEnabled, }; const globalGroup = { ...selectBaseOption, name: 'GLOBAL', proxies: [ ...functionalGroups.map((g) => g.name), ...customGroupNames, directGroup.name, ...generatedRegionGroups.map((g) => g.name), ], icon: `${iconBaseUrl}Global.svg`, }; return { globalGroup, functionalGroups, functionalRules, finalRuleProviders, directGroup }; } // ---dns和hosts相关处理--- // 常见的公共 DNS,用于过滤订阅中的公共 DNS const commonDnsList = [ // IPv4(国内) '223.5.5.5', '223.6.6.6', '119.29.29.29', '1.12.12.12', '120.53.53.53', '114.114.114.114', '180.76.76.76', '1.2.4.8', '116.116.116.116', '101.226.4.6', '123.125.81.6', '180.184.1.1', '180.184.2.2', // IPv6(国内) '2400:3200::1', '2400:3200:baba::1', '2402:4e00::', '2400:da00::6666', // IPv4(国外) '1.1.1.1', '1.0.0.1', '8.8.8.8', '8.8.4.4', '9.9.9.9', '149.112.112.112', '208.67.222.222', '208.67.220.220', '94.140.14.14', '94.140.15.15', '76.76.2.0', '76.76.10.0', '185.228.168.9', '185.228.169.9', '77.88.8.8', '77.88.8.1', '156.154.70.1', '156.154.71.1', // IPv6(国外) '2606:4700:4700::1111', '2606:4700:4700::1001', '2001:4860:4860::8888', '2001:4860:4860::8844', '2620:fe::fe', '2620:fe::9', '2620:119:35::35', '2620:119:53::53', '2a10:50c0::bad1:ff', '2a10:50c0::bad2:ff', '2a10:50c0::ad1:ff', '2a10:50c0::ad2:ff', '2a0d:2a00:1::2', '2a0d:2a00:2::2', '2a02:6b8::feed:0ff', '2a02:6b8:0:1::feed:0ff', '2610:a1:1018::1', '2610:a1:1019::1', // 关键词(国内) 'alidns', 'doh.pub', 'dot.pub', 'dns.pub', 'dnspod', 'dns.baidu', // 关键词(国外) 'dns.google', 'dns.cloudflare', 'dns.apple', 'cloudflare-dns', 'quad9', 'opendns', 'nextdns', 'adguard', 'one.one.one.one', ]; // 预编译公共 DNS 正则 const commonDnsRegex = new RegExp( commonDnsList.map((dns) => dns.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i', ); // 国内外 DNS 定义 const chinaDNS = ['223.5.5.5#DIRECT', '119.29.29.29#DIRECT']; const foreignDNS = ['https://cloudflare-dns.com/dns-query#默认代理', 'https://dns.google/dns-query#默认代理']; const defaultDNS = ['114.114.114.114#DIRECT', 'tls://223.5.5.5#DIRECT', 'https://1.12.12.12/dns-query#DIRECT']; const proxyServerDNS = ['114.114.114.114#DIRECT', 'tls://223.5.5.5#DIRECT', 'https://doh.pub/dns-query#DIRECT']; /** * hosts 匹配优先级:精确 > +. > . > *(同级按出现顺序) */ function hostSpecificity(pattern) { if (pattern.startsWith('+.')) return 2; if (pattern.startsWith('.')) return 1; if (pattern.includes('*')) return 0; return 3; } /** * 判断域名规则(精确/通配)是否匹配节点域名集合,忽略大小写 */ function matchDomainPattern(pattern, domains) { pattern = pattern.toLowerCase(); // 精确匹配 if (!pattern.includes('*') && !pattern.startsWith('+.') && !pattern.startsWith('.')) { return typeof domains === 'string' ? domains.toLowerCase() === pattern : [...domains].some((d) => d.toLowerCase() === pattern); } const domainList = typeof domains === 'string' ? [domains.toLowerCase()] : [...domains].map((d) => d.toLowerCase()); // +.example.com if (pattern.startsWith('+.')) { const suffix = pattern.slice(2); return domainList.some((domain) => domain === suffix || domain.endsWith(`.${suffix}`)); } // .example.com if (pattern.startsWith('.')) { const suffix = pattern.slice(1); return domainList.some((domain) => domain !== suffix && domain.endsWith(`.${suffix}`)); } // *.example.com、example.*.com 等 const patternParts = pattern.split('.'); return domainList.some((domain) => { const domainParts = domain.split('.'); return ( patternParts.length === domainParts.length && patternParts.every((part, index) => part === '*' || part === domainParts[index]) ); }); } /** * 根据订阅 hosts 映射改写节点 server,改写后无需再复制 hosts 进新配置。 * 支持链式映射(如 a: b、b: c 时节点 a 改写为 c); * 回环映射(a: b、b: a)由内核校验拒绝,此处仅以已访问集合防御性终止 */ function applyHostsToProxies(proxies, hosts) { if (!hosts || typeof hosts !== 'object') return proxies; const hostEntries = Object.entries(hosts) .filter( ([, value]) => (typeof value === 'string' && value.length > 0) || (Array.isArray(value) && value.length > 0), ) .sort((a, b) => hostSpecificity(b[0]) - hostSpecificity(a[0])); if (hostEntries.length === 0) return proxies; const targetOf = (value) => { if (Array.isArray(value)) value = value.find((v) => typeof v === 'string' && v.length > 0); return typeof value === 'string' && value.length > 0 ? value : null; }; const resolveCache = new Map(); const resolve = (server) => { const cached = resolveCache.get(server); if (cached !== undefined) return cached; const seen = new Set(); let current = server.toLowerCase(); let result = server; while (!seen.has(current)) { seen.add(current); const entry = hostEntries.find(([pattern]) => matchDomainPattern(pattern, current)); const target = entry && targetOf(entry[1]); if (!target) break; result = target; current = target.toLowerCase(); } resolveCache.set(server, result); return result; }; return proxies.map((proxy) => { if (typeof proxy.server !== 'string') return proxy; const server = resolve(proxy.server); return server === proxy.server ? proxy : { ...proxy, server }; }); } /** * 剥离 DNS 地址的 # 策略组后缀; * 参数包含 direct 或 直连 时,强制改为 #DIRECT */ function stripDnsSuffix(dns) { const str = String(dns); const hashIndex = str.indexOf('#'); if (hashIndex === -1) return str; const prefix = str.slice(0, hashIndex).trim(); const suffix = str .slice(hashIndex + 1) .toLowerCase() .trim(); if (suffix.includes('direct') || suffix.includes('直连')) return prefix + '#DIRECT'; return prefix; } /** * 判断节点 server 是否为 IP 地址(IPv4 / IPv6),用于从节点域名集合中排除 IP 类型的 server */ function isIpAddress(server) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(server) || server.includes(':'); } /** * 简化节点域名策略:将相同 DNS 的节点域名按后缀归类,至少三段的域名可合并为 +. 后缀形式 */ function simplifyDomainPolicy(policy) { const groups = new Map(); for (const [domain, dns] of Object.entries(policy)) { const dnsKey = JSON.stringify(Array.isArray(dns) ? [...dns].sort() : dns); if (domain.startsWith('+.') || domain.startsWith('.') || domain.includes('*')) { groups.set(`keep:${domain}`, [{ domain, dns, dnsKey }]); continue; } const parts = domain.split('.'); if (parts.length < 3) { groups.set(`keep:${domain}`, [{ domain, dns, dnsKey }]); continue; } const suffix = parts.slice(-2).join('.'); if (!groups.has(suffix)) { groups.set(suffix, []); } groups.get(suffix).push({ domain, dns, dnsKey }); } const result = {}; for (const [suffix, domains] of groups) { const firstDnsKey = domains[0].dnsKey; const sameDns = domains.every(({ dnsKey }) => dnsKey === firstDnsKey); if (domains.length >= 2 && sameDns) { result[`+.${suffix}`] = domains[0].dns; } else { for (const { domain, dns } of domains) { result[domain] = dns; } } } return result; } /** * 构建 DNS 与 hosts:保留私有 DNS、节点域名 policy/fake-ip-filter,并按 hosts 改写节点 server * hosts改写条件(满足任意一个条件即可): * 1. proxy-server-nameserver 有且仅有一个 DNS 并且该 DNS 包含非空的 listen 值 * 2. proxy-server-nameserver 有且仅有一个 DNS 并且该 DNS 包含 127.0.0.1 并且 listen 包含 0.0.0.0 */ function buildDnsAndHostsConfig(config, filteredProxies) { const minimalModeEnabled = ruleOptionsEnable.极简模式; const originalDnsConfig = config.dns || {}; const proxyServerNameservers = originalDnsConfig['proxy-server-nameserver'] || []; const listenValue = originalDnsConfig['listen']; const shouldRewriteByHosts = proxyServerNameservers.length === 1 && typeof listenValue === 'string' && listenValue.length > 0 && (proxyServerNameservers.some((dns) => String(dns).toLowerCase().includes(listenValue.toLowerCase())) || (listenValue.includes('0.0.0.0') && proxyServerNameservers.some((dns) => String(dns).toLowerCase().includes('127.0.0.1')))); const mappedProxies = shouldRewriteByHosts ? applyHostsToProxies(filteredProxies, config.hosts) : filteredProxies; const proxyDomains = new Set( mappedProxies .filter((proxy) => typeof proxy.server === 'string') .map((proxy) => proxy.server.toLowerCase()) .filter((server) => !isIpAddress(server)), ); const privateProxyServerNameservers = shouldRewriteByHosts ? [] : proxyServerNameservers; const isCommonDns = (dns) => { const value = String(dns).trim().toLowerCase(); if (value === 'system' || value === 'system://') return true; return commonDnsRegex.test(value); }; const privateDNS = [ ...new Set( [...(originalDnsConfig['nameserver'] || []), ...privateProxyServerNameservers] .map(stripDnsSuffix) .filter((dns) => dns.length > 0 && !isCommonDns(dns)), ), ]; const matchedProxyPolicy = {}; for (const [domain, dns] of Object.entries({ ...originalDnsConfig['nameserver-policy'], ...originalDnsConfig['proxy-server-nameserver-policy'], })) { if (!matchDomainPattern(domain, proxyDomains)) continue; const stripedDns = Array.isArray(dns) ? dns.map(stripDnsSuffix).filter((d) => d.length > 0) : stripDnsSuffix(dns); if (Array.isArray(stripedDns) && stripedDns.length === 0) continue; matchedProxyPolicy[domain] = stripedDns; } if (privateDNS.length > 0 && Object.keys(matchedProxyPolicy).length === 0) { for (const domain of proxyDomains) { matchedProxyPolicy[domain] = privateDNS; } } const matchedPolicyDomains = Object.keys(matchedProxyPolicy); const proxyServerPolicy = proxyDomains.size === matchedPolicyDomains.length && matchedPolicyDomains.every((domain) => proxyDomains.has(domain.toLowerCase())) ? simplifyDomainPolicy(matchedProxyPolicy) : matchedProxyPolicy; const originalFakeIpFilter = originalDnsConfig['fake-ip-filter'] || []; const proxyFakeIpFilter = originalFakeIpFilter.filter((pattern) => { const p = String(pattern); return matchDomainPattern(p, proxyDomains); }); const dns = { enable: true, ipv6: true, 'use-hosts': true, 'cache-algorithm': 'arc', 'use-system-hosts': true, 'enhanced-mode': 'fake-ip', 'fake-ip-range': '198.18.0.1/15', 'fake-ip-range6': '2001:2::1/48', 'fake-ip-filter': [ 'rule-set:private', 'rule-set:fakeip_filter', 'rule-set:geolocation-cn', ...(minimalModeEnabled ? [] : ruleOptionsEnable['FCM'] ? ['rule-set:googlefcm'] : []), ...proxyFakeIpFilter, ], 'default-nameserver': defaultDNS, 'proxy-server-nameserver': proxyServerDNS, ...(Object.keys(proxyServerPolicy).length > 0 && { 'proxy-server-nameserver-policy': proxyServerPolicy, }), nameserver: foreignDNS, 'nameserver-policy': { 'rule-set:private': 'system', 'rule-set:douyin': ['system', '180.184.1.1', '180.184.2.2'], 'rule-set:cn': chinaDNS, }, 'direct-nameserver': chinaDNS, 'direct-nameserver-follow-policy': true, }; const hosts = { 'doh.pub': ['1.12.12.12', '120.53.53.53'], 'cloudflare-dns.com': ['1.1.1.1', '1.0.0.1'], 'dns.google': ['8.8.8.8', '8.8.4.4'], // 解决谷歌商店无法下载的问题 'services.googleapis.cn': 'services.googleapis.com', // 屏蔽哔哩哔哩PCDN,解决访问视频/直播卡顿问题 '+.mcdn.bilivideo.com': ['0.0.0.0'], '+.mcdn.bilivideo.cn': ['0.0.0.0'], '+.edge.mountaintoys.cn': ['0.0.0.0'], '+.h2.smtcdns.net': ['0.0.0.0'], }; return { dns, hosts, proxies: mappedProxies }; } // --- 主入口 --- /** * 主入口:覆写机场订阅配置,生成完整 mihomo 配置 */ function main(config) { if (config['proxy-providers'] && Object.keys(config['proxy-providers']).length > 0) { throw new Error('配置文件中包含 proxy-providers,请使用机场提供的配置文件进行覆写'); } const newConfig = {}; const filteredProxies = filterAndNormalizeProxies(config); const { customProxies, customProxyNames, customGroup } = buildCustomizeGroups(filteredProxies); const generatedRegionGroups = ruleOptionsEnable.极简模式 ? [] : buildRegionGroups(filteredProxies, customProxies); const { globalGroup, functionalGroups, functionalRules, finalRuleProviders, directGroup } = buildFunctionalGroups( filteredProxies, generatedRegionGroups, { customProxyNames, customGroup }, ); const { dns, hosts, proxies: mappedProxies } = buildDnsAndHostsConfig(config, filteredProxies); newConfig['dns'] = dns; newConfig['hosts'] = hosts; newConfig['mixed-port'] = 7890; newConfig['allow-lan'] = true; newConfig['ipv6'] = true; newConfig['mode'] = 'rule'; newConfig['log-level'] = 'info'; newConfig['bind-address'] = '*'; newConfig['unified-delay'] = true; newConfig['tcp-concurrent'] = true; newConfig['keep-alive-interval'] = 60; newConfig['find-process-mode'] = 'strict'; newConfig['external-controller'] = '127.0.0.1:9090'; newConfig['external-ui'] = 'ui'; newConfig['external-ui-url'] = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip'; newConfig['profile'] = { 'store-selected': true, 'store-fake-ip': true, }; newConfig['ntp'] = { enable: true, 'write-to-system': false, server: 'ntp.aliyun.com', port: 123, interval: 60, }; newConfig['tun'] = { enable: true, stack: 'mips', 'auto-route': true, 'strict-route': true, 'auto-redirect': true, 'auto-detect-interface': true, 'dns-hijack': ['any:53', 'tcp://any:53'], }; newConfig['proxies'] = [...mappedProxies, ...customProxies, ...directProxies]; newConfig['proxy-groups'] = [ globalGroup, ...functionalGroups, ...(customGroup ? [customGroup] : []), directGroup, ...generatedRegionGroups, ]; newConfig['rule-providers'] = finalRuleProviders; newConfig['rules'] = [ ...prefixRules, ...(ruleOptionsEnable.屏蔽国外QUIC ? blockForeignQuic : []), ...functionalRules, // 兜底规则 'RULE-SET,geolocation-!cn,默认代理', 'RULE-SET,cn_ip,直连', 'RULE-SET,private_ip,直连', `MATCH,${ruleOptionsEnable.极简模式 ? '默认代理' : '漏网之鱼'}`, ]; return newConfig; }