npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

clash-balancer

v0.1.1

Published

通过编排 mihomo / Clash.Meta 实现自动代理负载均衡:客户端连固定端口,轮询分发到多个上游代理

Readme

clash-balancer

通过编排 mihomo / Clash.Meta 实现代理负载均衡的 Node.js 库 (TypeScript 编写,打包后同时提供 CommonJS / ESM 与类型声明)。

客户端 ──► http://127.0.0.1:<port> ──► mihomo ──► { 节点A, 节点B, 节点C … } 轮询/分发
                                         ▲
                          本库:生成配置 · 拉起并监督进程 · 健康检查 · 自动热重载

本库不自己实现负载均衡算法(交给 mihomo 的 load-balance proxy-group),而是负责把 「一组上游代理 / 订阅」编排成「一个或多个本地端口」,并管理 mihomo 进程的生命周期。

提供两种调用方式:

| 类 | 暴露 | 适用 | |---|---|---| | ClashBalancer | 一个固定本地端口,流量按策略(round-robin / url-test…)分发到多个节点 | 通用代理池:一个端口,多节点自动负载均衡 | | PinnedBalancer | 每个存活节点一个专属端口 + 一个 round-robin 主端口 | 高并发抓取:让每个出口 IP 同一时刻最多 1 个在途请求,最大化规避按 IP/子网 的 429 限流 |


前置条件

  • Node.js ≥ 18(需内置 fetch)。
  • 一个 mihomo / Clash.Meta 可执行文件。安装本包时会自动按当前平台从 GitHub Releases 下载 对应的 mihomo 二进制到包内 bin/,库运行时会自动发现,通常无需传 binPath
    • 自动下载失败也不会中断安装(只打警告);此时可手动下载放入 bin/,或调用时显式传 binPath
    • 二进制不随 npm 包分发(包仅几十 KB),始终是装包时按平台现取。

安装

pnpm install   # 或 npm install / yarn

pnpm 用户注意:pnpm v9+ 默认拦截依赖的 postinstall 脚本,自动下载不会执行。 需要在你的项目里允许本包构建:

pnpm approve-builds        # 交互式批准,或:

或在 package.json"pnpm": { "onlyBuiltDependencies": ["clash-balancer"] }。 不想走自动下载时,直接给 binPath 即可,无需开启。

自动下载的环境变量(可选)

| 变量 | 作用 | |---|---| | MIHOMO_VERSION | 指定 mihomo 版本(如 v1.19.26);默认取 latest release | | MIHOMO_MIRROR | GitHub 下载镜像前缀(国内加速),拼在 github.com 链接前,如 https://ghproxy.example/ | | MIHOMO_DOWNLOAD_URL | 直接指定二进制资源 URL(优先级最高,跳过 GitHub API) | | MIHOMO_FORCE=1 | 即使 bin/ 已有二进制也重新下载 | | CLASH_BALANCER_SKIP_DOWNLOAD=1 | 跳过自动下载(改为手动 / binPath) | | GITHUB_TOKEN | 提高 GitHub API 速率限制(可选) |

// ESM / TypeScript
import { ClashBalancer, PinnedBalancer } from 'clash-balancer';
// CommonJS
const { ClashBalancer, PinnedBalancer } = require('clash-balancer');

快速开始

最简用法:不必提供 binPath(用 bin/ 下的二进制),订阅直接给 URL 字符串

const lb = new ClashBalancer({ subscription: 'https://example.com/sub' });

await lb.start();
console.log(`就绪,把代理指向 http://127.0.0.1:${lb.port}`);  // 默认 7890

// 客户端示例(任何支持 HTTP 代理的工具都可):
//   curl -x http://127.0.0.1:7890 https://ipinfo.io/ip

await lb.stop();

ClashBalancer

把一组节点/订阅聚合成一个本地端口,按策略分发。

构造选项

