|
|
@@ -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)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|