批量检测mihomo节点里gemini和gpt可用性脚本
从 不科学手动测试节点质量的一点小思路 说起,当时我就在想,节点的质量到底怎么检测才是真实有效,从不科学手动测试里,我的想法就是,只有真实的去测试每个对应的项目,才能得出对应渠道数据库的黑ip,不黑的话,那就是好ip,但是手动太累了,就ai写了个py,批量检测下。
因为散热器炸了,换好新的散热,我就想,先把ai方面检测了吧,遂有了下面的python,恢复公益站 心尘公益 API 沉浸式翻译 第四批已领完 已恢复 的使用,以防其他没问题,被ip绊住了。
下午我自己测了下,基本没啥问题,但是有些前提哈,仅适合使用clash客户端或者mihomo(我自己用)这样的,然后有自己newapi聚合站有基础的打野gemini+论坛公益的gpt的佬们用。
我曾想过各种杂七杂八检测gemini和gpt的方式,论坛我看佬们也晒了蛮多,但是我用了下发现不尽人意,明明检测是ok的,实际使用就被墙,明明检测是无效节点,但是gemini和gpt跑的飞快,后来我想了下,还是最原始的操作吧,我直接对话批量检测,慢归慢,扫出来的节点有效能用才ok,对喽,因为有的节点延迟太高,对话时间超时,也会归类为无效节点,我想这类节点,哪怕能用,10秒都连不上,这么不稳定,无效就无效吧。
节点数量少,几个十几个,手动很方便,但是节点多了,真的太痛苦了,还是批量吧。
覆写js,保证auto分组
// AUTO测试组
function main(config) {
const proxyCount = config?.proxies?.length ?? 0;
try {
if (config.proxies && config.proxies.length > 0) {
console.log("原始代理节点列表:");
config.proxies.forEach(proxy => {
console.log(`- ${proxy.name || '未知名称'}`);
});
}
//全局屏蔽
const specificNodesToBlock = [
//gemini gpt双不支持
"节点名字",
].sort(naturalSort);
// 屏蔽包含特定关键字的节点
const blockedKeywords = ["氪金分割线", "????","中国", "群", "续费", "充值后可解锁更多节点哦", "分割", "充值", "剩余", "失联", "余额", "永久", "分享", "备用", "前往", "恢复", "webshare", "建议", "标准", "持续", "后台", "重置", "距离", "年付", "邀请", "返利", "循环", "官网", "客服", "网站", "网址", "获取", "订阅", "流量", "到期", "机场", "下次", "版本", "官址", "过期", "已用", "联系", "邮箱", "工单", "贩卖", "通知", "倒卖", "防止", "国内", "地址", "频道", "无法", "说明", "使用", "提示", "特别", "访问", "支持", "付费", "失联", "设置", "总计", "澳门", "升级", "解锁"];
if (config.proxies && config.proxies.length > 0) {
console.log("开始过滤代理节点...");
config.proxies = config.proxies.filter(proxy => {
// 增加一个检查,确保 proxy 是一个有效的对象
if (!proxy) {
console.log("过滤掉无效代理: null/undefined");
return false; // 过滤掉 null 或 undefined 的代理
}
const originalName = proxy.name || '';
const name = originalName.toLowerCase();
const server = proxy.server || '';
const port = proxy.port || 0;
const password = proxy.password || '';
console.log(`正在处理代理: ${originalName}`);
// 检查是否为需要精确屏蔽的节点
const isSpecificallyBlocked = specificNodesToBlock.includes(originalName);
if (isSpecificallyBlocked) {
console.log(` - ${originalName} 被精确屏蔽`);
return false; // 如果匹配,则屏蔽该节点
}
// 检查是否包含被屏蔽的关键字
const hasBlockedKeyword = blockedKeywords.some(keyword => {
if (typeof keyword === 'string') {
const result = name.includes(keyword);
if (result) console.log(` - ${originalName} 匹配字符串关键字: ${keyword}`);
return result;
} else if (keyword instanceof RegExp) {
const result = keyword.test(originalName); // 使用 originalName 进行正则匹配
if (result) console.log(` - ${originalName} 匹配正则表达式关键字: ${keyword}`);
return result;
}
return false;
});
const shouldKeep = !isSpecificallyBlocked && !hasBlockedKeyword;
console.log(` - ${originalName} 最终保留状态: ${shouldKeep}`);
return shouldKeep;
});
console.log("代理节点过滤完成,剩余节点数:", config.proxies.length);
// 处理重复节点名称,添加数字后缀
const nameCount = {};
config.proxies = config.proxies.map(proxy => {
const originalName = proxy.name || '';
if (nameCount[originalName]) {
nameCount[originalName]++;
proxy.name = `${originalName}${nameCount[originalName]}`;
} else {
nameCount[originalName] = 1;
}
return proxy;
});
console.log("节点重命名完成");
}
} catch (error) {
console.error("代理节点过滤过程中发生错误:", error);
}
if (proxyCount === 0) {
throw new Error("配置文件中未找到任何代理");
}
// 覆盖原配置中DNS配置
config["dns"] = dnsConfig;
// 覆盖原配置中的代理组
config["proxy-groups"] = proxyGroupConfig;
// 覆盖原配置中的规则
config["rule-providers"] = ruleProviders;
config["rules"] = rules;
//覆盖通用配置
config["mixed-port"] = 7890;
config["allow-lan"] = true;
config["bind-address"] = "*";
config["ipv6"] = true;
config["unified-delay"] = true;
// 返回修改后的配置
return config;
}
// DNS配置
const dnsConfig = {
"enable": true,
"ipv6": false,
"listen": ":53",
"prefer-h3": false,
"respect-rules": true,
"enhanced-mode": "fake-ip",
"fake-ip-range": "198.18.0.1/16",
"fake-ip-filter": [
// 本地主机/设备
"+.lan",
"+.local",
// Windows网络出现小地球图标
"+.msftconnecttest.com",
"+.msftncsi.com",
// QQ快速登录检测失败
"localhost.ptlogin2.qq.com",
"localhost.sec.qq.com",
// 微信快速登录检测失败
"localhost.work.weixin.qq.com"
],
"use-hosts": false,
"use-system-hosts": false,
"nameserver": ["https://1.1.1.1/dns-query", "https://dns.google/dns-query"], // 国外默认的域名解析服务器
"default-nameserver": ["tls://223.5.5.5", "tls://119.29.29.29"], //国内默认DNS 用于解析 DNS服务器 的域名
"proxy-server-nameserver": ["https://doh.pub/dns-query"],
"direct-nameserver": ['https://doh.pub/dns-query','https://dns.alidns.com/dns-query'], //用于 direct 出口域名解析的 DNS 服务器
"nameserver-policy": {
"geosite:cn": ["https://doh.pub/dns-query"],
"geolocation-!cn": ["https://1.1.1.1/dns-query", "https://dns.google/dns-query"]
}
}
// 代理组通用配置
// 代理组通用配置 - 基础版 (用于 select 等不主动测速的组)
const groupBaseOption = {
"timeout": 10000,
"url": "https://www.gstatic.com/generate_204",
"max-failed-times": 3,
"hidden": false
};
// 健康检查组通用配置 (用于 url-test, fallback 等)
const healthCheckGroupOption = {
...groupBaseOption,
"interval": 600, // 每600秒检查一次
"lazy": false, // 启动时立即检查
};
// 代理组排除过滤器
// Helper function to create a regex filter from a node list
const createExcludeFilter = (nodes) => {
if (!nodes || nodes.length === 0) {
return "";
}
// Escape special regex characters in each node name
const escapedNodes = nodes.map(name => name.replace(/[.*+?^${}()|[]]/g, '$&'));
return `(?i)${escapedNodes.join('|')}`;
};
const naturalSort = (a, b) => a.localeCompare(b, 'zh-CN', { numeric: true });
const excludeForAUTONodes = [
];
// Gemini 排除的节点列表【不支持】
const excludeForGeminiNodes = [
//批量检查不能用
"节点名字",
].sort(naturalSort);
// OpenAI 排除的节点列表【不支持】
const excludeForOpenAINodes = [
//批量检查不能用
"节点名字",
].sort(naturalSort);
// 使用辅助函数生成最终的过滤器字符串
const excludeForAUTO = createExcludeFilter(excludeForAUTONodes);
const excludeForGemini = createExcludeFilter(excludeForGeminiNodes);
const excludeForOpenAI = createExcludeFilter(excludeForOpenAINodes);
// 代理组规则
const proxyGroupConfig = [
{
...groupBaseOption,
"name": "AUTO",
"type": "select",
"include-all": true,
"tolerance": 50,
"exclude-filter": excludeForAUTO,
"icon": "https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Speedtest.png"
}
];
// 规则集通用配置
const ruleProviderCommon = {
"type": "http",
"format": "yaml",
"interval": 86400,
"proxy": "AUTO"
};
// 规则集配置
const ruleProviders = {
"OpenAI": {
...ruleProviderCommon,
"behavior": "classical",
"url": "https://cdn.jsdelivr.net/gh/lianwusuoai/clash_rule@master/rule/Clash/OpenAI/OpenAI.yaml",
"path": "./ruleset/OpenAI.yaml"
},
"Gemini": {
...ruleProviderCommon,
"behavior": "classical",
"url": "https://cdn.jsdelivr.net/gh/lianwusuoai/clash_rule@master/rule/Clash/Gemini/Gemini.yaml",
"path": "./ruleset/Gemini.yaml"
},
};
// 规则
const rules = [
"DOMAIN-SUFFIX,openai.com,AUTO",
"DOMAIN-SUFFIX,googleapis.com,AUTO",
//规则组
"RULE-SET,Gemini,AUTO",
"RULE-SET,OpenAI,AUTO",
//自定义 大公司包含关键字--代理
"DOMAIN-KEYWORD,googleapis,AUTO",
"DOMAIN-KEYWORD,openai.com,AUTO",
//自定义 大公司包含关键字--直连
"DOMAIN-KEYWORD,12306,DIRECT",
"DOMAIN-KEYWORD,alipay,DIRECT",
"DOMAIN-KEYWORD,baidu,DIRECT",
"DOMAIN-KEYWORD,douyin,DIRECT",
"DOMAIN-KEYWORD,gov.cn,DIRECT",
"DOMAIN-KEYWORD,immersivetranslate,DIRECT",
"DOMAIN-KEYWORD,jd,DIRECT",
"DOMAIN-KEYWORD,quark,DIRECT",
"DOMAIN-KEYWORD,qq,DIRECT",
"DOMAIN-KEYWORD,sogou,DIRECT",
"DOMAIN-KEYWORD,taobao,DIRECT",
"DOMAIN-KEYWORD,tencent,DIRECT",
"DOMAIN-KEYWORD,Thunder,DIRECT",
"DOMAIN-KEYWORD,xiaohongshu,DIRECT",
"DOMAIN-KEYWORD,xiaomi,DIRECT",
"DOMAIN-KEYWORD,xunlei,DIRECT",
"DOMAIN-KEYWORD,youku,DIRECT",
"DOMAIN-KEYWORD,zhihu,DIRECT",
//自定义网站组-代理
"DOMAIN-SUFFIX,linux.do,AUTO",
"DOMAIN-SUFFIX,linux.edu.com,AUTO",
"DOMAIN-SUFFIX,linux.edu.com.lv,AUTO",
"DOMAIN-SUFFIX,veloera.wei.bi,AUTO",
//软件组
"PROCESS-NAME,douyin_tray.exe,DIRECT",
// 自定义 IP-CIDR 规则
'IP-CIDR,127.0.0.0/8,DIRECT',
'IP-CIDR,192.168.0.0/16,DIRECT',
'GEOIP,CN,DIRECT',
'GEOIP,private,DIRECT',
// 国内直连
"MATCH,AUTO"
];
使用真实的url+key进行检测真实使用环境
CHAT_COMPLETIONS_URL = 填写自己newapi的url
API_KEY =填写自己newapi的key
AI连通性检测.py
import requests
import time
from datetime import datetime
import sys
import re
import os
import json
# --- 全局配置 ---
CONTROLLER_PORT = 7891
PROXY_PORT = 7890
API_KEY = "填写自己newapi的key"
CHAT_COMPLETIONS_URL = "http://填写自己的/v1/chat/completions"
MAIN_SELECTOR = "AUTO"
OVERRIDE_JS_PATH = "data/override/198289f8d6f.js"
SWITCH_DELAY = 2
API_TIMEOUT = 10 # API请求的超时时间(秒)
# --- JS解析与API交互 ---
def parse_js_exclusions(js_content, var_name):
"""从JS代码字符串中解析出指定的数组变量内容"""
try:
match = re.search(fr'consts+{var_name}s*=s*[([sS]*?)]', js_content)
if not match:
print(f" - 警告: 未在JS文件中找到变量 '{var_name}'。")
return set()
content_inside_brackets = match.group(1)
nodes = re.findall(r'["'](.*?)["']', content_inside_brackets)
cleaned_nodes = {node.strip() for node in nodes}
print(f" - 成功解析变量 '{var_name}',找到 {len(cleaned_nodes)} 个排除节点。")
return cleaned_nodes
except Exception as e:
print(f" - 错误: 解析JS变量 '{var_name}' 时发生错误: {e}")
return set()
def check_api_connection(port):
print(f"正在尝试连接到 API 控制器,地址: http://127.0.0.1:{port}")
try:
response = requests.get(f"http://127.0.0.1:{port}/", timeout=3)
if response.status_code in [200, 302]:
print("✓ API 控制器连接成功!")
return True
except Exception:
pass
print(f"✗ 无法连接到 API 控制器,请检查端口 {port} 是否正确以及Mihomo/Clash是否正在运行。")
return False
def get_group_proxies(port, group_name):
print(f"正在从策略组 '{group_name}' 获取全量节点列表...")
try:
url = f"http://127.0.0.1:{port}/proxies/{group_name}"
response = requests.get(url, timeout=5)
response.raise_for_status()
proxies = response.json().get('all', [])
cleaned_proxies = {p.strip() for p in proxies}
print(f"✓ 成功获取到 {len(cleaned_proxies)} 个唯一节点。")
return cleaned_proxies
except Exception as e:
print(f"✗ 获取策略组 '{group_name}' 节点时发生错误: {e}")
return set()
def get_nodes_delay(port, nodes_to_check):
print("正在一次性获取所有节点的延迟数据...")
try:
url = f"http://127.0.0.1:{port}/proxies"
response = requests.get(url, timeout=10)
response.raise_for_status()
proxies_data = response.json().get('proxies', {})
delay_info = {}
for node in nodes_to_check:
details = proxies_data.get(node, {})
history = details.get('history', [])
delay = history[-1].get('delay', 0) if history else 0
delay_info[node] = delay
print(f"✓ 成功获取到 {len(delay_info)} 个目标节点的延迟数据。")
return delay_info
except Exception as e:
print(f"✗ 获取节点延迟信息时发生错误: {e}")
return None
def switch_proxy(port, selector_name, proxy_name):
try:
url = f"http://127.0.0.1:{port}/proxies/{selector_name}"
payload = {"name": proxy_name}
requests.put(url, json=payload, timeout=3).raise_for_status()
return True
except Exception as e:
print(f" ✗ 切换到节点 '{proxy_name}' 失败: {e}")
return False
def get_current_proxy(port, selector_name):
try:
url = f"http://127.0.0.1:{port}/proxies/{selector_name}"
response = requests.get(url, timeout=3)
response.raise_for_status()
return response.json().get('now')
except Exception:
return None
def close_all_connections(port):
"""强制关闭所有活动的代理连接,确保切换后使用新节点。静默执行。"""
try:
requests.delete(f"http://127.0.0.1:{port}/connections", timeout=2)
# 静默执行,不打印任何信息
except Exception:
# 静默执行,忽略错误
pass
# --- 连接测试函数 (V22 最终版) ---
def test_gemini_connection(proxy_port):
proxies = {'http': f'http://127.0.0.1:{proxy_port}', 'https': f'http://127.0.0.1:{proxy_port}'}
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
data = {"model": "gemini-1.5-flash", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 10}
try:
# 直接使用 requests.post,确保每次都是独立连接,不受连接池影响
response = requests.post(CHAT_COMPLETIONS_URL, headers=headers, json=data, proxies=proxies, timeout=API_TIMEOUT)
if response.status_code == 200:
response_json = response.json()
if response_json and 'choices' in response_json and len(response_json['choices']) > 0:
return '支持'
return '不支持'
except Exception:
return '超时'
def test_openai_connection(proxy_port):
proxies = {'http': f'http://127.0.0.1:{proxy_port}', 'https': f'http://127.0.0.1:{proxy_port}'}
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
data = {"model": "gpt-4.1-nano", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 10}
try:
# 直接使用 requests.post,确保每次都是独立连接,不受连接池影响
response = requests.post(CHAT_COMPLETIONS_URL, headers=headers, json=data, proxies=proxies, timeout=API_TIMEOUT)
if response.status_code == 200:
response_json = response.json()
if response_json and 'choices' in response_json and len(response_json['choices']) > 0:
return '支持'
return '不支持'
except Exception:
return '超时'
# --- 结果处理与写入 ---
def write_final_report(results, timestamp):
filename = f"测速/AI问题节点报告_{timestamp}.yaml"
report = {"全部不支持": [], "不支持 Gemini": [], "不支持 OpenAI": [], "超时节点": results.get("timeout_nodes", [])}
for node, res in results.items():
if node == "timeout_nodes": continue
gemini_ok = res.get("Gemini") == "支持"
openai_ok = res.get("OpenAI") == "支持"
if "Gemini" in res and "OpenAI" in res:
if not gemini_ok and not openai_ok: report["全部不支持"].append(node)
if "Gemini" in res and not gemini_ok: report["不支持 Gemini"].append(node)
if "OpenAI" in res and not openai_ok: report["不支持 OpenAI"].append(node)
yaml_content = [f"报告名称: AI服务问题节点报告", f"测试时间: {timestamp}"]
for category, nodes in report.items():
yaml_content.append(f"
# {category}")
yaml_content.append(f"{category}:")
if nodes:
unique_nodes = sorted(list(set(nodes)))
for node in unique_nodes:
yaml_content.append(f' "{node}",')
with open(filename, 'w', encoding='utf-8') as f:
f.write('
'.join(yaml_content))
print(f"
✓ 问题节点报告已保存到: {filename}")
# --- 主函数 (V22) ---
def main():
print("=" * 50)
print(" Mihomo/Clash AI 服务测试脚本 (V22 - 最终版)")
print("=" * 50)
if not check_api_connection(CONTROLLER_PORT): sys.exit(1)
print("
--- 步骤 1: 解析JS获取排除列表 ---")
if not os.path.exists(OVERRIDE_JS_PATH):
print(f"✗ 错误: 找不到 override.js 文件,路径: {OVERRIDE_JS_PATH}")
sys.exit(1)
with open(OVERRIDE_JS_PATH, 'r', encoding='utf-8') as f:
js_code = f.read()
exclude_nodes = parse_js_exclusions(js_code, 'excludeForAUTONodes')
print("
--- 步骤 2: 计算测试范围 ---")
all_auto_nodes = get_group_proxies(CONTROLLER_PORT, MAIN_SELECTOR)
if not all_auto_nodes:
print("✗ 未能从'AUTO'组获取任何节点,测试无法继续。")
sys.exit(1)
total_nodes_to_check = all_auto_nodes - exclude_nodes
print(f" - 总计待测节点 (去重后): {len(total_nodes_to_check)} 个节点")
print("
--- 步骤 3: 预先筛选超时节点 ---")
delays = get_nodes_delay(CONTROLLER_PORT, total_nodes_to_check)
if delays is None: sys.exit(1)
timeout_nodes = {name for name, delay in delays.items() if delay == 0}
available_nodes = total_nodes_to_check - timeout_nodes
print(f" - 发现 {len(timeout_nodes)} 个超时节点,将不参与后续测试。")
print("
--- 步骤 4: 执行统一调度测试 ---")
final_results = {"timeout_nodes": list(timeout_nodes)}
sorted_available_nodes = sorted(list(available_nodes))
print(f"
--- 步骤 4: 执行独立连接测试 ---")
print(f"- 将对以下 {len(sorted_available_nodes)} 个可用节点进行测试...")
total_node_count = len(sorted_available_nodes)
for i, node in enumerate(sorted_available_nodes):
current_progress = f"{i+1}/{total_node_count}"
print(f"
- {current_progress} 测试节点: {node}")
if not switch_proxy(CONTROLLER_PORT, MAIN_SELECTOR, node):
continue
# 核心修复:强制关闭所有活动连接,确保新节点生效(静默)
close_all_connections(CONTROLLER_PORT)
time.sleep(SWITCH_DELAY) # 等待连接重置和路由刷新
final_results[node] = {}
results_line = []
# 同时测试 Gemini 和 OpenAI
gemini_result = test_gemini_connection(PROXY_PORT)
final_results[node]["Gemini"] = gemini_result
results_line.append(f"Gemini: {gemini_result}")
openai_result = test_openai_connection(PROXY_PORT)
final_results[node]["OpenAI"] = openai_result
results_line.append(f"OpenAI: {openai_result}")
print(f" → {' '.join(results_line)}")
print("
- 测试完成,不恢复节点选择。")
write_final_report(final_results, datetime.now().strftime("%Y-%m-%d_%H-%M"))
print("
--- 所有测试完成 ---")
if __name__ == "__main__":
main()
然后你会得到不支持的节点名,丢到覆写js的gemini和openai分组单独屏蔽就好啦。
对于超时的节点,有空再针对测试下超时的节点就好啦
HISTORY_REPORT_PATH = “测速/AI问题节点报告_2025-08-08_16-11.yaml”
就是找当前文件夹“测速”下面刚测完的文件,文件里有超时的节点信息,针对这些超时的单独测
import requests
import time
from datetime import datetime
import sys
import re
import os
import json
import yaml
# --- 全局配置 ---
CONTROLLER_PORT = 7891
PROXY_PORT = 7890
API_KEY = "同上 写自己的newapi的key"
CHAT_COMPLETIONS_URL = "http://同上/v1/chat/completions"
MAIN_SELECTOR = "AUTO"
OVERRIDE_JS_PATH = "data/override/auto.js"
HISTORY_REPORT_PATH = "测速/AI问题节点报告_2025-08-08_16-11.yaml" # 历史报告文件路径
SWITCH_DELAY = 2
API_TIMEOUT = 10 # API请求的超时时间(秒)
# --- JS解析与API交互 ---
def parse_js_exclusions(js_content, var_name):
"""从JS代码字符串中解析出指定的数组变量内容"""
try:
match = re.search(fr'consts+{var_name}s*=s*[([sS]*?)]', js_content)
if not match:
print(f" - 警告: 未在JS文件中找到变量 '{var_name}'。")
return set()
content_inside_brackets = match.group(1)
nodes = re.findall(r'["'](.*?)["']', content_inside_brackets)
cleaned_nodes = {node.strip() for node in nodes}
print(f" - 成功解析变量 '{var_name}',找到 {len(cleaned_nodes)} 个排除节点。")
return cleaned_nodes
except Exception as e:
print(f" - 错误: 解析JS变量 '{var_name}' 时发生错误: {e}")
return set()
def parse_timeout_nodes_from_report(report_path):
"""从历史报告文件中解析超时节点列表"""
print(f"正在从历史报告文件中解析超时节点: {report_path}")
try:
if not os.path.exists(report_path):
print(f" - 警告: 找不到历史报告文件,路径: {report_path}")
return set()
timeout_nodes = set()
current_category = None
with open(report_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
# 检查是否是分类标题
if line.startswith("# "):
current_category = line[2:] # 去掉"# "
# 检查是否是超时节点分类
elif current_category == "超时节点" and line.startswith('"'):
# 提取节点名称,去掉引号和可能的逗号
&
用户必须遵守《计算机软件保护条例(2013修订)》第十七条:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。鉴于此条例,用户从本平台下载的全部源码(软件)教程仅限学习研究,未经版权归属者授权不得商用,若因商用引起的版权纠纷,一切责任均由使用者自行承担,本平台所属公司及其雇员不承担任何法律责任。
前端与后端开发高频疑难问题 » 批量检测mihomo节点里gemini和gpt可用性脚本