new ClashBalancer({
  binPath,            // string?  mihomo 路径。省略则用 bin/ 下自带的二进制
  port,               // number?  客户端连接的混合端口(HTTP+SOCKS),默认 7890
  controllerPort,     // number?  external-controller 端口,默认 9090
  strategy,           // 'round-robin'(默认) | 'consistent-hashing' | 'sticky-sessions' | 'url-test'
  proxies,            // ProxyNode[]?  静态节点(Clash 原生格式)
  subscription,       // string?       单订阅 URL 简写
  subscriptions,      // (string | Subscription)[]?  订阅列表(字符串/对象混用)
  excludeFilter,      // string?  剔除名称匹配该正则的节点(机场信息节点)
  maxFailedTimes,     // number?  节点连续失败多少次判不可用,默认 3
  healthCheck: {      // 主动健康检查(JS 层,周期性测各静态节点延迟)
    enabled,          // boolean?  默认 true
    url,              // string?   默认 http://www.gstatic.com/generate_204
    interval,         // number?   ms,默认 60000
    timeout,          // number?   ms,默认 5000
    failuresToDrop,   // number?   连续失败几次永久剔除,默认 3
  },
  restart: { maxRetries, backoffMs },  // 进程崩溃重启:默认 5 次、退避 1000ms 起
  secret,             // string?  external-controller 鉴权,默认随机
  readyTimeoutMs,     // number?  就绪超时,默认 10000
  workDir,            // string?  运行时工作目录,默认系统临时目录
});

binPathsubscriptionsubscriptions 不传时分别回退到:自带二进制、空、空。 至少需要一个 proxies 或订阅,否则 start() 会抛错(mihomo 不接受空组)。

方法

await lb.start();                                   // 生成配置 → 拉起 mihomo → 轮询就绪 → 启动健康循环
await lb.reload({ proxies?, subscriptions? });      // 热更新节点列表(PUT /configs,不重启进程)
const s = await lb.stats();                         // { port, uptime, proxies: [{ name, alive, delay }] }
await lb.stop();                                    // 停健康循环 → 优雅关闭 mihomo → 清理临时配置

事件(继承 EventEmitter)

lb.on('ready',        () => {});       // 就绪
lb.on('proxy:up',     (name) => {});   // 节点恢复
lb.on('proxy:down',   (name) => {});   // 节点被剔除
lb.on('all:down',     () => {});       // 所有节点失效(保留旧配置继续尝试)
lb.on('clash:restart',(info) => {});   // mihomo 崩溃后退避重启
lb.on('clash:fatal',  (err) => {});    // 超过 maxRetries,放弃
lb.on('error',        (err) => {});

示例

// 静态节点 + 订阅,round-robin,剔除机场信息节点
const lb = new ClashBalancer({
  strategy: 'round-robin',
  proxies: [
    { name: 'hk-1', type: 'ss', server: '1.2.3.4', port: 8388, cipher: 'aes-256-gcm', password: '...' },
  ],
  subscriptions: ['https://example.com/sub'],
  excludeFilter: '剩余流量|到期|官网|过期',
  healthCheck: { interval: 30000, failuresToDrop: 3 },
});

lb.on('ready', () => console.log('客户端 → http://127.0.0.1:' + lb.port));
lb.on('proxy:down', (n) => console.warn('下线', n));
await lb.start();

// 运行中热更新节点
await lb.reload({ subscriptions: ['https://example.com/sub2'] });

// 查看各节点延迟
console.log(await lb.stats());

PinnedBalancer:每个出口 IP 单线程

当你高并发抓取又怕**单个出口 IP 触发限流(HTTP 429)**时:ClashBalancer 的 round-robin 是 「按 TCP 连接」轮换的,连接复用 / 节点数有限时,多个并发请求仍可能挤在同一个出口 IP 上。

PinnedBalancer每个存活节点单独开一个本地端口,端口的出站固定走那个节点。只要调用方 保证「每个端口最多一个并发请求」(worker-per-port),就能让每个出口 IP 同一时刻最多一个在途请求

它内部两阶段编排:① 以 round-robin 起一遍、等订阅解析 + 健康检查、读出存活节点(按延迟排序); ② 为选中的 N 个节点各建「一个 select 组 + 一个 mixed 监听端口」,重启 mihomo 后用 external-controller 把每个组钉定到对应节点。

