availability.js 9.3 KB

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