Env.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. /**
  2. * Env
  3. * @author: chavyleung
  4. * https://github.com/chavyleung/scripts/blob/master/Env.js
  5. */
  6. function Env(name, opts) {
  7. class Http {
  8. constructor(env) {
  9. this.env = env
  10. }
  11. send(opts, method = 'GET') {
  12. opts = typeof opts === 'string' ? { url: opts } : opts
  13. let sender = this.get
  14. if (method === 'POST') {
  15. sender = this.post
  16. }
  17. return new Promise((resolve, reject) => {
  18. sender.call(this, opts, (err, resp, body) => {
  19. if (err) reject(err)
  20. else resolve(resp)
  21. })
  22. })
  23. }
  24. get(opts) {
  25. return this.send.call(this.env, opts)
  26. }
  27. post(opts) {
  28. return this.send.call(this.env, opts, 'POST')
  29. }
  30. }
  31. return new (class {
  32. constructor(name, opts) {
  33. this.name = name
  34. this.http = new Http(this)
  35. this.data = null
  36. this.dataFile = 'box.dat'
  37. this.logs = []
  38. this.isMute = false
  39. this.isNeedRewrite = false
  40. this.logSeparator = '\n'
  41. this.encoding = 'utf-8'
  42. this.startTime = new Date().getTime()
  43. Object.assign(this, opts)
  44. this.log('', `🔔${this.name}, 开始!`)
  45. }
  46. getEnv() {
  47. if ('undefined' !== typeof $environment && $environment['surge-version'])
  48. return 'Surge'
  49. if ('undefined' !== typeof $environment && $environment['stash-version'])
  50. return 'Stash'
  51. if ('undefined' !== typeof module && !!module.exports) return 'Node.js'
  52. if ('undefined' !== typeof $task) return 'Quantumult X'
  53. if ('undefined' !== typeof $loon) return 'Loon'
  54. if ('undefined' !== typeof $rocket) return 'Shadowrocket'
  55. }
  56. isNode() {
  57. return 'Node.js' === this.getEnv()
  58. }
  59. isQuanX() {
  60. return 'Quantumult X' === this.getEnv()
  61. }
  62. isSurge() {
  63. return 'Surge' === this.getEnv()
  64. }
  65. isLoon() {
  66. return 'Loon' === this.getEnv()
  67. }
  68. isShadowrocket() {
  69. return 'Shadowrocket' === this.getEnv()
  70. }
  71. isStash() {
  72. return 'Stash' === this.getEnv()
  73. }
  74. toObj(str, defaultValue = null) {
  75. try {
  76. return JSON.parse(str)
  77. } catch {
  78. return defaultValue
  79. }
  80. }
  81. toStr(obj, defaultValue = null) {
  82. try {
  83. return JSON.stringify(obj)
  84. } catch {
  85. return defaultValue
  86. }
  87. }
  88. getjson(key, defaultValue) {
  89. let json = defaultValue
  90. const val = this.getdata(key)
  91. if (val) {
  92. try {
  93. json = JSON.parse(this.getdata(key))
  94. } catch { }
  95. }
  96. return json
  97. }
  98. setjson(val, key) {
  99. try {
  100. return this.setdata(JSON.stringify(val), key)
  101. } catch {
  102. return false
  103. }
  104. }
  105. getScript(url) {
  106. return new Promise((resolve) => {
  107. this.get({ url }, (err, resp, body) => resolve(body))
  108. })
  109. }
  110. runScript(script, runOpts) {
  111. return new Promise((resolve) => {
  112. let httpapi = this.getdata('@chavy_boxjs_userCfgs.httpapi')
  113. httpapi = httpapi ? httpapi.replace(/\n/g, '').trim() : httpapi
  114. let httpapi_timeout = this.getdata(
  115. '@chavy_boxjs_userCfgs.httpapi_timeout'
  116. )
  117. httpapi_timeout = httpapi_timeout ? httpapi_timeout * 1 : 20
  118. httpapi_timeout =
  119. runOpts && runOpts.timeout ? runOpts.timeout : httpapi_timeout
  120. const [key, addr] = httpapi.split('@')
  121. const opts = {
  122. url: `http://${addr}/v1/scripting/evaluate`,
  123. body: {
  124. script_text: script,
  125. mock_type: 'cron',
  126. timeout: httpapi_timeout
  127. },
  128. headers: { 'X-Key': key, 'Accept': '*/*' },
  129. timeout: httpapi_timeout
  130. }
  131. this.post(opts, (err, resp, body) => resolve(body))
  132. }).catch((e) => this.logErr(e))
  133. }
  134. loaddata() {
  135. if (this.isNode()) {
  136. this.fs = this.fs ? this.fs : require('fs')
  137. this.path = this.path ? this.path : require('path')
  138. const curDirDataFilePath = this.path.resolve(this.dataFile)
  139. const rootDirDataFilePath = this.path.resolve(
  140. process.cwd(),
  141. this.dataFile
  142. )
  143. const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath)
  144. const isRootDirDataFile =
  145. !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath)
  146. if (isCurDirDataFile || isRootDirDataFile) {
  147. const datPath = isCurDirDataFile
  148. ? curDirDataFilePath
  149. : rootDirDataFilePath
  150. try {
  151. return JSON.parse(this.fs.readFileSync(datPath))
  152. } catch (e) {
  153. return {}
  154. }
  155. } else return {}
  156. } else return {}
  157. }
  158. writedata() {
  159. if (this.isNode()) {
  160. this.fs = this.fs ? this.fs : require('fs')
  161. this.path = this.path ? this.path : require('path')
  162. const curDirDataFilePath = this.path.resolve(this.dataFile)
  163. const rootDirDataFilePath = this.path.resolve(
  164. process.cwd(),
  165. this.dataFile
  166. )
  167. const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath)
  168. const isRootDirDataFile =
  169. !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath)
  170. const jsondata = JSON.stringify(this.data)
  171. if (isCurDirDataFile) {
  172. this.fs.writeFileSync(curDirDataFilePath, jsondata)
  173. } else if (isRootDirDataFile) {
  174. this.fs.writeFileSync(rootDirDataFilePath, jsondata)
  175. } else {
  176. this.fs.writeFileSync(curDirDataFilePath, jsondata)
  177. }
  178. }
  179. }
  180. lodash_get(source, path, defaultValue = undefined) {
  181. const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
  182. let result = source
  183. for (const p of paths) {
  184. result = Object(result)[p]
  185. if (result === undefined) {
  186. return defaultValue
  187. }
  188. }
  189. return result
  190. }
  191. lodash_set(obj, path, value) {
  192. if (Object(obj) !== obj) return obj
  193. if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []
  194. path
  195. .slice(0, -1)
  196. .reduce(
  197. (a, c, i) =>
  198. Object(a[c]) === a[c]
  199. ? a[c]
  200. : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {}),
  201. obj
  202. )[path[path.length - 1]] = value
  203. return obj
  204. }
  205. getdata(key) {
  206. let val = this.getval(key)
  207. // 如果以 @
  208. if (/^@/.test(key)) {
  209. const [, objkey, paths] = /^@(.*?)\.(.*?)$/.exec(key)
  210. const objval = objkey ? this.getval(objkey) : ''
  211. if (objval) {
  212. try {
  213. const objedval = JSON.parse(objval)
  214. val = objedval ? this.lodash_get(objedval, paths, '') : val
  215. } catch (e) {
  216. val = ''
  217. }
  218. }
  219. }
  220. return val
  221. }
  222. setdata(val, key) {
  223. let issuc = false
  224. if (/^@/.test(key)) {
  225. const [, objkey, paths] = /^@(.*?)\.(.*?)$/.exec(key)
  226. const objdat = this.getval(objkey)
  227. const objval = objkey
  228. ? objdat === 'null'
  229. ? null
  230. : objdat || '{}'
  231. : '{}'
  232. try {
  233. const objedval = JSON.parse(objval)
  234. this.lodash_set(objedval, paths, val)
  235. issuc = this.setval(JSON.stringify(objedval), objkey)
  236. } catch (e) {
  237. const objedval = {}
  238. this.lodash_set(objedval, paths, val)
  239. issuc = this.setval(JSON.stringify(objedval), objkey)
  240. }
  241. } else {
  242. issuc = this.setval(val, key)
  243. }
  244. return issuc
  245. }
  246. getval(key) {
  247. switch (this.getEnv()) {
  248. case 'Surge':
  249. case 'Loon':
  250. case 'Stash':
  251. case 'Shadowrocket':
  252. return $persistentStore.read(key)
  253. case 'Quantumult X':
  254. return $prefs.valueForKey(key)
  255. case 'Node.js':
  256. this.data = this.loaddata()
  257. return this.data[key]
  258. default:
  259. return (this.data && this.data[key]) || null
  260. }
  261. }
  262. setval(val, key) {
  263. switch (this.getEnv()) {
  264. case 'Surge':
  265. case 'Loon':
  266. case 'Stash':
  267. case 'Shadowrocket':
  268. return $persistentStore.write(val, key)
  269. case 'Quantumult X':
  270. return $prefs.setValueForKey(val, key)
  271. case 'Node.js':
  272. this.data = this.loaddata()
  273. this.data[key] = val
  274. this.writedata()
  275. return true
  276. default:
  277. return (this.data && this.data[key]) || null
  278. }
  279. }
  280. initGotEnv(opts) {
  281. this.got = this.got ? this.got : require('got')
  282. this.cktough = this.cktough ? this.cktough : require('tough-cookie')
  283. this.ckjar = this.ckjar ? this.ckjar : new this.cktough.CookieJar()
  284. if (opts) {
  285. opts.headers = opts.headers ? opts.headers : {}
  286. if (undefined === opts.headers.Cookie && undefined === opts.cookieJar) {
  287. opts.cookieJar = this.ckjar
  288. }
  289. }
  290. }
  291. get(request, callback = () => { }) {
  292. if (request.headers) {
  293. delete request.headers['Content-Type']
  294. delete request.headers['Content-Length']
  295. // HTTP/2 全是小写
  296. delete request.headers['content-type']
  297. delete request.headers['content-length']
  298. }
  299. switch (this.getEnv()) {
  300. case 'Surge':
  301. case 'Loon':
  302. case 'Stash':
  303. case 'Shadowrocket':
  304. default:
  305. if (this.isSurge() && this.isNeedRewrite) {
  306. request.headers = request.headers || {}
  307. Object.assign(request.headers, { 'X-Surge-Skip-Scripting': false })
  308. }
  309. $httpClient.get(request, (err, resp, body) => {
  310. if (!err && resp) {
  311. resp.body = body
  312. resp.statusCode = resp.status ? resp.status : resp.statusCode
  313. resp.status = resp.statusCode
  314. }
  315. callback(err, resp, body)
  316. })
  317. break
  318. case 'Quantumult X':
  319. if (this.isNeedRewrite) {
  320. request.opts = request.opts || {}
  321. Object.assign(request.opts, { hints: false })
  322. }
  323. $task.fetch(request).then(
  324. (resp) => {
  325. const {
  326. statusCode: status,
  327. statusCode,
  328. headers,
  329. body,
  330. bodyBytes
  331. } = resp
  332. callback(
  333. null,
  334. { status, statusCode, headers, body, bodyBytes },
  335. body,
  336. bodyBytes
  337. )
  338. },
  339. (err) => callback((err && err.error) || 'UndefinedError')
  340. )
  341. break
  342. case 'Node.js':
  343. let iconv = require('iconv-lite')
  344. this.initGotEnv(request)
  345. this.got(request)
  346. .on('redirect', (resp, nextOpts) => {
  347. try {
  348. if (resp.headers['set-cookie']) {
  349. const ck = resp.headers['set-cookie']
  350. .map(this.cktough.Cookie.parse)
  351. .toString()
  352. if (ck) {
  353. this.ckjar.setCookieSync(ck, null)
  354. }
  355. nextOpts.cookieJar = this.ckjar
  356. }
  357. } catch (e) {
  358. this.logErr(e)
  359. }
  360. // this.ckjar.setCookieSync(resp.headers['set-cookie'].map(Cookie.parse).toString())
  361. })
  362. .then(
  363. (resp) => {
  364. const {
  365. statusCode: status,
  366. statusCode,
  367. headers,
  368. rawBody
  369. } = resp
  370. const body = iconv.decode(rawBody, this.encoding)
  371. callback(
  372. null,
  373. { status, statusCode, headers, rawBody, body },
  374. body
  375. )
  376. },
  377. (err) => {
  378. const { message: error, response: resp } = err
  379. callback(
  380. error,
  381. resp,
  382. resp && iconv.decode(resp.rawBody, this.encoding)
  383. )
  384. }
  385. )
  386. break
  387. }
  388. }
  389. post(request, callback = () => { }) {
  390. const method = request.method
  391. ? request.method.toLocaleLowerCase()
  392. : 'post'
  393. // 如果指定了请求体, 但没指定 `Content-Type`、`content-type`, 则自动生成。
  394. if (
  395. request.body &&
  396. request.headers &&
  397. !request.headers['Content-Type'] &&
  398. !request.headers['content-type']
  399. ) {
  400. // HTTP/1、HTTP/2 都支持小写 headers
  401. request.headers['content-type'] = 'application/x-www-form-urlencoded'
  402. }
  403. // 为避免指定错误 `content-length` 这里删除该属性,由工具端 (HttpClient) 负责重新计算并赋值
  404. if (request.headers) {
  405. delete request.headers['Content-Length']
  406. delete request.headers['content-length']
  407. }
  408. switch (this.getEnv()) {
  409. case 'Surge':
  410. case 'Loon':
  411. case 'Stash':
  412. case 'Shadowrocket':
  413. default:
  414. if (this.isSurge() && this.isNeedRewrite) {
  415. request.headers = request.headers || {}
  416. Object.assign(request.headers, { 'X-Surge-Skip-Scripting': false })
  417. }
  418. $httpClient[method](request, (err, resp, body) => {
  419. if (!err && resp) {
  420. resp.body = body
  421. resp.statusCode = resp.status ? resp.status : resp.statusCode
  422. resp.status = resp.statusCode
  423. }
  424. callback(err, resp, body)
  425. })
  426. break
  427. case 'Quantumult X':
  428. request.method = method
  429. if (this.isNeedRewrite) {
  430. request.opts = request.opts || {}
  431. Object.assign(request.opts, { hints: false })
  432. }
  433. $task.fetch(request).then(
  434. (resp) => {
  435. const {
  436. statusCode: status,
  437. statusCode,
  438. headers,
  439. body,
  440. bodyBytes
  441. } = resp
  442. callback(
  443. null,
  444. { status, statusCode, headers, body, bodyBytes },
  445. body,
  446. bodyBytes
  447. )
  448. },
  449. (err) => callback((err && err.error) || 'UndefinedError')
  450. )
  451. break
  452. case 'Node.js':
  453. let iconv = require('iconv-lite')
  454. this.initGotEnv(request)
  455. const { url, ..._request } = request
  456. this.got[method](url, _request).then(
  457. (resp) => {
  458. const { statusCode: status, statusCode, headers, rawBody } = resp
  459. const body = iconv.decode(rawBody, this.encoding)
  460. callback(
  461. null,
  462. { status, statusCode, headers, rawBody, body },
  463. body
  464. )
  465. },
  466. (err) => {
  467. const { message: error, response: resp } = err
  468. callback(
  469. error,
  470. resp,
  471. resp && iconv.decode(resp.rawBody, this.encoding)
  472. )
  473. }
  474. )
  475. break
  476. }
  477. }
  478. /**
  479. *
  480. * 示例:$.time('yyyy-MM-dd qq HH:mm:ss.S')
  481. * :$.time('yyyyMMddHHmmssS')
  482. * y:年 M:月 d:日 q:季 H:时 m:分 s:秒 S:毫秒
  483. * 其中y可选0-4位占位符、S可选0-1位占位符,其余可选0-2位占位符
  484. * @param {string} fmt 格式化参数
  485. * @param {number} 可选: 根据指定时间戳返回格式化日期
  486. *
  487. */
  488. time(fmt, ts = null) {
  489. const date = ts ? new Date(ts) : new Date()
  490. let o = {
  491. 'M+': date.getMonth() + 1,
  492. 'd+': date.getDate(),
  493. 'H+': date.getHours(),
  494. 'm+': date.getMinutes(),
  495. 's+': date.getSeconds(),
  496. 'q+': Math.floor((date.getMonth() + 3) / 3),
  497. 'S': date.getMilliseconds()
  498. }
  499. if (/(y+)/.test(fmt))
  500. fmt = fmt.replace(
  501. RegExp.$1,
  502. (date.getFullYear() + '').substr(4 - RegExp.$1.length)
  503. )
  504. for (let k in o)
  505. if (new RegExp('(' + k + ')').test(fmt))
  506. fmt = fmt.replace(
  507. RegExp.$1,
  508. RegExp.$1.length == 1
  509. ? o[k]
  510. : ('00' + o[k]).substr(('' + o[k]).length)
  511. )
  512. return fmt
  513. }
  514. /**
  515. *
  516. * @param {Object} options
  517. * @returns {String} 将 Object 对象 转换成 queryStr: key=val&name=senku
  518. */
  519. queryStr(options) {
  520. let queryString = ''
  521. for (const key in options) {
  522. let value = options[key]
  523. if (value != null && value !== '') {
  524. if (typeof value === 'object') {
  525. value = JSON.stringify(value)
  526. }
  527. queryString += `${key}=${value}&`
  528. }
  529. }
  530. queryString = queryString.substring(0, queryString.length - 1)
  531. return queryString
  532. }
  533. /**
  534. * 系统通知
  535. *
  536. * > 通知参数: 同时支持 QuanX 和 Loon 两种格式, EnvJs根据运行环境自动转换, Surge 环境不支持多媒体通知
  537. *
  538. * 示例:
  539. * $.msg(title, subt, desc, 'twitter://')
  540. * $.msg(title, subt, desc, { 'open-url': 'twitter://', 'media-url': 'https://github.githubassets.com/images/modules/open_graph/github-mark.png' })
  541. * $.msg(title, subt, desc, { 'open-url': 'https://bing.com', 'media-url': 'https://github.githubassets.com/images/modules/open_graph/github-mark.png' })
  542. *
  543. * @param {*} title 标题
  544. * @param {*} subt 副标题
  545. * @param {*} desc 通知详情
  546. * @param {*} opts 通知参数
  547. *
  548. */
  549. msg(title = name, subt = '', desc = '', opts) {
  550. const toEnvOpts = (rawopts) => {
  551. switch (typeof rawopts) {
  552. case undefined:
  553. return rawopts
  554. case 'string':
  555. switch (this.getEnv()) {
  556. case 'Surge':
  557. case 'Stash':
  558. default:
  559. return { url: rawopts }
  560. case 'Loon':
  561. case 'Shadowrocket':
  562. return rawopts
  563. case 'Quantumult X':
  564. return { 'open-url': rawopts }
  565. case 'Node.js':
  566. return undefined
  567. }
  568. case 'object':
  569. switch (this.getEnv()) {
  570. case 'Surge':
  571. case 'Stash':
  572. case 'Shadowrocket':
  573. default: {
  574. let openUrl =
  575. rawopts.url || rawopts.openUrl || rawopts['open-url']
  576. return { url: openUrl }
  577. }
  578. case 'Loon': {
  579. let openUrl =
  580. rawopts.openUrl || rawopts.url || rawopts['open-url']
  581. let mediaUrl = rawopts.mediaUrl || rawopts['media-url']
  582. return { openUrl, mediaUrl }
  583. }
  584. case 'Quantumult X': {
  585. let openUrl =
  586. rawopts['open-url'] || rawopts.url || rawopts.openUrl
  587. let mediaUrl = rawopts['media-url'] || rawopts.mediaUrl
  588. let updatePasteboard =
  589. rawopts['update-pasteboard'] || rawopts.updatePasteboard
  590. return {
  591. 'open-url': openUrl,
  592. 'media-url': mediaUrl,
  593. 'update-pasteboard': updatePasteboard
  594. }
  595. }
  596. case 'Node.js':
  597. return undefined
  598. }
  599. default:
  600. return undefined
  601. }
  602. }
  603. if (!this.isMute) {
  604. switch (this.getEnv()) {
  605. case 'Surge':
  606. case 'Loon':
  607. case 'Stash':
  608. case 'Shadowrocket':
  609. default:
  610. $notification.post(title, subt, desc, toEnvOpts(opts))
  611. break
  612. case 'Quantumult X':
  613. $notify(title, subt, desc, toEnvOpts(opts))
  614. break
  615. case 'Node.js':
  616. break
  617. }
  618. }
  619. if (!this.isMuteLog) {
  620. let logs = ['', '==============📣系统通知📣==============']
  621. logs.push(title)
  622. subt ? logs.push(subt) : ''
  623. desc ? logs.push(desc) : ''
  624. console.log(logs.join('\n'))
  625. this.logs = this.logs.concat(logs)
  626. }
  627. }
  628. log(...logs) {
  629. if (logs.length > 0) {
  630. this.logs = [...this.logs, ...logs]
  631. }
  632. console.log(logs.join(this.logSeparator))
  633. }
  634. logErr(err, msg) {
  635. switch (this.getEnv()) {
  636. case 'Surge':
  637. case 'Loon':
  638. case 'Stash':
  639. case 'Shadowrocket':
  640. case 'Quantumult X':
  641. default:
  642. this.log('', `❗️${this.name}, 错误!`, err)
  643. break
  644. case 'Node.js':
  645. this.log('', `❗️${this.name}, 错误!`, err.stack)
  646. break
  647. }
  648. }
  649. wait(time) {
  650. return new Promise((resolve) => setTimeout(resolve, time))
  651. }
  652. done(val = {}) {
  653. const endTime = new Date().getTime()
  654. const costTime = (endTime - this.startTime) / 1000
  655. this.log('', `🔔${this.name}, 结束! 🕛 ${costTime} 秒`)
  656. this.log()
  657. switch (this.getEnv()) {
  658. case 'Surge':
  659. case 'Loon':
  660. case 'Stash':
  661. case 'Shadowrocket':
  662. case 'Quantumult X':
  663. default:
  664. $done(val)
  665. break
  666. case 'Node.js':
  667. process.exit(1)
  668. break
  669. }
  670. }
  671. })(name, opts)
  672. }