ToolKit.js 39 KB

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