clash 實現訂閱節點與規則的分離

jinzhuming發表於2020-09-27

#Clash

背景

最近發現我的 mac 上 spark 客戶端收不到 gmail 了,多方排查發現最終原因是因為我一直使用的 ss 服務商把 iplc 的郵件服務給遮蔽了,只能使用中繼來接收郵件。但是服務商提供的訂閱裡沒有單獨的 mail 規則,意味著要麼放棄 iplc 全盤使用中繼線路,要麼自己改寫規則。

解決方案

但是檢視了 clash 規則之後發現,classh 並沒有實現類似 quan、surge 一樣的節點訂閱與規則分離,這意味著就算我修改了當前規則,每次更新訂閱後又回恢復原狀,還需要再手動修改一次,這顯然是不能接受的,所以寫了個 node 服務來實現這個功能。
先來理清一下思路,實現規則與訂閱分離需要的步驟

  • 有一份空的規則檔案
  • 有一份節點列表
  • 把節點列表的節點填充到規則檔案裡
  • 替換掉當前的 clash 的 config 檔案

規則檔案我寫了一個 空規則,有需要的可以直接使用或者按照自己的實際需求更改。

節點列表首先需要你的服務商有提供 clash 的訂閱連結(如果是其他訂閱連結,需要自己改寫程式碼來轉換,或者找現成的訂閱轉換服務轉換為 clash)。

合併的程式碼如下
** 這裡需要注意,clash 最近更新了新版訂閱檔案規則,所以程式碼也有所更新,我的 github 並沒有實時更新(每次更新都要去掉敏感資訊)**

github.com/jinzhuming/clash-trs-ru...

// 引入網路請求模組
const request = require(“request”);
// yml 轉為 json 方便操作
const yaml = require(“js-yaml”);
// cron 定時執行
const schedule = require(“node-schedule”);
// json 轉為 yml
const YAML = require(“json-to-pretty-yaml”);
// rxjs 相關
const { forkJoin, of, Observable } = require(“rxjs”);
const fs = require(“fs”);
const { map } = require(“rxjs/operators”);
// 訂閱地址
const url = “www.xxoo.com/list”;
const getRules = () => {
  // 獲取節點資訊
  forkJoin(
    new Observable(function (observer) {
      request(url, function (error, response, body) {
        if (error) {
          observer.error();
        } else {
          observer.next(body);
        }
        observer.complete();
      });
   // 轉換為 json
    }).pipe(map((proxiesConfigYml) => yaml.load(proxiesConfigYml))),
   // 獲取空的規則檔案,如果是網路規則,自行 寫 request,forkjoin 會合並兩個資料
    of(require(./rule.json”)),
  )
    .pipe(
// 這裡可以做一些規則轉換操作,比如修改名稱,新增 icon,或者過濾節點
      map(([proxiesConfigJson, rules]) => ({
        …rules,
        proxies: proxiesConfigJson.proxies
          .filter((item) => !item.name.includes(“專線”) || !item.name.includes(01) || !item.name.includes(“日本”))
      })),
// 這裡把過濾好的節點給寫入到 rules 的 proxy-groups 裡,同時對節點進行一個分組歸類效果,把指定地區的節點新增到各自的分類,還可以把節點新增到自己定義的 select 裡,按需操作即可
      map((rules) => ({
        …rules,
        [“proxy-groups”]: rules[“proxy-groups”].map((item) => {
          if (item.name === “Proxies”) {
            return {
              …item,
              proxies: [HK,SG,JP,US,TW].concat(rules.proxies.map((proxy) => proxy.name)),
            };
          }

          if (item.name ===HK) {
            return {
              …item,
              proxies: 
                rules.proxies.filter((proxy) => proxy.name.includes(“香港”)).map((proxy) => proxy.name),

            };
          }
          if (item.name ===SG) {
            return {
              …item,
              proxies: 
                rules.proxies.filter((proxy) => proxy.name.includes(“新加坡”)).map((proxy) => proxy.name),

            };
          }
          if (item.name ===JP) {
            return {
              …item,
              proxies: 
                rules.proxies.filter((proxy) => proxy.name.includes(“日本”)).map((proxy) => proxy.name),

            };
          }

          if (item.name ===US) {
            return {
              …item,
              proxies: 
                rules.proxies.filter((proxy) => proxy.name.includes(“美國”)).map((proxy) => proxy.name),

            };
          }
          if (item.name ===TW) {
            return {
              …item,
              proxies: 
                rules.proxies.filter((proxy) => proxy.name.includes(“臺灣”)).map((proxy) => proxy.name),

            };
          }
          if (item.name === “Mail”) {
            return {
              …item,
              proxies: rules.proxies.filter((proxy) => proxy.name.includes(“中繼”)).map((proxy) => proxy.name),
            };
          }

          return item;
        }),
      })),
    )
    .subscribe((rules) => {
// 把我們轉換好的節點重新生成為 yml,然後寫入到 clash 的 config 目錄裡
     const newRulesFile = YAML.stringify(rules);
      fs.readFile(/Users / 你的使用者名稱 /.config/clash/rules.yaml”, “utf-8, (err, data) => {
// 判斷一下如果沒有發生變化就不替換了
        if (data === newRulesFile) {
          console.log(“相同不觸發替換”);
        } else {
          fs.writeFile(/Users/jinzhuming/.config/clash/rules.yaml”, newRulesFile, () => {});
        }
      });
    });
};

這樣就完成了基本的節點規則分離效果,接下來需要把服務設定為定時任務以及開機啟動,這樣每次開機都會自動啟動服務,在指定的時間更新節點資訊

定時啟動

// 根據 cron 設定執行時間,每天 13 點 30 分執行一次更新
const scheduleCronstyle = () => {
  schedule.scheduleJob(30 13 1 * * *, () => {
    getRules();
  });
};
// 啟動服務的時候立即執行一次
getRules();
scheduleCronstyle();

開機啟動

  1. 寫一個 .startup.sh,然後在 偏好設定 - 使用者與群組 - 啟動項裡新增進這個檔案,每次開機就會自動執行命令
  2. pm2 維持服務
    cd ~/ 服務所在資料夾 && pm2 start index.js —name ssRules

這樣就完成了最基本的節點訂閱和規則訂閱分離效果,自行改寫的訂閱連結也不會再因為更新而被覆蓋掉。目前已經穩定跑了幾個月了,需要什麼規則自己加,再也不依賴服務商了

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章