构造选项

new PinnedBalancer({
  binPath,            // string?  省略则用 bin/ 下自带的二进制
  subscription,       // string?  单订阅 URL 简写
  subscriptions,      // (string | Subscription)[]?
  proxies,            // ProxyNode[]?  静态节点
  excludeFilter,      // string?  剔除信息节点(强烈建议机场场景设置)
  maxNodes,           // number?  最多钉多少个节点(=并发上限/端口数),默认 24
  healthUrl,          // string?  健康检查 URL,默认 gstatic generate_204
  healthIntervalSec,  // number?  健康检查间隔秒,默认 6
  maxFailedTimes,     // number?  默认 3
  settleMs,           // number?  发现阶段等待健康检查标活的时间,默认 12000
  readyTimeoutMs,     // number?  单阶段就绪超时,默认 20000
  workDir,            // string?
  log,                // (msg: string) => void   进度日志回调
});

属性与方法

await pb.start();      // 两阶段编排,完成后填充下列属性
pb.pins;               // { port: number; node: string }[]  —— 每个出口 IP 一个专属端口
pb.mixedPort;          // number  round-robin 主端口(供详情页/封面等少量非关键流量)
pb.controllerPort;     // number  external-controller 端口
pb.pid;                // number? 当前 mihomo 进程 pid(用于调用方异常退出时兜底清理)
pb.controller;         // Controller  可直接调 REST API
await pb.stop();       // 优雅关闭 + 清理(Windows 下含 taskkill 兜底)

pb.on('clash:restart', (info) => {});
pb.on('clash:fatal',   (err) => {});

消费 pins:worker-per-port

每个 worker 绑定一个 pin.port、内部串行处理任务,即可做到「每出口 IP 单线程」。 用 undici 的 ProxyAgent 把请求定向到某个端口:

import { ProxyAgent, fetch as undiciFetch } from 'undici';

const pb = new PinnedBalancer({ subscription: 'https://example.com/sub', maxNodes: 16, excludeFilter: '剩余流量|到期|官网' });
await pb.start();

const queue = [...tasks];                 // 待抓取任务
await Promise.all(pb.pins.map(async (pin) => {
  const dispatcher = new ProxyAgent(`http://127.0.0.1:${pin.port}`);  // 钉定到该节点
  for (let t; (t = queue.shift()); ) {     // 该 worker 串行 → 该节点同时仅 1 请求
    const res = await undiciFetch(t.url, { dispatcher });
    // …处理 res…
  }
  await dispatcher.close();
}));

await pb.stop();

⚠️ 同源坑:传给 fetchdispatcher 必须与该 fetch 出自同一个 undici。 用 Node 内置全局 fetch 搭配本包安装的 ProxyAgent 会报 invalid onRequestStart method。 解决:统一用 undici 包导出的 fetch(如上),或统一用 Node 内置的(但内置不导出 ProxyAgent)。


策略(strategy)

| 值 | mihomo 组类型 | 行为 | |---|---|---| | round-robin(默认) | load-balance | 每条新连接轮换下一个存活节点 —— 分散到多 IP | | consistent-hashing | load-balance | 同一目标域名 → 固定同一节点 | | sticky-sessions | load-balance | 同一来源+目标 → 一段时间内固定节点 | | url-test | url-test | 始终走最快的单一节点(非分发;全挂时会回退直连) |

库默认给 load-balance 组设了 lazy: false + max-failed-times,让 mihomo 主动健康检查、 先把死节点/信息节点剔除,否则首批连接可能轮到尚未探测的不可用节点而失败。


订阅与节点过滤

  • 订阅简写:subscription: 'https://…' 等价于 subscriptions: [{ name:'sub', url, interval:86400, userAgent:'clash.meta' }]
  • subscriptions 数组可混用 URL 字符串与完整对象;字符串自动补默认值(userAgent 默认 clash.meta,很多机场据此返回完整节点列表)。
  • excludeFilter:正则,剔除机场常见的非代理"信息节点"(如 剩余流量 / 套餐到期 / 官网…)—— 它们无法转发流量,round-robin 轮到或 PinnedBalancer 钉到都会失败。常用:
    剩余流量|剩余|套餐|到期|过期|重置|官网|网址|订阅|续费|购买|公告|失联|Traffic|Expire
    注意别误伤正常节点(如 GB 会命中英国 🇬🇧)。
