| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- /**
- * 节点测活(适配 Surge/Loon 版)
- *
- * 说明: https://t.me/zhetengsha/1210
- *
- * 欢迎加入 Telegram 群组 https://t.me/zhetengsha
- *
- * 参数
- * - [timeout] 请求超时(单位: 毫秒) 默认 5000
- * - [retries] 重试次数 默认 1
- * - [retry_delay] 重试延时(单位: 毫秒) 默认 1000
- * - [concurrency] 并发数 默认 10
- * - [url] 检测的 URL. 需要 encodeURIComponent. 默认 http://connectivitycheck.platform.hicloud.com/generate_204
- * - [ua] 请求头 User-Agent. 需要 encodeURIComponent. 默认 Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1
- * - [status] 合法的状态码的正则表达式. 需要 encodeURIComponent. 默认 204
- * - [method] 请求方法. 默认 head, 如果测试 URL 不支持, 可设为 get
- * - [show_latency] 显示延迟. 默认不显示. 注: 即使不开启这个参数, 节点上也会添加一个 _latency 字段
- * - [include_unsupported_proxy] 传递给运行环境时, 包含官方/商店版不支持的协议. 默认不包含. 若开启, 需要保证你的运行环境确实支持这些协议, 不然会报错
- * - [keep_incompatible] 保留当前客户端不兼容的协议. 默认不保留.
- * - [telegram_bot_token] Telegram Bot Token
- * - [telegram_chat_id] Telegram Chat ID
- * - [cache] 使用缓存, 默认不使用缓存
- * - [disable_failed_cache/ignore_failed_error] 禁用失败缓存. 即不缓存失败结果
- * 关于缓存时长
- * 当使用相关脚本时, 若在对应的脚本中使用参数(⚠ 别忘了这个, 一般为 cache, 值设为 true 即可)开启缓存
- * 可在前端(>=2.16.0) 配置各项缓存的默认时长
- * 持久化缓存数据在 JSON 里
- * 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活
- * async function operator() {
- * scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
- * }
- */
- async function operator() {
- scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
- }
- async function operator(proxies = [], targetPlatform, env) {
- const $ = $substore
- const { isLoon, isSurge } = $.env
- if (!isLoon && !isSurge) throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)')
- const telegram_chat_id = $arguments.telegram_chat_id
- const telegram_bot_token = $arguments.telegram_bot_token
- const cacheEnabled = $arguments.cache
- const disableFailedCache = $arguments.disable_failed_cache || $arguments.ignore_failed_error
- const cache = scriptResourceCache
- const method = $arguments.method || 'head'
- const keepIncompatible = $arguments.keep_incompatible
- const includeUnsupportedProxy = $arguments.include_unsupported_proxy
- const validStatus = new RegExp($arguments.status || '204')
- const url = decodeURIComponent($arguments.url || 'http://connectivitycheck.platform.hicloud.com/generate_204')
- const ua = decodeURIComponent(
- $arguments.ua ||
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1'
- )
- const target = isLoon ? 'Loon' : isSurge ? 'Surge' : undefined
- const validProxies = []
- const incompatibleProxies = []
- const failedProxies = []
- let name = ''
- for (const [key, value] of Object.entries(env.source)) {
- if (!key.startsWith('_')) {
- name = value.displayName || value.name
- break
- }
- }
- if (!name) {
- const collection = env.source._collection
- name = collection.displayName || collection.name
- }
- const concurrency = parseInt($arguments.concurrency || 10) // 一组并发数
- await executeAsyncTasks(
- proxies.map(proxy => () => check(proxy)),
- { concurrency }
- )
- // const batches = []
- // for (let i = 0; i < proxies.length; i += concurrency) {
- // const batch = proxies.slice(i, i + concurrency)
- // batches.push(batch)
- // }
- // for (const batch of batches) {
- // await Promise.all(batch.map(check))
- // }
- if (telegram_chat_id && telegram_bot_token && failedProxies.length > 0) {
- const text = `\`${name}\` 节点测试:\n${failedProxies
- .map(proxy => `❌ [${proxy.type}] \`${proxy.name}\``)
- .join('\n')}`
- await http({
- method: 'post',
- url: `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`,
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ chat_id: telegram_chat_id, text, parse_mode: 'MarkdownV2' }),
- retries: 0,
- timeout: 5000,
- })
- }
- return validProxies
- async function check(proxy) {
- // $.info(`[${proxy.name}] 检测`)
- // $.info(`检测 ${JSON.stringify(proxy, null, 2)}`)
- const id = cacheEnabled
- ? `availability:${url}:${method}:${validStatus}:${JSON.stringify(
- Object.fromEntries(
- Object.entries(proxy).filter(([key]) => !/^(name|collectionName|subName|id|_.*)$/i.test(key))
- )
- )}`
- : undefined
- // $.info(`检测 ${id}`)
- try {
- const node = ProxyUtils.produce([proxy], target, undefined, {
- 'include-unsupported-proxy': includeUnsupportedProxy,
- })
- if (node) {
- const cached = cache.get(id)
- if (cacheEnabled && cached) {
- if (cached.latency) {
- validProxies.push({
- ...proxy,
- name: `${$arguments.show_latency ? `[${cached.latency}] ` : ''}${proxy.name}`,
- _latency: cached.latency,
- })
- $.info(`[${proxy.name}] 使用成功缓存`)
- return
- } else if (disableFailedCache) {
- $.info(`[${proxy.name}] 不使用失败缓存`)
- } else {
- $.info(`[${proxy.name}] 使用失败缓存`)
- return
- }
- }
- // 请求
- const startedAt = Date.now()
- const res = await http({
- method,
- headers: {
- 'User-Agent': ua,
- },
- url,
- 'policy-descriptor': node,
- node,
- })
- const status = parseInt(res.status || res.statusCode || 200)
- let latency = ''
- latency = `${Date.now() - startedAt}`
- $.info(`[${proxy.name}] status: ${status}, latency: ${latency}`)
- // 判断响应
- if (validStatus.test(status)) {
- validProxies.push({
- ...proxy,
- name: `${$arguments.show_latency ? `[${latency}] ` : ''}${proxy.name}`,
- _latency: latency,
- })
- if (cacheEnabled) {
- $.info(`[${proxy.name}] 设置成功缓存`)
- cache.set(id, { latency })
- }
- } else {
- if (cacheEnabled) {
- $.info(`[${proxy.name}] 设置失败缓存`)
- cache.set(id, {})
- }
- failedProxies.push(proxy)
- }
- } else {
- if (keepIncompatible) {
- validProxies.push(proxy)
- }
- incompatibleProxies.push(proxy)
- }
- } catch (e) {
- $.error(`[${proxy.name}] ${e.message ?? e}`)
- if (cacheEnabled) {
- $.info(`[${proxy.name}] 设置失败缓存`)
- cache.set(id, {})
- }
- failedProxies.push(proxy)
- }
- }
- // 请求
- async function http(opt = {}) {
- const METHOD = opt.method || 'get'
- const TIMEOUT = parseFloat(opt.timeout || $arguments.timeout || 5000)
- const RETRIES = parseFloat(opt.retries ?? $arguments.retries ?? 1)
- const RETRY_DELAY = parseFloat(opt.retry_delay ?? $arguments.retry_delay ?? 1000)
- let count = 0
- const fn = async () => {
- try {
- return await $.http[METHOD]({ ...opt, timeout: TIMEOUT })
- } catch (e) {
- // $.error(e)
- if (count < RETRIES) {
- count++
- const delay = RETRY_DELAY * count
- // $.info(`第 ${count} 次请求失败: ${e.message || e}, 等待 ${delay / 1000}s 后重试`)
- await $.wait(delay)
- return await fn()
- } else {
- throw e
- }
- }
- }
- return await fn()
- }
- function executeAsyncTasks(tasks, { wrap, result, concurrency = 1 } = {}) {
- return new Promise(async (resolve, reject) => {
- try {
- let running = 0
- const results = []
- let index = 0
- function executeNextTask() {
- while (index < tasks.length && running < concurrency) {
- const taskIndex = index++
- const currentTask = tasks[taskIndex]
- running++
- currentTask()
- .then(data => {
- if (result) {
- results[taskIndex] = wrap ? { data } : data
- }
- })
- .catch(error => {
- if (result) {
- results[taskIndex] = wrap ? { error } : error
- }
- })
- .finally(() => {
- running--
- executeNextTask()
- })
- }
- if (running === 0) {
- return resolve(result ? results : undefined)
- }
- }
- await executeNextTask()
- } catch (e) {
- reject(e)
- }
- })
- }
- }
|