多协议节点聚合、订阅分发与真实测速面板。整个项目只有 index.js 和 package.json 两个文件,零第三方依赖,Node.js 18+ 直接跑。
手里攒了一堆节点链接和订阅,想知道哪些还活着、哪些真的快,然后把筛出来的喂给 Clash / sing-box。这类面板不少,NodePulse 想多回答两个问题:
- 节点回中国这一段有多快。 大部分面板测的是「节点 → Cloudflare」,可你实际用的是「你 → 节点 → 目标站」。境内带宽是另一条腿,量出来才知道慢在哪。
- 这个节点到底翻不翻得出去。 默认的延迟检测地址
www.gstatic.com/generate_204在墙内本来就通 —— 所以延迟绿了、评分很高的节点,照样可能打不开 Google。「节点可用」不等于「能翻墙」,这两件事得分开测。
另外这一整套在手机上也能做完:绑一个 Telegram 机器人,加节点、测速、查结果、拿订阅都不用开面板。
- 支持
ss/ssr/vmess/vless/trojan/hysteria/hysteria2(hy2) /tuic - 单条粘贴、批量粘贴、远端订阅导入(自动识别 base64、明文链接、Clash YAML)
- 导入带 SSRF 防护:默认拒绝内网地址,可用
allowPrivateTargets放开 - 自动去重,支持启用 / 禁用 / 批量删除
- 节点地址在界面上打码显示
| 顺序 | 测什么 | 范围 | 并发 |
|---|---|---|---|
| 1 | 延迟 + 抖动 | 全部节点 | 满并发 |
| 2 | 墙外带宽 | 只测连得上的 | ≤ 2 |
| 3 | 翻墙探测 | 只测连得上的 | 满并发(多地址并行) |
| 4 | 境内延迟 + 带宽 | 只测连得上的 | ≤ 2 |
带宽测试会把上行占满。要是把轻量探测和下载塞在一起跑,探测会被挤到超时,然后你看到的结论是「这个节点翻不出去」—— 其实是自己把自己饿死了。所以分四趟。
每轮开始前先量一次本机直连基准,节点的数字才有参照:如果直连本身就是墙外 0/3,那所有节点都 0/3 的时候你知道该去看代理链路,而不是怀疑机场。基准显示在状态栏里。
其他几条:
- 有 sing-box 内核时走真实代理测速:每个节点起一个
mixed入站,配一条专属路由规则,兜底出站是个死 socks,防止流量偷偷走直连把速度测虚高。 - 没有内核(或协议不被内核支持,比如
ssr)就降级成 TCP 握手测延迟,带宽留空、状态标degraded,不会假装测到了。 - 内核起不来时用二分法定位到具体是哪个节点的配置有问题,剩下的照测,不会整批放弃。
- 内核可以自动下载(GitHub Release,支持镜像加速),也可以直接指定本地可执行文件路径。
- 支持定时自动测速,默认 30 分钟一轮。
状态判定:
| 状态 | 含义 |
|---|---|
ok |
延迟通,且跑出了带宽 |
degraded |
延迟通,但没跑出带宽 |
fail |
一个延迟样本都没拿到 |
评分由状态权重、带宽、延迟三项加权得出。境内和墙外的数据不参与评分,只做展示 —— 免得你改一下探测地址,订阅里的节点就悄悄换了一批。订阅筛选也只排除 fail。
订阅输出四种格式,地址形如 <base>/sub/<token>/<format>:
| 格式 | 说明 |
|---|---|
base64 |
通用 base64 链接列表 |
clash |
Clash 配置,含 url-test 自动选择分组 |
singbox |
sing-box 配置,含 url-test 分组 |
plain |
明文链接,一行一个 |
订阅地址带随机 token,可一键轮换(旧地址立刻失效);/sub 有按 IP 的限流。可以选择发全部节点还是只发评分最高的 N 个,以及是否包含还没测过的节点。
不想开面板的时候,用手机把这套东西完整跑一遍:加节点、测速、看结果、拿订阅。
绑定过程不用手抄 chat id:面板「设置 → Telegram 机器人」里填上 @BotFather 给的 Token 并保存,会显示一个 8 位绑定码,去跟机器人说 /bind <绑定码> 就完事了。绑定码一次性,用掉立刻轮换;没绑定的会话什么都问不出来,只会收到一句「只服务已绑定的会话」(一分钟最多一句,免得被当成回声墙)。
| 命令 | 说明 |
|---|---|
/bind <绑定码> |
绑定当前会话 |
/nodes [页码] |
列出节点,每行带一个 6 位短号 |
/add <分享链接> |
添加节点,可以多行一次贴一批 |
/import <订阅地址> |
从远端订阅导入 |
/del、/on、/off <短号|all> |
删除、启用、停用 |
/test [短号...] |
开始一轮检测,不带参数就测全部启用的 |
/status |
当前进度、内核状态、直连基准 |
/result [短号] |
结果总览;带短号看单个节点的明细 |
/sub [格式] |
聚合订阅链接 |
/unbind |
把当前会话从白名单里去掉 |
直接粘贴分享链接(不带 /add)等同于 /add。节点一律用短号寻址而不是列表序号 —— 序号会随着增删漂移,你照着上一条消息点 /del 3,删掉的可能是别的节点。
几个刻意的取舍:
/sub只发链接,不发节点正文。 链接泄漏了可以一键轮换令牌,正文一旦贴进聊天记录,里面的节点凭据就永久留在 Telegram 的服务器上了。- 消息是纯文本,不用 Markdown。 节点名字里带一个
*或[就会让 Telegram 的解析器整条报错,不值得。回复太长会按行切片,每片 3500 字以内(Telegram 单条上限 4096)。 - Token 不回显。 存进
config.json(0600),面板只显示<botId>:***;/api/state、审计日志、错误信息里出现的 Token 都会被替换掉。设置里留空表示不改动,要清掉得勾「清除 Token」。 - 重启会丢掉停机期间的消息。 启动时先
offset=-1把积压跳过去 —— 半小时前发的/test现在补跑没有意义。 - 走长轮询,不用 webhook。 面板不需要公网入口,也不需要 HTTPS 证书。
api.telegram.org在中国大陆不通。如果面板跑在境内,把apiBase换成你自己的反代地址(Cloudflare Workers 之类都行),否则机器人起不来 —— 状态栏会显示连不上的原因。
- 单页应用,登录后原地切换,不刷新不跳转
- 节点表 14 列,境内、墙外各占一列,鼠标悬停能看到每个探测地址的明细(HTTP 状态码、首字节耗时,或者失败原因)
- 严格 CSP(
default-src 'none'+ 每请求 nonce),所有 DOM 写入走textContent - 设置页里能试连机器人、重新生成绑定码、把已绑会话一个个点掉
- 静态资源按内容哈希打指纹,带 ETag 和 304 协商
git clone <这个仓库>
cd nodepulse
npm start首次启动会在服务端日志里打印一次随机初始密码,之后不再打印,记下来。用户名默认 admin。
想自己定密码:
# Linux / macOS
NODEPULSE_PASSWORD='换成你的强密码' npm start# Windows PowerShell
$env:NODEPULSE_PASSWORD = '换成你的强密码'
npm start然后浏览器打开 http://<你的地址>:26228,登录后:粘贴节点或订阅地址 → 点「开始测速」→ 复制订阅链接。
面板默认监听
0.0.0.0。直接暴露到公网之前,请自己在前面套一层 HTTPS 反向代理,并把配置里的trustProxy打开、publicBaseUrl填上(订阅链接会用它来拼地址)。
想用手机操作的话,去 @BotFather 建个机器人拿到 Token,填进「设置 → Telegram 机器人」保存(Token 只能在面板或 config.json 里配,没有对应的环境变量),然后拿面板上显示的绑定码跟机器人说一句 /bind <绑定码>。命令清单见上面的机器人一节。
| 变量 | 作用 | 默认 |
|---|---|---|
NODEPULSE_DATA_DIR |
数据目录 | <项目目录>/data |
NODEPULSE_USERNAME |
登录用户名 | admin |
NODEPULSE_PASSWORD |
登录密码 | 首次启动随机生成并打印一次 |
SERVER_PORT / PORT |
监听端口 | 26228 |
SERVER_IP |
订阅链接里用的对外地址 | 自动探测 |
SINGBOX_PATH |
指定 sing-box 可执行文件 | 空(自动下载) |
SINGBOX_MIRROR |
内核下载镜像前缀 | 空 |
GITHUB_TOKEN |
拉 Release 列表时避免被限流 | 空 |
data/
├── config.json # 配置(含 Bot Token,权限 0600)
├── data.json # 节点、测速历史、订阅 token
├── audit.log # 审计日志(密码一类字段已脱敏)
├── bin/ # 自动下载的 sing-box
└── run/ # 测速时生成的临时内核配置
老版本留下的 <项目目录>/bin 和 <项目目录>/config.json 仍然认,不用手动迁移。
面板里改就行,会写进 data/config.json。下面是默认值。
server
| 键 | 默认 | 说明 |
|---|---|---|
port |
26228 |
监听端口 |
host |
0.0.0.0 |
监听地址 |
trustProxy |
false |
是否信任 X-Forwarded-For |
publicBaseUrl |
空 | 对外访问地址,影响订阅链接 |
auth
| 键 | 默认 | 说明 |
|---|---|---|
username |
admin |
用户名 |
sessionHours |
12 |
会话有效期(小时) |
maxLoginFails |
5 |
连续失败几次锁定 |
lockMinutes |
15 |
锁定时长(分钟) |
subscription
| 键 | 默认 | 说明 |
|---|---|---|
mode |
all |
all 全发,best 只发最好的几个 |
bestCount |
3 |
best 模式发几个 |
includeUntested |
true |
是否包含未测过的节点 |
cnDirect |
true |
生成的规则里国内流量走直连(GEOIP,CN,DIRECT) |
test
| 键 | 默认 | 说明 |
|---|---|---|
healthUrl |
https://www.gstatic.com/generate_204 |
延迟检测地址 |
downloadUrl |
https://speed.cloudflare.com/__down?bytes=20000000 |
墙外测速文件 |
latencySamples |
3 |
每个节点测几次延迟 |
timeoutMs |
8000 |
单次请求超时 |
downloadSeconds |
6 |
墙外测速最长时间 |
downloadMaxBytes |
8 MB |
墙外测速最多下多少 |
concurrency |
4 |
并发数 |
batchSize |
8 |
每批起多少个入站 |
intervalMinutes |
30 |
自动测速间隔 |
basePort |
39100 |
测速用的本地起始端口 |
test:境内速度与翻墙探测
| 键 | 默认 | 说明 |
|---|---|---|
cnCheck |
true |
是否测境内延迟与带宽 |
cnHealthUrl |
http://connect.rom.miui.com/generate_204 |
境内延迟地址 |
cnDownloadUrl |
https://mirrors.huaweicloud.com/ubuntu/ls-lR.gz |
境内测速文件 |
cnSeconds |
3 |
境内测速最长时间(1–30) |
cnMaxBytes |
4 MB |
境内测速最多下多少 |
gfwCheck |
true |
是否探测墙外可达性 |
gfwUrls |
google / youtube / openai 三个 | 探测地址,最多 6 个 |
境内地址拿不到 204 就直接跳过下载 —— 连个空响应都回不来,再等一轮几 MB 的超时纯属浪费时间。
singBox
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
true |
关掉就全部走 TCP 握手测延迟 |
autoDownload |
true |
自动下载内核 |
version |
latest |
想锁版本就填 1.14.0 这种 |
binaryPath |
空 | 指定本地可执行文件 |
mirror |
空 | 下载镜像前缀 |
importer
| 键 | 默认 | 说明 |
|---|---|---|
allowPrivateTargets |
false |
是否允许导入内网地址的订阅 |
timeoutMs |
15000 |
拉订阅超时 |
maxBytes |
2 MB |
订阅内容大小上限 |
telegram
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
false |
开关,关掉立刻停掉长轮询 |
token |
空 | BotFather 给的 Token,不回显 |
apiBase |
https://api.telegram.org |
Bot API 地址,境内换成反代 |
notifyOnFinish |
true |
每轮测速跑完主动推结果 |
requestTimeoutMs |
20000 |
单次 Bot API 请求超时 |
allowedChats |
[] |
已绑定的会话,只能靠 /bind 或面板增删 |
bindCode |
自动生成 | 一次性绑定码,绑成功后自动轮换 |
allowedChats 和 bindCode 不接受面板的设置提交 —— 白名单只能通过绑定码或者面板上的解绑按钮变动,免得一次误保存把一串会话放进来。
面板自己用的就是这些,写接口需要登录会话,另外还会校验同源。
GET
| 路径 | 说明 |
|---|---|
/healthz |
健康检查,不需要登录 |
/ |
面板页面 |
/assets/app.css、/assets/app.js |
静态资源,带指纹和 ETag |
/sub、/sub/<token>/<format> |
订阅,按 IP 限流 |
/api/state |
全量状态:节点、进度、配置、订阅链接、机器人状态 |
/api/audit |
审计日志 |
POST
| 路径 | 说明 |
|---|---|
/api/login、/api/logout |
登录、登出 |
/api/nodes/add、/api/nodes/import |
粘贴添加、远端订阅导入 |
/api/nodes/toggle、/api/nodes/delete |
启用/禁用、删除 |
/api/test |
开始一轮测速 |
/api/settings |
保存配置 |
/api/subscription/rotate |
轮换订阅 token |
/api/kernel/check |
检查/下载内核 |
/api/telegram/check |
试连 Bot API(getMe) |
/api/telegram/code |
重新生成绑定码 |
/api/telegram/unbind |
解绑某个会话,或者全部清空 |
/api/password |
改密码 |
- 密码用 scrypt 加盐哈希,不存明文
- 登录连续失败会按 IP 锁定
- 会话 cookie 是
HttpOnly+SameSite=Strict,HTTPS 下自动加Secure - 所有写接口双重把关:会话 + 同源校验(CSRF)
- 响应头:
Cache-Control: no-store、X-Content-Type-Options: nosniff、Referrer-Policy: no-referrer、X-Frame-Options: DENY、COOP/CORPsame-origin、Permissions-Policy,HTTPS 下带 HSTS - CSP 是
default-src 'none',脚本靠每请求 nonce 放行,connect-src 'self',frame-ancestors 'none' - 审计日志写入前会把密码、token 一类字段替换掉
- Bot Token 在日志、审计、接口响应、错误信息里都会被换成
<botId>:*** - 机器人默认谁都不理:只有在白名单里的会话能下命令,绑定码试错按会话限速(10 分钟 5 次)
- 导入订阅默认拒绝内网地址,防止拿面板当内网扫描器
- 测速要占满带宽,跑的时候别指望同时干别的。
- 翻墙探测的判定是「拿到任何 HTTP 状态码就算通过」。
api.openai.com未鉴权返回 401 也算翻出去了 —— 真被墙的站点连状态码都拿不到,只会是 RST 或者超时。 - 境内和墙外的结果不影响评分,也不影响订阅筛选,纯展示。
ssr没法走内核测速,只会有 TCP 延迟。
npm run check # 等价于 node --check index.jsindex.js 里的 HTML、CSS、前端 JS 都是模板字符串。在这些模板字符串里写反斜杠要写两遍('\\n'、/[\\s,]+/),少一个就被外层模板吃掉,前端整个脚本会报 Invalid or unexpected token。
MIT