Skip to content

Commit 5674248

Browse files
suanteaAtomCode (deepseek-v4-flash)
andcommitted
feat: 优选 IP 健康检查——剔除失效优选节点
订阅生成时并发探测候选 IP:80 可达性,剔除失效 IP 后输出, 结果按 TTL 缓存于 KV(30 分钟);sub:// 等指令条目原样保留; 全部失效时回退原列表避免订阅为空。env.IP_HEALTH_CHECK='0' 可关闭。 对应社区痛点:优选节点大量失效(issue #1435/#1400/#1445)。 局限:Worker 无法以指定 IP 出站,以 80 端口 HTTP 连通性近似判断 (CF 任播 IP 的 80/443 通常同生共死)。 Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
1 parent 92fc6cc commit 5674248

1 file changed

Lines changed: 36 additions & 0 deletions

File tree

_worker.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@ export default {
397397
完整优选IP = 完整优选IP.concat(优选生成器IP数组);
398398
其他节点LINK += 优选生成器其他节点;
399399
}
400+
// 优选 IP 健康检查:并发探测可达性,剔除失效 IP(结果缓存 30 分钟;env.IP_HEALTH_CHECK='0' 可关闭)
401+
if (env.IP_HEALTH_CHECK !== '0' && env.IP_HEALTH_CHECK !== 'false' && 完整优选IP.length > 0) {
402+
完整优选IP = await 优选IP健康筛选(env, 完整优选IP, 30);
403+
}
400404
const ECHLINK参数 = config_JSON.ECH ? `&ech=${encodeURIComponent((config_JSON.ECHConfig.SNI ? config_JSON.ECHConfig.SNI + '+' : '') + config_JSON.ECHConfig.DNS)}` : '';
401405
const isLoonOrSurge = ua.includes('loon') || ua.includes('surge');
402406
const { type: 传输协议, 路径字段名, 域名字段名 } = 获取传输协议配置(config_JSON);
@@ -5403,6 +5407,38 @@ async function MD5MD5(文本) {
54035407
return 第二次十六进制.toLowerCase();
54045408
}
54055409

5410+
// 优选 IP 健康检查:并发探测 IP:80 可达性,剔除失效 IP;结果按 TTL 缓存于 KV。
5411+
// 说明:Worker 无法以指定 IP 出站,故以 80 端口 HTTP 连通性近似(CF 任播 IP 的 80/443 通常同生共死)
5412+
async function 优选IP健康筛选(env, IP列表, 缓存分钟 = 30) {
5413+
const 缓存键 = '优选健康IP';
5414+
try {
5415+
const 缓存 = await env.KV?.get(缓存键);
5416+
if (缓存) return JSON.parse(缓存);
5417+
} catch (_) {}
5418+
const 待探测 = [];
5419+
const 保留条目 = []; // 无法探测的条目(如 sub:// 指令、异常格式)原样保留,不参与筛选
5420+
for (const 原始地址 of IP列表) {
5421+
const 备注位置 = 原始地址.indexOf('#');
5422+
const 地址部分 = (备注位置 > -1 ? 原始地址.slice(0, 备注位置) : 原始地址).trim();
5423+
if (/^(\[[\da-fA-F:]+\]|[\d.]+|[a-zA-Z0-9][\w.-]*)(?::\d+)?$/.test(地址部分)) 待探测.push(原始地址);
5424+
else 保留条目.push(原始地址);
5425+
}
5426+
const 探测结果 = await Promise.allSettled(待探测.map(async (原始地址) => {
5427+
const 备注位置 = 原始地址.indexOf('#');
5428+
const 地址部分 = (备注位置 > -1 ? 原始地址.slice(0, 备注位置) : 原始地址).trim();
5429+
const 主机 = 地址部分.replace(/:\d+$/, '');
5430+
try {
5431+
// redirect: manual 避免跟随 301 跳转后因 TLS 证书校验失败误判;任何 HTTP 响应(含 4xx/5xx)均视为可达
5432+
await fetch(`http://${主机}:80/`, { redirect: 'manual', signal: AbortSignal.timeout(4000) });
5433+
return { 原始地址, 可达: true };
5434+
} catch (_) { return { 原始地址, 可达: false }; }
5435+
}));
5436+
const 可达列表 = 探测结果.filter(r => r.status === 'fulfilled' && r.value.可达).map(r => r.value.原始地址);
5437+
const 最终列表 = [...可达列表, ...保留条目];
5438+
if (最终列表.length > 0) await env.KV?.put(缓存键, JSON.stringify(最终列表), { expirationTtl: 缓存分钟 * 60 });
5439+
return 最终列表.length > 0 ? 最终列表 : IP列表; // 全部失效时回退原列表,避免订阅为空
5440+
}
5441+
54065442
function 随机路径(完整节点路径 = "/") {
54075443
const 常用路径目录 = ["about", "account", "acg", "act", "activity", "ad", "ads", "ajax", "album", "albums", "anime", "api", "app", "apps", "archive", "archives", "article", "articles", "ask", "auth", "avatar", "bbs", "bd", "blog", "blogs", "book", "books", "bt", "buy", "cart", "category", "categories", "cb", "channel", "channels", "chat", "china", "city", "class", "classify", "clip", "clips", "club", "cn", "code", "collect", "collection", "comic", "comics", "community", "company", "config", "contact", "content", "course", "courses", "cp", "data", "detail", "details", "dh", "directory", "discount", "discuss", "dl", "dload", "doc", "docs", "document", "documents", "doujin", "download", "downloads", "drama", "edu", "en", "ep", "episode", "episodes", "event", "events", "f", "faq", "favorite", "favourites", "favs", "feedback", "file", "files", "film", "films", "forum", "forums", "friend", "friends", "game", "games", "gif", "go", "go.html", "go.php", "group", "groups", "help", "home", "hot", "htm", "html", "image", "images", "img", "index", "info", "intro", "item", "items", "ja", "jp", "jump", "jump.html", "jump.php", "jumping", "knowledge", "lang", "lesson", "lessons", "lib", "library", "link", "links", "list", "live", "lives", "m", "mag", "magnet", "mall", "manhua", "map", "member", "members", "message", "messages", "mobile", "movie", "movies", "music", "my", "new", "news", "note", "novel", "novels", "online", "order", "out", "out.html", "out.php", "outbound", "p", "page", "pages", "pay", "payment", "pdf", "photo", "photos", "pic", "pics", "picture", "pictures", "play", "player", "playlist", "post", "posts", "product", "products", "program", "programs", "project", "qa", "question", "rank", "ranking", "read", "readme", "redirect", "redirect.html", "redirect.php", "reg", "register", "res", "resource", "retrieve", "sale", "search", "season", "seasons", "section", "seller", "series", "service", "services", "setting", "settings", "share", "shop", "show", "shows", "site", "soft", "sort", "source", "special", "star", "stars", "static", "stock", "store", "stream", "streaming", "streams", "student", "study", "tag", "tags", "task", "teacher", "team", "tech", "temp", "test", "thread", "tool", "tools", "topic", "topics", "torrent", "trade", "travel", "tv", "txt", "type", "u", "upload", "uploads", "url", "urls", "user", "users", "v", "version", "videos", "view", "vip", "vod", "watch", "web", "wenku", "wiki", "work", "www", "zh", "zh-cn", "zh-tw", "zip"];
54085444
const 随机数 = Math.floor(Math.random() * 3 + 1);

0 commit comments

Comments
 (0)