type Subscription = {
  name: string; url: string; interval: number;
  userAgent?: string;                       // 默认 'clash.meta'
  header?: Record<string, string | string[]>;
};
type ProxyNode = { name: string; type: string; server?: string; port?: number; [k: string]: unknown };

让整个 Node 进程的流量都走代理

不想逐个请求传 dispatcher 时,可借助 Node 18+ 的 NODE_USE_ENV_PROXY,让进程内所有 fetch / http / https 自动走某个端口(配合 NO_PROXY 排除数据库等不该走代理的目标):

const lb = new ClashBalancer({ subscription: 'https://example.com/sub' });
await lb.start();

const proxy = `http://127.0.0.1:${lb.port}`;
const child = spawn(process.execPath, ['your-script.js'], {
  env: { ...process.env, NODE_USE_ENV_PROXY: '1', HTTP_PROXY: proxy, HTTPS_PROXY: proxy, NO_PROXY: '.your-db.com,127.0.0.1' },
  stdio: 'inherit',
});

注意:NODE_USE_ENV_PROXY 在进程启动时读取,故需以子进程方式注入(不能在已运行进程里改 process.env)。


辅助函数 / 底层构件

import { bundledBinPath, normalizeSubscriptions, buildConfig, Controller, Supervisor, HealthChecker } from 'clash-balancer';

bundledBinPath();                 // 解析 bin/ 下自带二进制的绝对路径(找不到返回 undefined)
normalizeSubscriptions(sub, subs);// 把字符串/对象混合的订阅归一化为 Subscription[]
buildConfig(opts);                // 纯函数:生成 mihomo YAML 字符串(支持 pins/excludeFilter…)
  • Controller — mihomo external-controller REST 薄封装:version() / proxies() / providers() / delay(name, {url,timeout}) / select(group, name) / reloadConfig(path)
  • Supervisor — 进程生命周期:拉起、捕获 stdout/stderr、崩溃指数退避重启、优雅关闭。
  • HealthChecker — 周期性测延迟、标记/剔除死节点、触发重建+热重载、发事件。

ClashBalancer / PinnedBalancer 即由以上构件组合而成;需要更细控制时可直接使用它们。


Windows / Clash Verge 注意事项

  • 若本机常驻 Clash Verge(verge-mihomo.exe),它默认占用 7890 / 9090。请给本库用别的端口 (port / controllerPort),或确保不冲突;否则会出现 mihomo 就绪超时(且 stderr 为空)。
  • Windows 下子进程在父进程异常退出时可能成为孤儿继续占端口。PinnedBalancer.stop() 已内置 taskkill 兜底;ClashBalancer 的调用方建议在进程退出钩子里补杀。清理残留:
    taskkill /F /IM mihomo-windows-amd64-compatible.exe

开发

源码为 TypeScript(src/),用 tsup 打包到 dist/, 测试与示例用 tsx 直接运行 .ts

pnpm build         # 产出 dist/index.js(CJS)、index.mjs(ESM)、index.d.ts
pnpm typecheck     # tsc --noEmit 全量类型检查
pnpm test          # node --test(经 tsx 运行 test/**/*.test.ts)
pnpm example       # 运行 examples/basic.ts

# 真实功能冒烟(需 bin/ 下有 mihomo 二进制):
pnpm tsx examples/smoke.ts        # 全程本地(本地上游代理 + 本地目标)
pnpm tsx examples/sub-smoke.ts    # 真实订阅(需自备订阅)
pnpm tsx examples/rr-local.ts     # 确定性验证 round-robin 在多节点间轮换
pnpm tsx examples/envproxy-local.ts  # 验证 NODE_USE_ENV_PROXY 经本库出网

License

MIT