availability.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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() {
  34. scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
  35. }
  36. async function operator(proxies = [], targetPlatform, env) {
  37. const $ = $substore
  38. const { isLoon, isSurge } = $.env
  39. if (!isLoon && !isSurge) throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)')
  40. const telegram_chat_id = $arguments.telegram_chat_id
  41. const telegram_bot_token = $arguments.telegram_bot_token
  42. const cacheEnabled = $arguments.cache
  43. const disableFailedCache = $arguments.disable_failed_cache || $arguments.ignore_failed_error
  44. const cache = scriptResourceCache
  45. const method = $arguments.method || 'head'
  46. const keepIncompatible = $arguments.keep_incompatible
  47. const includeUnsupportedProxy = $arguments.include_unsupported_proxy
  48. const validStatus = new RegExp($arguments.status || '204')
  49. const url = decodeURIComponent($arguments.url || 'http://connectivitycheck.platform.hicloud.com/generate_204')
  50. const ua = decodeURIComponent(
  51. $arguments.ua ||
  52. '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'
  53. )
  54. const target = isLoon ? 'Loon' : isSurge ? 'Surge' : undefined
  55. const validProxies = []
  56. const incompatibleProxies = []
  57. const failedProxies = []
  58. let name = ''
  59. for (const [key, value] of Object.entries(env.source)) {
  60. if (!key.startsWith('_')) {
  61. name = value.displayName || value.name
  62. break
  63. }
  64. }
  65. if (!name) {
  66. const collection = env.source._collection
  67. name = collection.displayName || collection.name
  68. }
  69. const concurrency = parseInt($arguments.concurrency || 10) // 一组并发数
  70. await executeAsyncTasks(
  71. proxies.map(proxy => () => check(proxy)),
  72. { concurrency }
  73. )
  74. // const batches = []
  75. // for (let i = 0; i < proxies.length; i += concurrency) {
  76. // const batch = proxies.slice(i, i + concurrency)
  77. // batches.push(batch)
  78. // }
  79. // for (const batch of batches) {
  80. // await Promise.all(batch.map(check))
  81. // }
  82. if (telegram_chat_id && telegram_bot_token && failedProxies.length > 0) {
  83. const text = `\`${name}\` 节点测试:\n${failedProxies
  84. .map(proxy => `❌ [${proxy.type}] \`${proxy.name}\``)
  85. .join('\n')}`
  86. await http({
  87. method: 'post',
  88. url: `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`,
  89. headers: {
  90. 'Content-Type': 'application/json',
  91. },
  92. body: JSON.stringify({ chat_id: telegram_chat_id, text, parse_mode: 'MarkdownV2' }),
  93. retries: 0,
  94. timeout: 5000,
  95. })
  96. }
  97. return validProxies
  98. async function check(proxy) {
  99. // $.info(`[${proxy.name}] 检测`)
  100. // $.info(`检测 ${JSON.stringify(proxy, null, 2)}`)
  101. const id = cacheEnabled
  102. ? `availability:${url}:${method}:${validStatus}:${JSON.stringify(
  103. Object.fromEntries(
  104. Object.entries(proxy).filter(([key]) => !/^(name|collectionName|subName|id|_.*)$/i.test(key))
  105. )
  106. )}`
  107. : undefined
  108. // $.info(`检测 ${id}`)
  109. try {
  110. const node = ProxyUtils.produce([proxy], target, undefined, {
  111. 'include-unsupported-proxy': includeUnsupportedProxy,
  112. })
  113. if (node) {
  114. const cached = cache.get(id)
  115. if (cacheEnabled && cached) {
  116. if (cached.latency) {
  117. validProxies.push({
  118. ...proxy,
  119. name: `${$arguments.show_latency ? `[${cached.latency}] ` : ''}${proxy.name}`,
  120. _latency: cached.latency,
  121. })
  122. $.info(`[${proxy.name}] 使用成功缓存`)
  123. return
  124. } else if (disableFailedCache) {
  125. $.info(`[${proxy.name}] 不使用失败缓存`)
  126. } else {
  127. $.info(`[${proxy.name}] 使用失败缓存`)
  128. return
  129. }
  130. }
  131. // 请求
  132. const startedAt = Date.now()
  133. const res = await http({
  134. method,
  135. headers: {
  136. 'User-Agent': ua,
  137. },
  138. url,
  139. 'policy-descriptor': node,
  140. node,
  141. })
  142. const status = parseInt(res.status || res.statusCode || 200)
  143. let latency = ''
  144. latency = `${Date.now() - startedAt}`
  145. $.info(`[${proxy.name}] status: ${status}, latency: ${latency}`)
  146. // 判断响应
  147. if (validStatus.test(status)) {
  148. validProxies.push({
  149. ...proxy,
  150. name: `${$arguments.show_latency ? `[${latency}] ` : ''}${proxy.name}`,
  151. _latency: latency,
  152. })
  153. if (cacheEnabled) {
  154. $.info(`[${proxy.name}] 设置成功缓存`)
  155. cache.set(id, { latency })
  156. }
  157. } else {
  158. if (cacheEnabled) {
  159. $.info(`[${proxy.name}] 设置失败缓存`)
  160. cache.set(id, {})
  161. }
  162. failedProxies.push(proxy)
  163. }
  164. } else {
  165. if (keepIncompatible) {
  166. validProxies.push(proxy)
  167. }
  168. incompatibleProxies.push(proxy)
  169. }
  170. } catch (e) {
  171. $.error(`[${proxy.name}] ${e.message ?? e}`)
  172. if (cacheEnabled) {
  173. $.info(`[${proxy.name}] 设置失败缓存`)
  174. cache.set(id, {})
  175. }
  176. failedProxies.push(proxy)
  177. }
  178. }
  179. // 请求
  180. async function http(opt = {}) {
  181. const METHOD = opt.method || 'get'
  182. const TIMEOUT = parseFloat(opt.timeout || $arguments.timeout || 5000)
  183. const RETRIES = parseFloat(opt.retries ?? $arguments.retries ?? 1)
  184. const RETRY_DELAY = parseFloat(opt.retry_delay ?? $arguments.retry_delay ?? 1000)
  185. let count = 0
  186. const fn = async () => {
  187. try {
  188. return await $.http[METHOD]({ ...opt, timeout: TIMEOUT })
  189. } catch (e) {
  190. // $.error(e)
  191. if (count < RETRIES) {
  192. count++
  193. const delay = RETRY_DELAY * count
  194. // $.info(`第 ${count} 次请求失败: ${e.message || e}, 等待 ${delay / 1000}s 后重试`)
  195. await $.wait(delay)
  196. return await fn()
  197. } else {
  198. throw e
  199. }
  200. }
  201. }
  202. return await fn()
  203. }
  204. function executeAsyncTasks(tasks, { wrap, result, concurrency = 1 } = {}) {
  205. return new Promise(async (resolve, reject) => {
  206. try {
  207. let running = 0
  208. const results = []
  209. let index = 0
  210. function executeNextTask() {
  211. while (index < tasks.length && running < concurrency) {
  212. const taskIndex = index++
  213. const currentTask = tasks[taskIndex]
  214. running++
  215. currentTask()
  216. .then(data => {
  217. if (result) {
  218. results[taskIndex] = wrap ? { data } : data
  219. }
  220. })
  221. .catch(error => {
  222. if (result) {
  223. results[taskIndex] = wrap ? { error } : error
  224. }
  225. })
  226. .finally(() => {
  227. running--
  228. executeNextTask()
  229. })
  230. }
  231. if (running === 0) {
  232. return resolve(result ? results : undefined)
  233. }
  234. }
  235. await executeNextTask()
  236. } catch (e) {
  237. reject(e)
  238. }
  239. })
  240. }
  241. }