UserscriptAPIWeb

https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI

สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greatest.deepsurf.us/scripts/432003/1381253/UserscriptAPIWeb.js

  1. /**
  2. * UserscriptAPIWeb
  3. *
  4. * 依赖于 `UserscriptAPI`。
  5. *
  6. * 需要通过 `@grant` 引入 `GM_xmlhttpRequest` 或 `GM_download`。
  7. * @version 1.4.0.20240522
  8. * @author Laster2800
  9. * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
  10. */
  11. class UserscriptAPIWeb {
  12. /**
  13. * @param {UserscriptAPI} api `UserscriptAPI`
  14. * @param {(xhr: GM_XHR) => void} [api.options.web.preproc] 请求预处理
  15. */
  16. constructor(api) {
  17. this.api = api
  18. api.options.web ??= { preproc: null }
  19. }
  20.  
  21. /**
  22. * @typedef {XMLHttpRequest} GM_XHR GM 定义的类 `XMLHttpRequest` 对象
  23. */
  24. /**
  25. * 发起网络请求,获取 `GM_XHR`
  26. * @param {Object} details 定义及细节类似于 `GM_xmlhttpRequest` `details`
  27. * @param {'GET' | 'HEAD' | 'POST'} [details.method='GET'] `METHOD`
  28. * @param {string} [details.url] `URL`
  29. * @param {number} [details.timeout] 超时时间
  30. * @param {(xhr: GM_XHR) => void} [details.ontimeout] 超时回调
  31. * @param {(xhr: GM_XHR) => void} [details.onerror] 错误回调
  32. * @param {(xhr: GM_XHR) => void} [details.onload] 加载回调
  33. * @param {string | URLSearchParams | FormData} [details.data] `DATA`
  34. * @param {Object} [options] 选项
  35. * @param {(xhr: GM_XHR) => boolean} [options.check] 检查 `GM_XHR` 是否符合条件
  36. * @param {boolean} [options.throwOnFailed = true] 失败时是否抛出异常,否则打印错误信息
  37. * @returns {Promise<GM_XHR>} `GM_XHR`
  38. * @throws 等待超时、达成终止条件、等待错误时抛出
  39. * @see {@link https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest GM_xmlhttpRequest}
  40. */
  41. requestXHR(details, options) {
  42. if (details) {
  43. const { api } = this
  44. const { check, throwOnFailed = true } = options ?? {}
  45. return new Promise((resolve, reject) => {
  46. if (details.data && details.data instanceof URLSearchParams) {
  47. details.data = details.data.toString()
  48. details.headers = {
  49. 'content-type': 'application/x-www-form-urlencoded',
  50. ...details.headers,
  51. }
  52. if (GM_info.scriptHandler === 'Violentmonkey' && !details.headers.origin) {
  53. details.headers.origin = ''
  54. }
  55. }
  56. details.ontimeout ??= xhr => fail('request: TIMEOUT', details, xhr)
  57. details.onerror ??= xhr => fail('request: ERROR', details, xhr)
  58. details.onload ??= xhr => {
  59. if (check && !check(xhr)) {
  60. fail('request: CHECK-FAIL', details, check, xhr)
  61. if (throwOnFailed) return
  62. }
  63. resolve(xhr)
  64. }
  65. GM_xmlhttpRequest(details)
  66.  
  67. function fail(msg, ...cause) {
  68. if (throwOnFailed) {
  69. reject(new Error(msg, cause.length > 0 ? { cause } : undefined))
  70. } else {
  71. api.logger.error(msg, ...cause)
  72. }
  73. }
  74. })
  75. }
  76. }
  77.  
  78. /**
  79. * 发起网络请求,获取解析结果
  80. * @param {Object} details 定义及细节类似于 `GM_xmlhttpRequest` `details`
  81. * @param {'GET' | 'HEAD' | 'POST'} [details.method='GET'] `METHOD`
  82. * @param {string} [details.url] `URL`
  83. * @param {number} [details.timeout] 超时时间
  84. * @param {(xhr: GM_XHR) => void} [details.ontimeout] 超时回调
  85. * @param {(xhr: GM_XHR) => void} [details.onerror] 错误回调
  86. * @param {(xhr: GM_XHR) => void} [details.onload] 加载回调
  87. * @param {string | URLSearchParams | FormData} [details.data] `DATA`
  88. * @param {Object} [options] 选项
  89. * @param {'json' | 'check' | 'silentCheck'} [options.parser='json'] ```text
  90. * json: 返回 JSON.parse(resp)
  91. * check: 返回 check(resp, xhr),检查失败时打印信息
  92. * silentCheck: 返回 check(resp, xhr),检查失败时不打印信息
  93. * ```
  94. * @param {(resp: Object, xhr: GM_XHR) => boolean} [options.check] 检查 `GM_XHR` 是否符合条件
  95. * @param {boolean} [options.throwOnFailed=true] 失败时是否抛出异常,否则打印错误信息
  96. * @returns {Promise<Object>} 解析结果
  97. * @see {@link https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest GM_xmlhttpRequest}
  98. */
  99. async request(details, options) {
  100. const { api } = this
  101. const { parser = 'json', check, throwOnFailed = true } = options ?? {}
  102. try {
  103. try {
  104. await api.options.web.preproc?.(details)
  105. } catch {
  106. fail('request: PREPROC', api.options.web.preproc, details)
  107. }
  108. const xhr = await this.requestXHR(details)
  109. let resp = null
  110. try {
  111. resp = JSON.parse(xhr.response)
  112. } catch {
  113. fail('request: PARSE', details, xhr)
  114. return null
  115. }
  116. const checkResult = !check || check(resp, xhr)
  117. if (parser === 'silentCheck') {
  118. return checkResult
  119. } else if (parser === 'check') {
  120. if (!checkResult) {
  121. api.logger.error('request: CHECK-FAIL', details, check, resp, xhr)
  122. }
  123. return checkResult
  124. } else {
  125. if (!checkResult) {
  126. fail('request: CHECK-FAIL', details, check, resp, xhr)
  127. }
  128. return resp
  129. }
  130. } catch (e) {
  131. if (throwOnFailed) {
  132. throw e
  133. } else {
  134. api.logger.error(e)
  135. }
  136. }
  137.  
  138. function fail(msg, ...cause) {
  139. if (throwOnFailed) {
  140. throw new Error(msg, cause.length > 0 ? { cause } : undefined)
  141. } else {
  142. api.logger.error(msg, ...cause)
  143. }
  144. }
  145. }
  146.  
  147. /**
  148. * 下载资源
  149. * @param {Object} details 定义及细节同 `GM_download` `details`
  150. * @returns {() => void} 用于终止下载的方法
  151. * @see {@link https://www.tampermonkey.net/documentation.php#GM_download GM_download}
  152. */
  153. download(details) {
  154. if (details) {
  155. const { api } = this
  156. try {
  157. let { name } = details
  158. if (name.includes('.')) {
  159. // name「.」后内容会被误认为后缀导致一系列问题,从 URL 找出真正的后缀名以修复之
  160. let parts = details.url.split('/')
  161. const last = parts.at(-1).split('?')[0]
  162. if (last.includes('.')) {
  163. parts = last.split('.')
  164. name = `${name}.${parts.at(-1)}`
  165. } else {
  166. name = name.replaceAll('.', '_') // 实在找不到后缀时才用这种消极的方案
  167. }
  168. details.name = name
  169. }
  170. details.onerror ??= (error, details) => api.logger.error('download: ERROR', error, details)
  171. details.ontimeout ??= () => api.logger.error('download: TIMEOUT')
  172. GM_download(details)
  173. } catch (e) {
  174. api.logger.error('download: ERROR', e)
  175. }
  176. }
  177. return () => {}
  178. }
  179. }
  180.  
  181. /* global UserscriptAPI */
  182. // eslint-disable-next-line no-lone-blocks
  183. { UserscriptAPI.registerModule('web', UserscriptAPIWeb) }