- wilsons2026-08-1826190
需求
大多数人的链式代理可能是这样的

但这有个缺点,入口和出口任意一个挂了,那么你的整个访问就挂掉了。
我想要的是,入口是一个集合,选择最优的路径,出口也是一个集合,一个故障切换另一个。如下:```
客户端访问
│
▼
┌────────── 自动选择 ──────────┐
│ │
香港节点 日本节点 ...
│ │
└──────── 自动择优 ────────────┘
↓
【当前最佳入口】
↓
┌──────── 出口自动选择 ────────┐
│ │
代理ip地址1... 代理ip地址2...
│ │
└────────── 故障转移 ──────────┘
↓
【最终出口】
↓
服务器
```这样的好处时,无论入口和出口都是多节点集合,防止单点故障引起不稳定。这里推荐入口用自动选择,可以优先选择最佳路径,而出口建议用故障转移,这样防止IP频繁变动。原理
怎么实现呢?其实很简单,因为clash verge支持给出口节点指定入口节点,因此,只需要给你的节点设置如下配置即可```
#proxies:
- type: socks5
name: SOCKS5 x.x.x.x:3000
server: x.x.x.x
port: 3000
username: yourname
password: yourpwd
dialer-proxy: 自动选择
```关键就是 “dialer-proxy: 自动选择” 这个,这个自动选择就是你机场的代理分组,自动选择这个一般都有。设置了dialer-proxy后则代表当你访问的出口是SOCKS5 x.x.x.x:3000时,则入口指向自动选择的代理分组。这就解决了入口集合问题。但,出口集合怎么设置呢?没错就是代理分组。可以新建一个代理分组,节点就是你的出口节点。这样当你在代理中选择了这个分组后,就会根据你设置的要求选择一个节点访问,然后又会触发上一步的dialer-proxy,从而选择最佳入口。这样就完成了从入口到出口的集合化。
出口配置示例如下:```
proxies:
- type: socks5
name: SOCKS5 111.111.111.111:1111
server: 111.111.111.111
port: 1111
username: xxxx
password: yyyy
dialer-proxy: 自动选择- type: socks5
name: SOCKS5 222.222.222.222:2222
server: 222.222.222.222
port: 2222
username: xxxx
password: yyyy
dialer-proxy: 自动选择proxy-groups:
- name: 出口自动选择
type: fallback
proxies:
- SOCKS5 111.111.111.111:1111
- SOCKS5 222.222.222.222:2222
url: http://www.gstatic.com/generate_204
interval: 300
tolerance: 50那么怎么和现有的配置融合呢?1是直接改配置文件,但有个确定刷新可能会重新被覆盖;2是通过扩展覆写配置改,但较为麻烦,你有时可能仅需覆盖某个子项,但必须连根节点一起复制再修改,非常不方便。 如果用扩展脚本呢?可以动态加载,但一个个适配也不方便。 于是让AI写了个智能覆盖与合并通用脚本,原理是,相同的叶子节点覆盖,不同则新增,也可以通过MERGE_MODES设置指定路径强制覆盖(无论是叶子或是非叶子),这样,你只需要把你关心的节点重写就好了。 通用脚本如下(需要自己修改配置):``` // ============================================================ // ① 你只需要修改这里:直接填写要合并进去的 YAML // ============================================================ const PATCH_YAML = ` ipv6: false # 添加你的节点,并通过dialer-proxy指定入口集合 proxies: - type: socks5 name: SOCKS5 111.111.111.111:1111 server: 111.111.111.111 port: 1111 username: xxxx password: yyyy dialer-proxy: 自动选择 - type: socks5 name: SOCKS5 222.222.222.222:2222 server: 222.222.222.222 port: 2222 username: xxxx password: yyyy dialer-proxy: 自动选择 # 配置代理分组,type:fallback代表故障转移 proxy-groups: - name: 出口自动选择 type: fallback proxies: - SOCKS5 111.111.111.111:1111 - SOCKS5 222.222.222.222:2222 url: http://www.gstatic.com/generate_204 interval: 300 tolerance: 50 # 把 出口自动选择 代理组加入到主代理组 - name: <这里是你主代理组名,通常是订阅节点名> proxies: - 出口自动选择 `; // ============================================================ // ② 高级设置 // 一般不需要修改 // ============================================================ // 数组中的对象,用哪些字段判断“这是同一个子节点”。 // Clash/Mihomo 最常用的是 name。 const IDENTITY_KEYS = ["name", "id", "tag"]; // 默认合并行为: // 普通对象 -> 递归合并 // 对象数组 -> 相同 name/id/tag 合并,不同则追加 // 普通数组 -> 去重后追加 // 叶子值 -> PATCH 覆盖原值 // // 如果某个路径希望完全替换,可在这里指定 "replace"。 // 路径可以是叶子、对象或数组。 // 数组如果想把新增内容插到原数组前面,可使用 "prepend"。 const MERGE_MODES = { "proxy-groups[name=出口自动选择].proxies": "replace" }; // ============================================================ // ③ 通用深度合并引擎 // 以下通常不需要修改 // ============================================================ function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } function clone(value) { if (Array.isArray(value)) return value.map(clone); if (isObject(value)) { const out = {}; Object.keys(value).forEach(key => { out[key] = clone(value[key]); }); return out; } return value; } function deepEqual(a, b) { if (a === b) return true; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; return true; } if (isObject(a) && isObject(b)) { const ka = Object.keys(a), kb = Object.keys(b); if (ka.length !== kb.length) return false; for (let i = 0; i < ka.length; i++) { const key = ka[i]; if (!hasOwn(b, key) || !deepEqual(a[key], b[key])) return false; } return true; } return false; } function getMergeMode(path) { return hasOwn(MERGE_MODES, path) ? MERGE_MODES[path] : "merge"; } function getIdentity(obj) { if (!isObject(obj)) return null; for (let i = 0; i < IDENTITY_KEYS.length; i++) { const key = IDENTITY_KEYS[i]; if (hasOwn(obj, key)) return { key: key, value: obj[key] }; } return null; } function buildIdentityPath(parentPath, identity) { return parentPath + "[" + identity.key + "=" + String(identity.value) + "]"; } function arrayContains(array, value) { for (let i = 0; i < array.length; i++) if (deepEqual(array[i], value)) return true; return false; } function mergeArray(baseArray, patchArray, path) { const mode = getMergeMode(path); if (mode === "replace") return clone(patchArray); let result = Array.isArray(baseArray) ? clone(baseArray) : []; for (let p = 0; p < patchArray.length; p++) { const patchItem = patchArray[p]; if (isObject(patchItem)) { const identity = getIdentity(patchItem); if (identity !== null) { let foundIndex = -1; for (let i = 0; i < result.length; i++) { const oldItem = result[i]; if (!isObject(oldItem)) continue; if (hasOwn(oldItem, identity.key) && oldItem[identity.key] === identity.value) { foundIndex = i; break; } } const itemPath = buildIdentityPath(path, identity); if (foundIndex >= 0) { result[foundIndex] = deepMerge(result[foundIndex], patchItem, itemPath); } else { result.push(clone(patchItem)); } continue; } if (!arrayContains(result, patchItem)) result.push(clone(patchItem)); continue; } if (!arrayContains(result, patchItem)) result.push(clone(patchItem)); } return result; } function deepMerge(base, patch, path) { path = path || ""; const mode = getMergeMode(path); if (mode === "replace") return clone(patch); if (Array.isArray(patch)) { return mergeArray(Array.isArray(base) ? base : [], patch, path); } if (isObject(patch)) { const result = isObject(base) ? clone(base) : {}; const keys = Object.keys(patch); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const childPath = path ? path + "." + key : key; if (hasOwn(result, key)) result[key] = deepMerge(result[key], patch[key], childPath); else result[key] = clone(patch[key]); } return result; } return clone(patch); } function stripComment(line) { let quote = null; for (let i = 0; i < line.length; i++) { const c = line[i]; if (quote === '"') { if (c === "\\") { i++; continue; } if (c === '"') quote = null; continue; } if (quote === "'") { if (c === "'" && line[i + 1] === "'") { i++; continue; } if (c === "'") quote = null; continue; } if (c === '"' || c === "'") { quote = c; continue; } if (c === "#" && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i); } return line; } function splitKeyValue(text, lineNo) { let quote = null, depth = 0; for (let i = 0; i < text.length; i++) { const c = text[i]; if (quote === '"') { if (c === "\\") { i++; continue; } if (c === '"') quote = null; continue; } if (quote === "'") { if (c === "'" && text[i + 1] === "'") { i++; continue; } if (c === "'") quote = null; continue; } if (c === '"' || c === "'") { quote = c; continue; } if (c === "[" || c === "{") depth++; else if (c === "]" || c === "}") depth--; else if (c === ":" && depth === 0) return [text.slice(0, i).trim(), text.slice(i + 1).trim()]; } throw new Error("YAML line " + lineNo + ": expected key: value -> " + text); } function hasBlockMappingPair(text) { let quote = null, depth = 0; for (let i = 0; i < text.length; i++) { const c = text[i]; if (quote === '"') { if (c === "\\") { i++; continue; } if (c === '"') quote = null; continue; } if (quote === "'") { if (c === "'" && text[i + 1] === "'") { i++; continue; } if (c === "'") quote = null; continue; } if (c === '"' || c === "'") { quote = c; continue; } if (c === "[" || c === "{") depth++; else if (c === "]" || c === "}") depth--; else if (c === ":" && depth === 0 && (i + 1 === text.length || /\s/.test(text[i + 1]))) return true; } return false; } function splitInline(text) { const out = []; let start = 0, quote = null, depth = 0; for (let i = 0; i < text.length; i++) { const c = text[i]; if (quote === '"') { if (c === "\\") { i++; continue; } if (c === '"') quote = null; continue; } if (quote === "'") { if (c === "'" && text[i + 1] === "'") { i++; continue; } if (c === "'") quote = null; continue; } if (c === '"' || c === "'") { quote = c; continue; } if (c === "[" || c === "{") depth++; else if (c === "]" || c === "}") depth--; else if (c === "," && depth === 0) { out.push(text.slice(start, i).trim()); start = i + 1; } } out.push(text.slice(start).trim()); return out; } function parseScalar(text) { const s = text.trim(); if (s === "") return null; if (s === "null" || s === "Null" || s === "NULL" || s === "~") return null; if (/^(true|True|TRUE)$/.test(s)) return true; if (/^(false|False|FALSE)$/.test(s)) return false; if (/^[-+]?\d+(\.\d+)?([eE][-+]?\d+)?$/.test(s)) return Number(s); if (s[0] === '"' && s[s.length - 1] === '"') { try { return JSON.parse(s); } catch (_) { return s.slice(1, -1); } } if (s[0] === "'" && s[s.length - 1] === "'") return s.slice(1, -1).replace(/''/g, "'"); if (s[0] === "[" && s[s.length - 1] === "]") { const inner = s.slice(1, -1).trim(); return inner ? splitInline(inner).map(parseScalar) : []; } if (s[0] === "{" && s[s.length - 1] === "}") { const inner = s.slice(1, -1).trim(); const obj = {}; if (!inner) return obj; const parts = splitInline(inner); for (let i = 0; i < parts.length; i++) { const kv = splitKeyValue(parts[i], "inline"); obj[parseKey(kv[0])] = parseScalar(kv[1]); } return obj; } return s; } function parseKey(s) { s = s.trim(); if ((s[0] === '"' && s[s.length - 1] === '"') || (s[0] === "'" && s[s.length - 1] === "'")) { return String(parseScalar(s)); } return s; } function parseYamlSubset(yamlText) { const raw = yamlText.replace(/\r\n?/g, "\n").split("\n"); const lines = []; for (let i = 0; i < raw.length; i++) { let line = raw[i]; if (/\t/.test(line.match(/^\s*/)[0])) throw new Error("YAML line " + (i + 1) + ": tabs are not supported"); line = stripComment(line).replace(/\s+$/, ""); if (!line.trim() || line.trim() === "---" || line.trim() === "...") continue; const indent = line.match(/^ */)[0].length; lines.push({ indent: indent, text: line.slice(indent), lineNo: i + 1 }); } if (!lines.length) return {}; function parseBlock(index, indent) { if (lines[index].indent !== indent) throw new Error("YAML line " + lines[index].lineNo + ": unexpected indentation"); return /^-($|\s)/.test(lines[index].text) ? parseSeq(index, indent) : parseMap(index, indent); } function parseMap(index, indent) { const obj = {}; while (index < lines.length && lines[index].indent === indent && !/^-($|\s)/.test(lines[index].text)) { const line = lines[index]; const kv = splitKeyValue(line.text, line.lineNo); const key = parseKey(kv[0]); const valText = kv[1]; index++; if (valText !== "") { obj[key] = parseScalar(valText); } else if (index < lines.length && lines[index].indent > indent) { const child = parseBlock(index, lines[index].indent); obj[key] = child[0]; index = child[1]; } else { obj[key] = {}; } } return [obj, index]; } function parseSeq(index, indent) { const arr = []; while (index < lines.length && lines[index].indent === indent && /^-($|\s)/.test(lines[index].text)) { const line = lines[index]; const content = line.text.replace(/^-\s?/, ""); index++; if (content === "") { if (index < lines.length && lines[index].indent > indent) { const child = parseBlock(index, lines[index].indent); arr.push(child[0]); index = child[1]; } else { arr.push(null); } continue; } // "- key: value" 形式的对象数组成员 let mappingPair = null; if (hasBlockMappingPair(content)) { mappingPair = splitKeyValue(content, line.lineNo); } if (mappingPair !== null) { const obj = {}; const key = parseKey(mappingPair[0]); const valText = mappingPair[1]; if (valText !== "") { obj[key] = parseScalar(valText); } else if (index < lines.length && lines[index].indent > indent) { const child = parseBlock(index, lines[index].indent); obj[key] = child[0]; index = child[1]; } else { obj[key] = {}; } // 同一个 sequence item 后续的映射字段 if (index < lines.length && lines[index].indent > indent) { const continuationIndent = lines[index].indent; if (!/^-($|\s)/.test(lines[index].text)) { const tail = parseMap(index, continuationIndent); const tailObj = tail[0]; Object.keys(tailObj).forEach(k => { obj[k] = tailObj[k]; }); index = tail[1]; } } arr.push(obj); } else { arr.push(parseScalar(content)); } } return [arr, index]; } const parsed = parseBlock(0, lines[0].indent); if (parsed[1] !== lines.length) { throw new Error("YAML line " + lines[parsed[1]].lineNo + ": could not parse remaining content"); } return parsed[0]; } function main(config, profileName) { const patch = parseYamlSubset(PATCH_YAML); return deepMerge(config, patch, ""); } ```把你待修改的配置放到PATCH_YAML变量中即可。 使用 然后在订阅的代理列表中选中“出口自动选择”即可。完美! 主楼 - liuqiang2026-08-1826480
我找GPT写了一份跟你这个类似的,入口机场自动选择,出口也可以根据自定义选择组。。
- hardy2026-08-1812342
怎么觉得有点像快连?这不就是节点池吗?自动轮询 坏了自动换 这样就永远不会挂了
- yangningbo2026-08-2143835
感谢L友的分享,学习到了,这就回去尝试一下