availability.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /**
  2. * 节点测活(适配 Surge/Loon 版)
  3. *
  4. * 说明: https://t.me/zhetengsha/1210
  5. *
  6. * 欢迎加入 Telegram 群组 https://t.me/zhetengsha
  7. *
  8. * 参数
  9. * - [timeout] 请求超时(单位: 毫秒) 默认 5000
  10. * - [retries] 重试次数 默认 1
  11. * - [retry_delay] 重试延时(单位: 毫秒) 默认 1000
  12. * - [concurrency] 并发数 默认 10
  13. * - [url] 检测的 URL. 需要 encodeURIComponent. 默认 http://connectivitycheck.platform.hicloud.com/generate_204
  14. * - [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
  15. * - [status] 合法的状态码的正则表达式. 需要 encodeURIComponent. 默认 204
  16. * - [method] 请求方法. 默认 head, 如果测试 URL 不支持, 可设为 get
  17. * - [show_latency] 显示延迟. 默认不显示. 注: 即使不开启这个参数, 节点上也会添加一个 _latency 字段
  18. * - [include_unsupported_proxy] 传递给运行环境时, 包含官方/商店版不支持的协议. 默认不包含. 若开启, 需要保证你的运行环境确实支持这些协议, 不然会报错
  19. * - [keep_incompatible] 保留当前客户端不兼容的协议. 默认不保留.
  20. * - [telegram_bot_token] Telegram Bot Token
  21. * - [telegram_chat_id] Telegram Chat ID
  22. * - [cache] 使用缓存, 默认不使用缓存
  23. * - [disable_failed_cache/ignore_failed_error] 禁用失败缓存. 即不缓存失败结果
  24. * 关于缓存时长
  25. * 当使用相关脚本时, 若在对应的脚本中使用参数(⚠ 别忘了这个, 一般为 cache, 值设为 true 即可)开启缓存
  26. * 可在前端(>=2.16.0) 配置各项缓存的默认时长
  27. * 持久化缓存数据在 JSON 里
  28. * 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活
  29. * async function operator() {
  30. * scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
  31. * }
  32. */
  33. async function operator(proxies = [], targetPlatform, env) {
  34. const $ = $substore
  35. const { isLoon, isSurge } = $.env
  36. if (!isLoon && !isSurge) throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)')
  37. const telegram_chat_id = $arguments.telegram_chat_id
  38. const telegram_bot_token = $arguments.telegram_bot_token
  39. const cacheEnabled = $arguments.cache
  40. const disableFailedCache = $arguments.disable_failed_cache || $arguments.ignore_failed_error
  41. const cache = scriptResourceCache
  42. const method = $arguments.method || 'head'
  43. const keepIncompatible = $arguments.keep_incompatible
  44. const includeUnsupportedProxy = $arguments.include_unsupported_proxy
  45. const validStatus = new RegExp($arguments.status || '204')
  46. const url = decodeURIComponent($arguments.url || 'http://connectivitycheck.platform.hicloud.com/generate_204')
  47. const ua = decodeURIComponent(
  48. $arguments.ua ||
  49. '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'
  50. )
  51. const target = isLoon ? 'Loon' : isSurge ? 'Surge' : undefined
  52. const validProxies = []
  53. const incompatibleProxies = []
  54. const failedProxies = []
  55. let name = ''
  56. for (const [key, value] of Object.entries(env.source)) {
  57. if (!key.startsWith('_')) {
  58. name = value.displayName || value.name
  59. break
  60. }
  61. }
  62. if (!name) {
  63. const collection = env.source._collection
  64. name = collection.displayName || collection.name
  65. }
  66. const concurrency = parseInt($arguments.concurrency || 10) // 一组并发数
  67. await executeAsyncTasks(
  68. proxies.map(proxy => () => check(proxy)),
  69. { concurrency }
  70. )
  71. // const batches = []
  72. // for (let i = 0; i < proxies.length; i += concurrency) {
  73. // const batch = proxies.slice(i, i + concurrency)
  74. // batches.push(batch)
  75. // }
  76. // for (const batch of batches) {
  77. // await Promise.all(batch.map(check))
  78. // }
  79. if (telegram_chat_id && telegram_bot_token && failedProxies.length > 0) {
  80. const text = `\`${name}\` 节点测试:\n${failedProxies
  81. .map(proxy => `❌ [${proxy.type}] \`${proxy.name}\``)
  82. .join('\n')}`
  83. await http({
  84. method: 'post',
  85. url: `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`,
  86. headers: {
  87. 'Content-Type': 'application/json',
  88. },
  89. body: JSON.stringify({ chat_id: telegram_chat_id, text, parse_mode: 'MarkdownV2' }),
  90. retries: 0,
  91. timeout: 5000,
  92. })
  93. }
  94. return validProxies
  95. async function check(proxy) {
  96. // $.info(`[${proxy.name}] 检测`)
  97. // $.info(`检测 ${JSON.stringify(proxy, null, 2)}`)
  98. const id = cacheEnabled
  99. ? `availability:${url}:${method}:${validStatus}:${JSON.stringify(
  100. Object.fromEntries(
  101. Object.entries(proxy).filter(([key]) => !/^(name|collectionName|subName|id|_.*)$/i.test(key))
  102. )
  103. )}`
  104. : undefined
  105. // $.info(`检测 ${id}`)
  106. try {
  107. const node = ProxyUtils.produce([proxy], target, undefined, {
  108. 'include-unsupported-proxy': includeUnsupportedProxy,
  109. })
  110. if (node) {
  111. const cached = cache.get(id)
  112. if (cacheEnabled && cached) {
  113. if (cached.latency) {
  114. validProxies.push({
  115. ...proxy,
  116. name: `${$arguments.show_latency ? `[${cached.latency}] ` : ''}${proxy.name}`,
  117. _latency: cached.latency,
  118. })
  119. $.info(`[${proxy.name}] 使用成功缓存`)
  120. return
  121. } else if (disableFailedCache) {
  122. $.info(`[${proxy.name}] 不使用失败缓存`)
  123. } else {
  124. $.info(`[${proxy.name}] 使用失败缓存`)
  125. return
  126. }
  127. }
  128. // 请求
  129. const startedAt = Date.now()
  130. const res = await http({
  131. method,
  132. headers: {
  133. 'User-Agent': ua,
  134. },
  135. url,
  136. 'policy-descriptor': node,
  137. node,
  138. })
  139. const status = parseInt(res.status || res.statusCode || 200)
  140. let latency = ''
  141. latency = `${Date.now() - startedAt}`
  142. $.info(`[${proxy.name}] status: ${status}, latency: ${latency}`)
  143. // 判断响应
  144. if (validStatus.test(status)) {
  145. validProxies.push({
  146. ...proxy,
  147. name: `${$arguments.show_latency ? `[${latency}] ` : ''}${proxy.name}`,
  148. _latency: latency,
  149. })
  150. if (cacheEnabled) {
  151. $.info(`[${proxy.name}] 设置成功缓存`)
  152. cache.set(id, { latency })
  153. }
  154. } else {
  155. if (cacheEnabled) {
  156. $.info(`[${proxy.name}] 设置失败缓存`)
  157. cache.set(id, {})
  158. }
  159. failedProxies.push(proxy)
  160. }
  161. } else {
  162. if (keepIncompatible) {
  163. validProxies.push(proxy)
  164. }
  165. incompatibleProxies.push(proxy)
  166. }
  167. } catch (e) {
  168. $.error(`[${proxy.name}] ${e.message ?? e}`)
  169. if (cacheEnabled) {
  170. $.info(`[${proxy.name}] 设置失败缓存`)
  171. cache.set(id, {})
  172. }
  173. failedProxies.push(proxy)
  174. }
  175. }
  176. // 请求
  177. async function http(opt = {}) {
  178. const METHOD = opt.method || 'get'
  179. const TIMEOUT = parseFloat(opt.timeout || $arguments.timeout || 5000)
  180. const RETRIES = parseFloat(opt.retries ?? $arguments.retries ?? 1)
  181. const RETRY_DELAY = parseFloat(opt.retry_delay ?? $arguments.retry_delay ?? 1000)
  182. let count = 0
  183. const fn = async () => {
  184. try {
  185. return await $.http[METHOD]({ ...opt, timeout: TIMEOUT })
  186. } catch (e) {
  187. // $.error(e)
  188. if (count < RETRIES) {
  189. count++
  190. const delay = RETRY_DELAY * count
  191. // $.info(`第 ${count} 次请求失败: ${e.message || e}, 等待 ${delay / 1000}s 后重试`)
  192. await $.wait(delay)
  193. return await fn()
  194. } else {
  195. throw e
  196. }
  197. }
  198. }
  199. return await fn()
  200. }
  201. function executeAsyncTasks(tasks, { wrap, result, concurrency = 1 } = {}) {
  202. return new Promise(async (resolve, reject) => {
  203. try {
  204. let running = 0
  205. const results = []
  206. let index = 0
  207. function executeNextTask() {
  208. while (index < tasks.length && running < concurrency) {
  209. const taskIndex = index++
  210. const currentTask = tasks[taskIndex]
  211. running++
  212. currentTask()
  213. .then(data => {
  214. if (result) {
  215. results[taskIndex] = wrap ? { data } : data
  216. }
  217. })
  218. .catch(error => {
  219. if (result) {
  220. results[taskIndex] = wrap ? { error } : error
  221. }
  222. })
  223. .finally(() => {
  224. running--
  225. executeNextTask()
  226. })
  227. }
  228. if (running === 0) {
  229. return resolve(result ? results : undefined)
  230. }
  231. }
  232. await executeNextTask()
  233. } catch (e) {
  234. reject(e)
  235. }
  236. })
  237. }
  238. }