浏览代码

添加 'availability.js'

aurora 3 周之前
父节点
当前提交
5dacf850da
共有 1 个文件被更改,包括 250 次插入0 次删除
  1. 250 0
      availability.js

+ 250 - 0
availability.js

@@ -0,0 +1,250 @@
+/**
+ * 节点测活(适配 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(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)
+      }
+    })
+  }
+}