• wilsons

    需求

    大多数人的链式代理可能是这样的

    图片

    但这有个缺点,入口和出口任意一个挂了,那么你的整个访问就挂掉了。

    我想要的是,入口是一个集合,选择最优的路径,出口也是一个集合,一个故障切换另一个。如下:```
    客户端访问
    │
    ▼
    ┌────────── 自动选择 ──────────┐
    │ │
    香港节点 日本节点 ...
    │ │
    └──────── 自动择优 ────────────┘
    ↓
    【当前最佳入口】
    ↓
    ┌──────── 出口自动选择 ────────┐
    │ │
    代理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变量中即可。
    
    使用
    
    然后在订阅的代理列表中选中“出口自动选择”即可。完美!
    
    ![图片](https://cdn3.ldstatic.com/original/4X/7/8/b/78ba53008266eee753a69ae60dc2655549f6ed0d.png)
    主楼
  • liuqiang

    我找GPT写了一份跟你这个类似的,入口机场自动选择,出口也可以根据自定义选择组。。

    #1
  • hardy

    怎么觉得有点像快连?这不就是节点池吗?自动轮询 坏了自动换 这样就永远不会挂了

    #2
  • yangningbo

    感谢L友的分享,学习到了,这就回去尝试一下

    #3

发表回复

登录后回复