ToolKit.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100
  1. /**
  2. * 根据自己的习惯整合各个开发者而形成的工具包(@NobyDa, @chavyleung)
  3. * 兼容surge,quantumult x,loon,node环境
  4. * 并且加入一些好用的方法
  5. * 方法如下:
  6. * isEmpty: 判断字符串是否是空(undefined,null,空串)
  7. * getRequestUrl: 获取请求的url(目前仅支持surge和quanx)
  8. * getResponseBody: 获取响应体(目前仅支持surge和quanx)
  9. * boxJsJsonBuilder:构建最简默认boxjs配置json
  10. * randomString: 生成随机字符串
  11. * autoComplete: 自动补齐字符串
  12. * customReplace: 自定义替换
  13. * hash: 字符串做hash
  14. *
  15. * ⚠️当开启当且仅当执行失败的时候通知选项,请在执行失败的地方执行execFail()
  16. *
  17. * @param scriptName 脚本名,用于通知时候的标题
  18. * @param scriptId 每个脚本唯一的id,用于存储持久化的时候加入key
  19. * @param options 传入一些参数,目前参数如下;
  20. * [email protected]:6166(这个是默认值,本人surge调试脚本用,可自行修改)
  21. * target_boxjs_json_path=/Users/lowking/Desktop/Scripts/lowking.boxjs.json(生成boxjs配置的目标文件路径)
  22. * @constructor
  23. */
  24. function ToolKit(scriptName, scriptId, options) {
  25. return new (class {
  26. constructor(scriptName, scriptId, options) {
  27. this.tgEscapeCharMapping = { '&': '&', '#': '#' }
  28. this.userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.2 Safari/605.1.15`
  29. this.prefix = `lk`
  30. this.name = scriptName
  31. this.id = scriptId
  32. this.data = null
  33. this.dataFile = this.getRealPath(`${this.prefix}${this.id}.dat`)
  34. this.boxJsJsonFile = this.getRealPath(`${this.prefix}${this.id}.boxjs.json`)
  35. //surge http api等一些扩展参数
  36. this.options = options
  37. //命令行入参
  38. this.isExecComm = false
  39. //默认脚本开关
  40. this.isEnableLog = this.getVal(`${this.prefix}IsEnableLog${this.id}`)
  41. this.isEnableLog = this.isEmpty(this.isEnableLog) ? true : JSON.parse(this.isEnableLog)
  42. this.isNotifyOnlyFail = this.getVal(`${this.prefix}NotifyOnlyFail${this.id}`)
  43. this.isNotifyOnlyFail = this.isEmpty(this.isNotifyOnlyFail) ? false : JSON.parse(this.isNotifyOnlyFail)
  44. //tg通知开关
  45. this.isEnableTgNotify = this.getVal(`${this.prefix}IsEnableTgNotify${this.id}`)
  46. this.isEnableTgNotify = this.isEmpty(this.isEnableTgNotify) ? false : JSON.parse(this.isEnableTgNotify)
  47. this.tgNotifyUrl = this.getVal(`${this.prefix}TgNotifyUrl${this.id}`)
  48. this.isEnableTgNotify = this.isEnableTgNotify ? !this.isEmpty(this.tgNotifyUrl) : this.isEnableTgNotify
  49. //计时部分
  50. this.costTotalStringKey = `${this.prefix}CostTotalString${this.id}`
  51. this.costTotalString = this.getVal(this.costTotalStringKey)
  52. this.costTotalString = this.isEmpty(this.costTotalString) ? `0,0` : this.costTotalString.replace("\"", "")
  53. this.costTotalMs = this.costTotalString.split(",")[0]
  54. this.execCount = this.costTotalString.split(",")[1]
  55. this.costTotalMs = this.isEmpty(this.costTotalMs) ? 0 : parseInt(this.costTotalMs)
  56. this.execCount = this.isEmpty(this.execCount) ? 0 : parseInt(this.execCount)
  57. this.logSeparator = '\n██'
  58. this.now = new Date()
  59. this.startTime = this.now.getTime()
  60. this.node = (() => {
  61. if (this.isNode()) {
  62. const request = require('request')
  63. return ({ request })
  64. } else {
  65. return (null)
  66. }
  67. })()
  68. this.execStatus = true
  69. this.notifyInfo = []
  70. this.log(`${this.name}, 开始执行!`)
  71. this.initCache()
  72. this.checkRecordRequestBody()
  73. this.execComm()
  74. }
  75. checkRecordRequestBody() {
  76. if (!this.isRequest()) {
  77. return;
  78. }
  79. const reqBody = $request.body;
  80. if (!reqBody) {
  81. return;
  82. }
  83. const path = $request.path;
  84. const cacheKey = this.id + "#" + path.replace("/", "_");
  85. if (this.isQuanX()) $prefs.setValueForKey(reqBody, cacheKey);
  86. if (this.isLoon() || this.isSurge()) $persistentStore.write(reqBody, cacheKey);
  87. if (this.isNode()) {
  88. this.node.fs.writeFileSync(
  89. `${cacheKey}.json`,
  90. reqBody,
  91. {
  92. flag: "w"
  93. },
  94. (err) => console.log(err)
  95. );
  96. }
  97. }
  98. getRequestBody() {
  99. const path = $request.path;
  100. const cacheKey = this.id + "#" + path.replace("/", "_");
  101. if (this.isSurge() || this.isLoon()) {
  102. return $persistentStore.read(cacheKey);
  103. }
  104. if (this.isQuanX()) {
  105. return $prefs.valueForKey(cacheKey);
  106. }
  107. if (this.isNode()) {
  108. const fpath = `${cacheKey}.json`;
  109. if (!this.node.fs.existsSync(fpath)) {
  110. return JSON.parse(
  111. this.node.fs.readFileSync(fpath)
  112. );
  113. }
  114. }
  115. }
  116. // persistence
  117. // initialize cache
  118. initCache() {
  119. const pKey = this.getPersistKey();
  120. if (this.isQuanX()) this.cache = JSON.parse($prefs.valueForKey(pKey) || "{}");
  121. if (this.isLoon() || this.isSurge())
  122. this.cache = JSON.parse($persistentStore.read(pKey) || "{}");
  123. if (this.isNode()) {
  124. // create a json for root cache
  125. let fpath = "root.json";
  126. if (!this.node.fs.existsSync(fpath)) {
  127. this.node.fs.writeFileSync(
  128. fpath,
  129. JSON.stringify({}),
  130. {
  131. flag: "wx"
  132. },
  133. (err) => console.log(err)
  134. );
  135. }
  136. this.root = {};
  137. // create a json file with the given name if not exists
  138. fpath = `${pKey}.json`;
  139. if (!this.node.fs.existsSync(fpath)) {
  140. this.node.fs.writeFileSync(
  141. fpath,
  142. JSON.stringify({}),
  143. {
  144. flag: "wx"
  145. },
  146. (err) => console.log(err)
  147. );
  148. this.cache = {};
  149. } else {
  150. this.cache = JSON.parse(
  151. this.node.fs.readFileSync(`${pKey}.json`)
  152. );
  153. }
  154. }
  155. }
  156. getPersistKey() {
  157. return `${this.id}#privateCache`;
  158. }
  159. // store cache
  160. persistCache() {
  161. const pKey = this.getPersistKey();
  162. const data = JSON.stringify(this.cache, null, 2);
  163. if (this.isQuanX()) $prefs.setValueForKey(data, pKey);
  164. if (this.isLoon() || this.isSurge()) $persistentStore.write(data, pKey);
  165. if (this.isNode()) {
  166. this.node.fs.writeFileSync(
  167. `${pKey}.json`,
  168. data,
  169. {
  170. flag: "w"
  171. },
  172. (err) => console.log(err)
  173. );
  174. this.node.fs.writeFileSync(
  175. "root.json",
  176. JSON.stringify(this.root, null, 2),
  177. {
  178. flag: "w"
  179. },
  180. (err) => console.log(err)
  181. );
  182. }
  183. }
  184. write(data, key) {
  185. this.log(`SET ${key}`);
  186. if (key.indexOf("#") !== -1) {
  187. key = key.substr(1);
  188. if (isSurge || this.isLoon()) {
  189. return $persistentStore.write(data, key);
  190. }
  191. if (this.isQuanX()) {
  192. return $prefs.setValueForKey(data, key);
  193. }
  194. if (this.isNode()) {
  195. this.root[key] = data;
  196. }
  197. } else {
  198. this.cache[key] = data;
  199. }
  200. this.persistCache();
  201. }
  202. read(key) {
  203. this.log(`READ ${key}`);
  204. if (key.indexOf("#") !== -1) {
  205. key = key.substr(1);
  206. if (this.isSurge() || this.isLoon()) {
  207. return $persistentStore.read(key);
  208. }
  209. if (this.isQuanX()) {
  210. return $prefs.valueForKey(key);
  211. }
  212. if (this.isNode()) {
  213. return this.root[key];
  214. }
  215. } else {
  216. return this.cache[key];
  217. }
  218. }
  219. delete(key) {
  220. this.log(`DELETE ${key}`);
  221. if (key.indexOf("#") !== -1) {
  222. key = key.substr(1);
  223. if (this.isSurge() || this.isLoon()) {
  224. return $persistentStore.write(null, key);
  225. }
  226. if (this.isQuanX()) {
  227. return $prefs.removeValueForKey(key);
  228. }
  229. if (this.isNode()) {
  230. delete this.root[key];
  231. }
  232. } else {
  233. delete this.cache[key];
  234. }
  235. this.persistCache();
  236. }
  237. //当执行命令的目录不是脚本所在目录时,自动把文件路径改成指令传入的路径并返回完整文件路径
  238. getRealPath(fileName) {
  239. if (this.isNode()) {
  240. let targetPath = process.argv.slice(1, 2)[0].split("/")
  241. targetPath[targetPath.length - 1] = fileName
  242. return targetPath.join("/")
  243. }
  244. return fileName
  245. }
  246. /**
  247. * http://boxjs.com/ => http://boxjs.com
  248. * http://boxjs.com/app/jd => http://boxjs.com
  249. */
  250. getUrlHost(url) {
  251. return url.slice(0, url.indexOf('/', 8))
  252. }
  253. /**
  254. * http://boxjs.com/ =>
  255. * http://boxjs.com/api/getdata => /api/getdata
  256. */
  257. getUrlPath(url) {
  258. // 如果以结尾, 去掉最后一个/
  259. const end = url.lastIndexOf('/') === url.length - 1 ? -1 : undefined
  260. // slice第二个参数传 undefined 会直接截到最后
  261. // indexOf第二个参数用来跳过前面的 "https://"
  262. return url.slice(url.indexOf('/', 8), end)
  263. }
  264. async execComm() {
  265. //支持node命令,实现发送手机测试
  266. if (this.isNode()) {
  267. this.comm = process.argv.slice(1)
  268. let isHttpApiErr = false
  269. if (this.comm[1] == "p") {
  270. this.isExecComm = true
  271. //phone
  272. this.log(`开始执行指令【${this.comm[1]}】=> 发送到手机测试脚本!`);
  273. if (this.isEmpty(this.options) || this.isEmpty(this.options.httpApi)) {
  274. this.log(`未设置options,使用默认值`)
  275. //设置默认值
  276. if (this.isEmpty(this.options)) {
  277. this.options = {}
  278. }
  279. this.options.httpApi = `[email protected]:6166`
  280. } else {
  281. //判断格式
  282. if (!/.*?@.*?:[0-9]+/.test(this.options.httpApi)) {
  283. isHttpApiErr = true
  284. this.log(`❌httpApi格式错误!格式:[email protected]:6166`)
  285. this.done()
  286. }
  287. }
  288. if (!isHttpApiErr) {
  289. this.callApi(this.comm[2])
  290. }
  291. }
  292. }
  293. }
  294. callApi(timeout) {
  295. // 直接用接收到文件路径,解决在不同目录下都可以使用 node xxxx/xxx.js p 指令发送脚本给手机执行
  296. // let fname = this.getCallerFileNameAndLine().split(":")[0].replace("[", "")
  297. let fname = this.comm[0]
  298. this.log(`获取【${fname}】内容传给手机`)
  299. let scriptStr = ''
  300. this.fs = this.fs ? this.fs : require('fs')
  301. this.path = this.path ? this.path : require('path')
  302. const curDirDataFilePath = this.path.resolve(fname)
  303. const rootDirDataFilePath = this.path.resolve(process.cwd(), fname)
  304. const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath)
  305. const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath)
  306. if (isCurDirDataFile || isRootDirDataFile) {
  307. const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath
  308. try {
  309. scriptStr = this.fs.readFileSync(datPath)
  310. } catch (e) {
  311. scriptStr = ''
  312. }
  313. } else {
  314. scriptStr = ''
  315. }
  316. let options = {
  317. url: `http://${this.options.httpApi.split("@")[1]}/v1/scripting/evaluate`,
  318. headers: {
  319. "X-Key": `${this.options.httpApi.split("@")[0]}`
  320. },
  321. body: {
  322. "script_text": `${scriptStr}`,
  323. "mock_type": "cron",
  324. "timeout": (!this.isEmpty(timeout) && timeout > 5) ? timeout : 5
  325. },
  326. json: true
  327. }
  328. this.post(options, (_error, _response, _data) => {
  329. this.log(`已将脚本【${fname}】发给手机!`)
  330. this.done()
  331. })
  332. }
  333. getCallerFileNameAndLine() {
  334. let error
  335. try {
  336. throw Error('')
  337. } catch (err) {
  338. error = err
  339. }
  340. const stack = error.stack
  341. const stackArr = stack.split('\n')
  342. let callerLogIndex = 1
  343. if (callerLogIndex !== 0) {
  344. const callerStackLine = stackArr[callerLogIndex]
  345. this.path = this.path ? this.path : require('path')
  346. return `[${callerStackLine.substring(callerStackLine.lastIndexOf(this.path.sep) + 1, callerStackLine.lastIndexOf(':'))}]`
  347. } else {
  348. return '[-]'
  349. }
  350. }
  351. getFunName(fun) {
  352. var ret = fun.toString()
  353. ret = ret.substr('function '.length)
  354. ret = ret.substr(0, ret.indexOf('('))
  355. return ret
  356. }
  357. boxJsJsonBuilder(info, param) {
  358. if (this.isNode()) {
  359. let boxjsJsonPath = "/Users/lowking/Desktop/Scripts/lowking.boxjs.json"
  360. // 从传入参数param读取配置的boxjs的json文件路径
  361. if (param && param.hasOwnProperty("target_boxjs_json_path")) {
  362. boxjsJsonPath = param["target_boxjs_json_path"]
  363. }
  364. if (!this.fs.existsSync(boxjsJsonPath)) {
  365. return
  366. }
  367. if (!this.isJsonObject(info) || !this.isJsonObject(param)) {
  368. this.log("构建BoxJsJson传入参数格式错误,请传入json对象")
  369. return
  370. }
  371. this.log('using node')
  372. let needAppendKeys = ["settings", "keys"]
  373. const domain = 'https://raw.githubusercontent.com/Orz-3'
  374. let boxJsJson = {}
  375. let scritpUrl = '#lk{script_url}'
  376. if (param && param.hasOwnProperty('script_url')) {
  377. scritpUrl = this.isEmpty(param['script_url']) ? "#lk{script_url}" : param['script_url']
  378. }
  379. boxJsJson.id = `${this.prefix}${this.id}`
  380. boxJsJson.name = this.name
  381. boxJsJson.desc_html = `⚠️使用说明</br>详情【<a href='${scritpUrl}?raw=true'><font class='red--text'>点我查看</font></a>】`
  382. boxJsJson.icons = [`${domain}/mini/master/Alpha/${this.id.toLocaleLowerCase()}.png`, `${domain}/mini/master/Color/${this.id.toLocaleLowerCase()}.png`]
  383. boxJsJson.keys = []
  384. boxJsJson.settings = [
  385. {
  386. "id": `${this.prefix}IsEnableLog${this.id}`,
  387. "name": "开启/关闭日志",
  388. "val": true,
  389. "type": "boolean",
  390. "desc": "默认开启"
  391. },
  392. {
  393. "id": `${this.prefix}NotifyOnlyFail${this.id}`,
  394. "name": "只当执行失败才通知",
  395. "val": false,
  396. "type": "boolean",
  397. "desc": "默认关闭"
  398. },
  399. {
  400. "id": `${this.prefix}IsEnableTgNotify${this.id}`,
  401. "name": "开启/关闭Telegram通知",
  402. "val": false,
  403. "type": "boolean",
  404. "desc": "默认关闭"
  405. },
  406. {
  407. "id": `${this.prefix}TgNotifyUrl${this.id}`,
  408. "name": "Telegram通知地址",
  409. "val": "",
  410. "type": "text",
  411. "desc": "Tg的通知地址,如:https://api.telegram.org/bot-token/sendMessage?chat_id=-100140&parse_mode=Markdown&text="
  412. }
  413. ]
  414. boxJsJson.author = "#lk{author}"
  415. boxJsJson.repo = "#lk{repo}"
  416. boxJsJson.script = `${scritpUrl}?raw=true`
  417. // 除了settings和keys追加,其他的都覆盖
  418. if (!this.isEmpty(info)) {
  419. for (let i in needAppendKeys) {
  420. let key = needAppendKeys[i]
  421. if (!this.isEmpty(info[key])) {
  422. // 处理传入的每项设置
  423. if (key === 'settings') {
  424. for (let i = 0; i < info[key].length; i++) {
  425. let input = info[key][i]
  426. for (let j = 0; j < boxJsJson.settings.length; j++) {
  427. let def = boxJsJson.settings[j]
  428. if (input.id === def.id) {
  429. // id相同,就使用外部传入的配置
  430. boxJsJson.settings.splice(j, 1)
  431. }
  432. }
  433. }
  434. }
  435. boxJsJson[key] = boxJsJson[key].concat(info[key])
  436. }
  437. delete info[key]
  438. }
  439. }
  440. Object.assign(boxJsJson, info)
  441. if (this.isNode()) {
  442. this.fs = this.fs ? this.fs : require('fs')
  443. this.path = this.path ? this.path : require('path')
  444. const curDirDataFilePath = this.path.resolve(this.boxJsJsonFile)
  445. const rootDirDataFilePath = this.path.resolve(process.cwd(), this.boxJsJsonFile)
  446. const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath)
  447. const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath)
  448. const jsondata = JSON.stringify(boxJsJson, null, '\t')
  449. if (isCurDirDataFile) {
  450. this.fs.writeFileSync(curDirDataFilePath, jsondata)
  451. } else if (isRootDirDataFile) {
  452. this.fs.writeFileSync(rootDirDataFilePath, jsondata)
  453. } else {
  454. this.fs.writeFileSync(curDirDataFilePath, jsondata)
  455. }
  456. // 写到项目的boxjs订阅json中
  457. let boxjsJson = JSON.parse(this.fs.readFileSync(boxjsJsonPath))
  458. if (boxjsJson.hasOwnProperty("apps") && Array.isArray(boxjsJson["apps"]) && boxjsJson["apps"].length > 0) {
  459. let apps = boxjsJson.apps
  460. let targetIdx = apps.indexOf(apps.filter((app) => {
  461. return app.id == boxJsJson.id
  462. })[0])
  463. if (targetIdx >= 0) {
  464. boxjsJson.apps[targetIdx] = boxJsJson
  465. } else {
  466. boxjsJson.apps.push(boxJsJson)
  467. }
  468. let ret = JSON.stringify(boxjsJson, null, 2)
  469. if (!this.isEmpty(param)) {
  470. for (const key in param) {
  471. let val = ''
  472. if (param.hasOwnProperty(key)) {
  473. val = param[key]
  474. } else if (key === 'author') {
  475. val = '@lowking'
  476. } else if (key === 'repo') {
  477. val = 'https://github.com/lowking/Scripts'
  478. }
  479. ret = ret.replace(`#lk{${key}}`, val)
  480. }
  481. }
  482. // 全部处理完毕检查是否有漏掉未配置的参数,进行提醒
  483. const regex = /(?:#lk\{)(.+?)(?=\})/
  484. let m = regex.exec(ret)
  485. if (m !== null) {
  486. this.log('生成BoxJs还有未配置的参数,请参考https://github.com/lowking/Scripts/blob/master/util/example/ToolKitDemo.js#L17-L18传入参数:\n')
  487. }
  488. let loseParamSet = new Set()
  489. while ((m = regex.exec(ret)) !== null) {
  490. loseParamSet.add(m[1])
  491. ret = ret.replace(`#lk{${m[1]}}`, ``)
  492. }
  493. loseParamSet.forEach(p => {
  494. console.log(`${p} `)
  495. })
  496. this.fs.writeFileSync(boxjsJsonPath, ret)
  497. }
  498. }
  499. }
  500. }
  501. isJsonObject(obj) {
  502. return typeof (obj) == "object" && Object.prototype.toString.call(obj).toLowerCase() == "[object object]" && !obj.length
  503. }
  504. appendNotifyInfo(info, type) {
  505. if (type == 1) {
  506. this.notifyInfo = info
  507. } else {
  508. this.notifyInfo.push(info)
  509. }
  510. }
  511. prependNotifyInfo(info) {
  512. this.notifyInfo.splice(0, 0, info)
  513. }
  514. execFail() {
  515. this.execStatus = false
  516. }
  517. isRequest() {
  518. return typeof $request != "undefined"
  519. }
  520. isSurge() {
  521. return typeof $httpClient != "undefined"
  522. }
  523. isQuanX() {
  524. return typeof $task != "undefined"
  525. }
  526. isLoon() {
  527. return typeof $loon != "undefined"
  528. }
  529. isJSBox() {
  530. return typeof $app != "undefined" && typeof $http != "undefined"
  531. }
  532. isStash() {
  533. return 'undefined' !== typeof $environment && $environment['stash-version']
  534. }
  535. isNode() {
  536. return typeof require == "function" && !this.isJSBox()
  537. }
  538. sleep(time) {
  539. return new Promise((resolve) => setTimeout(resolve, time))
  540. }
  541. wait(time) {
  542. return sleep(time);
  543. }
  544. delay(time) {
  545. return sleep(time);
  546. }
  547. log(message) {
  548. if (this.isEnableLog) console.log(`${this.logSeparator}${message}`)
  549. }
  550. logErr(message) {
  551. this.execStatus = true
  552. if (this.isEnableLog) {
  553. console.log(`${this.logSeparator}${this.name}执行异常:`)
  554. console.log(message)
  555. console.log('\n' + `${message.message}`)
  556. }
  557. }
  558. msg(subtitle, message, openUrl, mediaUrl) {
  559. if (!this.isRequest() && this.isNotifyOnlyFail && this.execStatus) {
  560. //开启了当且仅当执行失败的时候通知,并且执行成功了,这时候不通知
  561. } else {
  562. if (this.isEmpty(message)) {
  563. if (Array.isArray(this.notifyInfo)) {
  564. message = this.notifyInfo.join("\n")
  565. } else {
  566. message = this.notifyInfo
  567. }
  568. }
  569. if (!this.isEmpty(message)) {
  570. if (this.isEnableTgNotify) {
  571. this.log(`${this.name}Tg通知开始`)
  572. //处理特殊字符
  573. for (let key in this.tgEscapeCharMapping) {
  574. if (!this.tgEscapeCharMapping.hasOwnProperty(key)) {
  575. continue
  576. }
  577. message = message.replace(key, this.tgEscapeCharMapping[key])
  578. }
  579. this.get({
  580. url: encodeURI(`${this.tgNotifyUrl}📌${this.name}` + '\n' + `${message}`)
  581. }, (_error, _statusCode, _body) => {
  582. this.log(`Tg通知完毕`)
  583. })
  584. } else {
  585. let options = {}
  586. const hasOpenUrl = !this.isEmpty(openUrl)
  587. const hasMediaUrl = !this.isEmpty(mediaUrl)
  588. if (this.isQuanX()) {
  589. if (hasOpenUrl) options["open-url"] = openUrl
  590. if (hasMediaUrl) options["media-url"] = mediaUrl
  591. $notify(this.name, subtitle, message, options)
  592. }
  593. if (this.isSurge() || this.isStash()) {
  594. if (hasOpenUrl) options["url"] = openUrl
  595. $notification.post(this.name, subtitle, message, options)
  596. }
  597. if (this.isNode()) this.log("⭐️" + this.name + "\n" + subtitle + "\n" + message)
  598. if (this.isJSBox()) $push.schedule({
  599. title: this.name,
  600. body: subtitle ? subtitle + "\n" + message : message
  601. })
  602. }
  603. }
  604. }
  605. }
  606. pushWxMsg(summary, content, url, callback = () => { }) {
  607. let data = {
  608. appToken: "AT_rTc93GQYIdMU8XLRnoJaSea8WkfhSzhX",
  609. content: content,
  610. summary: summary,
  611. contentType: 1,
  612. topicIds: [],
  613. uids: [
  614. "UID_6P4B00X6Zv8U2oKC0I2R09emxtqq"
  615. ],
  616. url: "",
  617. verifyPay: false
  618. };
  619. if (url) {
  620. data.url = url;
  621. }
  622. const headers = this.getJsonDoneHeaders();
  623. headers.Host = 'wxpusher.zjiecode.com';
  624. headers['Content-Type'] = 'application/json;charset=UTF-8';
  625. let options = {
  626. url: 'https://wxpusher.zjiecode.com/api/send/message',
  627. headers: headers,
  628. body: JSON.stringify(data),
  629. };
  630. this.post(options, callback);
  631. }
  632. getVal(key, defaultValue = "") {
  633. let value
  634. if (this.isSurge() || this.isLoon() || this.isStash()) {
  635. value = $persistentStore.read(key)
  636. } else if (this.isQuanX()) {
  637. value = $prefs.valueForKey(key)
  638. } else if (this.isNode()) {
  639. this.data = this.loadData()
  640. value = process.env[key] || this.data[key]
  641. } else {
  642. value = (this.data && this.data[key]) || null
  643. }
  644. return !value ? defaultValue : value
  645. }
  646. setVal(key, val) {
  647. if (this.isSurge() || this.isLoon() || this.isStash()) {
  648. return $persistentStore.write(val, key)
  649. } else if (this.isQuanX()) {
  650. return $prefs.setValueForKey(val, key)
  651. } else if (this.isNode()) {
  652. this.data = this.loadData()
  653. this.data[key] = val
  654. this.writeData()
  655. return true
  656. } else {
  657. return (this.data && this.data[key]) || null
  658. }
  659. }
  660. loadData() {
  661. if (this.isNode()) {
  662. this.fs = this.fs ? this.fs : require('fs')
  663. this.path = this.path ? this.path : require('path')
  664. const curDirDataFilePath = this.path.resolve(this.dataFile)
  665. const rootDirDataFilePath = this.path.resolve(process.cwd(), this.dataFile)
  666. const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath)
  667. const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath)
  668. if (isCurDirDataFile || isRootDirDataFile) {
  669. const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath
  670. try {
  671. return JSON.parse(this.fs.readFileSync(datPath))
  672. } catch (e) {
  673. return {}
  674. }
  675. } else return {}
  676. } else return {}
  677. }
  678. writeData() {
  679. if (this.isNode()) {
  680. this.fs = this.fs ? this.fs : require('fs')
  681. this.path = this.path ? this.path : require('path')
  682. const curDirDataFilePath = this.path.resolve(this.dataFile)
  683. const rootDirDataFilePath = this.path.resolve(process.cwd(), this.dataFile)
  684. const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath)
  685. const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath)
  686. const jsondata = JSON.stringify(this.data)
  687. if (isCurDirDataFile) {
  688. this.fs.writeFileSync(curDirDataFilePath, jsondata)
  689. } else if (isRootDirDataFile) {
  690. this.fs.writeFileSync(rootDirDataFilePath, jsondata)
  691. } else {
  692. this.fs.writeFileSync(curDirDataFilePath, jsondata)
  693. }
  694. }
  695. }
  696. adapterStatus(response) {
  697. if (response) {
  698. if (response.status) {
  699. response["statusCode"] = response.status
  700. } else if (response.statusCode) {
  701. response["status"] = response.statusCode
  702. }
  703. }
  704. return response
  705. }
  706. get(options, callback = () => { }) {
  707. if (this.isQuanX()) {
  708. if (typeof options == "string") options = {
  709. url: options
  710. }
  711. options["method"] = "GET"
  712. $task.fetch(options).then(response => {
  713. callback(null, this.adapterStatus(response), response.body)
  714. }, reason => callback(reason.error, null, null))
  715. }
  716. if (this.isSurge() || this.isLoon() || this.isStash()) $httpClient.get(options, (error, response, body) => {
  717. callback(error, this.adapterStatus(response), body)
  718. })
  719. if (this.isNode()) {
  720. this.node.request(options, (error, response, body) => {
  721. callback(error, this.adapterStatus(response), body)
  722. })
  723. }
  724. if (this.isJSBox()) {
  725. if (typeof options == "string") options = {
  726. url: options
  727. }
  728. options["header"] = options["headers"]
  729. options["handler"] = function (resp) {
  730. let error = resp.error
  731. if (error) error = JSON.stringify(resp.error)
  732. let body = resp.data
  733. if (typeof body == "object") body = JSON.stringify(resp.data)
  734. callback(error, this.adapterStatus(resp.response), body)
  735. }
  736. $http.get(options)
  737. }
  738. }
  739. post(options, callback = () => { }) {
  740. if (this.isQuanX()) {
  741. if (typeof options == "string") options = {
  742. url: options
  743. }
  744. options["method"] = "POST"
  745. $task.fetch(options).then(response => {
  746. callback(null, this.adapterStatus(response), response.body)
  747. }, reason => callback(reason.error, null, null))
  748. }
  749. if (this.isSurge() || this.isLoon() || this.isStash()) {
  750. $httpClient.post(options, (error, response, body) => {
  751. callback(error, this.adapterStatus(response), body)
  752. })
  753. }
  754. if (this.isNode()) {
  755. this.node.request.post(options, (error, response, body) => {
  756. callback(error, this.adapterStatus(response), body)
  757. })
  758. }
  759. if (this.isJSBox()) {
  760. if (typeof options == "string") options = {
  761. url: options
  762. }
  763. options["header"] = options["headers"]
  764. options["handler"] = function (resp) {
  765. let error = resp.error
  766. if (error) error = JSON.stringify(resp.error)
  767. let body = resp.data
  768. if (typeof body == "object") body = JSON.stringify(resp.data)
  769. callback(error, this.adapterStatus(resp.response), body)
  770. }
  771. $http.post(options)
  772. }
  773. }
  774. put(options, callback = () => { }) {
  775. if (this.isQuanX()) {
  776. // no test
  777. if (typeof options == "string") options = {
  778. url: options
  779. }
  780. options["method"] = "PUT"
  781. $task.fetch(options).then(response => {
  782. callback(null, this.adapterStatus(response), response.body)
  783. }, reason => callback(reason.error, null, null))
  784. }
  785. if (this.isSurge() || this.isLoon() || this.isStash()) {
  786. options.method = "PUT"
  787. $httpClient.put(options, (error, response, body) => {
  788. callback(error, this.adapterStatus(response), body)
  789. })
  790. }
  791. if (this.isNode()) {
  792. options.method = "PUT"
  793. this.node.request.put(options, (error, response, body) => {
  794. callback(error, this.adapterStatus(response), body)
  795. })
  796. }
  797. if (this.isJSBox()) {
  798. // no test
  799. if (typeof options == "string") options = {
  800. url: options
  801. }
  802. options["header"] = options["headers"]
  803. options["handler"] = function (resp) {
  804. let error = resp.error
  805. if (error) error = JSON.stringify(resp.error)
  806. let body = resp.data
  807. if (typeof body == "object") body = JSON.stringify(resp.data)
  808. callback(error, this.adapterStatus(resp.response), body)
  809. }
  810. $http.post(options)
  811. }
  812. }
  813. costTime() {
  814. let info = `${this.name}执行完毕!`
  815. if (this.isNode() && this.isExecComm) {
  816. info = `指令【${this.comm[1]}】执行完毕!`
  817. }
  818. const endTime = new Date().getTime()
  819. const ms = endTime - this.startTime
  820. const costTime = ms / 1000
  821. this.execCount++
  822. this.costTotalMs += ms
  823. this.log(`${info}耗时【${costTime}】秒\n总共执行【${this.execCount}】次,平均耗时【${((this.costTotalMs / this.execCount) / 1000).toFixed(4)}】秒`)
  824. this.setVal(this.costTotalStringKey, JSON.stringify(`${this.costTotalMs},${this.execCount}`))
  825. // this.setVal(this.execCountKey, JSON.stringify(0))
  826. // this.setVal(this.costTotalMsKey, JSON.stringify(0))
  827. }
  828. done(value = {}) {
  829. this.costTime()
  830. if (this.isSurge() || this.isQuanX() || this.isLoon() || this.isStash()) {
  831. $done(value)
  832. }
  833. }
  834. getRequestUrl() {
  835. return $request.url
  836. }
  837. getResponseBody() {
  838. if ($response) {
  839. return $response.body
  840. }
  841. }
  842. isGetCookie(reg) {
  843. return !!($request.method != 'OPTIONS' && this.getRequestUrl().match(reg))
  844. }
  845. isEmpty(obj) {
  846. return typeof obj == "undefined" || obj == null || obj == "" || obj == "null" || obj == "undefined" || obj.length === 0
  847. }
  848. randomString(len) {
  849. len = len || 32
  850. var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
  851. var maxPos = $chars.length
  852. var pwd = ''
  853. for (let i = 0; i < len; i++) {
  854. pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
  855. }
  856. return pwd
  857. }
  858. /**
  859. * 自动补齐字符串
  860. * @param str 原始字符串
  861. * @param prefix 前缀
  862. * @param suffix 后缀
  863. * @param fill 补齐用字符
  864. * @param len 目标补齐长度,不包含前后缀
  865. * @param direction 方向:0往后补齐
  866. * @param ifCode 是否打码
  867. * @param clen 打码长度
  868. * @param startIndex 起始坐标
  869. * @param cstr 打码字符
  870. * @returns {*}
  871. */
  872. autoComplete(str, prefix, suffix, fill, len, direction, ifCode, clen, startIndex, cstr) {
  873. str += ''
  874. if (str.length < len) {
  875. while (str.length < len) {
  876. if (direction == 0) {
  877. str += fill
  878. } else {
  879. str = fill + str
  880. }
  881. }
  882. }
  883. if (ifCode) {
  884. let temp = ''
  885. for (var i = 0; i < clen; i++) {
  886. temp += cstr
  887. }
  888. str = str.substring(0, startIndex) + temp + str.substring(clen + startIndex)
  889. }
  890. str = prefix + str + suffix
  891. return this.toDBC(str)
  892. }
  893. /**
  894. * @param str 源字符串 "#{code}, #{value}"
  895. * @param param 用于替换的数据,结构如下
  896. * @param prefix 前缀 "#{"
  897. * @param suffix 后缀 "}"
  898. * {
  899. * "code": 1,
  900. * "value": 2
  901. * }
  902. * 按上面的传入,输出为"1, 2"
  903. * 对应的#{code}用param里面code的值替换,#{value}也是
  904. * @returns {*|void|string}
  905. */
  906. customReplace(str, param, prefix, suffix) {
  907. try {
  908. if (this.isEmpty(prefix)) {
  909. prefix = "#{"
  910. }
  911. if (this.isEmpty(suffix)) {
  912. suffix = "}"
  913. }
  914. for (let i in param) {
  915. str = str.replace(`${prefix}${i}${suffix}`, param[i])
  916. }
  917. } catch (e) {
  918. this.logErr(e)
  919. }
  920. return str
  921. }
  922. toDBC(txtstring) {
  923. var tmp = ""
  924. for (var i = 0; i < txtstring.length; i++) {
  925. if (txtstring.charCodeAt(i) == 32) {
  926. tmp = tmp + String.fromCharCode(12288)
  927. } else if (txtstring.charCodeAt(i) < 127) {
  928. tmp = tmp + String.fromCharCode(txtstring.charCodeAt(i) + 65248)
  929. }
  930. }
  931. return tmp
  932. }
  933. hash(str) {
  934. let h = 0,
  935. i,
  936. chr
  937. for (i = 0; i < str.length; i++) {
  938. chr = str.charCodeAt(i)
  939. h = (h << 5) - h + chr
  940. h |= 0 // Convert to 32bit integer
  941. }
  942. return String(h)
  943. }
  944. /**
  945. * formatDate y:年 M:月 d:日 q:季 H:时 m:分 s:秒 S:毫秒
  946. */
  947. formatDate(date, format) {
  948. let o = {
  949. 'M+': date.getMonth() + 1,
  950. 'd+': date.getDate(),
  951. 'H+': date.getHours(),
  952. 'm+': date.getMinutes(),
  953. 's+': date.getSeconds(),
  954. 'q+': Math.floor((date.getMonth() + 3) / 3),
  955. 'S': date.getMilliseconds()
  956. }
  957. if (/(y+)/.test(format)) format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
  958. for (let k in o)
  959. if (new RegExp('(' + k + ')').test(format))
  960. format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
  961. return format
  962. }
  963. objToQueryStr(obj, encode) {
  964. let str = ''
  965. for (const key in obj) {
  966. let value = obj[key]
  967. if (value != null && value !== '') {
  968. if (typeof value === 'object') {
  969. value = JSON.stringify(value)
  970. } else if (encode) {
  971. value = encodeURIComponent(value)
  972. }
  973. str += `${key}=${value}&`
  974. }
  975. }
  976. str = str.substring(0, str.length - 1)
  977. return str
  978. }
  979. parseQueryStr(str) {
  980. let obj = {}
  981. if (str.indexOf("?") > -1) {
  982. str = str.split("?")[1]
  983. }
  984. let arr = str.split("&")
  985. for (let i = 0; i < arr.length; i++) {
  986. let kv = arr[i].split("=")
  987. obj[kv[0]] = kv[1]
  988. }
  989. return obj
  990. }
  991. deepClone(obj, newObj) {
  992. newObj = newObj || {};
  993. for (let key in obj) {
  994. if (typeof obj[key] == 'object') {
  995. newObj[key] = (obj[key].constructor === Array) ? [] : {}
  996. this.deepClone(obj[key], newObj[key]);
  997. } else {
  998. newObj[key] = obj[key]
  999. }
  1000. }
  1001. return newObj;
  1002. }
  1003. getBaseDoneHeaders(mixHeaders = {}) {
  1004. return Object.assign(
  1005. {
  1006. 'Access-Control-Allow-Origin': '*',
  1007. 'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PUT,DELETE',
  1008. 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
  1009. },
  1010. mixHeaders
  1011. )
  1012. }
  1013. getHtmlDoneHeaders() {
  1014. return this.getBaseDoneHeaders({
  1015. 'Content-Type': 'text/html;charset=UTF-8'
  1016. })
  1017. }
  1018. getJsonDoneHeaders() {
  1019. return this.getBaseDoneHeaders({
  1020. 'Content-Type': 'text/json; charset=utf-8',
  1021. 'Connection': 'keep-alive'
  1022. })
  1023. }
  1024. })(scriptName, scriptId, options)
  1025. }