// ==================== // 0. 特殊域名处理(手动添加) // ==================== // 强制直连 const BYPASS_DOMAINS = [ "example.com", "example.org" ]; // 强制代理 const FORCE_PROXY_DOMAINS = [ "test.com", "test.org" ]; // ==================== // 1. 常量配置 // ==================== const SETTINGS = { ICON_BASE: "https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/", RULE_PROVIDER_URL_BASE: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo", REGION_ORDER:["HK", "TW", "SG", "JP", "KR", "AS", "US"], URL_TEST_EXTRA: { hidden: true, url: "https://www.g.cn/generate_204", interval: 900, tolerance: 75, lazy: true, timeout: 750, "max-failed-times": 1 }, FILTER_REGEX: /群|邀请|返利|官网|官方|网址|订阅|购买|续费|剩余|到期|过期|流量|备用|邮箱|客服|联系|工单|倒卖|防止|梯子|tg|telegram|电报|发布|重置/i }; // ==================== // 2. 基础工具 // ==================== const uniq = (arr = []) => [...new Set(arr.filter(Boolean))]; const escapeRegex = (s = "") => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const normalizeName = (name = "") => String(name) .replace(/(IEPL|IPLC|BGP|RELAY|PRO|V\d+)/ig, " $1 ") .replace(/[【】\[\]()()|_\-.,/:~]/g, " ") .replace(/🇭🇰/g, " HK ") .replace(/🇹🇼/g, " TW ") .replace(/🇸🇬/g, " SG ") .replace(/🇯🇵/g, " JP ") .replace(/🇰🇷/g, " KR ") .replace(/🇻🇳|🇹🇭|🇲🇾|🇮🇩|🇵🇭/g, " AS ") .replace(/🇺🇸/g, " US ") .toUpperCase() .replace(/\s+/g, " ") .trim(); const buildRegex = (arr =[]) => new RegExp( arr .map((raw) => { const token = String(raw).trim().toUpperCase(); const escaped = escapeRegex(token); return /^[A-Z]{2,3}$/.test(token) ? `(?:^|[^A-Z])${escaped}(?:[^A-Z]|$)` : escaped; }) .join("|"), "i" ); const buildRegions = () => ([ { name: "HK", pattern:["香港", "HK", "HKG", "HONGKONG", "HONG KONG"], icon: "Hong_Kong.png" }, { name: "TW", pattern:["台湾", "台北", "新北", "TW", "TWN", "TAIWAN", "TAIPEI"], icon: "Taiwan.png" }, { name: "SG", pattern:["新加坡", "狮城", "SG", "SGP", "SINGAPORE"], icon: "Singapore.png" }, { name: "JP", pattern:["日本", "东京", "大阪", "JP", "JPN", "JAPAN", "TOKYO", "OSAKA"], icon: "Japan.png" }, { name: "KR", pattern:["韩国", "首尔", "KR", "KOR", "KOREA", "SEOUL"], icon: "Korea.png" }, { name: "AS", pattern:[ "越南", "泰国", "马来西亚", "印尼", "菲律宾", "VN", "TH", "MY", "ID", "PH", "VIETNAM", "THAILAND", "MALAYSIA", "INDONESIA", "PHILIPPINES", "MANILA" ], icon: "Available.png" }, { name: "US", pattern:[ "美国", "纽约", "旧金山", "洛杉矶", "西雅图", "芝加哥", "US", "USA", "NEWYORK", "NEW YORK", "SANFRANCISCO", "SAN FRANCISCO", "LOSANGELES", "LOS ANGELES", "SEATTLE", "CHICAGO" ], icon: "United_States.png" } ]).map((r) => ({ ...r, regex: buildRegex(r.pattern) })); const REGIONS = buildRegions(); const buildFakeIpFilter = (bypass = []) => uniq([ "rule-set:private", "rule-set:google-cn", "rule-set:synology", "rule-set:cn", ...uniq( bypass.flatMap((domain) => { const d = String(domain || "").trim(); if (!d) return []; return d.includes("*") || d.startsWith("+.") ?[d] : [`+.${d}`]; }) ) ]); const mergeRules = (baseRules = [], extraRules =[]) => { const extra = Array.isArray(extraRules) ? extraRules.filter(Boolean) :[]; if (!extra.length) return baseRules.slice(); const matchIndex = baseRules.findIndex( (rule) => String(rule).trim().toUpperCase() === "MATCH,main" ); if (matchIndex === -1) return uniq([...baseRules, ...extra]); return uniq([ ...baseRules.slice(0, matchIndex), ...extra, ...baseRules.slice(matchIndex) ]); }; const pickDirectRules = (rules =[]) => rules.filter((rule) => { const r = String(rule || "").trim(); if (!r || r.startsWith("#")) return false; return /,DIRECT(?:,|$)/i.test(r); }); // ==================== // 3. 规则集与固定规则 // ==================== const RULE_PROVIDERS_DOMAINS = [ "category-ads-all", "private", "google-cn", "synology", "microsoft@cn", "category-game-platforms-download@cn", "category-ai-!cn", "telegram", "gfw", "cn", "googlefcm", "epicgames", "nvidia@cn", "cloudflare@cn", "steam@cn", "category-ntp", "connectivity-check", "apple", "spotify", "microsoft" ]; const RULE_PROVIDERS_IPS = [ "private", "cn" ]; const buildRuleProviders = () => { const providers = {}; RULE_PROVIDERS_DOMAINS.forEach(name => { providers[name] = { type: "http", behavior: "domain", format: "mrs", path: `./rules/${name}.mrs`, url: `${SETTINGS.RULE_PROVIDER_URL_BASE}/geosite/${name}.mrs`, interval: 86400 }; }); RULE_PROVIDERS_IPS.forEach(name => { providers[`${name}-ip`] = { type: "http", behavior: "ipcidr", format: "mrs", path: `./rules/${name}-ip.mrs`, url: `${SETTINGS.RULE_PROVIDER_URL_BASE}/geoip/${name}.mrs`, interval: 86400 }; }); return providers; }; const STATIC_RULES =[ "RULE-SET,category-ads-all,REJECT", ...uniq(BYPASS_DOMAINS).map((d) => `DOMAIN-SUFFIX,${d},DIRECT`), ...uniq(FORCE_PROXY_DOMAINS).map((d) => `DOMAIN,${d},main`), "RULE-SET,private,DIRECT", "RULE-SET,private-ip,DIRECT,no-resolve", "RULE-SET,google-cn,DIRECT", "RULE-SET,synology,DIRECT", "DOMAIN-SUFFIX,sharepoint.com,DIRECT", "RULE-SET,microsoft@cn,DIRECT", "RULE-SET,category-game-platforms-download@cn,DIRECT", "RULE-SET,category-ai-!cn,ai", "RULE-SET,telegram,tg", "RULE-SET,gfw,main", "RULE-SET,cn,DIRECT", "RULE-SET,cn-ip,DIRECT,no-resolve", "MATCH,main" ]; const STATIC_FAKE_IP_FILTER = buildFakeIpFilter(BYPASS_DOMAINS); // ==================== // 4. 节点处理 // ==================== const ensureConfigObject = (input) => input && typeof input === "object" ? input : {}; const getOriginalProxies = (input) => Array.isArray(input.proxies) ? input.proxies :[]; const makeProxyNamesUnique = (proxies =[]) => { const used = new Set(); const nextIdx = new Map(); proxies.forEach((p) => { if (!p || !p.name) return; const base = String(p.name); if (!used.has(base)) { used.add(base); nextIdx.set(base, 1); return; } let idx = nextIdx.get(base) ?? 1; let candidate = `${base}_${idx}`; while (used.has(candidate)) candidate = `${base}_${++idx}`; p.name = candidate; used.add(candidate); nextIdx.set(base, idx + 1); }); }; const splitInfoAndNormalProxies = (proxies =[], filterRegex) => proxies.reduce( (acc, proxy) => { if (!proxy || !proxy.name) return acc; (filterRegex.test(proxy.name) ? acc.infoProxies : acc.normalProxies).push(proxy); return acc; }, { infoProxies: [], normalProxies:[] } ); const classifyProxiesByRegion = (normalProxies = [], regions =[]) => { const regionGroupsData = regions.map((r) => ({ name: r.name, icon: r.icon, proxies:[] })); const regionGroupMap = new Map(regionGroupsData.map((r) => [r.name, r])); const regionSeen = new Map(regionGroupsData.map((r) =>[r.name, new Set()])); const otherProxyNames =[]; const otherSeen = new Set(); normalProxies.forEach((proxy) => { const proxyName = proxy.name; const normName = normalizeName(proxyName); const matchedRegion = regions.find((r) => r.regex.test(normName)); if (matchedRegion) { const group = regionGroupMap.get(matchedRegion.name); const seen = regionSeen.get(matchedRegion.name); if (group && seen && !seen.has(proxyName)) { group.proxies.push(proxyName); seen.add(proxyName); } } else if (!otherSeen.has(proxyName)) { otherProxyNames.push(proxyName); otherSeen.add(proxyName); } }); const activeRegions = regionGroupsData .map((r) => ({ ...r, proxies: uniq(r.proxies) })) .filter((r) => r.proxies.length > 0); const activeRegionNameSet = new Set(activeRegions.map((r) => r.name)); const activeRegionMap = new Map(activeRegions.map((r) => [r.name, r])); return { activeRegions, activeRegionNameSet, activeRegionMap, otherProxyNames: uniq(otherProxyNames) }; }; const buildAiProxyList = (activeRegions = [], otherProxyNames = [], allNames =[]) => { const nonHk = uniq([ ...activeRegions.filter((r) => r.name !== "HK").flatMap((r) => r.proxies), ...otherProxyNames ]); return nonHk.length ? nonHk : allNames; }; // ==================== // 5. 策略组 // ==================== const buildProxyGroups = ({ allNames, aiNames, activeRegionMap, activeRegionNameSet, otherProxyNames, infoNames }) => { const groups =[]; const add = (name, type, proxies, icon = "Available.png", extra = {}) => { proxies = uniq(proxies); if (name && proxies.length) { groups.push({ name, type, proxies, icon: SETTINGS.ICON_BASE + icon, ...extra }); } }; const regionEntries = SETTINGS.REGION_ORDER.filter((rName) => activeRegionNameSet.has(rName)); if (allNames.length) { const mainEntries = ["All", ...regionEntries]; if (otherProxyNames.length) mainEntries.push("Other"); add("main", "select", mainEntries, "Available.png"); add("URL Test - All", "url-test", allNames, "Available.png", SETTINGS.URL_TEST_EXTRA); add("All", "select", ["URL Test - All", ...allNames], "Available.png"); } if (aiNames.length) { add("URL Test - ai", "url-test", aiNames, "ChatGPT.png", SETTINGS.URL_TEST_EXTRA); add("ai", "select", ["URL Test - ai", ...aiNames], "ChatGPT.png"); } if (allNames.length) { const hasSG = activeRegionNameSet.has("SG"); const tgProxies = hasSG ? ["SG", "main"] : ["main"]; add("tg", "select", tgProxies, "Telegram.png"); } SETTINGS.REGION_ORDER.forEach((rName) => { const region = activeRegionMap.get(rName); if (!region) return; add(`URL Test - ${region.name}`, "url-test", region.proxies, region.icon, SETTINGS.URL_TEST_EXTRA); add(region.name, "select", [`URL Test - ${region.name}`, ...region.proxies], region.icon); }); if (otherProxyNames.length) { add("URL Test - Other", "url-test", otherProxyNames, "Available.png", SETTINGS.URL_TEST_EXTRA); add("Other", "select", ["URL Test - Other", ...otherProxyNames], "Available.png"); } if (infoNames.length) { add("info", "select", infoNames, "Available.png"); } add( "GLOBAL", "select",[ ...(allNames.length ? ["main", "All"] : []), ...(aiNames.length ? ["ai"] :[]), ...(allNames.length ? ["tg"] :[]), ...regionEntries, ...(otherProxyNames.length ? ["Other"] : []), ...(infoNames.length ? ["info"] :[]) ], "Global.png" ); return groups; }; // ==================== // 6. 网络配置 // ==================== const removeGeoDataConfig = (cfg) => { delete cfg["geodata-mode"]; delete cfg["geo-auto-update"]; delete cfg["geo-update-interval"]; delete cfg["geox-url"]; }; const applySniffer = (cfg) => { cfg.sniffer = { ...(cfg.sniffer || {}), enable: true, "force-dns-mapping": true, "parse-pure-ip": true, "override-destination": true, sniff: { HTTP: { ports:[80, "8080-8880"], "override-destination": true }, TLS: { ports: [443, 8443] }, QUIC: { ports:[443, 8443] } } }; }; const applyTun = (cfg) => { cfg.tun = { ...(cfg.tun || {}), enable: true, stack: "system", "auto-route": true, "auto-detect-interface": true, "strict-route": true, "dns-hijack":["any:53", "tcp://any:53"] }; }; const applyDns = (cfg) => { const dns = cfg.dns || {}; const fakeIpFilterFromCfg = Array.isArray(dns["fake-ip-filter"]) ? dns["fake-ip-filter"] :[]; const chinaDNS =[ "system", "https://dns.alidns.com/dns-query", "https://doh.pub/dns-query" ]; const foreignDNS =["https://1.1.1.1/dns-query#main"]; const directRuleSetsForChinaDNS =[ "rule-set:cn", "rule-set:google-cn", "rule-set:synology", "rule-set:googlefcm", "rule-set:epicgames", "rule-set:nvidia@cn", "rule-set:microsoft@cn", "rule-set:cloudflare@cn", "rule-set:steam@cn", "rule-set:category-game-platforms-download@cn", "rule-set:category-ntp", "rule-set:connectivity-check", "rule-set:apple", "rule-set:spotify", "rule-set:microsoft" ]; const fullFakeIpFilter = uniq([ "+.cn", "rule-set:private", ...directRuleSetsForChinaDNS, ...STATIC_FAKE_IP_FILTER, ...fakeIpFilterFromCfg ]); cfg.dns = { ...dns, enable: true, listen: "0.0.0.0:1053", ipv6: false, "cache-algorithm": "arc", "prefer-h3": false, "use-hosts": true, "use-system-hosts": true, "respect-rules": true, "enhanced-mode": "fake-ip", "fake-ip-filter-mode": "blacklist", "fake-ip-filter": fullFakeIpFilter, "default-nameserver":["223.5.5.5", "119.29.29.29"], nameserver: foreignDNS, "proxy-server-nameserver":[ "https://doh.pub/dns-query#DIRECT", "https://dns.alidns.com/dns-query#DIRECT" ], "direct-nameserver": chinaDNS }; cfg.hosts = { "dns.alidns.com":["223.5.5.5", "223.6.6.6"], "doh.pub":["1.12.12.12", "120.53.53.53"], "services.googleapis.cn": ["services.googleapis.com"], "+.mcdn.bilivideo.com": ["0.0.0.0"], "+.mcdn.bilivideo.cn":["0.0.0.0"] }; }; const applyProfile = (cfg) => { cfg.profile = { ...(cfg.profile || {}), "store-selected": true, "store-fake-ip": false }; }; const applyRuntime = (cfg) => { cfg.mode = "rule"; cfg["log-level"] = "warning"; }; // ==================== // 7. 主流程 // ==================== function main(config) { config = ensureConfigObject(config); const originalProxies = getOriginalProxies(config); const existingRules = Array.isArray(config.rules) ? config.rules : []; config["rule-providers"] = { ...(config["rule-providers"] || {}), ...buildRuleProviders() }; config.rules = mergeRules(STATIC_RULES, pickDirectRules(existingRules)); if (originalProxies.length) { makeProxyNamesUnique(originalProxies); const { infoProxies, normalProxies } = splitInfoAndNormalProxies( originalProxies, SETTINGS.FILTER_REGEX ); const baseProxies = normalProxies; const allNames = uniq(baseProxies.map((p) => p.name)); const infoNames = uniq(infoProxies.map((p) => p.name)); const { activeRegions, activeRegionNameSet, activeRegionMap, otherProxyNames } = classifyProxiesByRegion(baseProxies, REGIONS); const aiNames = buildAiProxyList(activeRegions, otherProxyNames, allNames); config["proxy-groups"] = buildProxyGroups({ allNames, aiNames, activeRegionMap, activeRegionNameSet, otherProxyNames, infoNames }); config.proxies = originalProxies; } removeGeoDataConfig(config); applyRuntime(config); applySniffer(config); applyTun(config); applyDns(config); applyProfile(config); return config; }