├── .editorconfig ├── .env.example ├── .github └── workflows │ ├── build.yml │ └── docker.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .umirc.ts ├── LICENSE ├── README.md ├── back ├── api │ ├── auth.ts │ ├── config.ts │ ├── cookie.ts │ ├── cron.ts │ ├── index.ts │ └── log.ts ├── app.ts ├── config │ ├── index.ts │ └── util.ts ├── data │ ├── cookie.ts │ └── cron.ts ├── loaders │ ├── dependencyInjector.ts │ ├── express.ts │ ├── index.ts │ ├── initData.ts │ └── logger.ts ├── schedule.ts └── services │ ├── cookie.ts │ └── cron.ts ├── docker ├── Dockerfile ├── docker-entrypoint.sh └── front.conf ├── nodemon.json ├── package.json ├── pnpm-lock.yaml ├── sample ├── auth.sample.json ├── config.sample.sh ├── extra.sample.sh ├── notify.js ├── notify.py ├── package.json └── requirements.txt ├── shell ├── api.sh ├── bot.sh ├── code.sh ├── notify.js ├── notify.sh ├── reset.sh ├── rmlog.sh ├── share.sh ├── task.sh └── update.sh ├── src ├── app.tsx ├── assets │ └── fonts │ │ ├── SourceCodePro-Regular.otf.woff │ │ ├── SourceCodePro-Regular.ttf │ │ └── SourceCodePro-Regular.ttf.woff2 ├── layouts │ ├── defaultProps.tsx │ ├── index.less │ └── index.tsx ├── pages │ ├── config │ │ ├── index.less │ │ └── index.tsx │ ├── cookie │ │ ├── index.less │ │ ├── index.tsx │ │ └── modal.tsx │ ├── crontab │ │ ├── index.less │ │ ├── index.tsx │ │ ├── logModal.tsx │ │ └── modal.tsx │ ├── diff │ │ ├── index.less │ │ └── index.tsx │ ├── diy │ │ ├── index.less │ │ └── index.tsx │ ├── log │ │ ├── index.module.less │ │ └── index.tsx │ ├── login │ │ ├── index.less │ │ └── index.tsx │ └── setting │ │ ├── index.less │ │ └── index.tsx ├── styles │ └── variable.less ├── utils │ ├── config.ts │ └── http.ts └── version.ts ├── tsconfig.back.json ├── tsconfig.json └── typings.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI='mongodb://' 2 | YIYAN_MONGODB_URI='' 3 | 4 | CRON_PORT=5500 5 | PORT=5600 6 | 7 | LOG_LEVEL='debug' 8 | 9 | SECRET='whyour' 10 | -------------------------------------------------------------------------------- /.github/workflows/ build.yml: -------------------------------------------------------------------------------- 1 | name: build static 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '14' 15 | 16 | - name: build front and back 17 | run: | 18 | yarn install 19 | yarn build 20 | yarn build-back 21 | 22 | - name: copy to static repo 23 | env: 24 | # GITHUB_REPO: gitee.com/whyour/qinglong-static 25 | GITHUB_REPO: github.com/Zy143L/qinglong-static 26 | run: | 27 | mkdir -p static 28 | cd ./static 29 | cp -rf ../dist ./ && cp -rf ../build ./ 30 | git init && git add . 31 | git config user.name "whyour" 32 | git config user.email "imwhyour@gmail.com" 33 | git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" 34 | git push --force --quiet "https://whyour:${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" master:master -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout base 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | # https://github.com/docker/setup-qemu-action 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v1 20 | # https://github.com/docker/setup-buildx-action 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v1 23 | 24 | - name: Docker login 25 | env: 26 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 27 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 28 | run: | 29 | echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin 30 | 31 | - name: Docker buildx image and push on master branch 32 | env: 33 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 34 | if: github.ref == 'refs/heads/master' 35 | run: | 36 | docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/ 37 | 38 | - name: Replace tag without `v` 39 | if: startsWith(github.ref, 'refs/tags/') 40 | uses: actions/github-script@v1 41 | id: version 42 | with: 43 | script: | 44 | return context.payload.ref.replace(/\/?refs\/tags\/v/, '') 45 | result-encoding: string 46 | 47 | - name: Docker buildx image and push on release 48 | env: 49 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 50 | if: startsWith(github.ref, 'refs/tags/') 51 | run: | 52 | docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:${{steps.version.outputs.result}} docker/ 53 | docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/ 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | 17 | # umi 18 | /src/.umi 19 | /src/.umi-production 20 | /src/.umi-test 21 | /.env.local 22 | .env 23 | .history 24 | 25 | /config 26 | /log 27 | /db 28 | /manual_log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | const CompressionPlugin = require('compression-webpack-plugin'); 3 | 4 | export default defineConfig({ 5 | hash: true, 6 | layout: false, 7 | nodeModulesTransform: { 8 | type: 'none', 9 | }, 10 | fastRefresh: {}, 11 | favicon: 'https://qinglong.whyour.cn/g5.ico', 12 | proxy: { 13 | '/api': { 14 | target: 'http://127.0.0.1:5678/', 15 | changeOrigin: true, 16 | }, 17 | }, 18 | chainWebpack: (config) => { 19 | config.plugin('compression-webpack-plugin').use( 20 | new CompressionPlugin({ 21 | algorithm: 'gzip', 22 | test: new RegExp('\\.(js|css)$'), 23 | threshold: 10240, 24 | minRatio: 0.6, 25 | }), 26 | ); 27 | }, 28 | externals: { 29 | react: 'window.React', 30 | 'react-dom': 'window.ReactDOM', 31 | codemirror: 'window.CodeMirror', 32 | darkreader: 'window.DarkReader', 33 | }, 34 | scripts: [ 35 | 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 36 | 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', 37 | 'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/lib/codemirror.min.js', 38 | 'https://cdn.jsdelivr.net/npm/darkreader@4.9.27/darkreader.min.js', 39 | 'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/mode/shell/shell.js', 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2021-present whyour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

青龙(WIP)

8 | 9 |
10 | 11 | python和javaScript的定时任务管理面板 12 | 13 | # 青龙2.2-066 去升级版本 14 | 15 | ``` 16 | docker run -dit \ 17 | -v $PWD/ql2/config:/ql/config \ 18 | -v $PWD/ql2/log:/ql/log \ 19 | -v $PWD/ql2/db:/ql/db \ 20 | -p 5700:5700 \ 21 | --name ql2 \ 22 | --hostname ql2 \ 23 | --restart always \ 24 | limoe/qinglong:latest 25 | ``` 26 | [![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] 27 | 28 | [donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge 29 | [donate-url]: https://qinglong.whyour.cn/nice.png 30 | [build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge 31 | [build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong 32 | [docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge 33 | [docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong 34 | [docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=for-the-badge 35 | [docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated 36 | [docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=for-the-badge 37 | [docker-stars-url]: https://hub.docker.com/r/whyour/qinglong 38 | [docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge 39 | [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong 40 | 41 |
42 | 43 | 青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。 44 | 45 | 《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。 46 | 47 | 在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。 48 | 49 | ## 多谢 50 | 51 | * [nevinee](https://gitee.com/evine) 52 | 53 | * [crontab-ui](https://github.com/alseambusher/crontab-ui) 54 | 55 | * [Ant Design](https://ant.design) 56 | 57 | * [Ant Design Pro](https://pro.ant.design/) 58 | 59 | * [Umijs3.0](https://umijs.org) 60 | 61 | * [darkreader](https://github.com/darkreader/darkreader) 62 | 63 | ## 免责声明 64 | 65 | 1. 此仓储脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。 66 | 67 | 2. 由于此仓储脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。 68 | 69 | 3. 请勿将此仓储脚本用于任何商业或非法目的,若违反规定请自行对此负责。 70 | 71 | 4. 此仓储脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。 72 | 73 | 5. 本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。 74 | 75 | 6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。 76 | 77 | 7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。 78 | -------------------------------------------------------------------------------- /back/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { Logger } from 'winston'; 4 | import * as fs from 'fs'; 5 | import config from '../config'; 6 | import jwt from 'jsonwebtoken'; 7 | import { createRandomString } from '../config/util'; 8 | const route = Router(); 9 | 10 | export default (app: Router) => { 11 | app.use('/', route); 12 | route.post( 13 | '/login', 14 | async (req: Request, res: Response, next: NextFunction) => { 15 | const logger: Logger = Container.get('logger'); 16 | try { 17 | let username = req.body.username; 18 | let password = req.body.password; 19 | fs.readFile(config.authConfigFile, 'utf8', function (err, data) { 20 | if (err) console.log(err); 21 | const authInfo = JSON.parse(data); 22 | if (username && password) { 23 | if ( 24 | authInfo.username === 'admin' && 25 | authInfo.password === 'adminadmin' 26 | ) { 27 | const newPassword = createRandomString(16, 22); 28 | fs.writeFileSync( 29 | config.authConfigFile, 30 | JSON.stringify({ 31 | username: authInfo.username, 32 | password: newPassword, 33 | }), 34 | ); 35 | return res.send({ 36 | code: 100, 37 | msg: '已初始化密码,请前往auth.json查看并重新登录', 38 | }); 39 | } 40 | if ( 41 | username == authInfo.username && 42 | password == authInfo.password 43 | ) { 44 | const data = createRandomString(50, 100); 45 | let token = jwt.sign({ data }, config.secret as any, { 46 | expiresIn: 60 * 60 * 24 * 3, 47 | algorithm: 'HS384', 48 | }); 49 | fs.writeFileSync( 50 | config.authConfigFile, 51 | JSON.stringify({ 52 | username: authInfo.username, 53 | password: authInfo.password, 54 | token, 55 | }), 56 | ); 57 | res.send({ code: 200, token }); 58 | } else { 59 | res.send({ code: 400, msg: config.authError }); 60 | } 61 | } else { 62 | res.send({ err: 400, msg: '请输入用户名密码!' }); 63 | } 64 | }); 65 | } catch (e) { 66 | logger.error('🔥 error: %o', e); 67 | return next(e); 68 | } 69 | }, 70 | ); 71 | 72 | route.post( 73 | '/logout', 74 | async (req: Request, res: Response, next: NextFunction) => { 75 | const logger: Logger = Container.get('logger'); 76 | try { 77 | fs.readFile(config.authConfigFile, 'utf8', function (err, data) { 78 | if (err) console.log(err); 79 | const authInfo = JSON.parse(data); 80 | fs.writeFileSync( 81 | config.authConfigFile, 82 | JSON.stringify({ 83 | username: authInfo.username, 84 | password: authInfo.password, 85 | }), 86 | ); 87 | res.send({ code: 200 }); 88 | }); 89 | } catch (e) { 90 | logger.error('🔥 error: %o', e); 91 | return next(e); 92 | } 93 | }, 94 | ); 95 | 96 | route.post( 97 | '/user', 98 | async (req: Request, res: Response, next: NextFunction) => { 99 | const logger: Logger = Container.get('logger'); 100 | try { 101 | fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => { 102 | if (err) console.log(err); 103 | res.send({ code: 200, msg: '更新成功' }); 104 | }); 105 | } catch (e) { 106 | logger.error('🔥 error: %o', e); 107 | return next(e); 108 | } 109 | }, 110 | ); 111 | 112 | route.get( 113 | '/user', 114 | async (req: Request, res: Response, next: NextFunction) => { 115 | const logger: Logger = Container.get('logger'); 116 | try { 117 | fs.readFile(config.authConfigFile, 'utf8', (err, data) => { 118 | if (err) console.log(err); 119 | const authInfo = JSON.parse(data); 120 | res.send({ code: 200, data: { username: authInfo.username } }); 121 | }); 122 | } catch (e) { 123 | logger.error('🔥 error: %o', e); 124 | return next(e); 125 | } 126 | }, 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /back/api/config.ts: -------------------------------------------------------------------------------- 1 | import { getFileContentByName, getLastModifyFilePath } from '../config/util'; 2 | import { Router, Request, Response, NextFunction } from 'express'; 3 | import { Container } from 'typedi'; 4 | import { Logger } from 'winston'; 5 | import config from '../config'; 6 | import * as fs from 'fs'; 7 | import { celebrate, Joi } from 'celebrate'; 8 | import { execSync } from 'child_process'; 9 | const route = Router(); 10 | 11 | export default (app: Router) => { 12 | app.use('/', route); 13 | route.get( 14 | '/config/:key', 15 | async (req: Request, res: Response, next: NextFunction) => { 16 | const logger: Logger = Container.get('logger'); 17 | try { 18 | let content = '未找到文件'; 19 | switch (req.params.key) { 20 | case 'config': 21 | content = getFileContentByName(config.confFile); 22 | break; 23 | case 'sample': 24 | content = getFileContentByName(config.sampleFile); 25 | break; 26 | case 'crontab': 27 | content = getFileContentByName(config.crontabFile); 28 | break; 29 | case 'extra': 30 | content = getFileContentByName(config.extraFile); 31 | break; 32 | default: 33 | break; 34 | } 35 | res.send({ code: 200, data: content }); 36 | } catch (e) { 37 | logger.error('🔥 error: %o', e); 38 | return next(e); 39 | } 40 | }, 41 | ); 42 | 43 | route.post( 44 | '/save', 45 | celebrate({ 46 | body: Joi.object({ 47 | name: Joi.string().required(), 48 | content: Joi.string().required(), 49 | }), 50 | }), 51 | async (req: Request, res: Response, next: NextFunction) => { 52 | const logger: Logger = Container.get('logger'); 53 | try { 54 | const { name, content } = req.body; 55 | const path = (config.fileMap as any)[name]; 56 | fs.writeFileSync(path, content); 57 | if (name === 'crontab.list') { 58 | execSync(`crontab ${path}`); 59 | } 60 | res.send({ code: 200, msg: '保存成功' }); 61 | } catch (e) { 62 | logger.error('🔥 error: %o', e); 63 | return next(e); 64 | } 65 | }, 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /back/api/cookie.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import { Container } from 'typedi'; 3 | import CookieService from '../services/cookie'; 4 | import { Logger } from 'winston'; 5 | import { celebrate, Joi } from 'celebrate'; 6 | const route = Router(); 7 | 8 | export default (app: Router) => { 9 | app.use('/', route); 10 | route.get( 11 | '/cookies', 12 | async (req: Request, res: Response, next: NextFunction) => { 13 | const logger: Logger = Container.get('logger'); 14 | try { 15 | const cookieService = Container.get(CookieService); 16 | const data = await cookieService.cookies('', { position: -1 }, true); 17 | return res.send({ code: 200, data }); 18 | } catch (e) { 19 | logger.error('🔥 error: %o', e); 20 | return next(e); 21 | } 22 | }, 23 | ); 24 | 25 | route.post( 26 | '/cookies', 27 | celebrate({ 28 | body: Joi.array().items(Joi.string().required()).min(1), 29 | }), 30 | async (req: Request, res: Response, next: NextFunction) => { 31 | const logger: Logger = Container.get('logger'); 32 | try { 33 | const cookieService = Container.get(CookieService); 34 | const data = await cookieService.create(req.body); 35 | return res.send({ code: 200, data }); 36 | } catch (e) { 37 | logger.error('🔥 error: %o', e); 38 | return next(e); 39 | } 40 | }, 41 | ); 42 | 43 | route.put( 44 | '/cookies', 45 | celebrate({ 46 | body: Joi.object({ 47 | value: Joi.string().required(), 48 | _id: Joi.string().required(), 49 | }), 50 | }), 51 | async (req: Request, res: Response, next: NextFunction) => { 52 | const logger: Logger = Container.get('logger'); 53 | try { 54 | const cookieService = Container.get(CookieService); 55 | const data = await cookieService.update(req.body); 56 | return res.send({ code: 200, data }); 57 | } catch (e) { 58 | logger.error('🔥 error: %o', e); 59 | return next(e); 60 | } 61 | }, 62 | ); 63 | 64 | route.delete( 65 | '/cookies', 66 | celebrate({ 67 | body: Joi.array().items(Joi.string().required()), 68 | }), 69 | async (req: Request, res: Response, next: NextFunction) => { 70 | const logger: Logger = Container.get('logger'); 71 | try { 72 | const cookieService = Container.get(CookieService); 73 | const data = await cookieService.remove(req.body); 74 | return res.send({ code: 200, data }); 75 | } catch (e) { 76 | logger.error('🔥 error: %o', e); 77 | return next(e); 78 | } 79 | }, 80 | ); 81 | 82 | route.put( 83 | '/cookies/:id/move', 84 | celebrate({ 85 | params: Joi.object({ 86 | id: Joi.string().required(), 87 | }), 88 | body: Joi.object({ 89 | fromIndex: Joi.number().required(), 90 | toIndex: Joi.number().required(), 91 | }), 92 | }), 93 | async (req: Request, res: Response, next: NextFunction) => { 94 | const logger: Logger = Container.get('logger'); 95 | try { 96 | const cookieService = Container.get(CookieService); 97 | const data = await cookieService.move(req.params.id, req.body); 98 | return res.send({ code: 200, data }); 99 | } catch (e) { 100 | logger.error('🔥 error: %o', e); 101 | return next(e); 102 | } 103 | }, 104 | ); 105 | 106 | route.get( 107 | '/cookies/:id/refresh', 108 | celebrate({ 109 | params: Joi.object({ 110 | id: Joi.string().required(), 111 | }), 112 | }), 113 | async (req: Request, res: Response, next: NextFunction) => { 114 | const logger: Logger = Container.get('logger'); 115 | try { 116 | const cookieService = Container.get(CookieService); 117 | const data = await cookieService.refreshCookie(req.params.id); 118 | return res.send({ code: 200, data }); 119 | } catch (e) { 120 | logger.error('🔥 error: %o', e); 121 | return next(e); 122 | } 123 | }, 124 | ); 125 | 126 | route.put( 127 | '/cookies/disable', 128 | celebrate({ 129 | body: Joi.array().items(Joi.string().required()), 130 | }), 131 | async (req: Request, res: Response, next: NextFunction) => { 132 | const logger: Logger = Container.get('logger'); 133 | try { 134 | const cookieService = Container.get(CookieService); 135 | const data = await cookieService.disabled(req.body); 136 | return res.send({ code: 200, data }); 137 | } catch (e) { 138 | logger.error('🔥 error: %o', e); 139 | return next(e); 140 | } 141 | }, 142 | ); 143 | 144 | route.put( 145 | '/cookies/enable', 146 | celebrate({ 147 | body: Joi.array().items(Joi.string().required()), 148 | }), 149 | async (req: Request, res: Response, next: NextFunction) => { 150 | const logger: Logger = Container.get('logger'); 151 | try { 152 | const cookieService = Container.get(CookieService); 153 | const data = await cookieService.enabled(req.body); 154 | return res.send({ code: 200, data }); 155 | } catch (e) { 156 | logger.error('🔥 error: %o', e); 157 | return next(e); 158 | } 159 | }, 160 | ); 161 | 162 | route.get( 163 | '/cookies/:id', 164 | celebrate({ 165 | params: Joi.object({ 166 | id: Joi.string().required(), 167 | }), 168 | }), 169 | async (req: Request, res: Response, next: NextFunction) => { 170 | const logger: Logger = Container.get('logger'); 171 | try { 172 | const cookieService = Container.get(CookieService); 173 | const data = await cookieService.get(req.params.id); 174 | return res.send({ code: 200, data }); 175 | } catch (e) { 176 | logger.error('🔥 error: %o', e); 177 | return next(e); 178 | } 179 | }, 180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /back/api/cron.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { Logger } from 'winston'; 4 | import CronService from '../services/cron'; 5 | import { celebrate, Joi } from 'celebrate'; 6 | import cron_parser from 'cron-parser'; 7 | const route = Router(); 8 | 9 | export default (app: Router) => { 10 | app.use('/', route); 11 | route.get( 12 | '/crons', 13 | async (req: Request, res: Response, next: NextFunction) => { 14 | const logger: Logger = Container.get('logger'); 15 | try { 16 | const cronService = Container.get(CronService); 17 | const data = await cronService.crontabs( 18 | req.query.searchValue as string, 19 | ); 20 | return res.send({ code: 200, data }); 21 | } catch (e) { 22 | logger.error('🔥 error: %o', e); 23 | return next(e); 24 | } 25 | }, 26 | ); 27 | 28 | route.post( 29 | '/crons', 30 | celebrate({ 31 | body: Joi.object({ 32 | command: Joi.string().required(), 33 | schedule: Joi.string().required(), 34 | name: Joi.string().optional(), 35 | }), 36 | }), 37 | async (req: Request, res: Response, next: NextFunction) => { 38 | const logger: Logger = Container.get('logger'); 39 | try { 40 | if (cron_parser.parseExpression(req.body.schedule).hasNext()) { 41 | const cronService = Container.get(CronService); 42 | const data = await cronService.create(req.body); 43 | return res.send({ code: 200, data }); 44 | } else { 45 | return res.send({ code: 400, message: 'param schedule error' }); 46 | } 47 | } catch (e) { 48 | logger.error('🔥 error: %o', e); 49 | return next(e); 50 | } 51 | }, 52 | ); 53 | 54 | route.put( 55 | '/crons/run', 56 | celebrate({ 57 | body: Joi.array().items(Joi.string().required()), 58 | }), 59 | async (req: Request, res: Response, next: NextFunction) => { 60 | const logger: Logger = Container.get('logger'); 61 | try { 62 | const cronService = Container.get(CronService); 63 | const data = await cronService.run(req.body); 64 | return res.send({ code: 200, data }); 65 | } catch (e) { 66 | logger.error('🔥 error: %o', e); 67 | return next(e); 68 | } 69 | }, 70 | ); 71 | 72 | route.put( 73 | '/crons/stop', 74 | celebrate({ 75 | body: Joi.array().items(Joi.string().required()), 76 | }), 77 | async (req: Request, res: Response, next: NextFunction) => { 78 | const logger: Logger = Container.get('logger'); 79 | try { 80 | const cronService = Container.get(CronService); 81 | const data = await cronService.stop(req.body); 82 | return res.send({ code: 200, data }); 83 | } catch (e) { 84 | logger.error('🔥 error: %o', e); 85 | return next(e); 86 | } 87 | }, 88 | ); 89 | 90 | route.put( 91 | '/crons/disable', 92 | celebrate({ 93 | body: Joi.array().items(Joi.string().required()), 94 | }), 95 | async (req: Request, res: Response, next: NextFunction) => { 96 | const logger: Logger = Container.get('logger'); 97 | try { 98 | const cronService = Container.get(CronService); 99 | const data = await cronService.disabled(req.body); 100 | return res.send({ code: 200, data }); 101 | } catch (e) { 102 | logger.error('🔥 error: %o', e); 103 | return next(e); 104 | } 105 | }, 106 | ); 107 | 108 | route.put( 109 | '/crons/enable', 110 | celebrate({ 111 | body: Joi.array().items(Joi.string().required()), 112 | }), 113 | async (req: Request, res: Response, next: NextFunction) => { 114 | const logger: Logger = Container.get('logger'); 115 | try { 116 | const cronService = Container.get(CronService); 117 | const data = await cronService.enabled(req.body); 118 | return res.send({ code: 200, data }); 119 | } catch (e) { 120 | logger.error('🔥 error: %o', e); 121 | return next(e); 122 | } 123 | }, 124 | ); 125 | 126 | route.get( 127 | '/crons/:id/log', 128 | celebrate({ 129 | params: Joi.object({ 130 | id: Joi.string().required(), 131 | }), 132 | }), 133 | async (req: Request, res: Response, next: NextFunction) => { 134 | const logger: Logger = Container.get('logger'); 135 | try { 136 | const cronService = Container.get(CronService); 137 | const data = await cronService.log(req.params.id); 138 | return res.send({ code: 200, data }); 139 | } catch (e) { 140 | logger.error('🔥 error: %o', e); 141 | return next(e); 142 | } 143 | }, 144 | ); 145 | 146 | route.put( 147 | '/crons', 148 | celebrate({ 149 | body: Joi.object({ 150 | command: Joi.string().optional(), 151 | schedule: Joi.string().optional(), 152 | name: Joi.string().optional(), 153 | _id: Joi.string().required(), 154 | }), 155 | }), 156 | async (req: Request, res: Response, next: NextFunction) => { 157 | const logger: Logger = Container.get('logger'); 158 | try { 159 | if ( 160 | !req.body.schedule || 161 | cron_parser.parseExpression(req.body.schedule).hasNext() 162 | ) { 163 | const cronService = Container.get(CronService); 164 | const data = await cronService.update(req.body); 165 | return res.send({ code: 200, data }); 166 | } else { 167 | return res.send({ code: 400, message: 'param schedule error' }); 168 | } 169 | } catch (e) { 170 | logger.error('🔥 error: %o', e); 171 | return next(e); 172 | } 173 | }, 174 | ); 175 | 176 | route.delete( 177 | '/crons', 178 | celebrate({ 179 | body: Joi.array().items(Joi.string().required()), 180 | }), 181 | async (req: Request, res: Response, next: NextFunction) => { 182 | const logger: Logger = Container.get('logger'); 183 | try { 184 | const cronService = Container.get(CronService); 185 | const data = await cronService.remove(req.body); 186 | return res.send({ code: 200, data }); 187 | } catch (e) { 188 | logger.error('🔥 error: %o', e); 189 | return next(e); 190 | } 191 | }, 192 | ); 193 | 194 | route.get( 195 | '/crons/import', 196 | async (req: Request, res: Response, next: NextFunction) => { 197 | const logger: Logger = Container.get('logger'); 198 | try { 199 | const cronService = Container.get(CronService); 200 | const data = await cronService.import_crontab(); 201 | return res.send({ code: 200, data }); 202 | } catch (e) { 203 | logger.error('🔥 error: %o', e); 204 | return next(e); 205 | } 206 | }, 207 | ); 208 | 209 | route.get( 210 | '/crons/:id', 211 | celebrate({ 212 | params: Joi.object({ 213 | id: Joi.string().required(), 214 | }), 215 | }), 216 | async (req: Request, res: Response, next: NextFunction) => { 217 | const logger: Logger = Container.get('logger'); 218 | try { 219 | const cronService = Container.get(CronService); 220 | const data = await cronService.get(req.params.id); 221 | return res.send({ code: 200, data }); 222 | } catch (e) { 223 | logger.error('🔥 error: %o', e); 224 | return next(e); 225 | } 226 | }, 227 | ); 228 | 229 | route.put( 230 | '/crons/status', 231 | celebrate({ 232 | body: Joi.object({ 233 | ids: Joi.array().items(Joi.string().required()), 234 | status: Joi.string().required(), 235 | }), 236 | }), 237 | async (req: Request, res: Response, next: NextFunction) => { 238 | const logger: Logger = Container.get('logger'); 239 | try { 240 | const cronService = Container.get(CronService); 241 | const data = await cronService.status({ 242 | ...req.body, 243 | status: parseInt(req.body.status), 244 | }); 245 | return res.send({ code: 200, data }); 246 | } catch (e) { 247 | logger.error('🔥 error: %o', e); 248 | return next(e); 249 | } 250 | }, 251 | ); 252 | }; 253 | -------------------------------------------------------------------------------- /back/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import auth from './auth'; 3 | import cookie from './cookie'; 4 | import config from './config'; 5 | import log from './log'; 6 | import cron from './cron'; 7 | 8 | export default () => { 9 | const app = Router(); 10 | auth(app); 11 | cookie(app); 12 | config(app); 13 | log(app); 14 | cron(app); 15 | 16 | return app; 17 | }; 18 | -------------------------------------------------------------------------------- /back/api/log.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import { Container } from 'typedi'; 3 | import { Logger } from 'winston'; 4 | import * as fs from 'fs'; 5 | import config from '../config'; 6 | import { getFileContentByName } from '../config/util'; 7 | const route = Router(); 8 | 9 | export default (app: Router) => { 10 | app.use('/', route); 11 | route.get( 12 | '/logs', 13 | async (req: Request, res: Response, next: NextFunction) => { 14 | const logger: Logger = Container.get('logger'); 15 | try { 16 | const fileList = fs.readdirSync(config.logPath, 'utf-8'); 17 | const dirs = []; 18 | for (let i = 0; i < fileList.length; i++) { 19 | const stat = fs.lstatSync(config.logPath + fileList[i]); 20 | if (stat.isDirectory()) { 21 | const fileListTmp = fs.readdirSync( 22 | `${config.logPath}/${fileList[i]}`, 23 | 'utf-8', 24 | ); 25 | dirs.push({ 26 | name: fileList[i], 27 | isDir: true, 28 | files: fileListTmp.reverse(), 29 | }); 30 | } else { 31 | dirs.push({ 32 | name: fileList[i], 33 | isDir: false, 34 | files: [], 35 | }); 36 | } 37 | } 38 | res.send({ code: 200, dirs }); 39 | } catch (e) { 40 | logger.error('🔥 error: %o', e); 41 | return next(e); 42 | } 43 | }, 44 | ); 45 | 46 | route.get( 47 | '/logs/:dir/:file', 48 | async (req: Request, res: Response, next: NextFunction) => { 49 | const logger: Logger = Container.get('logger'); 50 | try { 51 | const { dir, file } = req.params; 52 | const content = getFileContentByName( 53 | `${config.logPath}/${dir}/${file}`, 54 | ); 55 | res.send({ code: 200, data: content }); 56 | } catch (e) { 57 | logger.error('🔥 error: %o', e); 58 | return next(e); 59 | } 60 | }, 61 | ); 62 | 63 | route.get( 64 | '/logs/:file', 65 | async (req: Request, res: Response, next: NextFunction) => { 66 | const logger: Logger = Container.get('logger'); 67 | try { 68 | const { file } = req.params; 69 | const content = getFileContentByName(`${config.logPath}/${file}`); 70 | res.send({ code: 200, data: content }); 71 | } catch (e) { 72 | logger.error('🔥 error: %o', e); 73 | return next(e); 74 | } 75 | }, 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /back/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; // We need this in order to use @Decorators 2 | 3 | import config from './config'; 4 | 5 | import express from 'express'; 6 | 7 | import Logger from './loaders/logger'; 8 | 9 | async function startServer() { 10 | const app = express(); 11 | 12 | await require('./loaders').default({ expressApp: app }); 13 | 14 | app 15 | .listen(config.port, () => { 16 | Logger.info(` 17 | ################################################ 18 | 🛡️ Server listening on port: ${config.port} 🛡️ 19 | ################################################ 20 | `); 21 | }) 22 | .on('error', (err) => { 23 | Logger.error(err); 24 | process.exit(1); 25 | }); 26 | } 27 | 28 | startServer(); 29 | -------------------------------------------------------------------------------- /back/config/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import path from 'path'; 3 | import { createRandomString } from './util'; 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 6 | 7 | const envFound = dotenv.config(); 8 | const rootPath = path.resolve(__dirname, '../../'); 9 | const cookieFile = path.join(rootPath, 'config/cookie.sh'); 10 | const confFile = path.join(rootPath, 'config/config.sh'); 11 | const sampleFile = path.join(rootPath, 'sample/config.sample.sh'); 12 | const crontabFile = path.join(rootPath, 'config/crontab.list'); 13 | const confBakDir = path.join(rootPath, 'config/bak/'); 14 | const authConfigFile = path.join(rootPath, 'config/auth.json'); 15 | const extraFile = path.join(rootPath, 'config/extra.sh'); 16 | const logPath = path.join(rootPath, 'log/'); 17 | const authError = '错误的用户名密码,请重试'; 18 | const loginFaild = '请先登录!'; 19 | const configString = 'config sample crontab shareCode diy'; 20 | const dbPath = path.join(rootPath, 'db/'); 21 | const manualLogPath = path.join(rootPath, 'manual_log/'); 22 | const cronDbFile = path.join(rootPath, 'db/crontab.db'); 23 | const cookieDbFile = path.join(rootPath, 'db/cookie.db'); 24 | const configFound = dotenv.config({ path: confFile }); 25 | 26 | if (envFound.error) { 27 | throw new Error("⚠️ Couldn't find .env file ⚠️"); 28 | } 29 | 30 | if (configFound.error) { 31 | throw new Error("⚠️ Couldn't find config.sh file ⚠️"); 32 | } 33 | 34 | export default { 35 | port: parseInt(process.env.PORT as string, 10), 36 | cronPort: parseInt(process.env.CRON_PORT as string, 10), 37 | secret: process.env.SECRET || createRandomString(16, 32), 38 | logs: { 39 | level: process.env.LOG_LEVEL || 'silly', 40 | }, 41 | api: { 42 | prefix: '/api', 43 | }, 44 | configString, 45 | loginFaild, 46 | authError, 47 | logPath, 48 | extraFile, 49 | authConfigFile, 50 | confBakDir, 51 | crontabFile, 52 | sampleFile, 53 | confFile, 54 | cookieFile, 55 | fileMap: { 56 | 'config.sh': confFile, 57 | 'crontab.list': crontabFile, 58 | 'extra.sh': extraFile, 59 | }, 60 | dbPath, 61 | cronDbFile, 62 | cookieDbFile, 63 | manualLogPath, 64 | }; 65 | -------------------------------------------------------------------------------- /back/config/util.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export function getFileContentByName(fileName: string) { 5 | if (fs.existsSync(fileName)) { 6 | return fs.readFileSync(fileName, 'utf8'); 7 | } 8 | return ''; 9 | } 10 | 11 | export function getLastModifyFilePath(dir: string) { 12 | let filePath = ''; 13 | 14 | if (fs.existsSync(dir)) { 15 | const arr = fs.readdirSync(dir); 16 | 17 | arr.forEach((item) => { 18 | const fullpath = path.join(dir, item); 19 | const stats = fs.statSync(fullpath); 20 | if (stats.isFile()) { 21 | if (stats.mtimeMs >= 0) { 22 | filePath = fullpath; 23 | } 24 | } 25 | }); 26 | } 27 | return filePath; 28 | } 29 | 30 | export function createRandomString(min: number, max: number): string { 31 | const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 32 | const english = [ 33 | 'a', 34 | 'b', 35 | 'c', 36 | 'd', 37 | 'e', 38 | 'f', 39 | 'g', 40 | 'h', 41 | 'i', 42 | 'j', 43 | 'k', 44 | 'l', 45 | 'm', 46 | 'n', 47 | 'o', 48 | 'p', 49 | 'q', 50 | 'r', 51 | 's', 52 | 't', 53 | 'u', 54 | 'v', 55 | 'w', 56 | 'x', 57 | 'y', 58 | 'z', 59 | ]; 60 | const ENGLISH = [ 61 | 'A', 62 | 'B', 63 | 'C', 64 | 'D', 65 | 'E', 66 | 'F', 67 | 'G', 68 | 'H', 69 | 'I', 70 | 'J', 71 | 'K', 72 | 'L', 73 | 'M', 74 | 'N', 75 | 'O', 76 | 'P', 77 | 'Q', 78 | 'R', 79 | 'S', 80 | 'T', 81 | 'U', 82 | 'V', 83 | 'W', 84 | 'X', 85 | 'Y', 86 | 'Z', 87 | ]; 88 | const special = ['-', '_', '#']; 89 | const config = num.concat(english).concat(ENGLISH).concat(special); 90 | 91 | const arr = []; 92 | arr.push(getOne(num)); 93 | arr.push(getOne(english)); 94 | arr.push(getOne(ENGLISH)); 95 | arr.push(getOne(special)); 96 | 97 | const len = min + Math.floor(Math.random() * (max - min + 1)); 98 | 99 | for (let i = 4; i < len; i++) { 100 | arr.push(config[Math.floor(Math.random() * config.length)]); 101 | } 102 | 103 | const newArr = []; 104 | for (let j = 0; j < len; j++) { 105 | newArr.push(arr.splice(Math.random() * arr.length, 1)[0]); 106 | } 107 | 108 | function getOne(arr) { 109 | return arr[Math.floor(Math.random() * arr.length)]; 110 | } 111 | 112 | return newArr.join(''); 113 | } 114 | 115 | export function getToken(req: any) { 116 | const { authorization } = req.headers; 117 | if (authorization && authorization.split(' ')[0] === 'Bearer') { 118 | return authorization.split(' ')[1]; 119 | } 120 | return ''; 121 | } 122 | -------------------------------------------------------------------------------- /back/data/cookie.ts: -------------------------------------------------------------------------------- 1 | export class Cookie { 2 | value?: string; 3 | timestamp?: string; 4 | created?: number; 5 | _id?: string; 6 | status?: CookieStatus; 7 | position?: number; 8 | 9 | constructor(options: Cookie) { 10 | this.value = options.value; 11 | this._id = options._id; 12 | this.created = options.created || new Date().valueOf(); 13 | this.status = options.status || CookieStatus.noacquired; 14 | this.timestamp = new Date().toString(); 15 | this.position = options.position; 16 | } 17 | } 18 | 19 | export enum CookieStatus { 20 | 'noacquired', 21 | 'normal', 22 | 'disabled', 23 | 'invalid', 24 | 'abnormal', 25 | } 26 | 27 | export const initCookiePosition = 9999999999; 28 | -------------------------------------------------------------------------------- /back/data/cron.ts: -------------------------------------------------------------------------------- 1 | export class Crontab { 2 | name?: string; 3 | command: string; 4 | schedule: string; 5 | timestamp?: string; 6 | created?: number; 7 | saved?: boolean; 8 | _id?: string; 9 | status?: CrontabStatus; 10 | isSystem?: 1 | 0; 11 | pid?: number; 12 | isDisabled?: 1 | 0; 13 | 14 | constructor(options: Crontab) { 15 | this.name = options.name; 16 | this.command = options.command; 17 | this.schedule = options.schedule; 18 | this.saved = options.saved; 19 | this._id = options._id; 20 | this.created = options.created; 21 | this.status = options.status || CrontabStatus.idle; 22 | this.timestamp = new Date().toString(); 23 | this.isSystem = options.isSystem || 0; 24 | this.pid = options.pid; 25 | this.isDisabled = options.isDisabled || 0; 26 | } 27 | } 28 | 29 | export enum CrontabStatus { 30 | 'running', 31 | 'idle', 32 | 'disabled', 33 | 'queued', 34 | } 35 | -------------------------------------------------------------------------------- /back/loaders/dependencyInjector.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'typedi'; 2 | import LoggerInstance from './logger'; 3 | 4 | export default ({ models }: { models: { name: string; model: any }[] }) => { 5 | try { 6 | models.forEach((m) => { 7 | Container.set(m.name, m.model); 8 | }); 9 | 10 | Container.set('logger', LoggerInstance); 11 | } catch (e) { 12 | LoggerInstance.error('🔥 Error on dependency injector loader: %o', e); 13 | throw e; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /back/loaders/express.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, Application } from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import cors from 'cors'; 4 | import routes from '../api'; 5 | import config from '../config'; 6 | import jwt from 'express-jwt'; 7 | import fs from 'fs'; 8 | import { getToken } from '../config/util'; 9 | 10 | export default ({ app }: { app: Application }) => { 11 | app.enable('trust proxy'); 12 | app.use(cors()); 13 | 14 | app.use(bodyParser.json({ limit: '50mb' })); 15 | app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); 16 | app.use( 17 | jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({ 18 | path: ['/api/login'], 19 | }), 20 | ); 21 | app.use((req, res, next) => { 22 | const data = fs.readFileSync(config.authConfigFile, 'utf8'); 23 | const headerToken = getToken(req); 24 | if (data) { 25 | const { token } = JSON.parse(data); 26 | if (token && headerToken === token) { 27 | return next(); 28 | } 29 | if (!headerToken && req.path && req.path.includes('/api/login')) { 30 | return next(); 31 | } 32 | } 33 | 34 | const err: any = new Error('UnauthorizedError'); 35 | err['status'] = 401; 36 | next(err); 37 | }); 38 | app.use(config.api.prefix, routes()); 39 | 40 | app.use((req, res, next) => { 41 | const err: any = new Error('Not Found'); 42 | err['status'] = 404; 43 | next(err); 44 | }); 45 | 46 | app.use( 47 | ( 48 | err: Error & { status: number }, 49 | req: Request, 50 | res: Response, 51 | next: NextFunction, 52 | ) => { 53 | if (err.name === 'UnauthorizedError') { 54 | return res 55 | .status(err.status) 56 | .send({ code: 401, message: err.message }) 57 | .end(); 58 | } 59 | return next(err); 60 | }, 61 | ); 62 | 63 | app.use( 64 | ( 65 | err: Error & { status: number }, 66 | req: Request, 67 | res: Response, 68 | next: NextFunction, 69 | ) => { 70 | res.status(err.status || 500); 71 | res.json({ 72 | code: err.status || 500, 73 | message: err.message, 74 | }); 75 | }, 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /back/loaders/index.ts: -------------------------------------------------------------------------------- 1 | import expressLoader from './express'; 2 | import dependencyInjectorLoader from './dependencyInjector'; 3 | import Logger from './logger'; 4 | import initData from './initData'; 5 | 6 | export default async ({ expressApp }: { expressApp: any }) => { 7 | Logger.info('✌️ DB loaded and connected!'); 8 | 9 | await dependencyInjectorLoader({ 10 | models: [], 11 | }); 12 | Logger.info('✌️ Dependency Injector loaded'); 13 | 14 | await expressLoader({ app: expressApp }); 15 | Logger.info('✌️ Express loaded'); 16 | 17 | await initData(); 18 | Logger.info('✌️ init data loaded'); 19 | }; 20 | -------------------------------------------------------------------------------- /back/loaders/initData.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { Container } from 'typedi'; 3 | import { Crontab, CrontabStatus } from '../data/cron'; 4 | import CronService from '../services/cron'; 5 | import CookieService from '../services/cookie'; 6 | 7 | const initData = [ 8 | { 9 | name: '更新面板', 10 | command: `ql update`, 11 | schedule: `${randomSchedule(60, 1)} ${randomSchedule( 12 | 6, 13 | 1, 14 | ).toString()} * * *`, 15 | status: CrontabStatus.disabled, 16 | }, 17 | { 18 | name: '删除日志', 19 | command: 'ql rmlog 7', 20 | schedule: '30 7 */7 * *', 21 | status: CrontabStatus.idle, 22 | }, 23 | { 24 | name: '互助码', 25 | command: 'ql code', 26 | schedule: '30 7 * * *', 27 | status: CrontabStatus.idle, 28 | }, 29 | ]; 30 | 31 | export default async () => { 32 | const cronService = Container.get(CronService); 33 | const cookieService = Container.get(CookieService); 34 | const cronDb = cronService.getDb(); 35 | 36 | cronDb.count({}, async (err, count) => { 37 | if (count === 0) { 38 | const data = initData.map((x) => { 39 | const tab = new Crontab(x); 40 | tab.created = new Date().valueOf(); 41 | tab.saved = false; 42 | if (tab.name === '更新面板') { 43 | tab.isSystem = 1; 44 | } else { 45 | tab.isSystem = 0; 46 | } 47 | return tab; 48 | }); 49 | cronDb.insert(data); 50 | await cronService.autosave_crontab(); 51 | } 52 | }); 53 | 54 | // patch更新面板任务状态 55 | cronDb.find({ name: '更新面板' }).exec((err, docs) => { 56 | const doc = docs[0]; 57 | if (doc && doc.status === CrontabStatus.running) { 58 | cronDb.update( 59 | { name: '更新面板' }, 60 | { $set: { status: CrontabStatus.idle } }, 61 | ); 62 | } 63 | }); 64 | 65 | // 初始化时执行一次所有的ql repo 任务 66 | cronDb 67 | .find({ 68 | command: /ql (repo|raw)/, 69 | }) 70 | .exec((err, docs) => { 71 | for (let i = 0; i < docs.length; i++) { 72 | const doc = docs[i]; 73 | if (doc && doc.isDisabled !== 1) { 74 | exec(doc.command); 75 | } 76 | } 77 | }); 78 | 79 | // patch 禁用状态字段改变 80 | cronDb 81 | .find({ 82 | status: CrontabStatus.disabled, 83 | }) 84 | .exec((err, docs) => { 85 | if (docs.length > 0) { 86 | const ids = docs.map((x) => x._id); 87 | cronDb.update( 88 | { _id: { $in: ids } }, 89 | { $set: { status: CrontabStatus.idle, isDisabled: 1 } }, 90 | { multi: true }, 91 | (err) => { 92 | cronService.autosave_crontab(); 93 | }, 94 | ); 95 | } 96 | }); 97 | 98 | // 初始化保存一次ck和定时任务数据 99 | await cronService.autosave_crontab(); 100 | await cookieService.set_cookies(); 101 | }; 102 | 103 | function randomSchedule(from: number, to: number) { 104 | const result = []; 105 | const arr = [...Array(from).keys()]; 106 | let count = arr.length; 107 | for (let i = 0; i < to; i++) { 108 | const index = ~~(Math.random() * count) + i; 109 | if (result.includes(arr[index])) { 110 | continue; 111 | } 112 | result[i] = arr[index]; 113 | arr[index] = arr[i]; 114 | count--; 115 | } 116 | return result; 117 | } 118 | -------------------------------------------------------------------------------- /back/loaders/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import config from '../config'; 3 | 4 | const transports = []; 5 | if (process.env.NODE_ENV !== 'development') { 6 | transports.push(new winston.transports.Console()); 7 | } else { 8 | transports.push( 9 | new winston.transports.Console({ 10 | format: winston.format.combine( 11 | winston.format.cli(), 12 | winston.format.splat(), 13 | ), 14 | }), 15 | ); 16 | } 17 | 18 | const LoggerInstance = winston.createLogger({ 19 | level: config.logs.level, 20 | levels: winston.config.npm.levels, 21 | format: winston.format.combine( 22 | winston.format.timestamp({ 23 | format: 'YYYY-MM-DD HH:mm:ss', 24 | }), 25 | winston.format.errors({ stack: true }), 26 | winston.format.splat(), 27 | winston.format.json(), 28 | ), 29 | transports, 30 | }); 31 | 32 | export default LoggerInstance; 33 | -------------------------------------------------------------------------------- /back/schedule.ts: -------------------------------------------------------------------------------- 1 | import schedule from 'node-schedule'; 2 | import express from 'express'; 3 | import { exec } from 'child_process'; 4 | import Logger from './loaders/logger'; 5 | import { Container } from 'typedi'; 6 | import CronService from './services/cron'; 7 | import { CrontabStatus } from './data/cron'; 8 | import config from './config'; 9 | 10 | const app = express(); 11 | 12 | const run = async () => { 13 | const cronService = Container.get(CronService); 14 | const cronDb = cronService.getDb(); 15 | 16 | cronDb 17 | .find({}) 18 | .sort({ created: 1 }) 19 | .exec((err, docs) => { 20 | if (err) { 21 | Logger.error(err); 22 | process.exit(1); 23 | } 24 | 25 | if (docs && docs.length > 0) { 26 | for (let i = 0; i < docs.length; i++) { 27 | const task = docs[i]; 28 | const _schedule = task.schedule && task.schedule.split(' '); 29 | if ( 30 | _schedule && 31 | _schedule.length > 5 && 32 | task.status !== CrontabStatus.disabled && 33 | !task.isDisabled 34 | ) { 35 | schedule.scheduleJob(task.schedule, function () { 36 | let command = task.command as string; 37 | if (!command.includes('task ') && !command.includes('ql ')) { 38 | command = `task ${command}`; 39 | } 40 | exec(command); 41 | }); 42 | } 43 | } 44 | } 45 | }); 46 | }; 47 | 48 | app 49 | .listen(config.cronPort, () => { 50 | run(); 51 | Logger.info(` 52 | ################################################ 53 | 🛡️ Schedule listening on port: ${config.cronPort} 🛡️ 54 | ################################################ 55 | `); 56 | }) 57 | .on('error', (err) => { 58 | Logger.error(err); 59 | process.exit(1); 60 | }); 61 | -------------------------------------------------------------------------------- /back/services/cookie.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from 'typedi'; 2 | import winston from 'winston'; 3 | import fetch from 'node-fetch'; 4 | import { getFileContentByName } from '../config/util'; 5 | import config from '../config'; 6 | import * as fs from 'fs'; 7 | import got from 'got'; 8 | import DataStore from 'nedb'; 9 | import { Cookie, CookieStatus, initCookiePosition } from '../data/cookie'; 10 | 11 | @Service() 12 | export default class CookieService { 13 | private cronDb = new DataStore({ filename: config.cookieDbFile }); 14 | constructor(@Inject('logger') private logger: winston.Logger) { 15 | this.cronDb.loadDatabase((err) => { 16 | if (err) throw err; 17 | }); 18 | } 19 | 20 | public async getCookies() { 21 | const content = getFileContentByName(config.cookieFile); 22 | return this.formatCookie(content.split('\n').filter((x) => !!x)); 23 | } 24 | 25 | public async addCookie(cookies: string[]) { 26 | let content = getFileContentByName(config.cookieFile); 27 | const originCookies = content.split('\n').filter((x) => !!x); 28 | const result = originCookies.concat(cookies); 29 | fs.writeFileSync(config.cookieFile, result.join('\n')); 30 | return ''; 31 | } 32 | 33 | public async updateCookie({ cookie, oldCookie }) { 34 | let content = getFileContentByName(config.cookieFile); 35 | const cookies = content.split('\n'); 36 | const index = cookies.findIndex((x) => x === oldCookie); 37 | if (index !== -1) { 38 | cookies[index] = cookie; 39 | fs.writeFileSync(config.cookieFile, cookies.join('\n')); 40 | return ''; 41 | } else { 42 | return '未找到要原有Cookie'; 43 | } 44 | } 45 | 46 | public async deleteCookie(cookie: string) { 47 | let content = getFileContentByName(config.cookieFile); 48 | const cookies = content.split('\n'); 49 | const index = cookies.findIndex((x) => x === cookie); 50 | if (index !== -1) { 51 | cookies.splice(index, 1); 52 | fs.writeFileSync(config.cookieFile, cookies.join('\n')); 53 | return ''; 54 | } else { 55 | return '未找到要删除的Cookie'; 56 | } 57 | } 58 | 59 | private async formatCookie(data: any[]) { 60 | const result = []; 61 | for (const x of data) { 62 | const { nickname, status } = await this.getJdInfo(x); 63 | if (/pt_pin=(.+?);/.test(x)) { 64 | result.push({ 65 | pin: x.match(/pt_pin=(.+?);/)[1], 66 | cookie: x, 67 | status, 68 | nickname: nickname, 69 | }); 70 | } else { 71 | result.push({ 72 | pin: 'pin未匹配到', 73 | cookie: x, 74 | status, 75 | nickname: nickname, 76 | }); 77 | } 78 | } 79 | return result; 80 | } 81 | 82 | public async refreshCookie(_id: string) { 83 | const current = await this.get(_id); 84 | const { status, nickname } = await this.getJdInfo(current.value); 85 | return { 86 | ...current, 87 | status, 88 | nickname, 89 | }; 90 | } 91 | 92 | private getJdInfo(cookie: string) { 93 | return fetch( 94 | `https://me-api.jd.com/user_new/info/GetJDUserInfoUnion?orgFlag=JD_PinGou_New&callSource=mainorder&channel=4&isHomewhite=0&sceneval=2&_=${Date.now()}&sceneval=2&g_login_type=1&g_ty=ls`, 95 | { 96 | method: 'get', 97 | headers: { 98 | Accept: '*/*', 99 | 'Accept-Encoding': 'gzip, deflate, br', 100 | 'Accept-Language': 'zh-cn', 101 | Connection: 'keep-alive', 102 | Cookie: cookie, 103 | Referer: 'https://home.m.jd.com/myJd/newhome.action', 104 | 'User-Agent': 105 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1', 106 | Host: 'me-api.jd.com', 107 | }, 108 | }, 109 | ) 110 | .then((x) => x.json()) 111 | .then((x) => { 112 | if (x.retcode === '0' && x.data && x.data.userInfo) { 113 | return { 114 | nickname: x.data.userInfo.baseInfo.nickname, 115 | status: CookieStatus.normal, 116 | }; 117 | } else if (x.retcode === 13) { 118 | return { status: CookieStatus.invalid, nickname: '-' }; 119 | } 120 | return { status: CookieStatus.abnormal, nickname: '-' }; 121 | }); 122 | } 123 | 124 | private async formatCookies(cookies: Cookie[]) { 125 | const result = []; 126 | for (let i = 0; i < cookies.length; i++) { 127 | const cookie = cookies[i]; 128 | if (cookie.status !== CookieStatus.disabled) { 129 | const { status, nickname } = await this.getJdInfo(cookie.value); 130 | result.push({ ...cookie, status, nickname }); 131 | } else { 132 | result.push({ ...cookie, nickname: '-' }); 133 | } 134 | } 135 | return result; 136 | } 137 | 138 | public async create(payload: string[]): Promise { 139 | const cookies = await this.cookies(); 140 | let position = initCookiePosition; 141 | if (cookies && cookies.length > 0) { 142 | position = cookies[cookies.length - 1].position; 143 | } 144 | const tabs = payload.map((x) => { 145 | const cookie = new Cookie({ value: x, position }); 146 | position = position / 2; 147 | cookie.position = position; 148 | return cookie; 149 | }); 150 | const docs = await this.insert(tabs); 151 | await this.set_cookies(); 152 | return await this.formatCookies(docs); 153 | } 154 | 155 | public async insert(payload: Cookie[]): Promise { 156 | return new Promise((resolve) => { 157 | this.cronDb.insert(payload, (err, docs) => { 158 | if (err) { 159 | this.logger.error(err); 160 | } else { 161 | resolve(docs); 162 | } 163 | }); 164 | }); 165 | } 166 | 167 | public async update(payload: Cookie): Promise { 168 | const { _id, ...other } = payload; 169 | const doc = await this.get(_id); 170 | const tab = new Cookie({ ...doc, ...other }); 171 | const newDoc = await this.updateDb(tab); 172 | await this.set_cookies(); 173 | const [newCookie] = await this.formatCookies([newDoc]); 174 | return newCookie; 175 | } 176 | 177 | private async updateDb(payload: Cookie): Promise { 178 | return new Promise((resolve) => { 179 | this.cronDb.update( 180 | { _id: payload._id }, 181 | payload, 182 | { returnUpdatedDocs: true }, 183 | (err, num, doc) => { 184 | if (err) { 185 | this.logger.error(err); 186 | } else { 187 | resolve(doc as Cookie); 188 | } 189 | }, 190 | ); 191 | }); 192 | } 193 | 194 | public async remove(ids: string[]) { 195 | return new Promise((resolve: any) => { 196 | this.cronDb.remove( 197 | { _id: { $in: ids } }, 198 | { multi: true }, 199 | async (err) => { 200 | await this.set_cookies(); 201 | resolve(); 202 | }, 203 | ); 204 | }); 205 | } 206 | 207 | public async move( 208 | _id: string, 209 | { 210 | fromIndex, 211 | toIndex, 212 | }: { 213 | fromIndex: number; 214 | toIndex: number; 215 | }, 216 | ) { 217 | let targetPosition: number; 218 | const isUpward = fromIndex > toIndex; 219 | const cookies = await this.cookies(); 220 | if (toIndex === 0 || toIndex === cookies.length - 1) { 221 | targetPosition = isUpward 222 | ? cookies[0].position * 2 223 | : cookies[toIndex].position / 2; 224 | } else { 225 | targetPosition = isUpward 226 | ? (cookies[toIndex].position + cookies[toIndex - 1].position) / 2 227 | : (cookies[toIndex].position + cookies[toIndex + 1].position) / 2; 228 | } 229 | this.update({ 230 | _id, 231 | position: targetPosition, 232 | }); 233 | await this.set_cookies(); 234 | } 235 | 236 | public async cookies( 237 | searchText?: string, 238 | sort: any = { position: -1 }, 239 | needDetail: boolean = false, 240 | ): Promise { 241 | let query = {}; 242 | if (searchText) { 243 | const reg = new RegExp(searchText); 244 | query = { 245 | $or: [ 246 | { 247 | name: reg, 248 | }, 249 | { 250 | command: reg, 251 | }, 252 | ], 253 | }; 254 | } 255 | const newDocs = await this.find(query, sort); 256 | if (needDetail) { 257 | return await this.formatCookies(newDocs); 258 | } else { 259 | return newDocs; 260 | } 261 | } 262 | 263 | private async find(query: any, sort: any): Promise { 264 | return new Promise((resolve) => { 265 | this.cronDb 266 | .find(query) 267 | .sort({ ...sort }) 268 | .exec((err, docs) => { 269 | resolve(docs); 270 | }); 271 | }); 272 | } 273 | 274 | public async get(_id: string): Promise { 275 | return new Promise((resolve) => { 276 | this.cronDb.find({ _id }).exec((err, docs) => { 277 | resolve(docs[0]); 278 | }); 279 | }); 280 | } 281 | 282 | public async getBySort(sort: any): Promise { 283 | return new Promise((resolve) => { 284 | this.cronDb 285 | .find({}) 286 | .sort({ ...sort }) 287 | .limit(1) 288 | .exec((err, docs) => { 289 | resolve(docs[0]); 290 | }); 291 | }); 292 | } 293 | 294 | public async disabled(ids: string[]) { 295 | return new Promise((resolve: any) => { 296 | this.cronDb.update( 297 | { _id: { $in: ids } }, 298 | { $set: { status: CookieStatus.disabled } }, 299 | { multi: true }, 300 | async (err) => { 301 | await this.set_cookies(); 302 | resolve(); 303 | }, 304 | ); 305 | }); 306 | } 307 | 308 | public async enabled(ids: string[]) { 309 | return new Promise((resolve: any) => { 310 | this.cronDb.update( 311 | { _id: { $in: ids } }, 312 | { $set: { status: CookieStatus.noacquired } }, 313 | { multi: true }, 314 | async (err, num) => { 315 | await this.set_cookies(); 316 | resolve(); 317 | }, 318 | ); 319 | }); 320 | } 321 | 322 | public async set_cookies() { 323 | const cookies = await this.cookies(); 324 | let cookie_string = ''; 325 | cookies.forEach((tab) => { 326 | if (tab.status !== CookieStatus.disabled) { 327 | cookie_string += tab.value; 328 | cookie_string += '\n'; 329 | } 330 | }); 331 | fs.writeFileSync(config.cookieFile, cookie_string); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /back/services/cron.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from 'typedi'; 2 | import winston from 'winston'; 3 | import DataStore from 'nedb'; 4 | import config from '../config'; 5 | import { Crontab, CrontabStatus } from '../data/cron'; 6 | import { exec, execSync, spawn } from 'child_process'; 7 | import fs from 'fs'; 8 | import cron_parser from 'cron-parser'; 9 | import { getFileContentByName } from '../config/util'; 10 | import PQueue from 'p-queue'; 11 | 12 | @Service() 13 | export default class CronService { 14 | private cronDb = new DataStore({ filename: config.cronDbFile }); 15 | 16 | private queue = new PQueue({ 17 | concurrency: parseInt(process.env.MaxConcurrentNum) || 5, 18 | }); 19 | 20 | constructor(@Inject('logger') private logger: winston.Logger) { 21 | this.cronDb.loadDatabase((err) => { 22 | if (err) throw err; 23 | }); 24 | } 25 | 26 | public getDb(): DataStore { 27 | return this.cronDb; 28 | } 29 | 30 | private isSixCron(cron: Crontab) { 31 | const { schedule } = cron; 32 | if (schedule.split(' ').length === 6) { 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | public async create(payload: Crontab): Promise { 39 | const tab = new Crontab(payload); 40 | tab.created = new Date().valueOf(); 41 | tab.saved = false; 42 | const doc = await this.insert(tab); 43 | await this.set_crontab(this.isSixCron(doc)); 44 | return doc; 45 | } 46 | 47 | public async insert(payload: Crontab): Promise { 48 | return new Promise((resolve) => { 49 | this.cronDb.insert(payload, (err, docs) => { 50 | if (err) { 51 | this.logger.error(err); 52 | } else { 53 | resolve(docs); 54 | } 55 | }); 56 | }); 57 | } 58 | 59 | public async update(payload: Crontab): Promise { 60 | const { _id, ...other } = payload; 61 | const doc = await this.get(_id); 62 | const tab = new Crontab({ ...doc, ...other }); 63 | tab.saved = false; 64 | const newDoc = await this.updateDb(tab); 65 | await this.set_crontab(this.isSixCron(newDoc)); 66 | return newDoc; 67 | } 68 | 69 | public async updateDb(payload: Crontab): Promise { 70 | return new Promise((resolve) => { 71 | this.cronDb.update( 72 | { _id: payload._id }, 73 | payload, 74 | { returnUpdatedDocs: true }, 75 | (err, num, docs: any) => { 76 | if (err) { 77 | this.logger.error(err); 78 | } else { 79 | resolve(docs); 80 | } 81 | }, 82 | ); 83 | }); 84 | } 85 | 86 | public async status({ 87 | ids, 88 | status, 89 | }: { 90 | ids: string[]; 91 | status: CrontabStatus; 92 | }) { 93 | this.cronDb.update({ _id: { $in: ids } }, { $set: { status } }); 94 | } 95 | 96 | public async remove(ids: string[]) { 97 | return new Promise((resolve: any) => { 98 | this.cronDb.remove( 99 | { _id: { $in: ids } }, 100 | { multi: true }, 101 | async (err) => { 102 | await this.set_crontab(true); 103 | resolve(); 104 | }, 105 | ); 106 | }); 107 | } 108 | 109 | public async crontabs(searchText?: string): Promise { 110 | let query = {}; 111 | if (searchText) { 112 | const reg = new RegExp(searchText, 'i'); 113 | query = { 114 | $or: [ 115 | { 116 | name: reg, 117 | }, 118 | { 119 | command: reg, 120 | }, 121 | { 122 | schedule: reg, 123 | }, 124 | ], 125 | }; 126 | } 127 | return new Promise((resolve) => { 128 | this.cronDb 129 | .find(query) 130 | .sort({ created: -1 }) 131 | .exec((err, docs) => { 132 | resolve(docs); 133 | }); 134 | }); 135 | } 136 | 137 | public async get(_id: string): Promise { 138 | return new Promise((resolve) => { 139 | this.cronDb.find({ _id }).exec((err, docs) => { 140 | resolve(docs[0]); 141 | }); 142 | }); 143 | } 144 | 145 | public async run(ids: string[]) { 146 | this.cronDb.update( 147 | { _id: { $in: ids } }, 148 | { $set: { status: CrontabStatus.queued } }, 149 | { multi: true }, 150 | ); 151 | for (let i = 0; i < ids.length; i++) { 152 | const id = ids[i]; 153 | this.queue.add(() => this.runSingle(id)); 154 | } 155 | } 156 | 157 | public async stop(ids: string[]) { 158 | return new Promise((resolve: any) => { 159 | this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => { 160 | this.cronDb.update( 161 | { _id: { $in: ids } }, 162 | { $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, 163 | ); 164 | const pids = docs 165 | .map((x) => x.pid) 166 | .filter((x) => !!x) 167 | .join('\n'); 168 | exec(`echo - e "${pids}" | xargs kill - 9`, (err) => { 169 | resolve(); 170 | }); 171 | }); 172 | }); 173 | } 174 | 175 | private async runSingle(id: string): Promise { 176 | return new Promise(async (resolve: any) => { 177 | const cron = await this.get(id); 178 | if (cron.status !== CrontabStatus.queued) { 179 | resolve(); 180 | return; 181 | } 182 | 183 | let { _id, command } = cron; 184 | 185 | this.logger.silly('Running job'); 186 | this.logger.silly('ID: ' + _id); 187 | this.logger.silly('Original command: ' + command); 188 | 189 | let logFile = `${config.manualLogPath}${_id}.log`; 190 | fs.writeFileSync( 191 | logFile, 192 | `开始执行... ${new Date().toLocaleString()}\n\n`, 193 | ); 194 | 195 | let cmdStr = command; 196 | if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { 197 | cmdStr = `task ${cmdStr}`; 198 | } 199 | if (cmdStr.endsWith('.js')) { 200 | cmdStr = `${cmdStr} now`; 201 | } 202 | const cmd = spawn(cmdStr, { shell: true }); 203 | 204 | this.cronDb.update( 205 | { _id }, 206 | { $set: { status: CrontabStatus.running, pid: cmd.pid } }, 207 | ); 208 | 209 | cmd.stdout.on('data', (data) => { 210 | this.logger.silly(`stdout: ${data}`); 211 | fs.appendFileSync(logFile, data); 212 | }); 213 | 214 | cmd.stderr.on('data', (data) => { 215 | this.logger.silly(`stderr: ${data}`); 216 | fs.appendFileSync(logFile, data); 217 | }); 218 | 219 | cmd.on('close', (code) => { 220 | this.logger.silly(`child process exited with code ${code}`); 221 | this.cronDb.update( 222 | { _id }, 223 | { $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, 224 | ); 225 | }); 226 | 227 | cmd.on('error', (err) => { 228 | this.logger.info(err); 229 | fs.appendFileSync(logFile, err.stack); 230 | }); 231 | 232 | cmd.on('exit', (code: number, signal: any) => { 233 | this.logger.silly(`cmd exit ${code}`); 234 | this.cronDb.update( 235 | { _id }, 236 | { $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, 237 | ); 238 | fs.appendFileSync(logFile, `\n执行结束...`); 239 | resolve(); 240 | }); 241 | 242 | process.on('SIGINT', function () { 243 | fs.appendFileSync(logFile, `\n执行结束...`); 244 | resolve(); 245 | }); 246 | }); 247 | } 248 | 249 | public async disabled(ids: string[]) { 250 | return new Promise((resolve: any) => { 251 | this.cronDb.update( 252 | { _id: { $in: ids } }, 253 | { $set: { isDisabled: 1 } }, 254 | { multi: true }, 255 | async (err) => { 256 | await this.set_crontab(true); 257 | resolve(); 258 | }, 259 | ); 260 | }); 261 | } 262 | 263 | public async enabled(ids: string[]) { 264 | return new Promise((resolve: any) => { 265 | this.cronDb.update( 266 | { _id: { $in: ids } }, 267 | { $set: { isDisabled: 0 } }, 268 | { multi: true }, 269 | async (err) => { 270 | await this.set_crontab(true); 271 | resolve(); 272 | }, 273 | ); 274 | }); 275 | } 276 | 277 | public async log(_id: string) { 278 | let logFile = `${config.manualLogPath}${_id}.log`; 279 | return getFileContentByName(logFile); 280 | } 281 | 282 | private make_command(tab: Crontab) { 283 | const crontab_job_string = `ID=${tab._id} ${tab.command}`; 284 | return crontab_job_string; 285 | } 286 | 287 | private async set_crontab(needReloadSchedule: boolean = false) { 288 | const tabs = await this.crontabs(); 289 | var crontab_string = ''; 290 | tabs.forEach((tab) => { 291 | const _schedule = tab.schedule && tab.schedule.split(' '); 292 | if (tab.isDisabled === 1 || _schedule.length !== 5) { 293 | crontab_string += '# '; 294 | crontab_string += tab.schedule; 295 | crontab_string += ' '; 296 | crontab_string += this.make_command(tab); 297 | crontab_string += '\n'; 298 | } else { 299 | crontab_string += tab.schedule; 300 | crontab_string += ' '; 301 | crontab_string += this.make_command(tab); 302 | crontab_string += '\n'; 303 | } 304 | }); 305 | 306 | this.logger.silly(crontab_string); 307 | fs.writeFileSync(config.crontabFile, crontab_string); 308 | 309 | execSync(`crontab ${config.crontabFile}`); 310 | if (needReloadSchedule) { 311 | exec(`pm2 reload schedule`); 312 | } 313 | this.cronDb.update({}, { $set: { saved: true } }, { multi: true }); 314 | } 315 | 316 | private reload_db() { 317 | this.cronDb.loadDatabase(); 318 | } 319 | 320 | public import_crontab() { 321 | exec('crontab -l', (error, stdout, stderr) => { 322 | var lines = stdout.split('\n'); 323 | var namePrefix = new Date().getTime(); 324 | 325 | lines.reverse().forEach((line, index) => { 326 | line = line.replace(/\t+/g, ' '); 327 | var regex = 328 | /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/; 329 | var command = line.replace(regex, '').trim(); 330 | var schedule = line.replace(command, '').trim(); 331 | 332 | if ( 333 | command && 334 | schedule && 335 | cron_parser.parseExpression(schedule).hasNext() 336 | ) { 337 | var name = namePrefix + '_' + index; 338 | 339 | this.cronDb.findOne({ command, schedule }, (err, doc) => { 340 | if (err) { 341 | throw err; 342 | } 343 | if (!doc) { 344 | this.create({ name, command, schedule }); 345 | } else { 346 | doc.command = command; 347 | doc.schedule = schedule; 348 | this.update(doc); 349 | } 350 | }); 351 | } 352 | }); 353 | }); 354 | } 355 | 356 | public autosave_crontab() { 357 | return this.set_crontab(); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | LABEL maintainer="limoe" 3 | ARG QL_URL=https://github.com/Zy143L/qinglong.git 4 | ARG QL_BRANCH=master 5 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ 6 | LANG=zh_CN.UTF-8 \ 7 | SHELL=/bin/bash \ 8 | PS1="\u@\h:\w \$ " \ 9 | QL_DIR=/ql 10 | WORKDIR ${QL_DIR} 11 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ 12 | && apk update -f \ 13 | && apk upgrade \ 14 | && apk --no-cache add -f bash \ 15 | coreutils \ 16 | moreutils \ 17 | git \ 18 | curl \ 19 | wget \ 20 | tzdata \ 21 | perl \ 22 | openssl \ 23 | nginx \ 24 | python3 \ 25 | jq \ 26 | openssh \ 27 | && rm -rf /var/cache/apk/* \ 28 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 29 | && echo "Asia/Shanghai" > /etc/timezone \ 30 | && touch ~/.bashrc \ 31 | && mkdir /run/nginx \ 32 | && git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ 33 | && cd ${QL_DIR} \ 34 | && cp -f .env.example .env \ 35 | && chmod 777 ${QL_DIR}/shell/*.sh \ 36 | && chmod 777 ${QL_DIR}/docker/*.sh \ 37 | && npm install -g pm2 \ 38 | && npm install -g pnpm \ 39 | && rm -rf /root/.npm \ 40 | && pnpm install --prod \ 41 | && rm -rf /root/.pnpm-store 42 | ENTRYPOINT ["./docker/docker-entrypoint.sh"] 43 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | link_shell 7 | echo -e "======================1. 检测配置文件========================\n" 8 | fix_config 9 | cp -fv $dir_root/docker/front.conf /etc/nginx/conf.d/front.conf 10 | echo 11 | 12 | echo -e "======================2. 更新源代码========================\n" 13 | ql update "no-restart" 14 | pm2 l >/dev/null 2>&1 15 | echo 16 | 17 | echo -e "======================3. 启动nginx========================\n" 18 | nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf 19 | echo -e "nginx启动成功...\n" 20 | 21 | echo -e "======================4. 启动控制面板========================\n" 22 | if [[ $(pm2 info panel 2>/dev/null) ]]; then 23 | pm2 reload panel 24 | else 25 | pm2 start $dir_root/build/app.js -n panel 26 | fi 27 | echo -e "控制面板启动成功...\n" 28 | 29 | echo -e "======================5. 启动定时任务========================\n" 30 | if [[ $(pm2 info schedule 2>/dev/null) ]]; then 31 | pm2 reload schedule 32 | else 33 | pm2 start $dir_root/build/schedule.js -n schedule 34 | fi 35 | echo -e "定时任务启动成功...\n" 36 | 37 | if [[ $AutoStartBot == true ]]; then 38 | echo -e "======================6. 启动bot========================\n" 39 | ql bot 40 | fi 41 | 42 | echo -e "############################################################\n" 43 | echo -e "容器启动成功..." 44 | echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..." 45 | echo -e "############################################################\n" 46 | 47 | crond -f >/dev/null 48 | 49 | exec "$@" 50 | -------------------------------------------------------------------------------- /docker/front.conf: -------------------------------------------------------------------------------- 1 | upstream api { 2 | server localhost:5600; 3 | } 4 | 5 | server { 6 | listen 5700; 7 | root /ql/dist; 8 | ssl_session_timeout 5m; 9 | 10 | location /api { 11 | proxy_pass http://api; 12 | } 13 | 14 | gzip on; 15 | gzip_static on; 16 | gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript; 17 | gzip_proxied any; 18 | gzip_vary on; 19 | gzip_comp_level 6; 20 | gzip_buffers 16 8k; 21 | gzip_http_version 1.0; 22 | 23 | location / { 24 | index index.html index.htm; 25 | try_files $uri $uri/ /index.html; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["back", ".env"], 3 | "ext": "js,ts,json", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node --transpile-only ./back/app.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "umi dev", 5 | "build": "umi build", 6 | "build-back": "tsc -p tsconfig.back.json", 7 | "start-back": "nodemon", 8 | "panel": "npm run build-back && node build/app.js", 9 | "schedule": "npm run build-back && node build/schedule.js", 10 | "prepare": "umi generate tmp", 11 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 12 | "test": "umi-test", 13 | "test:coverage": "umi-test --coverage" 14 | }, 15 | "gitHooks": { 16 | "pre-commit": "lint-staged" 17 | }, 18 | "lint-staged": { 19 | "*.{js,jsx,less,md,json}": [ 20 | "prettier --write" 21 | ], 22 | "*.ts?(x)": [ 23 | "prettier --parser=typescript --write" 24 | ] 25 | }, 26 | "dependencies": { 27 | "body-parser": "^1.19.0", 28 | "celebrate": "^13.0.3", 29 | "cors": "^2.8.5", 30 | "cron-parser": "^3.5.0", 31 | "dotenv": "^8.2.0", 32 | "express": "^4.17.1", 33 | "express-jwt": "^6.0.0", 34 | "got": "^11.8.2", 35 | "jsonwebtoken": "^8.5.1", 36 | "nedb": "^1.8.0", 37 | "node-fetch": "^2.6.1", 38 | "node-schedule": "^2.0.0", 39 | "p-queue": "6.6.2", 40 | "reflect-metadata": "^0.1.13", 41 | "typedi": "^0.8.0", 42 | "winston": "^3.3.3" 43 | }, 44 | "devDependencies": { 45 | "@ant-design/pro-layout": "^6.5.0", 46 | "@ant-design/icons": "^4.6.2", 47 | "@types/cors": "^2.8.10", 48 | "@types/express": "^4.17.8", 49 | "@types/express-jwt": "^6.0.1", 50 | "@types/jsonwebtoken": "^8.5.0", 51 | "@types/nedb": "^1.8.11", 52 | "@types/node": "^14.11.2", 53 | "@types/node-fetch": "^2.5.8", 54 | "@types/qrcode.react": "^1.0.1", 55 | "@types/react": "^17.0.0", 56 | "@types/react-dom": "^17.0.0", 57 | "@umijs/plugin-antd": "^0.9.1", 58 | "@umijs/test": "^3.3.9", 59 | "codemirror": "^5.59.4", 60 | "compression-webpack-plugin": "6.1.1", 61 | "darkreader": "^4.9.27", 62 | "lint-staged": "^10.0.7", 63 | "nodemon": "^2.0.4", 64 | "prettier": "^2.2.0", 65 | "qrcode.react": "^1.0.1", 66 | "react": "17.x", 67 | "react-codemirror2": "^7.2.1", 68 | "react-diff-viewer": "^3.1.1", 69 | "react-dnd": "^14.0.2", 70 | "react-dnd-html5-backend": "^14.0.0", 71 | "react-dom": "17.x", 72 | "ts-node": "^9.0.0", 73 | "typescript": "^4.1.2", 74 | "umi": "^3.3.9", 75 | "umi-request": "^1.3.5", 76 | "vh-check": "^2.0.5", 77 | "webpack": "^5.28.0", 78 | "yorkie": "^2.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sample/auth.sample.json: -------------------------------------------------------------------------------- 1 | { "username": "admin", "password": "adminadmin" } 2 | -------------------------------------------------------------------------------- /sample/config.sample.sh: -------------------------------------------------------------------------------- 1 | ## Version: v2.2.0-066 2 | ## Date: 2021-06-17 3 | ## Update Content: \n1. 修复版本号样式\n2. 修复查看日志或者编辑任务后列表异常 4 | 5 | ## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新 6 | 7 | ## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务 8 | AutoDelCron="true" 9 | 10 | ## 在运行 ql repo 命令时,是否自动增加新的本地定时任务 11 | AutoAddCron="true" 12 | 13 | ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 14 | RepoFileExtensions="js py" 15 | 16 | ## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除 17 | GithubProxyUrl="https://ghproxy.com/" 18 | 19 | ## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 20 | CommandTimeoutTime="1h" 21 | 22 | ## 设置批量执行任务时的并发数,默认同时执行5个任务 23 | MaxConcurrentNum="5" 24 | 25 | ## 在运行 task 命令时,随机延迟启动任务的最大延迟时间 26 | ## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空 27 | RandomDelay="300" 28 | 29 | ## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true 30 | EnableExtraShell="true" 31 | 32 | ## 自动按顺序进行账号间互助(选填) 设置为 true 时,将直接导入code最新日志来进行互助 33 | AutoHelpOther="" 34 | 35 | ## 定义 jcode 脚本导出的互助码模板样式(选填) 36 | ## 不填 使用“按编号顺序助力模板”,Cookie编号在前的优先助力 37 | ## 填 0 使用“全部一致助力模板”,所有账户要助力的码全部一致 38 | ## 填 1 使用“均等机会助力模板”,所有账户获得助力次数一致 39 | ## 填 2 使用“随机顺序助力模板”,本套脚本内账号间随机顺序助力,每次生成的顺序都不一致。 40 | HelpType="" 41 | 42 | ## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot 43 | AutoStartBot="" 44 | 45 | ## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空 46 | PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple" 47 | 48 | ## 通知环境变量 49 | ## 1. Server酱 50 | ## https://sct.ftqq.com 51 | ## 下方填写 SCHKEY 值或 SendKey 值 52 | export PUSH_KEY="" 53 | 54 | ## 2. BARK 55 | ## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123 56 | export BARK_PUSH="" 57 | ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 58 | export BARK_SOUND="" 59 | 60 | ## 3. Telegram 61 | ## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw 62 | export TG_BOT_TOKEN="" 63 | ## 下方填写 @getuseridbot 中获取到的纯数字ID 64 | export TG_USER_ID="" 65 | ## Telegram 代理IP(选填) 66 | ## 下方填写代理IP地址,代理类型为 http,比如您代理是 http://127.0.0.1:1080,则填写 "127.0.0.1" 67 | ## 如需使用,请自行解除下一行的注释 68 | export TG_PROXY_HOST="" 69 | ## Telegram 代理端口(选填) 70 | ## 下方填写代理端口号,代理类型为 http,比如您代理是 http://127.0.0.1:1080,则填写 "1080" 71 | ## 如需使用,请自行解除下一行的注释 72 | export TG_PROXY_PORT="" 73 | ## Telegram 代理的认证参数(选填) 74 | export TG_PROXY_AUTH="" 75 | ## Telegram api自建反向代理地址(选填) 76 | ## 教程:https://www.hostloc.com/thread-805441-1-1.html 77 | ## 如反向代理地址 http://aaa.bbb.ccc 则填写 aaa.bbb.ccc 78 | ## 如需使用,请赋值代理地址链接,并自行解除下一行的注释 79 | export TG_API_HOST="" 80 | 81 | ## 4. 钉钉 82 | ## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access 83 | ## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可 84 | export DD_BOT_TOKEN="" 85 | export DD_BOT_SECRET="" 86 | 87 | ## 5. 企业微信机器人 88 | ## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770 89 | ## 下方填写密钥,企业微信推送 webhook 后面的 key 90 | export QYWX_KEY="" 91 | 92 | ## 6. 企业微信应用 93 | ## 参考文档:http://note.youdao.com/s/HMiudGkb 94 | ## 下方填写素材库图片id(corpid,corpsecret,touser,agentid),素材库图片填0为图文消息, 填1为纯文本消息 95 | export QYWX_AM="" 96 | 97 | ## 7. iGot聚合 98 | ## 参考文档:https://wahao.github.io/Bark-MP-helper 99 | ## 下方填写iGot的推送key,支持多方式推送,确保消息可达 100 | export IGOT_PUSH_KEY="" 101 | 102 | ## 8. Push Plus 103 | ## 官方网站:http://www.pushplus.plus 104 | ## 下方填写您的Token,微信扫码登录后一对一推送或一对多推送下面的token,只填 PUSH_PLUS_TOKEN 默认为一对一推送 105 | export PUSH_PLUS_TOKEN="" 106 | ## 一对一多推送(选填) 107 | ## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码) 108 | ## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送 109 | export PUSH_PLUS_USER="" 110 | 111 | ## 8. go-cqhttp 112 | ## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg 113 | ## gobot_token 填写在go-cqhttp文件设置的访问密钥 114 | ## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群 115 | ## go-cqhttp相关API https://docs.go-cqhttp.org/api 116 | export GOBOT_URL="" 117 | export GOBOT_TOKEN="" 118 | export GOBOT_QQ="" 119 | 120 | ## 如果只是想要屏蔽某个ck不执行某个脚本,可以参考下面 case 这个命令的例子来控制,脚本名称包含后缀 121 | ## case $1 in 122 | ## test.js) 123 | ## TempBlockCookie="5" 124 | ## ;; 125 | ## esac 126 | 127 | ## 需组合的环境变量列表,env_name需要和var_name一一对应,如何有新活动按照格式添加(不懂勿动) 128 | env_name=( 129 | JD_COOKIE 130 | FRUITSHARECODES 131 | PETSHARECODES 132 | PLANT_BEAN_SHARECODES 133 | DREAM_FACTORY_SHARE_CODES 134 | DDFACTORY_SHARECODES 135 | JDZZ_SHARECODES 136 | JDJOY_SHARECODES 137 | JXNC_SHARECODES 138 | BOOKSHOP_SHARECODES 139 | JD_CASH_SHARECODES 140 | JDSGMH_SHARECODES 141 | JDCFD_SHARECODES 142 | JDHEALTH_SHARECODES 143 | ) 144 | var_name=( 145 | Cookie 146 | ForOtherFruit 147 | ForOtherPet 148 | ForOtherBean 149 | ForOtherDreamFactory 150 | ForOtherJdFactory 151 | ForOtherJdzz 152 | ForOtherJoy 153 | ForOtherJxnc 154 | ForOtherBookShop 155 | ForOtherCash 156 | ForOtherSgmh 157 | ForOtherCfd 158 | ForOtherHealth 159 | ) 160 | 161 | ## name_js为脚本文件名,如果使用ql repo命令拉取,文件名含有作者名 162 | ## 所有有互助码的活动,把脚本名称列在 name_js 中,对应 config.sh 中互助码后缀列在 name_config 中,中文名称列在 name_chinese 中。 163 | ## name_js、name_config 和 name_chinese 中的三个名称必须一一对应。 164 | name_js=( 165 | jd_fruit 166 | jd_pet 167 | jd_plantBean 168 | jd_dreamFactory 169 | jd_jdfactory 170 | jd_jdzz 171 | jd_crazy_joy 172 | jd_jxnc 173 | jd_bookshop 174 | jd_cash 175 | jd_sgmh 176 | jd_cfd 177 | jd_health 178 | ) 179 | name_config=( 180 | Fruit 181 | Pet 182 | Bean 183 | DreamFactory 184 | JdFactory 185 | Jdzz 186 | Joy 187 | Jxnc 188 | BookShop 189 | Cash 190 | Sgmh 191 | Cfd 192 | Health 193 | ) 194 | name_chinese=( 195 | 东东农场 196 | 东东萌宠 197 | 京东种豆得豆 198 | 京喜工厂 199 | 东东工厂 200 | 京东赚赚 201 | crazyJoy任务 202 | 京喜农场 203 | 口袋书店 204 | 签到领现金 205 | 闪购盲盒 206 | 京喜财富岛 207 | 东东健康社区 208 | ) 209 | 210 | ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 211 | -------------------------------------------------------------------------------- /sample/extra.sample.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 添加你需要重启自动执行的任意命令,比如 ql repo 4 | -------------------------------------------------------------------------------- /sample/notify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # _*_ coding:utf-8 _*_ 3 | 4 | import sys 5 | import os 6 | cur_path = os.path.abspath(os.path.dirname(__file__)) 7 | root_path = os.path.split(cur_path)[0] 8 | sys.path.append(root_path) 9 | import requests 10 | import json 11 | import traceback 12 | import time 13 | import hmac 14 | import hashlib 15 | import base64 16 | import urllib.parse 17 | from requests.adapters import HTTPAdapter 18 | from urllib3.util import Retry 19 | import re 20 | 21 | # 通知服务 22 | BARK = '' # bark服务,此参数如果以http或者https开头则判定为自建bark服务; secrets可填; 23 | SCKEY = '' # Server酱的SCKEY; secrets可填 24 | TG_BOT_TOKEN = '' # tg机器人的TG_BOT_TOKEN; secrets可填 25 | TG_USER_ID = '' # tg机器人的TG_USER_ID; secrets可填 26 | TG_PROXY_IP = '' # tg机器人的TG_PROXY_IP; secrets可填 27 | TG_PROXY_PORT = '' # tg机器人的TG_PROXY_PORT; secrets可填 28 | DD_BOT_ACCESS_TOKEN = '' # 钉钉机器人的DD_BOT_ACCESS_TOKEN; secrets可填 29 | DD_BOT_SECRET = '' # 钉钉机器人的DD_BOT_SECRET; secrets可填 30 | QYWX_APP = '' # 企业微信应用的QYWX_APP; secrets可填 参考http://note.youdao.com/s/HMiudGkb 31 | 32 | notify_mode = [] 33 | 34 | # GitHub action运行需要填写对应的secrets 35 | if "BARK" in os.environ and os.environ["BARK"]: 36 | BARK = os.environ["BARK"] 37 | if "SCKEY" in os.environ and os.environ["SCKEY"]: 38 | SCKEY = os.environ["SCKEY"] 39 | if "TG_BOT_TOKEN" in os.environ and os.environ["TG_BOT_TOKEN"] and "TG_USER_ID" in os.environ and os.environ["TG_USER_ID"]: 40 | TG_BOT_TOKEN = os.environ["TG_BOT_TOKEN"] 41 | TG_USER_ID = os.environ["TG_USER_ID"] 42 | if "DD_BOT_ACCESS_TOKEN" in os.environ and os.environ["DD_BOT_ACCESS_TOKEN"] and "DD_BOT_SECRET" in os.environ and os.environ["DD_BOT_SECRET"]: 43 | DD_BOT_ACCESS_TOKEN = os.environ["DD_BOT_ACCESS_TOKEN"] 44 | DD_BOT_SECRET = os.environ["DD_BOT_SECRET"] 45 | if "QYWX_APP" in os.environ and os.environ["QYWX_APP"]: 46 | QYWX_APP = os.environ["QYWX_APP"] 47 | 48 | if BARK: 49 | notify_mode.append('bark') 50 | print("BARK 推送打开") 51 | if SCKEY: 52 | notify_mode.append('sc_key') 53 | print("Server酱 推送打开") 54 | if TG_BOT_TOKEN and TG_USER_ID: 55 | notify_mode.append('telegram_bot') 56 | print("Telegram 推送打开") 57 | if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET: 58 | notify_mode.append('dingding_bot') 59 | print("钉钉机器人 推送打开") 60 | if QYWX_APP: 61 | notify_mode.append('qywxapp_bot') 62 | print("企业微信应用 推送打开") 63 | 64 | def bark(title, content): 65 | print("\n") 66 | if not BARK: 67 | print("bark服务的bark_token未设置!!\n取消推送") 68 | return 69 | print("bark服务启动") 70 | url = None 71 | if BARK.startswith('http'): 72 | url = f"""{BARK}/{title}/{content}""" 73 | else: 74 | url = f"""https://api.day.app/{BARK}/{title}/{content}""" 75 | response = requests.get(url).json() 76 | if response['code'] == 200: 77 | print('推送成功!') 78 | else: 79 | print('推送失败!') 80 | 81 | def serverJ(title, content): 82 | print("\n") 83 | if not SCKEY: 84 | print("server酱服务的SCKEY未设置!!\n取消推送") 85 | return 86 | print("serverJ服务启动") 87 | data = { 88 | "text": title, 89 | "desp": content.replace("\n", "\n\n") 90 | } 91 | response = requests.post(f"https://sc.ftqq.com/{SCKEY}.send", data=data).json() 92 | if response['errno'] == 0: 93 | print('推送成功!') 94 | else: 95 | print('推送失败!') 96 | 97 | def telegram_bot(title, content): 98 | print("\n") 99 | bot_token = TG_BOT_TOKEN 100 | user_id = TG_USER_ID 101 | if not bot_token or not user_id: 102 | print("tg服务的bot_token或者user_id未设置!!\n取消推送") 103 | return 104 | print("tg服务启动") 105 | url=f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage" 106 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 107 | payload = {'chat_id': str(TG_USER_ID), 'text': f'{title}\n\n{content}', 'disable_web_page_preview': 'true'} 108 | proxies = None 109 | if TG_PROXY_IP and TG_PROXY_PORT: 110 | proxyStr = "http://{}:{}".format(TG_PROXY_IP, TG_PROXY_PORT) 111 | proxies = {"http": proxyStr, "https": proxyStr} 112 | response = requests.post(url=url, headers=headers, params=payload, proxies=proxies).json() 113 | if response['ok']: 114 | print('推送成功!') 115 | else: 116 | print('推送失败!') 117 | 118 | def dingding_bot(title, content): 119 | timestamp = str(round(time.time() * 1000)) # 时间戳 120 | secret_enc = DD_BOT_SECRET.encode('utf-8') 121 | string_to_sign = '{}\n{}'.format(timestamp, DD_BOT_SECRET) 122 | string_to_sign_enc = string_to_sign.encode('utf-8') 123 | hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() 124 | sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) # 签名 125 | print('开始使用 钉钉机器人 推送消息...', end='') 126 | url = f'https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_ACCESS_TOKEN}×tamp={timestamp}&sign={sign}' 127 | headers = {'Content-Type': 'application/json;charset=utf-8'} 128 | data = { 129 | 'msgtype': 'text', 130 | 'text': {'content': f'{title}\n\n{content}'} 131 | } 132 | response = requests.post(url=url, data=json.dumps(data), headers=headers, timeout=15).json() 133 | if not response['errcode']: 134 | print('推送成功!') 135 | else: 136 | print('推送失败!') 137 | 138 | def qywxapp_bot(title, content): 139 | print("\n") 140 | if not QYWX_APP: 141 | print("企业微信应用的QYWX_APP未设置!!\n取消推送") 142 | return 143 | print("企业微信应用启动") 144 | qywx_app_params = QYWX_APP.split(',') 145 | url='https://qyapi.weixin.qq.com/cgi-bin/gettoken' 146 | headers= { 147 | 'Content-Type': 'application/json', 148 | } 149 | payload = { 150 | 'corpid': qywx_app_params[0], 151 | 'corpsecret': qywx_app_params[1], 152 | } 153 | response = requests.post(url=url, headers=headers, data=json.dumps(payload), timeout=15).json() 154 | accesstoken = response["access_token"] 155 | html = content.replace("\n", "
") 156 | 157 | options = None 158 | if not qywx_app_params[4]: 159 | options = { 160 | 'msgtype': 'text', 161 | 'text': { 162 | content: f'{title}\n\n${content}' 163 | } 164 | } 165 | elif qywx_app_params[4] == '0': 166 | options = { 167 | 'msgtype': 'textcard', 168 | 'textcard': { 169 | title: f'{title}', 170 | description: f'{content}', 171 | btntxt: '更多' 172 | } 173 | } 174 | elif qywx_app_params[4] == '1': 175 | options = { 176 | 'msgtype': 'text', 177 | 'text': { 178 | content: f'{title}\n\n${content}' 179 | } 180 | } 181 | else: 182 | options = { 183 | 'msgtype': 'mpnews', 184 | 'mpnews': { 185 | 'articles': [ 186 | { 187 | 'title': f'{title}', 188 | 'thumb_media_id': f'{qywx_app_params[4]}', 189 | 'author': '智能助手', 190 | 'content_source_url': '', 191 | 'content': f'{html}', 192 | 'digest': f'{content}' 193 | } 194 | ] 195 | } 196 | } 197 | 198 | url=f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={accesstoken}" 199 | data = { 200 | 'touser': f'{change_user_id(content)}', 201 | 'agentid': f'{qywx_app_params[3]}', 202 | 'safe': '0' 203 | } 204 | data.update(options) 205 | headers = { 206 | 'Content-Type': 'application/json', 207 | } 208 | response = requests.post(url=url, headers=headers, data=json.dumps(data)).json() 209 | 210 | if response['errcode'] == 0: 211 | print('推送成功!') 212 | else: 213 | print('推送失败!') 214 | 215 | def change_user_id(desp): 216 | qywx_app_params = QYWX_APP.split(',') 217 | if qywx_app_params[2]: 218 | userIdTmp = qywx_app_params[2].split("|") 219 | userId = "" 220 | for i in range(len(userIdTmp)): 221 | count1 = f"账号{i + 1}" 222 | count2 = f"签到号{i + 1}" 223 | if re.search(count1, desp) or re.search(count2, desp): 224 | userId = userIdTmp[i] 225 | if not userId: 226 | userId = qywx_app_params[2] 227 | return userId 228 | else: 229 | return "@all" 230 | 231 | def send(title, content): 232 | """ 233 | 使用 bark, telegram bot, dingding bot, serverJ 发送手机推送 234 | :param title: 235 | :param content: 236 | :return: 237 | """ 238 | for i in notify_mode: 239 | if i == 'bark': 240 | if BARK: 241 | bark(title=title, content=content) 242 | else: 243 | print('未启用 bark') 244 | continue 245 | if i == 'sc_key': 246 | if SCKEY: 247 | serverJ(title=title, content=content) 248 | else: 249 | print('未启用 Server酱') 250 | continue 251 | elif i == 'dingding_bot': 252 | if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET: 253 | dingding_bot(title=title, content=content) 254 | else: 255 | print('未启用 钉钉机器人') 256 | continue 257 | elif i == 'telegram_bot': 258 | if TG_BOT_TOKEN and TG_USER_ID: 259 | telegram_bot(title=title, content=content) 260 | else: 261 | print('未启用 telegram机器人') 262 | continue 263 | elif i == 'qywxapp_bot': 264 | if QYWX_APP: 265 | qywxapp_bot(title=title, content=content) 266 | else: 267 | print('未启用 企业微信应用推送') 268 | continue 269 | else: 270 | print('此类推送方式不存在') 271 | 272 | def main(): 273 | send('title', 'content') 274 | 275 | 276 | if __name__ == '__main__': 277 | main() -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependence", 3 | "author": "", 4 | "license": "ISC", 5 | "dependencies": { 6 | "crypto-js": "^4.0.0", 7 | "download": "^8.0.0", 8 | "got": "^11.5.1", 9 | "http-server": "^0.12.3", 10 | "qrcode-terminal": "^0.12.0", 11 | "request": "^2.88.2", 12 | "tough-cookie": "^4.0.0", 13 | "tunnel": "0.0.6", 14 | "ws": "^7.4.3" 15 | } 16 | } -------------------------------------------------------------------------------- /sample/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /shell/api.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | get_token() { 4 | token=$(cat $file_auth_user | jq -r .token) 5 | } 6 | 7 | add_cron_api() { 8 | local currentTimeStamp=$(date +%s) 9 | if [ $# -eq 1 ]; then 10 | local schedule=$(echo "$1" | awk -F ":" '{print $1}') 11 | local command=$(echo "$1" | awk -F ":" '{print $2}') 12 | local name=$(echo "$1" | awk -F ":" '{print $3}') 13 | else 14 | local schedule=$1 15 | local command=$2 16 | local name=$3 17 | fi 18 | 19 | local api=$( 20 | curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ 21 | -H "Accept: application/json" \ 22 | -H "Authorization: Bearer $token" \ 23 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ 24 | -H "Content-Type: application/json;charset=UTF-8" \ 25 | -H "Origin: http://localhost:5700" \ 26 | -H "Referer: http://localhost:5700/crontab" \ 27 | -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ 28 | --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ 29 | --compressed 30 | ) 31 | code=$(echo $api | jq -r .code) 32 | message=$(echo $api | jq -r .message) 33 | if [[ $code == 200 ]]; then 34 | echo -e "$name -> 添加成功" 35 | else 36 | echo -e "$name -> 添加失败(${message})" 37 | fi 38 | } 39 | 40 | update_cron_api() { 41 | local currentTimeStamp=$(date +%s) 42 | if [ $# -eq 1 ]; then 43 | local schedule=$(echo "$1" | awk -F ":" '{print $1}') 44 | local command=$(echo "$1" | awk -F ":" '{print $2}') 45 | local name=$(echo "$1" | awk -F ":" '{print $3}') 46 | local id=$(echo "$1" | awk -F ":" '{print $4}') 47 | else 48 | local schedule=$1 49 | local command=$2 50 | local name=$3 51 | local id=$4 52 | fi 53 | 54 | local api=$( 55 | curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ 56 | -X 'PUT' \ 57 | -H "Accept: application/json" \ 58 | -H "Authorization: Bearer $token" \ 59 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ 60 | -H "Content-Type: application/json;charset=UTF-8" \ 61 | -H "Origin: http://localhost:5700" \ 62 | -H "Referer: http://localhost:5700/crontab" \ 63 | -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ 64 | --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \ 65 | --compressed 66 | ) 67 | code=$(echo $api | jq -r .code) 68 | message=$(echo $api | jq -r .message) 69 | if [[ $code == 200 ]]; then 70 | echo -e "$name -> 更新成功" 71 | else 72 | echo -e "$name -> 更新失败(${message})" 73 | fi 74 | } 75 | 76 | update_cron_command_api() { 77 | local currentTimeStamp=$(date +%s) 78 | if [ $# -eq 1 ]; then 79 | local command=$(echo "$1" | awk -F ":" '{print $1}') 80 | local id=$(echo "$1" | awk -F ":" '{print $2}') 81 | else 82 | local command=$1 83 | local id=$2 84 | fi 85 | 86 | local api=$( 87 | curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ 88 | -X 'PUT' \ 89 | -H "Accept: application/json" \ 90 | -H "Authorization: Bearer $token" \ 91 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ 92 | -H "Content-Type: application/json;charset=UTF-8" \ 93 | -H "Origin: http://localhost:5700" \ 94 | -H "Referer: http://localhost:5700/crontab" \ 95 | -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ 96 | --data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \ 97 | --compressed 98 | ) 99 | code=$(echo $api | jq -r .code) 100 | message=$(echo $api | jq -r .message) 101 | if [[ $code == 200 ]]; then 102 | echo -e "$command -> 更新成功" 103 | else 104 | echo -e "$command -> 更新失败(${message})" 105 | fi 106 | } 107 | 108 | del_cron_api() { 109 | local ids=$1 110 | local currentTimeStamp=$(date +%s) 111 | local api=$( 112 | curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ 113 | -X 'DELETE' \ 114 | -H "Accept: application/json" \ 115 | -H "Authorization: Bearer $token" \ 116 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ 117 | -H "Content-Type: application/json;charset=UTF-8" \ 118 | -H "Origin: http://localhost:5700" \ 119 | -H "Referer: http://localhost:5700/crontab" \ 120 | -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ 121 | --data-raw "[$ids]" \ 122 | --compressed 123 | ) 124 | code=$(echo $api | jq -r .code) 125 | message=$(echo $api | jq -r .message) 126 | if [[ $code == 200 ]]; then 127 | echo -e "成功" 128 | else 129 | echo -e "失败(${message})" 130 | fi 131 | } 132 | 133 | get_user_info() { 134 | local currentTimeStamp=$(date +%s) 135 | local api=$( 136 | curl -s "http://localhost:5600/api/user?t=$currentTimeStamp" \ 137 | -H 'Accept: */*' \ 138 | -H "Authorization: Bearer $token" \ 139 | -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \ 140 | -H 'Referer: http://localhost:5700/crontab' \ 141 | -H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \ 142 | --compressed 143 | ) 144 | code=$(echo $api | jq -r .code) 145 | if [[ $code != 200 ]]; then 146 | echo -e "请先登录!" 147 | exit 0 148 | fi 149 | } 150 | 151 | update_cron_status() { 152 | local ids=$1 153 | local status=$2 154 | local currentTimeStamp=$(date +%s) 155 | local api=$( 156 | curl -s "http://localhost:5600/api/crons/status?t=$currentTimeStamp" \ 157 | -X 'PUT' \ 158 | -H "Accept: application/json" \ 159 | -H "Authorization: Bearer $token" \ 160 | -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ 161 | -H "Content-Type: application/json;charset=UTF-8" \ 162 | -H "Origin: http://localhost:5700" \ 163 | -H "Referer: http://localhost:5700/crontab" \ 164 | -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ 165 | --data-raw "{\"ids\":[$ids],\"status\":\"$status\"}" \ 166 | --compressed 167 | ) 168 | code=$(echo $api | jq -r .code) 169 | message=$(echo $api | jq -r .message) 170 | if [[ $code == 200 ]]; then 171 | echo -e "更新任务状态成功" 172 | else 173 | echo -e "更新任务状态失败(${message})" 174 | fi 175 | } 176 | 177 | get_token -------------------------------------------------------------------------------- /shell/bot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 导入通用变量与函数 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git" 7 | repo_path="${dir_repo}/dockerbot" 8 | 9 | echo -e "\n1、安装bot依赖...\n" 10 | apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev 11 | echo -e "\nbot依赖安装成功...\n" 12 | 13 | echo -e "2、下载bot所需文件...\n" 14 | if [ -d ${repo_path}/.git ]; then 15 | git_pull_scripts ${repo_path} 16 | else 17 | rm -rf ${repo_path} 18 | git_clone_scripts ${url} ${repo_path} "main" 19 | fi 20 | 21 | cp -rf "$repo_path/jbot" $dir_root 22 | if [[ ! -f "$dir_root/config/bot.json" ]]; then 23 | cp -f "$repo_path/config/bot.json" "$dir_root/config" 24 | fi 25 | echo -e "\nbot文件下载成功...\n" 26 | 27 | echo -e "3、安装python3依赖...\n" 28 | if [[ $PipMirror ]]; then 29 | pip3 config set global.index-url $PipMirror 30 | fi 31 | cp -f "$repo_path/jbot/requirements.txt" "$dir_root" 32 | pip3 --default-timeout=100 install -r requirements.txt --no-cache-dir 33 | echo -e "\npython3依赖安装成功...\n" 34 | 35 | echo -e "4、启动bot程序...\n" 36 | make_dir $dir_log/bot 37 | cd $dir_root 38 | ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null 39 | nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 & 40 | echo -e "bot启动成功...\n" 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /shell/code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 导入通用变量与函数 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | 7 | ## 生成pt_pin清单 8 | gen_pt_pin_array() { 9 | local tmp1 tmp2 i pt_pin_temp 10 | for ((user_num = 1; user_num <= $user_sum; user_num++)); do 11 | tmp1=Cookie$user_num 12 | tmp2=${!tmp1} 13 | i=$(($user_num - 1)) 14 | pt_pin_temp=$(echo $tmp2 | perl -pe "{s|.*pt_pin=([^; ]+)(?=;?).*|\1|; s|%|\\\x|g}") 15 | [[ $pt_pin_temp == *\\x* ]] && pt_pin[i]=$(printf $pt_pin_temp) || pt_pin[i]=$pt_pin_temp 16 | done 17 | } 18 | 19 | ## 导出互助码的通用程序,$1:去掉后缀的脚本名称,$2:config.sh中的后缀,$3:活动中文名称 20 | export_codes_sub() { 21 | local task_name=$1 22 | local config_name=$2 23 | local chinese_name=$3 24 | local config_name_my=My$config_name 25 | local config_name_for_other=ForOther$config_name 26 | local i j k m n pt_pin_in_log code tmp_grep tmp_my_code tmp_for_other user_num random_num_list 27 | if cd $dir_log/$task_name &>/dev/null && [[ $(ls) ]]; then 28 | ## 寻找所有互助码以及对应的pt_pin 29 | i=0 30 | pt_pin_in_log=() 31 | code=() 32 | pt_pin_and_code=$(ls -r *.log | xargs awk -v var="的$chinese_name好友互助码" 'BEGIN{FS="[( )】]+"; OFS="&"} $3~var {print $2,$4}') 33 | for line in $pt_pin_and_code; do 34 | pt_pin_in_log[i]=$(echo $line | awk -F "&" '{print $1}') 35 | code[i]=$(echo $line | awk -F "&" '{print $2}') 36 | let i++ 37 | done 38 | 39 | ## 输出My系列变量 40 | if [[ ${#code[*]} -gt 0 ]]; then 41 | for ((m = 0; m < ${#pt_pin[*]}; m++)); do 42 | tmp_my_code="" 43 | j=$((m + 1)) 44 | for ((n = 0; n < ${#code[*]}; n++)); do 45 | if [[ ${pt_pin[m]} == ${pt_pin_in_log[n]} ]]; then 46 | tmp_my_code=${code[n]} 47 | break 48 | fi 49 | done 50 | echo "$config_name_my$j='$tmp_my_code'" 51 | done 52 | else 53 | echo "## 从日志中未找到任何互助码" 54 | fi 55 | 56 | ## 输出ForOther系列变量 57 | if [[ ${#code[*]} -gt 0 ]]; then 58 | echo 59 | case $HelpType in 60 | 0) ## 全部一致 61 | tmp_for_other="" 62 | for ((m = 0; m < ${#pt_pin[*]}; m++)); do 63 | j=$((m + 1)) 64 | tmp_for_other="$tmp_for_other@\${$config_name_my$j}" 65 | done 66 | echo "${config_name_for_other}1=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|" 67 | for ((m = 1; m < ${#pt_pin[*]}; m++)); do 68 | j=$((m + 1)) 69 | echo "$config_name_for_other$j=\"\${${config_name_for_other}1}\"" 70 | done 71 | ;; 72 | 73 | 1) ## 均等助力 74 | for ((m = 0; m < ${#pt_pin[*]}; m++)); do 75 | tmp_for_other="" 76 | j=$((m + 1)) 77 | for ((n = $m; n < $(($user_sum + $m)); n++)); do 78 | [[ $m -eq $n ]] && continue 79 | if [[ $((n + 1)) -le $user_sum ]]; then 80 | k=$((n + 1)) 81 | else 82 | k=$((n + 1 - $user_sum)) 83 | fi 84 | tmp_for_other="$tmp_for_other@\${$config_name_my$k}" 85 | done 86 | echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|" 87 | done 88 | ;; 89 | 90 | 2) ## 本套脚本内账号间随机顺序助力 91 | for ((m = 0; m < ${#pt_pin[*]}; m++)); do 92 | tmp_for_other="" 93 | random_num_list=$(seq $user_sum | sort -R) 94 | j=$((m + 1)) 95 | for n in $random_num_list; do 96 | [[ $j -eq $n ]] && continue 97 | tmp_for_other="$tmp_for_other@\${$config_name_my$n}" 98 | done 99 | echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|" 100 | done 101 | ;; 102 | 103 | *) ## 按编号优先 104 | for ((m = 0; m < ${#pt_pin[*]}; m++)); do 105 | tmp_for_other="" 106 | j=$((m + 1)) 107 | for ((n = 0; n < ${#pt_pin[*]}; n++)); do 108 | [[ $m -eq $n ]] && continue 109 | k=$((n + 1)) 110 | tmp_for_other="$tmp_for_other@\${$config_name_my$k}" 111 | done 112 | echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|" 113 | done 114 | ;; 115 | esac 116 | fi 117 | else 118 | echo "## 未运行过 $task_name.js 脚本,未产生日志" 119 | fi 120 | } 121 | 122 | ## 汇总输出 123 | export_all_codes() { 124 | gen_pt_pin_array 125 | echo -e "\n# 从日志提取互助码,编号和配置文件中Cookie编号完全对应,如果为空就是所有日志中都没有。\n\n# 即使某个MyXxx变量未赋值,也可以将其变量名填在ForOtherXxx中,jtask脚本会自动过滤空值。\n" 126 | echo -n "# 你选择的互助码模板为:" 127 | case $HelpType in 128 | 0) 129 | echo "所有账号助力码全部一致。" 130 | ;; 131 | 1) 132 | echo "所有账号机会均等助力。" 133 | ;; 134 | 2) 135 | echo "本套脚本内账号间随机顺序助力。" 136 | ;; 137 | *) 138 | echo "按账号编号优先。" 139 | ;; 140 | esac 141 | for ((i = 0; i < ${#name_js[*]}; i++)); do 142 | echo -e "\n## ${name_chinese[i]}:" 143 | export_codes_sub "${name_js[i]}" "${name_config[i]}" "${name_chinese[i]}" 144 | done 145 | } 146 | 147 | ## 执行并写入日志 148 | log_time=$(date "+%Y-%m-%d-%H-%M-%S") 149 | log_path="$dir_code/$log_time.log" 150 | make_dir "$dir_code" 151 | export_all_codes | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee $log_path 152 | -------------------------------------------------------------------------------- /shell/notify.js: -------------------------------------------------------------------------------- 1 | const notify = require('/ql/scripts/sendNotify.js'); 2 | const title = process.argv[2]; 3 | const content = process.argv[3]; 4 | 5 | notify.sendNotify(`${title}`, `${content}`); 6 | -------------------------------------------------------------------------------- /shell/notify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #author:spark thanks to: https://github.com/sparkssssssss/scripts 3 | 4 | . /ql/config/config.sh 5 | title=$(echo $1|sed 's/-/_/g') 6 | msg=$(echo -e $2) 7 | 8 | node /ql/shell/notify.js "$title" "$msg" -------------------------------------------------------------------------------- /shell/reset.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 导入通用变量与函数 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | 7 | echo -e "1. 开始安装青龙依赖\n" 8 | npm_install_2 $dir_root 9 | echo -e "青龙依赖安装完成\n" 10 | 11 | echo -e "2. 开始安装脚本依赖\n" 12 | cp -f $dir_sample/package.json $dir_scripts/package.json 13 | npm_install_2 $dir_scripts 14 | echo -e "脚本依赖安装完成\n" 15 | 16 | echo -e "3. 复制通知文件\n" 17 | echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n" 18 | cp -fv $file_notify_py_sample $file_notify_py 19 | echo 20 | 21 | echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n" 22 | cp -fv $file_notify_js_sample $file_notify_js 23 | 24 | echo -e "通知文件复制完成\n" 25 | 26 | exit 0 27 | -------------------------------------------------------------------------------- /shell/rmlog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 导入通用变量与函数 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | 7 | days=$1 8 | 9 | ## 删除运行js脚本的旧日志 10 | remove_js_log() { 11 | local log_full_path_list=$(find $dir_log/ -name "*.log") 12 | local diff_time 13 | for log in $log_full_path_list; do 14 | local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠 15 | if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then 16 | if [[ $is_macos -eq 1 ]]; then 17 | diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s))) 18 | else 19 | diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) 20 | fi 21 | [[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log 22 | fi 23 | done 24 | } 25 | 26 | ## 删除空文件夹 27 | remove_empty_dir() { 28 | cd $dir_log 29 | for dir in $(ls); do 30 | if [ -d $dir ] && [[ -z $(ls $dir) ]]; then 31 | rm -rf $dir 32 | fi 33 | done 34 | } 35 | 36 | ## 运行 37 | if [[ ${days} ]]; then 38 | echo -e "查找旧日志文件中...\n" 39 | remove_js_log 40 | remove_empty_dir 41 | echo -e "删除旧日志执行完毕\n" 42 | fi 43 | -------------------------------------------------------------------------------- /shell/share.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 目录 4 | dir_root=/ql 5 | dir_shell=$dir_root/shell 6 | dir_sample=$dir_root/sample 7 | dir_config=$dir_root/config 8 | dir_scripts=$dir_root/scripts 9 | dir_repo=$dir_root/repo 10 | dir_raw=$dir_root/raw 11 | dir_log=$dir_root/log 12 | dir_db=$dir_root/db 13 | dir_manual_log=$dir_root/manual_log 14 | dir_list_tmp=$dir_log/.tmp 15 | dir_code=$dir_log/code 16 | dir_update_log=$dir_log/update 17 | ql_static_repo=$dir_repo/static 18 | 19 | ## 文件 20 | file_config_sample=$dir_sample/config.sample.sh 21 | file_cookie=$dir_config/cookie.sh 22 | file_sharecode=$dir_config/sharecode.sh 23 | file_config_user=$dir_config/config.sh 24 | file_auth_sample=$dir_sample/auth.sample.json 25 | file_auth_user=$dir_config/auth.json 26 | file_extra_shell=$dir_config/extra.sh 27 | file_extra_sample=$dir_sample/extra.sample.sh 28 | file_notify_js_sample=$dir_sample/notify.js 29 | file_notify_py_sample=$dir_sample/notify.py 30 | file_notify_py=$dir_scripts/notify.py 31 | file_notify_js=$dir_scripts/sendNotify.js 32 | 33 | ## 清单文件 34 | list_crontab_user=$dir_config/crontab.list 35 | list_crontab_sample=$dir_sample/crontab.sample.list 36 | list_own_scripts=$dir_list_tmp/own_scripts.list 37 | list_own_user=$dir_list_tmp/own_user.list 38 | list_own_add=$dir_list_tmp/own_add.list 39 | list_own_drop=$dir_list_tmp/own_drop.list 40 | 41 | ## 软连接及其原始文件对应关系 42 | link_name=( 43 | task 44 | ql 45 | notify 46 | ) 47 | original_name=( 48 | task.sh 49 | update.sh 50 | notify.sh 51 | ) 52 | 53 | ## 导入配置文件 54 | import_config() { 55 | [ -f $file_config_user ] && . $file_config_user 56 | user_sum=0 57 | for line in $(cat $file_cookie); do 58 | let user_sum+=1 59 | eval Cookie${user_sum}="\"${line}\"" 60 | done 61 | 62 | command_timeout_time=${CommandTimeoutTime:-"1h"} 63 | github_proxy_url=${GithubProxyUrl:-""} 64 | block_cookie=${TempBlockCookie:-""} 65 | file_extensions=${RepoFileExtensions:-"js py"} 66 | } 67 | 68 | ## 创建目录,$1:目录的绝对路径 69 | make_dir() { 70 | local dir=$1 71 | if [[ ! -d $dir ]]; then 72 | mkdir -p $dir 73 | fi 74 | } 75 | 76 | ## 检测termux 77 | detect_termux() { 78 | if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then 79 | is_termux=1 80 | else 81 | is_termux=0 82 | fi 83 | } 84 | 85 | ## 检测macos 86 | detect_macos() { 87 | [[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0 88 | } 89 | 90 | ## 生成随机数,$1:用来求余的数字 91 | gen_random_num() { 92 | local divi=$1 93 | echo $((${RANDOM} % $divi)) 94 | } 95 | 96 | ## 创建软连接的子函数,$1:软连接文件路径,$2:要连接的对象 97 | link_shell_sub() { 98 | local link_path="$1" 99 | local original_path="$2" 100 | if [ ! -L $link_path ] || [[ $(readlink -f $link_path) != $original_path ]]; then 101 | rm -f $link_path 2>/dev/null 102 | ln -sf $original_path $link_path 103 | fi 104 | } 105 | 106 | ## 创建软连接 107 | link_shell() { 108 | if [[ $is_termux -eq 1 ]]; then 109 | local path="/data/data/com.termux/files/usr/bin/" 110 | elif [[ $PATH == */usr/local/bin* ]] && [ -d /usr/local/bin ]; then 111 | local path="/usr/local/bin/" 112 | else 113 | local path="" 114 | echo -e "脚本功能受限,请自行添加命令的软连接...\n" 115 | fi 116 | if [[ $path ]]; then 117 | for ((i = 0; i < ${#link_name[*]}; i++)); do 118 | link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}" 119 | done 120 | fi 121 | } 122 | 123 | ## 定义各命令 124 | define_cmd() { 125 | local cmd_prefix cmd_suffix 126 | if type task >/dev/null 2>&1; then 127 | cmd_suffix="" 128 | if [ -x "$dir_shell/task.sh" ]; then 129 | cmd_prefix="" 130 | else 131 | cmd_prefix="bash " 132 | fi 133 | else 134 | cmd_suffix=".sh" 135 | if [ -x "$dir_shell/task.sh" ]; then 136 | cmd_prefix="$dir_shell/" 137 | else 138 | cmd_prefix="bash $dir_shell/" 139 | fi 140 | fi 141 | for ((i = 0; i < ${#link_name[*]}; i++)); do 142 | export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}" 143 | done 144 | } 145 | 146 | ## 修复配置文件 147 | fix_config() { 148 | make_dir $dir_config 149 | make_dir $dir_log 150 | make_dir $dir_db 151 | make_dir $dir_manual_log 152 | make_dir $dir_scripts 153 | make_dir $dir_list_tmp 154 | make_dir $dir_repo 155 | make_dir $dir_raw 156 | make_dir $dir_update_log 157 | 158 | if [ ! -s $file_config_user ]; then 159 | echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" 160 | cp -fv $file_config_sample $file_config_user 161 | echo 162 | fi 163 | 164 | if [ ! -f $file_cookie ]; then 165 | echo -e "检测到config配置目录下不存在cookie.sh,创建一个空文件用于初始化...\n" 166 | touch $file_cookie 167 | echo 168 | fi 169 | 170 | if [ ! -f $file_extra_shell ]; then 171 | echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n" 172 | cp -fv $file_extra_sample $file_extra_shell 173 | echo 174 | fi 175 | 176 | if [ ! -s $file_auth_user ]; then 177 | echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n" 178 | cp -fv $file_auth_sample $file_auth_user 179 | echo 180 | fi 181 | 182 | if [ ! -s $file_notify_py ]; then 183 | echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n" 184 | cp -fv $file_notify_py_sample $file_notify_py 185 | echo 186 | fi 187 | 188 | if [ ! -s $file_notify_js ]; then 189 | echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n" 190 | cp -fv $file_notify_js_sample $file_notify_js 191 | echo 192 | fi 193 | 194 | if [ -s /etc/nginx/conf.d/default.conf ]; then 195 | echo -e "检测到默认nginx配置文件,删除...\n" 196 | rm -f /etc/nginx/conf.d/default.conf 197 | echo 198 | fi 199 | } 200 | 201 | ## npm install 子程序,判断是否为安卓,判断是否安装有pnpm 202 | npm_install_sub() { 203 | if [ $is_termux -eq 1 ]; then 204 | npm install --production --no-save --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links --no-save 205 | elif ! type pnpm >/dev/null 2>&1; then 206 | npm install --production --no-save --registry=https://registry.npm.taobao.org || npm install --production --no-save 207 | else 208 | echo -e "检测到本机安装了 pnpm,使用 pnpm 替代 ...\n" 209 | pnpm install --prod 210 | fi 211 | } 212 | 213 | ## npm install,$1:package.json文件所在路径 214 | npm_install_1() { 215 | local dir_current=$(pwd) 216 | local dir_work=$1 217 | 218 | cd $dir_work 219 | echo -e "运行 npm install...\n" 220 | npm_install_sub 221 | [[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n" 222 | cd $dir_current 223 | } 224 | 225 | npm_install_2() { 226 | local dir_current=$(pwd) 227 | local dir_work=$1 228 | 229 | cd $dir_work 230 | echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n" 231 | npm_install_sub 232 | if [[ $? -ne 0 ]]; then 233 | echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n" 234 | npm_install_1 $dir_work 235 | fi 236 | cd $dir_current 237 | } 238 | 239 | ## 比对两个文件,$1比$2新时,将$1复制为$2 240 | diff_and_copy() { 241 | local copy_source=$1 242 | local copy_to=$2 243 | if [ ! -s $copy_to ] || [[ $(diff $copy_source $copy_to) ]]; then 244 | cp -f $copy_source $copy_to 245 | fi 246 | } 247 | 248 | ## 更新依赖 249 | update_depend() { 250 | local dir_current=$(pwd) 251 | 252 | if [ ! -s $dir_scripts/package.json ] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then 253 | cp -f $dir_sample/package.json $dir_scripts/package.json 254 | npm_install_2 $dir_scripts 255 | fi 256 | 257 | if [ ! -s $dir_scripts/requirements.txt ] || [[ $(diff $dir_sample/requirements.txt $dir_scripts/requirements.txt) ]]; then 258 | cp -f $dir_sample/requirements.txt $dir_scripts/requirements.txt 259 | cd $dir_scripts 260 | pip3 install -r $dir_scripts/requirements.txt 261 | fi 262 | 263 | cd $dir_current 264 | } 265 | 266 | ## 克隆脚本,$1:仓库地址,$2:仓库保存路径,$3:分支(可省略) 267 | git_clone_scripts() { 268 | local url=$1 269 | local dir=$2 270 | local branch=$3 271 | [[ $branch ]] && local cmd="-b $branch " 272 | echo -e "开始克隆仓库 $url 到 $dir\n" 273 | git clone $cmd $url $dir 274 | exit_status=$? 275 | } 276 | 277 | ## 更新脚本,$1:仓库保存路径 278 | git_pull_scripts() { 279 | local dir_current=$(pwd) 280 | local dir_work="$1" 281 | local branch="$2" 282 | [[ $branch ]] && local cmd="origin/${branch}" 283 | cd $dir_work 284 | echo -e "开始更新仓库:$dir_work\n" 285 | git fetch --all 286 | exit_status=$? 287 | git reset --hard $cmd 288 | git pull 289 | cd $dir_current 290 | } 291 | 292 | init_env() { 293 | TempBlockCookie="" 294 | } 295 | 296 | ## 导入配置文件,检测平台,创建软连接,识别命令,修复配置文件 297 | detect_termux 298 | detect_macos 299 | define_cmd 300 | init_env 301 | import_config $1 302 | -------------------------------------------------------------------------------- /shell/task.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 导入通用变量与函数 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | . $dir_shell/api.sh 7 | 8 | ## 组合Cookie和互助码子程序,$1:要组合的内容 9 | combine_sub() { 10 | local what_combine=$1 11 | local combined_all="" 12 | local tmp1 tmp2 13 | for ((i = 1; i <= $user_sum; i++)); do 14 | for num in $block_cookie; do 15 | [[ $i -eq $num ]] && continue 2 16 | done 17 | local tmp1=$what_combine$i 18 | local tmp2=${!tmp1} 19 | combined_all="$combined_all&$tmp2" 20 | done 21 | echo $combined_all | perl -pe "{s|^&||; s|^@+||; s|&@|&|g; s|@+&|&|g; s|@+|@|g; s|@+$||}" 22 | } 23 | 24 | ## 正常依次运行时,组合所有账号的Cookie与互助码 25 | combine_all() { 26 | for ((i = 0; i < ${#env_name[*]}; i++)); do 27 | result=$(combine_sub ${var_name[i]}) 28 | if [[ $result ]]; then 29 | export ${env_name[i]}="$result" 30 | fi 31 | done 32 | } 33 | 34 | ## 并发运行时,直接申明每个账号的Cookie与互助码,$1:用户Cookie编号 35 | combine_one() { 36 | local user_num=$1 37 | for ((i = 0; i < ${#env_name[*]}; i++)); do 38 | local tmp=${var_name[i]}$user_num 39 | export ${env_name[i]}=${!tmp} 40 | done 41 | } 42 | 43 | ## 选择python3还是node 44 | define_program() { 45 | local p1=$1 46 | if [[ $p1 == *.js ]]; then 47 | which_program="node" 48 | elif [[ $p1 == *.py ]]; then 49 | which_program="python3" 50 | else 51 | which_program="" 52 | fi 53 | } 54 | 55 | random_delay() { 56 | local random_delay_max=$RandomDelay 57 | if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then 58 | local current_min=$(date "+%-M") 59 | if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then 60 | delay_second=$(($(gen_random_num $random_delay_max) + 1)) 61 | echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n" 62 | sleep $delay_second 63 | fi 64 | fi 65 | } 66 | 67 | ## scripts目录下所有可运行脚本数组 68 | gen_array_scripts() { 69 | local dir_current=$(pwd) 70 | local i="-1" 71 | cd $dir_scripts 72 | for file in $(ls); do 73 | if [ -f $file ] && [[ $file == *.js && $file != sendNotify.js ]]; then 74 | let i++ 75 | array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g") 76 | array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1) 77 | [[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>" 78 | fi 79 | done 80 | cd $dir_current 81 | } 82 | 83 | ## 使用说明 84 | usage() { 85 | define_cmd 86 | gen_array_scripts 87 | echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:" 88 | echo -e "1.$cmd_task # 依次执行,如果设置了随机延迟,将随机延迟一定秒数" 89 | echo -e "2.$cmd_task now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中" 90 | echo -e "3.$cmd_task conc # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中" 91 | if [[ ${#array_scripts[*]} -gt 0 ]]; then 92 | echo -e "\n当前有以下脚本可以运行:" 93 | for ((i = 0; i < ${#array_scripts[*]}; i++)); do 94 | echo -e "$(($i + 1)). ${array_scripts_name[i]}:${array_scripts[i]}" 95 | done 96 | else 97 | echo -e "\n暂无脚本可以执行" 98 | fi 99 | } 100 | 101 | ## run nohup,$1:文件名,不含路径,带后缀 102 | run_nohup() { 103 | local file_name=$1 104 | nohup node $file_name &>$log_path & 105 | } 106 | 107 | ## 正常运行单个脚本,$1:传入参数 108 | run_normal() { 109 | local p1=$1 110 | cd $dir_scripts 111 | define_program "$p1" 112 | if [[ $p1 == *.js ]]; then 113 | if [[ $AutoHelpOther == true ]] && [[ $(ls $dir_code) ]]; then 114 | local latest_log=$(ls -r $dir_code | head -1) 115 | . $dir_code/$latest_log 116 | fi 117 | if [[ $# -eq 1 ]]; then 118 | random_delay 119 | fi 120 | fi 121 | combine_all 122 | log_time=$(date "+%Y-%m-%d-%H-%M-%S") 123 | log_dir_tmp="${p1##*/}" 124 | log_dir="$dir_log/${log_dir_tmp%%.*}" 125 | log_path="$log_dir/$log_time.log" 126 | make_dir "$log_dir" 127 | 128 | local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1$" | perl -pe "s|.*ID=(.*) $cmd_task $p1$|\1|" | xargs | sed 's/ /","/g') 129 | update_cron_status "\"$id\"" "0" 130 | timeout $command_timeout_time $which_program $p1 2>&1 | tee $log_path 131 | update_cron_status "\"$id\"" "1" 132 | } 133 | 134 | ## 并发执行,因为是并发,所以日志只能直接记录在日志文件中(日志文件以Cookie编号结尾),前台执行并发跑时不会输出日志 135 | ## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行 136 | run_concurrent() { 137 | local p1=$1 138 | cd $dir_scripts 139 | define_program "$p1" 140 | log_dir="$dir_log/${p1%%.*}" 141 | make_dir $log_dir 142 | log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") 143 | echo -e "\n各账号间已经在后台开始并发执行,前台不输入日志,日志直接写入文件中。\n" 144 | for ((user_num = 1; user_num <= $user_sum; user_num++)); do 145 | combine_one $user_num 146 | log_path="$log_dir/${log_time}_${user_num}.log" 147 | timeout $command_timeout_time $which_program $p1 &>$log_path & 148 | done 149 | } 150 | 151 | ## 运行其他命令 152 | run_else() { 153 | local log_time=$(date "+%Y-%m-%d-%H-%M-%S") 154 | local log_dir="$dir_log/$1" 155 | local log_path="$log_dir/$log_time.log" 156 | make_dir "$log_dir" 157 | timeout $command_timeout_time "$@" 2>&1 | tee $log_path 158 | } 159 | 160 | ## 命令检测 161 | main() { 162 | case $# in 163 | 0) 164 | echo 165 | usage 166 | ;; 167 | 1) 168 | run_normal $1 169 | ;; 170 | 2) 171 | case $2 in 172 | now) 173 | run_normal $1 $2 174 | ;; 175 | conc) 176 | run_concurrent $1 $2 177 | ;; 178 | *) 179 | run_else "$@" 180 | ;; 181 | esac 182 | ;; 183 | *) 184 | run_else "$@" 185 | ;; 186 | esac 187 | } 188 | 189 | main "$@" 190 | 191 | exit 0 192 | -------------------------------------------------------------------------------- /shell/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 导入通用变量与函数 4 | dir_shell=/ql/shell 5 | . $dir_shell/share.sh 6 | . $dir_shell/api.sh 7 | 8 | send_mark=$dir_shell/send_mark 9 | 10 | ## 重置仓库remote url,docker专用,$1:要重置的目录,$2:要重置为的网址 11 | reset_romote_url() { 12 | local dir_current=$(pwd) 13 | local dir_work=$1 14 | local url=$2 15 | local branch="$3" 16 | 17 | [[ $branch ]] && local cmd="origin/${branch}" 18 | 19 | if [ -d "$dir_work/.git" ]; then 20 | cd $dir_work 21 | git remote set-url origin $url >/dev/null 22 | git reset --hard $cmd >/dev/null 23 | cd $dir_current 24 | fi 25 | } 26 | 27 | ## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径 28 | diff_cron() { 29 | local list_scripts="$1" 30 | local list_task="$2" 31 | local list_add="$3" 32 | local list_drop="$4" 33 | if [ -s $list_task ]; then 34 | grep -vwf $list_task $list_scripts >$list_add 35 | elif [ ! -s $list_task ] && [ -s $list_scripts ]; then 36 | cp -f $list_scripts $list_add 37 | fi 38 | if [ -s $list_scripts ]; then 39 | grep -vwf $list_scripts $list_task >$list_drop 40 | else 41 | cp -f $list_task $list_drop 42 | fi 43 | } 44 | 45 | ## 检测配置文件版本 46 | detect_config_version() { 47 | ## 识别出两个文件的版本号 48 | ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|") 49 | [ -f $file_config_user ] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|") 50 | 51 | ## 删除旧的发送记录文件 52 | [ -f $send_mark ] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark 53 | 54 | ## 识别出更新日期和更新内容 55 | update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}') 56 | update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}') 57 | 58 | ## 如果是今天,并且版本号不一致,则发送通知 59 | if [ -f $file_config_user ] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then 60 | if [ ! -f $send_mark ]; then 61 | local notify_title="配置文件更新通知" 62 | local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n" 63 | echo -e $notify_content 64 | notify "$notify_title" "$notify_content" 65 | [[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark 66 | fi 67 | else 68 | [ -f $send_mark ] && rm -f $send_mark 69 | fi 70 | } 71 | 72 | ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 73 | output_list_add_drop() { 74 | local list=$1 75 | local type=$2 76 | if [ -s $list ]; then 77 | echo -e "检测到有$type的定时任务:\n" 78 | cat $list 79 | echo 80 | fi 81 | } 82 | 83 | ## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true;2.正常更新js脚本,没有报错;3.存在失效任务 84 | ## $1:失效任务清单文件路径 85 | del_cron() { 86 | local list_drop=$1 87 | local path=$2 88 | local detail="" 89 | local ids="" 90 | echo -e "开始尝试自动删除失效的定时任务...\n" 91 | for cron in $(cat $list_drop); do 92 | local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron|\1|" | xargs | sed 's/ /","/g' | head -1) 93 | if [[ $ids ]]; then 94 | ids="$ids,\"$id\"" 95 | else 96 | ids="\"$id\"" 97 | fi 98 | cron_file="$dir_scripts/${cron}" 99 | if [[ -f $cron_file ]]; then 100 | cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) 101 | rm -f $cron_file 102 | fi 103 | [[ -z $cron_name ]] && cron_name="$cron" 104 | if [[ $detail ]]; then 105 | detail="${detail}\n${cron_name}" 106 | else 107 | detail="${cron_name}" 108 | fi 109 | done 110 | if [[ $ids ]]; then 111 | result=$(del_cron_api "$ids") 112 | notify "$path 删除任务${result}" "$detail" 113 | fi 114 | } 115 | 116 | ## 自动增加定时任务,需要:1.AutoAddCron 设置为 true;2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空 117 | ## $1:新任务清单文件路径 118 | add_cron() { 119 | local list_add=$1 120 | local path=$2 121 | echo -e "开始尝试自动添加定时任务...\n" 122 | local detail="" 123 | cd $dir_scripts 124 | for file in $(cat $list_add); do 125 | local file_name=${file/${path}\_/} 126 | if [ -f $file ]; then 127 | cron_line=$( 128 | perl -ne "{ 129 | print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ 130 | }" $file | 131 | perl -pe "{ 132 | s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g; 133 | s|\*([\d\*])(.*)|\1\2|g; 134 | s| | |g; 135 | }" | sort -u | head -1 136 | ) 137 | cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) 138 | [[ -z $cron_name ]] && cron_name="$file_name" 139 | [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | xargs) 140 | [[ -z $cron_line ]] && cron_line="0 6 * * *" 141 | result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") 142 | echo -e "$result" 143 | if [[ $detail ]]; then 144 | detail="${detail}\n${result}" 145 | else 146 | detail="${result}" 147 | fi 148 | fi 149 | done 150 | notify "$path 新增任务" "$detail" 151 | } 152 | 153 | ## 更新仓库 154 | update_repo() { 155 | echo -e "--------------------------------------------------------------\n" 156 | local url="$1" 157 | local path="$2" 158 | local blackword="$3" 159 | local dependence="$4" 160 | local branch="$5" 161 | local urlTmp="${url%*/}" 162 | local repoTmp="${urlTmp##*/}" 163 | local repo="${repoTmp%.*}" 164 | local tmp="${url%/*}" 165 | local authorTmp1="${tmp##*/}" 166 | local authorTmp2="${authorTmp1##*:}" 167 | local author="${authorTmp2##*.}" 168 | 169 | local repo_path="${dir_repo}/${author}_${repo}" 170 | [[ $branch ]] && repo_path="${repo_path}_${branch}" 171 | 172 | if [ -d ${repo_path}/.git ]; then 173 | reset_romote_url ${repo_path} "${github_proxy_url}${url/https:\/\/ghproxy.com\//}" "${branch}" 174 | git_pull_scripts ${repo_path} "${branch}" 175 | else 176 | git_clone_scripts ${url} ${repo_path} "${branch}" 177 | fi 178 | if [[ $exit_status -eq 0 ]]; then 179 | echo -e "\n更新${repo_path}成功...\n" 180 | diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" 181 | else 182 | echo -e "\n更新${repo_path}失败,请检查原因...\n" 183 | fi 184 | } 185 | 186 | ## 更新所有 raw 文件 187 | update_raw() { 188 | echo -e "--------------------------------------------------------------\n" 189 | local raw_url="$1" 190 | raw_file_name=$(echo ${raw_url} | awk -F "/" '{print $NF}') 191 | echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" 192 | wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url} 193 | if [[ $? -eq 0 ]]; then 194 | mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" 195 | echo -e "下载 ${raw_file_name} 成功...\n" 196 | cd $dir_raw 197 | local filename="raw_${raw_file_name}" 198 | local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1) 199 | cp -f $raw_file_name $dir_scripts/${filename} 200 | cron_line=$( 201 | perl -ne "{ 202 | print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ 203 | }" $raw_file_name | 204 | perl -pe "{ 205 | s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; 206 | s|\*([\d\*])(.*)|\1\2|g; 207 | s| | |g; 208 | }" | sort -u | head -1 209 | ) 210 | cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) 211 | [[ -z $cron_name ]] && cron_name="$raw_file_name" 212 | [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | xargs) 213 | [[ -z $cron_line ]] && cron_line="0 6 * * *" 214 | if [[ -z $cron_id ]]; then 215 | result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") 216 | echo -e "$result" 217 | notify "新增任务通知" "\n$result" 218 | # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" 219 | fi 220 | else 221 | echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n" 222 | [ -f "$dir_raw/${raw_file_name}.new" ] && rm -f "$dir_raw/${raw_file_name}.new" 223 | fi 224 | 225 | } 226 | 227 | ## 调用用户自定义的extra.sh 228 | run_extra_shell() { 229 | if [[ ${EnableExtraShell} == true ]]; then 230 | if [ -f $file_extra_shell ]; then 231 | echo -e "--------------------------------------------------------------\n" 232 | . $file_extra_shell 233 | else 234 | echo -e "$file_extra_shell文件不存在,跳过执行...\n" 235 | fi 236 | fi 237 | } 238 | 239 | ## 脚本用法 240 | usage() { 241 | echo -e "本脚本用法:" 242 | echo -e "1. $cmd_update update # 更新并重启青龙" 243 | echo -e "1. $cmd_update extra # 运行自定义脚本" 244 | echo -e "3. $cmd_update raw # 更新单个脚本文件" 245 | echo -e "4. $cmd_update repo # 更新单个仓库的脚本" 246 | echo -e "5. $cmd_update rmlog # 删除旧日志" 247 | echo -e "6. $cmd_update code # 获取互助码" 248 | echo -e "6. $cmd_update bot # 启动tg-bot" 249 | echo -e "7. $cmd_update reset # 重置青龙基础环境" 250 | } 251 | 252 | ## 更新qinglong 253 | update_qinglong() { 254 | local no_restart="$1" 255 | echo -e "--------------------------------------------------------------\n" 256 | [ -f $dir_root/package.json ] && ql_depend_old=$(cat $dir_root/package.json) 257 | reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/Zy143L/qinglong.git" 258 | git_pull_scripts $dir_root 259 | 260 | if [[ $exit_status -eq 0 ]]; then 261 | echo -e "\n更新$dir_root成功...\n" 262 | cp -f $file_config_sample $dir_config/config.sample.sh 263 | detect_config_version 264 | update_depend 265 | 266 | [ -f $dir_root/package.json ] && ql_depend_new=$(cat $dir_root/package.json) 267 | [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root 268 | else 269 | echo -e "\n更新$dir_root失败,请检查原因...\n" 270 | fi 271 | 272 | local url="${github_proxy_url}https://github.com/Zy143L/qinglong-static.git" 273 | if [ -d ${ql_static_repo}/.git ]; then 274 | reset_romote_url ${ql_static_repo} ${url} 275 | cd ${ql_static_repo} 276 | git fetch --all 277 | exit_status=$? 278 | git reset --hard origin/master 279 | cd $dir_root 280 | else 281 | git_clone_scripts ${url} ${ql_static_repo} 282 | fi 283 | if [[ $exit_status -eq 0 ]]; then 284 | echo -e "\n更新$ql_static_repo成功...\n" 285 | cd $ql_static_repo 286 | commit_id=$(git rev-parse --short HEAD) 287 | echo -e "\n当前静态资源版本 $commit_id...\n" 288 | cd $dir_root 289 | rm -rf $dir_root/build && rm -rf $dir_root/dist 290 | cp -rf $ql_static_repo/* $dir_root 291 | if [[ $no_restart != "no-restart" ]]; then 292 | echo -e "重启面板中..." 293 | nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf 294 | sleep 1 295 | reload_pm2 296 | fi 297 | else 298 | echo -e "\n更新$dir_root失败,请检查原因...\n" 299 | fi 300 | 301 | } 302 | 303 | reload_pm2() { 304 | pm2 l >/dev/null 2>&1 305 | 306 | if [[ $(pm2 info panel 2>/dev/null) ]]; then 307 | pm2 reload panel >/dev/null 2>&1 308 | else 309 | pm2 start $dir_root/build/app.js -n panel >/dev/null 2>&1 310 | fi 311 | 312 | if [[ $(pm2 info schedule 2>/dev/null) ]]; then 313 | pm2 reload schedule >/dev/null 2>&1 314 | else 315 | pm2 start $dir_root/build/schedule.js -n schedule >/dev/null 2>&1 316 | fi 317 | } 318 | 319 | ## 对比脚本 320 | diff_scripts() { 321 | local dir_current=$(pwd) 322 | local repo_path="$1" 323 | local author="$2" 324 | local path="$3" 325 | local blackword="$4" 326 | local dependence="$5" 327 | 328 | gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" 329 | 330 | local repo="${repo_path##*/}" 331 | local list_add="$dir_list_tmp/${repo}_add.list" 332 | local list_drop="$dir_list_tmp/${repo}_drop.list" 333 | diff_cron "$dir_list_tmp/${repo}_scripts.list" "$dir_list_tmp/${repo}_user.list" $list_add $list_drop 334 | 335 | if [ -s $list_drop ]; then 336 | output_list_add_drop $list_drop "失效" 337 | if [[ ${AutoDelCron} == true ]]; then 338 | del_cron $list_drop $repo 339 | fi 340 | fi 341 | if [ -s $list_add ]; then 342 | output_list_add_drop $list_add "新" 343 | if [[ ${AutoAddCron} == true ]]; then 344 | add_cron $list_add $repo 345 | fi 346 | fi 347 | cd $dir_current 348 | } 349 | 350 | ## 生成脚本的路径清单文件 351 | gen_list_repo() { 352 | local dir_current=$(pwd) 353 | local repo_path="$1" 354 | local author="$2" 355 | local path="$3" 356 | local blackword="$4" 357 | local dependence="$5" 358 | 359 | local repo="${repo_path##*/}" 360 | rm -f $dir_list_tmp/${repo}*.list >/dev/null 2>&1 361 | 362 | cd ${repo_path} 363 | 364 | local cmd="find ." 365 | local index=0 366 | for extension in $file_extensions; do 367 | if [[ $index -eq 0 ]]; then 368 | cmd="${cmd} -name \"*.${extension}\"" 369 | else 370 | cmd="${cmd} -o -name \"*.${extension}\"" 371 | fi 372 | let index+=1 373 | done 374 | files=$(eval $cmd | sed 's/^..//') 375 | if [[ $path ]]; then 376 | files=$(echo "$files" | egrep $path) 377 | fi 378 | if [[ $blackword ]]; then 379 | files=$(echo "$files" | egrep -v $blackword) 380 | fi 381 | if [[ $dependence ]]; then 382 | eval $cmd | sed 's/^..//' | egrep $dependence | xargs -i cp {} $dir_scripts 383 | fi 384 | for file in ${files}; do 385 | filename=$(basename $file) 386 | cp -f $file $dir_scripts/${repo}_${filename} 387 | echo ${repo}_${filename} >>"$dir_list_tmp/${repo}_scripts.list" 388 | cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${author}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${author}_${filename}\.*|\1|" | head -1) 389 | if [[ $cron_id ]]; then 390 | result=$(update_cron_command_api "$cmd_task ${repo}_${filename}:$cron_id") 391 | fi 392 | done 393 | grep -E "$cmd_task $repo" $list_crontab_user | perl -pe "s|.*ID=(.*) $cmd_task ($repo_.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${repo}_user.list" 394 | cd $dir_current 395 | } 396 | 397 | main() { 398 | local p1=$1 399 | local p2=$2 400 | local p3=$3 401 | local p4=$4 402 | local p5=$5 403 | local p6=$6 404 | log_time=$(date "+%Y-%m-%d-%H-%M-%S") 405 | log_path="$dir_log/update/${log_time}_$p1.log" 406 | case $p1 in 407 | update) 408 | update_qinglong "$2" | tee $log_path 409 | ;; 410 | extra) 411 | run_extra_shell | tee -a $log_path 412 | ;; 413 | repo) 414 | get_user_info 415 | local name=$(echo "${p2##*/}" | awk -F "." '{print $1}') 416 | log_path="$dir_log/update/${log_time}_$name.log" 417 | if [[ -n $p2 ]]; then 418 | update_repo "$p2" "$p3" "$p4" "$p5" "$p6" | tee $log_path 419 | else 420 | echo -e "命令输入错误...\n" 421 | usage 422 | fi 423 | ;; 424 | raw) 425 | get_user_info 426 | local name=$(echo "${p2##*/}" | awk -F "." '{print $1}') 427 | log_path="$dir_log/update/${log_time}_$name.log" 428 | if [[ -n $p2 ]]; then 429 | update_raw "$p2" | tee $log_path 430 | else 431 | echo -e "命令输入错误...\n" 432 | usage 433 | fi 434 | ;; 435 | rmlog) 436 | . $dir_shell/rmlog.sh "$p2" | tee $log_path 437 | ;; 438 | code) 439 | . $dir_shell/code.sh 440 | ;; 441 | bot) 442 | . $dir_shell/bot.sh 443 | ;; 444 | reset) 445 | . $dir_shell/reset.sh 446 | ;; 447 | *) 448 | echo -e "命令输入错误...\n" 449 | usage 450 | ;; 451 | esac 452 | } 453 | 454 | main "$@" 455 | 456 | exit 0 457 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { history } from 'umi'; 2 | import { request } from '@/utils/http'; 3 | import config from '@/utils/config'; 4 | 5 | const titleMap: any = { 6 | '/': '控制面板', 7 | '/login': '登录', 8 | '/crontab': '定时任务', 9 | '/cookie': 'Session管理', 10 | '/config': '配置文件', 11 | '/diy': '自定义脚本', 12 | '/diff': '对比工具', 13 | '/log': '日志', 14 | '/setting': '系统设置', 15 | }; 16 | 17 | export function render(oldRender: any) { 18 | request 19 | .get(`${config.apiPrefix}user`) 20 | .then((data) => { 21 | if (data.data && data.data.username) { 22 | return oldRender(); 23 | } 24 | localStorage.removeItem(config.authKey); 25 | history.push('/login'); 26 | oldRender(); 27 | }) 28 | .catch((e) => { 29 | console.log(e); 30 | if (e.response && e.response.status === 401) { 31 | localStorage.removeItem(config.authKey); 32 | history.push('/login'); 33 | oldRender(); 34 | } 35 | }); 36 | } 37 | 38 | export function onRouteChange({ matchedRoutes }: any) { 39 | if (matchedRoutes.length) { 40 | const path: string = matchedRoutes[matchedRoutes.length - 1].route.path; 41 | document.title = `${titleMap[path]} - 控制面板`; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/fonts/SourceCodePro-Regular.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/assets/fonts/SourceCodePro-Regular.otf.woff -------------------------------------------------------------------------------- /src/assets/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/assets/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/SourceCodePro-Regular.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/assets/fonts/SourceCodePro-Regular.ttf.woff2 -------------------------------------------------------------------------------- /src/layouts/defaultProps.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormOutlined, 3 | FieldTimeOutlined, 4 | DiffOutlined, 5 | SettingOutlined, 6 | CodeOutlined, 7 | FolderOutlined, 8 | RadiusSettingOutlined, 9 | ControlOutlined, 10 | } from '@ant-design/icons'; 11 | 12 | export default { 13 | route: { 14 | routes: [ 15 | { 16 | name: '登录', 17 | path: '/login', 18 | hideInMenu: true, 19 | component: '@/pages/login/index', 20 | }, 21 | { 22 | path: '/crontab', 23 | name: '定时任务', 24 | icon: , 25 | component: '@/pages/crontab/index', 26 | }, 27 | { 28 | path: '/cookie', 29 | name: 'Session管理', 30 | icon: , 31 | component: '@/pages/cookie/index', 32 | }, 33 | { 34 | path: '/config', 35 | name: '配置文件', 36 | icon: , 37 | component: '@/pages/config/index', 38 | }, 39 | { 40 | path: '/diy', 41 | name: '自定义脚本', 42 | icon: , 43 | component: '@/pages/diy/index', 44 | }, 45 | { 46 | path: '/diff', 47 | name: '对比工具', 48 | icon: , 49 | component: '@/pages/diff/index', 50 | }, 51 | { 52 | path: '/log', 53 | name: '日志', 54 | icon: , 55 | component: '@/pages/log/index', 56 | }, 57 | { 58 | path: '/setting', 59 | name: '系统设置', 60 | icon: , 61 | component: '@/pages/password/index', 62 | }, 63 | ], 64 | }, 65 | location: { 66 | pathname: '/', 67 | }, 68 | navTheme: 'light', 69 | fixSiderbar: true, 70 | contentWidth: 'Fixed', 71 | splitMenus: false, 72 | logo: 'https://qinglong.whyour.cn/qinglong.png', 73 | } as any; 74 | -------------------------------------------------------------------------------- /src/layouts/index.less: -------------------------------------------------------------------------------- 1 | @import '~@/styles/variable.less'; 2 | 3 | @font-face { 4 | font-family: 'Source Code Pro'; 5 | src: url('../assets/fonts/SourceCodePro-Regular.ttf.woff2') format('woff2'), 6 | url('../assets/fonts/SourceCodePro-Regular.otf.woff') format('woff'), 7 | url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); 8 | } 9 | 10 | body { 11 | height: 100%; 12 | overflow-y: hidden; 13 | background-color: rgb(248, 248, 248); 14 | } 15 | 16 | @import '~codemirror/lib/codemirror.css'; 17 | 18 | .ql-container-wrapper { 19 | .CodeMirror { 20 | position: absolute; 21 | height: calc(100vh - 128px); 22 | height: calc(100vh - var(--vh-offset, 0px) - 128px); 23 | width: calc(100% - 32px); 24 | } 25 | } 26 | 27 | .ant-pro-grid-content.wide { 28 | max-width: unset !important; 29 | overflow: auto; 30 | .ant-pro-page-container-children-content { 31 | overflow: auto; 32 | height: calc(100vh - 96px); 33 | height: calc(100vh - var(--vh-offset, 0px) - 96px); 34 | background-color: #fff; 35 | padding: 16px; 36 | } 37 | } 38 | 39 | .session-wrapper { 40 | th { 41 | white-space: nowrap; 42 | } 43 | } 44 | 45 | .log-wrapper { 46 | .log-select { 47 | width: 300px; 48 | } 49 | .ant-page-header-heading-left { 50 | min-width: 100px; 51 | } 52 | } 53 | 54 | @media (max-width: 768px) { 55 | .ant-pro-grid-content.wide { 56 | .ant-pro-page-container-children-content { 57 | height: calc(100vh - 144px); 58 | height: calc(100vh - var(--vh-offset, 0px) - 144px); 59 | } 60 | } 61 | .ql-container-wrapper { 62 | &.crontab-wrapper, 63 | &.log-wrapper { 64 | .ant-pro-grid-content.wide .ant-pro-page-container-children-content { 65 | height: calc(100vh - 184px); 66 | height: calc(100vh - var(--vh-offset, 0px) - 184px); 67 | margin-left: 0; 68 | margin-right: 0; 69 | } 70 | .CodeMirror { 71 | height: calc(100vh - 216px); 72 | height: calc(100vh - var(--vh-offset, 0px) - 216px); 73 | width: calc(100vw - 32px); 74 | } 75 | } 76 | .CodeMirror { 77 | height: calc(100vh - 176px); 78 | height: calc(100vh - var(--vh-offset, 0px) - 176px); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ProLayout from '@ant-design/pro-layout'; 3 | import { 4 | enable as enableDarkMode, 5 | disable as disableDarkMode, 6 | auto as followSystemColorScheme, 7 | setFetchMethod, 8 | } from 'darkreader'; 9 | import defaultProps from './defaultProps'; 10 | import { Link, history } from 'umi'; 11 | import { LogoutOutlined } from '@ant-design/icons'; 12 | import config from '@/utils/config'; 13 | import 'codemirror/mode/shell/shell.js'; 14 | import { request } from '@/utils/http'; 15 | import './index.less'; 16 | import vhCheck from 'vh-check'; 17 | import { version, changeLog } from '../version'; 18 | 19 | export default function (props: any) { 20 | const logout = () => { 21 | request.post(`${config.apiPrefix}logout`).then(() => { 22 | localStorage.removeItem(config.authKey); 23 | history.push('/login'); 24 | }); 25 | }; 26 | 27 | useEffect(() => { 28 | const isAuth = localStorage.getItem(config.authKey); 29 | if (!isAuth) { 30 | history.push('/login'); 31 | } 32 | vhCheck(); 33 | }, []); 34 | 35 | useEffect(() => { 36 | if (props.location.pathname === '/') { 37 | history.push('/crontab'); 38 | } 39 | }, [props.location.pathname]); 40 | 41 | useEffect(() => { 42 | const theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; 43 | setFetchMethod(window.fetch); 44 | if (theme === 'dark') { 45 | enableDarkMode({ 46 | brightness: 100, 47 | contrast: 90, 48 | sepia: 10, 49 | }); 50 | } else if (theme === 'light') { 51 | disableDarkMode(); 52 | } else { 53 | followSystemColorScheme({ 54 | brightness: 100, 55 | contrast: 90, 56 | sepia: 10, 57 | }); 58 | } 59 | }, []); 60 | 61 | if (props.location.pathname === '/login') { 62 | return props.children; 63 | } 64 | 65 | const isFirefox = navigator.userAgent.includes('Firefox'); 66 | const isSafari = 67 | navigator.userAgent.includes('Safari') && 68 | !navigator.userAgent.includes('Chrome'); 69 | return ( 70 | 74 | 控制面板 75 | 76 | 84 | {version} 85 | 86 | 87 | 88 | } 89 | menuItemRender={(menuItemProps: any, defaultDom: any) => { 90 | if ( 91 | menuItemProps.isUrl || 92 | !menuItemProps.path || 93 | location.pathname === menuItemProps.path 94 | ) { 95 | return defaultDom; 96 | } 97 | return {defaultDom}; 98 | }} 99 | postMenuData={(menuData) => { 100 | return [ 101 | ...(menuData || []), 102 | { 103 | icon: , 104 | name: '退出登录', 105 | path: 'logout', 106 | onTitleClick: () => logout(), 107 | }, 108 | ]; 109 | }} 110 | pageTitleRender={() => '控制面板'} 111 | {...defaultProps} 112 | > 113 | {props.children} 114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/pages/config/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/pages/config/index.less -------------------------------------------------------------------------------- /src/pages/config/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment, useState, useEffect } from 'react'; 2 | import { Button, message, Modal } from 'antd'; 3 | import config from '@/utils/config'; 4 | import { PageContainer } from '@ant-design/pro-layout'; 5 | import { Controlled as CodeMirror } from 'react-codemirror2'; 6 | import { request } from '@/utils/http'; 7 | 8 | const Config = () => { 9 | const [width, setWidth] = useState('100%'); 10 | const [marginLeft, setMarginLeft] = useState(0); 11 | const [marginTop, setMarginTop] = useState(-72); 12 | const [value, setValue] = useState(''); 13 | const [loading, setLoading] = useState(true); 14 | 15 | const getConfig = () => { 16 | setLoading(true); 17 | request 18 | .get(`${config.apiPrefix}config/config`) 19 | .then((data: any) => { 20 | setValue(data.data); 21 | }) 22 | .finally(() => setLoading(false)); 23 | }; 24 | 25 | const updateConfig = () => { 26 | request 27 | .post(`${config.apiPrefix}save`, { 28 | data: { content: value, name: 'config.sh' }, 29 | }) 30 | .then((data: any) => { 31 | message.success(data.msg); 32 | }); 33 | }; 34 | 35 | useEffect(() => { 36 | if (document.body.clientWidth < 768) { 37 | setWidth('auto'); 38 | setMarginLeft(0); 39 | setMarginTop(0); 40 | } else { 41 | setWidth('100%'); 42 | setMarginLeft(0); 43 | setMarginTop(-72); 44 | } 45 | getConfig(); 46 | }, []); 47 | 48 | return ( 49 | 54 | 保存 55 | , 56 | ]} 57 | header={{ 58 | style: { 59 | padding: '4px 16px 4px 15px', 60 | position: 'sticky', 61 | top: 0, 62 | left: 0, 63 | zIndex: 20, 64 | marginTop, 65 | width, 66 | marginLeft, 67 | }, 68 | }} 69 | > 70 | { 79 | setValue(value); 80 | }} 81 | onChange={(editor, data, value) => {}} 82 | /> 83 | 84 | ); 85 | }; 86 | 87 | export default Config; 88 | -------------------------------------------------------------------------------- /src/pages/cookie/index.less: -------------------------------------------------------------------------------- 1 | tr.drop-over-downward td { 2 | border-bottom: 2px dashed #1890ff; 3 | } 4 | 5 | tr.drop-over-upward td { 6 | border-top: 2px dashed #1890ff; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/cookie/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState, useEffect } from 'react'; 2 | import { 3 | Button, 4 | message, 5 | Modal, 6 | Table, 7 | Tag, 8 | Space, 9 | Typography, 10 | Tooltip, 11 | } from 'antd'; 12 | import { 13 | EditOutlined, 14 | DeleteOutlined, 15 | SyncOutlined, 16 | CheckCircleOutlined, 17 | StopOutlined, 18 | } from '@ant-design/icons'; 19 | import config from '@/utils/config'; 20 | import { PageContainer } from '@ant-design/pro-layout'; 21 | import { request } from '@/utils/http'; 22 | import QRCode from 'qrcode.react'; 23 | import CookieModal from './modal'; 24 | import { DndProvider, useDrag, useDrop } from 'react-dnd'; 25 | import { HTML5Backend } from 'react-dnd-html5-backend'; 26 | import './index.less'; 27 | 28 | const { Text } = Typography; 29 | 30 | enum Status { 31 | '未获取', 32 | '正常', 33 | '已禁用', 34 | '已失效', 35 | '状态异常', 36 | } 37 | 38 | enum StatusColor { 39 | 'default', 40 | 'success', 41 | 'warning', 42 | 'error', 43 | } 44 | 45 | enum OperationName { 46 | '启用', 47 | '禁用', 48 | } 49 | 50 | enum OperationPath { 51 | 'enable', 52 | 'disable', 53 | } 54 | 55 | const type = 'DragableBodyRow'; 56 | 57 | const DragableBodyRow = ({ 58 | index, 59 | moveRow, 60 | className, 61 | style, 62 | ...restProps 63 | }: any) => { 64 | const ref = useRef(); 65 | const [{ isOver, dropClassName }, drop] = useDrop( 66 | () => ({ 67 | accept: type, 68 | collect: (monitor) => { 69 | const { index: dragIndex } = monitor.getItem() || ({} as any); 70 | if (dragIndex === index) { 71 | return {}; 72 | } 73 | return { 74 | isOver: monitor.isOver(), 75 | dropClassName: 76 | dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', 77 | }; 78 | }, 79 | drop: (item: any) => { 80 | moveRow(item.index, index); 81 | }, 82 | }), 83 | [index], 84 | ); 85 | const [, drag, preview] = useDrag( 86 | () => ({ 87 | type, 88 | item: { index }, 89 | collect: (monitor) => ({ 90 | isDragging: monitor.isDragging(), 91 | }), 92 | }), 93 | [index], 94 | ); 95 | drop(drag(ref)); 96 | 97 | return ( 98 | 104 | {restProps.children} 105 | 106 | ); 107 | }; 108 | 109 | const Config = () => { 110 | const columns = [ 111 | { 112 | title: '序号', 113 | align: 'center' as const, 114 | render: (text: string, record: any, index: number) => { 115 | return {index + 1} ; 116 | }, 117 | }, 118 | { 119 | title: '昵称', 120 | dataIndex: 'nickname', 121 | key: 'nickname', 122 | align: 'center' as const, 123 | width: '15%', 124 | render: (text: string, record: any, index: number) => { 125 | const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/); 126 | const val = (match && match[1]) || '未匹配用户名'; 127 | return ( 128 | {record.nickname || val} 129 | ); 130 | }, 131 | }, 132 | { 133 | title: '值', 134 | dataIndex: 'value', 135 | key: 'value', 136 | align: 'center' as const, 137 | width: '50%', 138 | render: (text: string, record: any) => { 139 | return ( 140 | 148 | {text} 149 | 150 | ); 151 | }, 152 | }, 153 | { 154 | title: '状态', 155 | key: 'status', 156 | dataIndex: 'status', 157 | align: 'center' as const, 158 | width: '15%', 159 | render: (text: string, record: any, index: number) => { 160 | return ( 161 | 162 | 166 | {Status[record.status]} 167 | 168 | {record.status !== Status.已禁用 && ( 169 | 170 | refreshStatus(record, index)}> 171 | 172 | 173 | 174 | )} 175 | 176 | ); 177 | }, 178 | }, 179 | { 180 | title: '操作', 181 | key: 'action', 182 | align: 'center' as const, 183 | render: (text: string, record: any, index: number) => ( 184 | 185 | 186 | editCookie(record, index)}> 187 | 188 | 189 | 190 | 191 | enabledOrDisabledCookie(record, index)}> 192 | {record.status === Status.已禁用 ? ( 193 | 194 | ) : ( 195 | 196 | )} 197 | 198 | 199 | 200 | deleteCookie(record, index)}> 201 | 202 | 203 | 204 | 205 | ), 206 | }, 207 | ]; 208 | const [width, setWidth] = useState('100%'); 209 | const [marginLeft, setMarginLeft] = useState(0); 210 | const [marginTop, setMarginTop] = useState(-72); 211 | const [value, setValue] = useState([]); 212 | const [loading, setLoading] = useState(true); 213 | const [isModalVisible, setIsModalVisible] = useState(false); 214 | const [editedCookie, setEditedCookie] = useState(); 215 | const [selectedRowIds, setSelectedRowIds] = useState([]); 216 | 217 | const getCookies = () => { 218 | setLoading(true); 219 | request 220 | .get(`${config.apiPrefix}cookies`) 221 | .then((data: any) => { 222 | setValue(data.data); 223 | }) 224 | .finally(() => setLoading(false)); 225 | }; 226 | 227 | const refreshStatus = (record: any, index: number) => { 228 | request 229 | .get(`${config.apiPrefix}cookies/${record._id}/refresh`) 230 | .then(async (data: any) => { 231 | if (data.data && data.data.value) { 232 | (value as any).splice(index, 1, data.data); 233 | setValue([...(value as any)] as any); 234 | } else { 235 | message.error('更新状态失败'); 236 | } 237 | }); 238 | }; 239 | 240 | const enabledOrDisabledCookie = (record: any, index: number) => { 241 | Modal.confirm({ 242 | title: `确认${record.status === Status.已禁用 ? '启用' : '禁用'}`, 243 | content: ( 244 | <> 245 | 确认{record.status === Status.已禁用 ? '启用' : '禁用'} 246 | Cookie{' '} 247 | 248 | {record.value} 249 | {' '} 250 | 吗 251 | 252 | ), 253 | onOk() { 254 | request 255 | .put( 256 | `${config.apiPrefix}cookies/${ 257 | record.status === Status.已禁用 ? 'enable' : 'disable' 258 | }`, 259 | { 260 | data: [record._id], 261 | }, 262 | ) 263 | .then((data: any) => { 264 | if (data.code === 200) { 265 | message.success( 266 | `${record.status === Status.已禁用 ? '启用' : '禁用'}成功`, 267 | ); 268 | const newStatus = 269 | record.status === Status.已禁用 ? Status.未获取 : Status.已禁用; 270 | const result = [...value]; 271 | result.splice(index, 1, { 272 | ...record, 273 | status: newStatus, 274 | }); 275 | setValue(result); 276 | } else { 277 | message.error(data); 278 | } 279 | }); 280 | }, 281 | onCancel() { 282 | console.log('Cancel'); 283 | }, 284 | }); 285 | }; 286 | 287 | const addCookie = () => { 288 | setEditedCookie(null as any); 289 | setIsModalVisible(true); 290 | }; 291 | 292 | const editCookie = (record: any, index: number) => { 293 | setEditedCookie(record); 294 | setIsModalVisible(true); 295 | }; 296 | 297 | const deleteCookie = (record: any, index: number) => { 298 | Modal.confirm({ 299 | title: '确认删除', 300 | content: ( 301 | <> 302 | 确认删除Cookie{' '} 303 | 304 | {record.value} 305 | {' '} 306 | 吗 307 | 308 | ), 309 | onOk() { 310 | request 311 | .delete(`${config.apiPrefix}cookies`, { data: [record._id] }) 312 | .then((data: any) => { 313 | if (data.code === 200) { 314 | message.success('删除成功'); 315 | const result = [...value]; 316 | result.splice(index, 1); 317 | setValue(result); 318 | } else { 319 | message.error(data); 320 | } 321 | }); 322 | }, 323 | onCancel() { 324 | console.log('Cancel'); 325 | }, 326 | }); 327 | }; 328 | 329 | const handleCancel = (cookies?: any[]) => { 330 | setIsModalVisible(false); 331 | if (cookies && cookies.length > 0) { 332 | handleCookies(cookies); 333 | } 334 | }; 335 | 336 | const handleCookies = (cookies: any[]) => { 337 | const result = [...value]; 338 | for (let i = 0; i < cookies.length; i++) { 339 | const cookie = cookies[i]; 340 | const index = value.findIndex((x) => x._id === cookie._id); 341 | if (index === -1) { 342 | result.push(cookie); 343 | } else { 344 | result.splice(index, 1, { 345 | ...cookie, 346 | }); 347 | } 348 | } 349 | setValue(result); 350 | }; 351 | 352 | const components = { 353 | body: { 354 | row: DragableBodyRow, 355 | }, 356 | }; 357 | 358 | const moveRow = useCallback( 359 | (dragIndex, hoverIndex) => { 360 | if (dragIndex === hoverIndex) { 361 | return; 362 | } 363 | const dragRow = value[dragIndex]; 364 | const newData = [...value]; 365 | newData.splice(dragIndex, 1); 366 | newData.splice(hoverIndex, 0, dragRow); 367 | setValue([...newData]); 368 | request 369 | .put(`${config.apiPrefix}cookies/${dragRow._id}/move`, { 370 | data: { fromIndex: dragIndex, toIndex: hoverIndex }, 371 | }) 372 | .then((data: any) => { 373 | if (data.code !== 200) { 374 | message.error(data); 375 | } 376 | }); 377 | }, 378 | [value], 379 | ); 380 | 381 | const onSelectChange = (selectedIds: any[]) => { 382 | setSelectedRowIds(selectedIds); 383 | }; 384 | 385 | const rowSelection = { 386 | selectedRowIds, 387 | onChange: onSelectChange, 388 | }; 389 | 390 | const delCookies = () => { 391 | Modal.confirm({ 392 | title: '确认删除', 393 | content: <>确认删除选中的Cookie吗, 394 | onOk() { 395 | request 396 | .delete(`${config.apiPrefix}cookies`, { data: selectedRowIds }) 397 | .then((data: any) => { 398 | if (data.code === 200) { 399 | message.success('批量删除成功'); 400 | setSelectedRowIds([]); 401 | getCookies(); 402 | } else { 403 | message.error(data); 404 | } 405 | }); 406 | }, 407 | onCancel() { 408 | console.log('Cancel'); 409 | }, 410 | }); 411 | }; 412 | 413 | const operateCookies = (operationStatus: number) => { 414 | Modal.confirm({ 415 | title: `确认${OperationName[operationStatus]}`, 416 | content: <>确认{OperationName[operationStatus]}选中的Cookie吗, 417 | onOk() { 418 | request 419 | .put(`${config.apiPrefix}cookies/${OperationPath[operationStatus]}`, { 420 | data: selectedRowIds, 421 | }) 422 | .then((data: any) => { 423 | if (data.code === 200) { 424 | getCookies(); 425 | } else { 426 | message.error(data); 427 | } 428 | }); 429 | }, 430 | onCancel() { 431 | console.log('Cancel'); 432 | }, 433 | }); 434 | }; 435 | 436 | useEffect(() => { 437 | if (document.body.clientWidth < 768) { 438 | setWidth('auto'); 439 | setMarginLeft(0); 440 | setMarginTop(0); 441 | } else { 442 | setWidth('100%'); 443 | setMarginLeft(0); 444 | setMarginTop(-72); 445 | } 446 | getCookies(); 447 | }, []); 448 | 449 | return ( 450 | addCookie()}> 455 | 添加Cookie 456 | , 457 | ]} 458 | header={{ 459 | style: { 460 | padding: '4px 16px 4px 15px', 461 | position: 'sticky', 462 | top: 0, 463 | left: 0, 464 | zIndex: 20, 465 | marginTop, 466 | width, 467 | marginLeft, 468 | }, 469 | }} 470 | > 471 | {selectedRowIds.length > 0 && ( 472 |
473 | 480 | 487 | 494 | 495 | 已选择 496 | {selectedRowIds?.length}项 497 | 498 |
499 | )} 500 | 501 | { 512 | return { 513 | index, 514 | moveRow, 515 | } as any; 516 | }} 517 | /> 518 | 519 | 524 | 525 | ); 526 | }; 527 | 528 | export default Config; 529 | -------------------------------------------------------------------------------- /src/pages/cookie/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal, message, Input, Form } from 'antd'; 3 | import { request } from '@/utils/http'; 4 | import config from '@/utils/config'; 5 | 6 | const CookieModal = ({ 7 | cookie, 8 | handleCancel, 9 | visible, 10 | }: { 11 | cookie?: any; 12 | visible: boolean; 13 | handleCancel: (cks?: any[]) => void; 14 | }) => { 15 | const [form] = Form.useForm(); 16 | const [loading, setLoading] = useState(false); 17 | 18 | const handleOk = async (values: any) => { 19 | const cookies = values.value 20 | .split('\n') 21 | .map((x: any) => x.trim().replace(/\s/g, '')); 22 | let flag = false; 23 | for (const coo of cookies) { 24 | if (!/pt_key=\S*;\s*pt_pin=\S*;\s*/.test(coo)) { 25 | message.error(`${coo}格式有误`); 26 | flag = true; 27 | break; 28 | } 29 | } 30 | if (flag) { 31 | return; 32 | } 33 | setLoading(true); 34 | const method = cookie ? 'put' : 'post'; 35 | const payload = cookie ? { value: cookies[0], _id: cookie._id } : cookies; 36 | const { code, data } = await request[method](`${config.apiPrefix}cookies`, { 37 | data: payload, 38 | }); 39 | if (code === 200) { 40 | message.success(cookie ? '更新Cookie成功' : '添加Cookie成功'); 41 | } else { 42 | message.error(data); 43 | } 44 | setLoading(false); 45 | handleCancel(cookie ? [data] : data); 46 | }; 47 | 48 | useEffect(() => { 49 | form.resetFields(); 50 | }, [cookie]); 51 | 52 | return ( 53 | { 58 | form 59 | .validateFields() 60 | .then((values) => { 61 | handleOk(values); 62 | }) 63 | .catch((info) => { 64 | console.log('Validate Failed:', info); 65 | }); 66 | }} 67 | onCancel={() => handleCancel()} 68 | confirmLoading={loading} 69 | destroyOnClose 70 | > 71 |
78 | 88 | 93 | 94 | 95 |
96 | ); 97 | }; 98 | 99 | export default CookieModal; 100 | -------------------------------------------------------------------------------- /src/pages/crontab/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/pages/crontab/index.less -------------------------------------------------------------------------------- /src/pages/crontab/logModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal, message, Input, Form } from 'antd'; 3 | import { request } from '@/utils/http'; 4 | import config from '@/utils/config'; 5 | import { 6 | Loading3QuartersOutlined, 7 | CheckCircleOutlined, 8 | } from '@ant-design/icons'; 9 | import { Controlled as CodeMirror } from 'react-codemirror2'; 10 | 11 | enum CrontabStatus { 12 | 'running', 13 | 'idle', 14 | 'disabled', 15 | 'queued', 16 | } 17 | 18 | const CronLogModal = ({ 19 | cron, 20 | handleCancel, 21 | visible, 22 | }: { 23 | cron?: any; 24 | visible: boolean; 25 | handleCancel: () => void; 26 | }) => { 27 | const [value, setValue] = useState('启动中...'); 28 | const [loading, setLoading] = useState(true); 29 | const [excuting, setExcuting] = useState(true); 30 | const [isPhone, setIsPhone] = useState(false); 31 | 32 | const getCronLog = (isFirst?: boolean) => { 33 | if (isFirst) { 34 | setLoading(true); 35 | } 36 | request 37 | .get(`${config.apiPrefix}crons/${cron._id}/log`) 38 | .then((data: any) => { 39 | if (localStorage.getItem('logCron') === cron._id) { 40 | const log = data.data as string; 41 | setValue(log || '暂无日志'); 42 | setExcuting( 43 | log && !log.includes('执行结束') && !log.includes('重启面板'), 44 | ); 45 | if (log && !log.includes('执行结束') && !log.includes('重启面板')) { 46 | setTimeout(() => { 47 | getCronLog(); 48 | }, 2000); 49 | } 50 | if ( 51 | log && 52 | log.includes('重启面板') && 53 | cron.status === CrontabStatus.running 54 | ) { 55 | message.warning({ content: '系统将在5秒后自动刷新', duration: 5 }); 56 | setTimeout(() => { 57 | window.location.reload(); 58 | }, 5000); 59 | } 60 | } 61 | }) 62 | .finally(() => { 63 | if (isFirst) { 64 | setLoading(false); 65 | } 66 | }); 67 | }; 68 | 69 | const cancel = () => { 70 | localStorage.removeItem('logCron'); 71 | handleCancel(); 72 | }; 73 | 74 | const titleElement = () => { 75 | return ( 76 | <> 77 | {excuting && } 78 | {!excuting && } 79 | 日志-{cron && cron.name}{' '} 80 | 81 | ); 82 | }; 83 | 84 | useEffect(() => { 85 | if (cron) { 86 | getCronLog(true); 87 | } 88 | }, [cron]); 89 | 90 | useEffect(() => { 91 | setIsPhone(document.body.clientWidth < 768); 92 | }, []); 93 | 94 | return ( 95 | cancel()} 105 | onCancel={() => cancel()} 106 | > 107 | {!loading && value && ( 108 |
121 |           {value}
122 |         
123 | )} 124 |
125 | ); 126 | }; 127 | 128 | export default CronLogModal; 129 | -------------------------------------------------------------------------------- /src/pages/crontab/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal, message, Input, Form } from 'antd'; 3 | import { request } from '@/utils/http'; 4 | import config from '@/utils/config'; 5 | import cronParse from 'cron-parser'; 6 | 7 | const CronModal = ({ 8 | cron, 9 | handleCancel, 10 | visible, 11 | }: { 12 | cron?: any; 13 | visible: boolean; 14 | handleCancel: (needUpdate?: boolean) => void; 15 | }) => { 16 | const [form] = Form.useForm(); 17 | const [loading, setLoading] = useState(false); 18 | 19 | const handleOk = async (values: any) => { 20 | setLoading(true); 21 | const method = cron ? 'put' : 'post'; 22 | const payload = { ...values }; 23 | if (cron) { 24 | payload._id = cron._id; 25 | } 26 | const { code, data } = await request[method](`${config.apiPrefix}crons`, { 27 | data: payload, 28 | }); 29 | if (code === 200) { 30 | message.success(cron ? '更新Cron成功' : '添加Cron成功'); 31 | } else { 32 | message.error(data); 33 | } 34 | setLoading(false); 35 | handleCancel(data); 36 | }; 37 | 38 | useEffect(() => { 39 | if (cron) { 40 | form.setFieldsValue(cron); 41 | } else { 42 | form.resetFields(); 43 | } 44 | }, [cron]); 45 | 46 | return ( 47 | { 52 | form 53 | .validateFields() 54 | .then((values) => { 55 | handleOk(values); 56 | }) 57 | .catch((info) => { 58 | console.log('Validate Failed:', info); 59 | }); 60 | }} 61 | onCancel={() => handleCancel()} 62 | confirmLoading={loading} 63 | destroyOnClose 64 | > 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | { 79 | if (cronParse.parseExpression(value).hasNext()) { 80 | return Promise.resolve(); 81 | } else { 82 | return Promise.reject('Cron表达式格式有误'); 83 | } 84 | }, 85 | }, 86 | ]} 87 | > 88 | 89 | 90 | 91 |
92 | ); 93 | }; 94 | 95 | export default CronModal; 96 | -------------------------------------------------------------------------------- /src/pages/diff/index.less: -------------------------------------------------------------------------------- 1 | .d2h-files-diff { 2 | height: calc(100vh - 130px); 3 | height: calc(100vh - var(--vh-offset, 0px) - 130px); 4 | overflow: auto; 5 | } 6 | 7 | .d2h-code-side-linenumber { 8 | position: relative; 9 | } 10 | 11 | .d2h-code-side-line { 12 | padding: 0 0.5em; 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/diff/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment, useState, useEffect } from 'react'; 2 | import { Button, message, Modal } from 'antd'; 3 | import config from '@/utils/config'; 4 | import { PageContainer } from '@ant-design/pro-layout'; 5 | import { request } from '@/utils/http'; 6 | import ReactDiffViewer from 'react-diff-viewer'; 7 | import './index.less'; 8 | 9 | const Crontab = () => { 10 | const [width, setWidth] = useState('100%'); 11 | const [marginLeft, setMarginLeft] = useState(0); 12 | const [marginTop, setMarginTop] = useState(-72); 13 | const [value, setValue] = useState(''); 14 | const [sample, setSample] = useState(''); 15 | const [loading, setLoading] = useState(true); 16 | 17 | const getConfig = () => { 18 | request.get(`${config.apiPrefix}config/config`).then((data) => { 19 | setValue(data.data); 20 | }); 21 | }; 22 | 23 | const getSample = () => { 24 | setLoading(true); 25 | request 26 | .get(`${config.apiPrefix}config/sample`) 27 | .then((data) => { 28 | setSample(data.data); 29 | }) 30 | .finally(() => setLoading(false)); 31 | }; 32 | 33 | useEffect(() => { 34 | if (document.body.clientWidth < 768) { 35 | setWidth('auto'); 36 | setMarginLeft(0); 37 | setMarginTop(0); 38 | } else { 39 | setWidth('100%'); 40 | setMarginLeft(0); 41 | setMarginTop(-72); 42 | } 43 | getConfig(); 44 | getSample(); 45 | }, []); 46 | 47 | return ( 48 | 65 | 90 | {/* */} 96 | 97 | ); 98 | }; 99 | 100 | export default Crontab; 101 | -------------------------------------------------------------------------------- /src/pages/diy/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/pages/diy/index.less -------------------------------------------------------------------------------- /src/pages/diy/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment, useState, useEffect } from 'react'; 2 | import { Button, message, Modal } from 'antd'; 3 | import config from '@/utils/config'; 4 | import { PageContainer } from '@ant-design/pro-layout'; 5 | import { Controlled as CodeMirror } from 'react-codemirror2'; 6 | import { request } from '@/utils/http'; 7 | 8 | const Crontab = () => { 9 | const [width, setWidth] = useState('100%'); 10 | const [marginLeft, setMarginLeft] = useState(0); 11 | const [marginTop, setMarginTop] = useState(-72); 12 | const [value, setValue] = useState(''); 13 | const [loading, setLoading] = useState(true); 14 | 15 | const getConfig = () => { 16 | setLoading(true); 17 | request 18 | .get(`${config.apiPrefix}config/extra`) 19 | .then((data) => { 20 | setValue(data.data); 21 | }) 22 | .finally(() => setLoading(false)); 23 | }; 24 | 25 | const updateConfig = () => { 26 | request 27 | .post(`${config.apiPrefix}save`, { 28 | data: { content: value, name: 'extra.sh' }, 29 | }) 30 | .then((data) => { 31 | message.success(data.msg); 32 | }); 33 | }; 34 | 35 | useEffect(() => { 36 | if (document.body.clientWidth < 768) { 37 | setWidth('auto'); 38 | setMarginLeft(0); 39 | setMarginTop(0); 40 | } else { 41 | setWidth('100%'); 42 | setMarginLeft(0); 43 | setMarginTop(-72); 44 | } 45 | getConfig(); 46 | }, []); 47 | 48 | return ( 49 | 54 | 保存 55 | , 56 | ]} 57 | header={{ 58 | style: { 59 | padding: '4px 16px 4px 15px', 60 | position: 'sticky', 61 | top: 0, 62 | left: 0, 63 | zIndex: 20, 64 | marginTop, 65 | width, 66 | marginLeft, 67 | }, 68 | }} 69 | > 70 | { 80 | setValue(value); 81 | }} 82 | onChange={(editor, data, value) => {}} 83 | /> 84 | 85 | ); 86 | }; 87 | 88 | export default Crontab; 89 | -------------------------------------------------------------------------------- /src/pages/log/index.module.less: -------------------------------------------------------------------------------- 1 | @import '~@/styles/variable.less'; 2 | 3 | .left-tree { 4 | &-container { 5 | overflow: hidden; 6 | position: relative; 7 | background-color: #fff; 8 | height: calc(100vh - 128px); 9 | height: calc(100vh - var(--vh-offset, 0px) - 128px); 10 | width: @tree-width; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | &-scroller { 15 | flex: 1; 16 | overflow: auto; 17 | } 18 | &-search { 19 | margin-bottom: 16px; 20 | } 21 | } 22 | 23 | .log-container { 24 | display: flex; 25 | } 26 | 27 | :global { 28 | .log-wrapper { 29 | .ant-pro-grid-content.wide .ant-pro-page-container-children-content { 30 | background-color: #f8f8f8; 31 | } 32 | 33 | .CodeMirror { 34 | width: calc(100% - 32px - @tree-width); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/log/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, Key } from 'react'; 2 | import { TreeSelect, Tree, Input } from 'antd'; 3 | import config from '@/utils/config'; 4 | import { PageContainer } from '@ant-design/pro-layout'; 5 | import { Controlled as CodeMirror } from 'react-codemirror2'; 6 | import { request } from '@/utils/http'; 7 | import styles from './index.module.less'; 8 | 9 | function getFilterData(keyword: string, data: any) { 10 | const expandedKeys: string[] = []; 11 | if (keyword) { 12 | const tree: any = []; 13 | data.forEach((item: any) => { 14 | if (item.title.toLocaleLowerCase().includes(keyword)) { 15 | tree.push(item); 16 | expandedKeys.push(...item.children.map((x: any) => x.key)); 17 | } else { 18 | const children: any[] = []; 19 | (item.children || []).forEach((subItem: any) => { 20 | if (subItem.title.toLocaleLowerCase().includes(keyword)) { 21 | children.push(subItem); 22 | } 23 | }); 24 | if (children.length > 0) { 25 | tree.push({ 26 | ...item, 27 | children, 28 | }); 29 | expandedKeys.push(...children.map((x) => x.key)); 30 | } 31 | } 32 | }); 33 | return { tree, expandedKeys }; 34 | } 35 | return { tree: data, expandedKeys }; 36 | } 37 | 38 | const Log = () => { 39 | const [width, setWidth] = useState('100%'); 40 | const [marginLeft, setMarginLeft] = useState(0); 41 | const [marginTop, setMarginTop] = useState(-72); 42 | const [title, setTitle] = useState('请选择日志文件'); 43 | const [value, setValue] = useState('请选择日志文件'); 44 | const [select, setSelect] = useState(); 45 | const [data, setData] = useState([]); 46 | const [filterData, setFilterData] = useState([]); 47 | const [loading, setLoading] = useState(false); 48 | const [isPhone, setIsPhone] = useState(false); 49 | 50 | const getConfig = () => { 51 | request.get(`${config.apiPrefix}logs`).then((data) => { 52 | const result = formatData(data.dirs) as any; 53 | setData(result); 54 | setFilterData(result); 55 | }); 56 | }; 57 | 58 | const formatData = (tree: any[]) => { 59 | return tree.map((x) => { 60 | x.title = x.name; 61 | x.value = x.name; 62 | x.disabled = x.isDir; 63 | x.key = x.name; 64 | x.children = x.files.map((y: string) => ({ 65 | title: y, 66 | value: `${x.name}/${y}`, 67 | key: `${x.name}/${y}`, 68 | parent: x.name, 69 | isLeaf: true, 70 | })); 71 | return x; 72 | }); 73 | }; 74 | 75 | const getLog = (node: any) => { 76 | setLoading(true); 77 | request 78 | .get(`${config.apiPrefix}logs/${node.value}`) 79 | .then((data) => { 80 | setValue(data.data); 81 | }) 82 | .finally(() => setLoading(false)); 83 | }; 84 | 85 | const onSelect = (value: any, node: any) => { 86 | setSelect(value); 87 | setTitle(node.parent || node.value); 88 | getLog(node); 89 | }; 90 | 91 | const onTreeSelect = useCallback((keys: Key[], e: any) => { 92 | onSelect(keys[0], e.node); 93 | }, []); 94 | 95 | const onSearch = useCallback( 96 | (e) => { 97 | const keyword = e.target.value; 98 | const { tree } = getFilterData(keyword.toLocaleLowerCase(), data); 99 | setFilterData(tree); 100 | }, 101 | [data, setFilterData], 102 | ); 103 | 104 | useEffect(() => { 105 | if (document.body.clientWidth < 768) { 106 | setWidth('auto'); 107 | setMarginLeft(0); 108 | setMarginTop(0); 109 | setIsPhone(true); 110 | } else { 111 | setWidth('100%'); 112 | setMarginLeft(0); 113 | setMarginTop(-72); 114 | setIsPhone(false); 115 | } 116 | getConfig(); 117 | }, []); 118 | 119 | return ( 120 | , 135 | ] 136 | } 137 | header={{ 138 | style: { 139 | padding: '4px 16px 4px 15px', 140 | position: 'sticky', 141 | top: 0, 142 | left: 0, 143 | zIndex: 20, 144 | marginTop, 145 | width, 146 | marginLeft, 147 | }, 148 | }} 149 | > 150 |
151 | {!isPhone && ( 152 |
153 | 157 |
158 | 163 |
164 |
165 | )} 166 | { 177 | setValue(value); 178 | }} 179 | onChange={(editor, data, value) => {}} 180 | /> 181 |
182 |
183 | ); 184 | }; 185 | 186 | export default Log; 187 | -------------------------------------------------------------------------------- /src/pages/login/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | height: calc(100vh - var(--vh-offset, 0px)); 8 | overflow: auto; 9 | background: @layout-body-background; 10 | } 11 | 12 | .lang { 13 | width: 100%; 14 | height: 40px; 15 | line-height: 44px; 16 | text-align: right; 17 | :global(.ant-dropdown-trigger) { 18 | margin-right: 24px; 19 | } 20 | } 21 | 22 | .content { 23 | position: absolute; 24 | top: 45%; 25 | left: 50%; 26 | margin: -160px 0 0 -160px; 27 | width: 320px; 28 | height: 320px; 29 | padding: 36px; 30 | box-shadow: 0 0 100px rgba(0, 0, 0, 0.08); 31 | } 32 | 33 | @media (min-width: @screen-md-min) { 34 | .container { 35 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 36 | background-repeat: no-repeat; 37 | background-position: center 110px; 38 | background-size: 100%; 39 | } 40 | } 41 | 42 | .top { 43 | text-align: center; 44 | } 45 | 46 | .header { 47 | height: 44px; 48 | line-height: 44px; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | 54 | .logo { 55 | width: 30px; 56 | margin-right: 8px; 57 | } 58 | 59 | .desc { 60 | margin-top: 12px; 61 | margin-bottom: 40px; 62 | color: @text-color-secondary; 63 | font-size: @font-size-base; 64 | } 65 | 66 | .main { 67 | margin: 35px auto 0; 68 | @media screen and (max-width: @screen-sm) { 69 | width: 95%; 70 | max-width: 320px; 71 | } 72 | 73 | :global { 74 | .@{ant-prefix}-tabs-nav-list { 75 | margin: auto; 76 | font-size: 16px; 77 | } 78 | } 79 | 80 | .icon { 81 | margin-left: 16px; 82 | color: rgba(0, 0, 0, 0.2); 83 | font-size: 24px; 84 | vertical-align: middle; 85 | cursor: pointer; 86 | transition: color 0.3s; 87 | 88 | &:hover { 89 | color: @primary-color; 90 | } 91 | } 92 | 93 | .other { 94 | margin-top: 24px; 95 | line-height: 22px; 96 | text-align: left; 97 | .register { 98 | float: right; 99 | } 100 | } 101 | 102 | .prefixIcon { 103 | color: @primary-color; 104 | font-size: @font-size-base; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from 'react'; 2 | import { Button, Row, Input, Form, message } from 'antd'; 3 | import config from '@/utils/config'; 4 | import { history, Link } from 'umi'; 5 | import styles from './index.less'; 6 | import { request } from '@/utils/http'; 7 | 8 | const FormItem = Form.Item; 9 | 10 | const Login = () => { 11 | const handleOk = (values: any) => { 12 | request 13 | .post(`${config.apiPrefix}login`, { 14 | data: { 15 | username: values.username, 16 | password: values.password, 17 | }, 18 | }) 19 | .then((data) => { 20 | if (data.code === 200) { 21 | localStorage.setItem(config.authKey, data.token); 22 | history.push('/crontab'); 23 | } else if (data.code === 100) { 24 | message.warn(data.msg); 25 | } else { 26 | message.error(data.msg); 27 | } 28 | }) 29 | .catch(function (error) { 30 | console.log(error); 31 | }); 32 | }; 33 | 34 | useEffect(() => { 35 | const isAuth = localStorage.getItem(config.authKey); 36 | if (isAuth) { 37 | history.push('/crontab'); 38 | } 39 | }, []); 40 | 41 | return ( 42 |
43 |
44 |
45 |
46 | logo 51 | {config.siteName} 52 |
53 |
54 |
55 |
56 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 78 | 79 | 80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Login; 87 | -------------------------------------------------------------------------------- /src/pages/setting/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zy143L/qinglong/a2bc0f675c1e9a0cefe660b43bb864053e2055da/src/pages/setting/index.less -------------------------------------------------------------------------------- /src/pages/setting/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Button, Input, Form, Radio, Tabs } from 'antd'; 3 | import config from '@/utils/config'; 4 | import { PageContainer } from '@ant-design/pro-layout'; 5 | import { request } from '@/utils/http'; 6 | import { 7 | enable as enableDarkMode, 8 | disable as disableDarkMode, 9 | auto as followSystemColorScheme, 10 | setFetchMethod, 11 | } from 'darkreader'; 12 | import { history } from 'umi'; 13 | 14 | const optionsWithDisabled = [ 15 | { label: '亮色', value: 'light' }, 16 | { label: '暗色', value: 'dark' }, 17 | { label: '跟随系统', value: 'auto' }, 18 | ]; 19 | 20 | const Password = () => { 21 | const [width, setWidth] = useState('100%'); 22 | const [marginLeft, setMarginLeft] = useState(0); 23 | const [marginTop, setMarginTop] = useState(-72); 24 | const [value, setValue] = useState(''); 25 | const [loading, setLoading] = useState(true); 26 | const defaultDarken = localStorage.getItem('qinglong_dark_theme') || 'auto'; 27 | const [theme, setTheme] = useState(defaultDarken); 28 | 29 | const handleOk = (values: any) => { 30 | request 31 | .post(`${config.apiPrefix}user`, { 32 | data: { 33 | username: values.username, 34 | password: values.password, 35 | }, 36 | }) 37 | .then((data: any) => { 38 | localStorage.removeItem(config.authKey); 39 | history.push('/login'); 40 | }) 41 | .catch((error: any) => { 42 | console.log(error); 43 | }); 44 | }; 45 | 46 | const themeChange = (e: any) => { 47 | setTheme(e.target.value); 48 | localStorage.setItem('qinglong_dark_theme', e.target.value); 49 | }; 50 | 51 | const importJob = () => { 52 | request.get(`${config.apiPrefix}crons/import`).then((data: any) => { 53 | console.log(data); 54 | }); 55 | }; 56 | 57 | useEffect(() => { 58 | if (document.body.clientWidth < 768) { 59 | setWidth('auto'); 60 | setMarginLeft(0); 61 | setMarginTop(0); 62 | } else { 63 | setWidth('100%'); 64 | setMarginLeft(0); 65 | setMarginTop(-72); 66 | } 67 | }, []); 68 | 69 | useEffect(() => { 70 | setFetchMethod(window.fetch); 71 | if (theme === 'dark') { 72 | enableDarkMode({ 73 | brightness: 100, 74 | contrast: 90, 75 | sepia: 10, 76 | }); 77 | } else if (theme === 'light') { 78 | disableDarkMode(); 79 | } else { 80 | followSystemColorScheme({ 81 | brightness: 100, 82 | contrast: 90, 83 | sepia: 10, 84 | }); 85 | } 86 | }, [theme]); 87 | 88 | return ( 89 | 105 | 111 | 112 |
113 | 120 | 121 | 122 | 129 | 130 | 131 | 134 | 135 |
136 | 137 |
138 | 139 | 146 | 147 | 148 |
149 |
150 |
151 | ); 152 | }; 153 | 154 | export default Password; 155 | -------------------------------------------------------------------------------- /src/styles/variable.less: -------------------------------------------------------------------------------- 1 | @tree-width: 300px; 2 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | siteName: '青龙控制面板', 3 | apiPrefix: '/api/', 4 | authKey: 'token', 5 | 6 | /* Layout configuration, specify which layout to use for route. */ 7 | layouts: [ 8 | { 9 | name: 'primary', 10 | include: [/.*/], 11 | exclude: [/(\/(en|zh))*\/login/], 12 | }, 13 | ], 14 | 15 | /* I18n configuration, `languages` and `defaultLanguage` are required currently. */ 16 | i18n: { 17 | /* Countrys flags: https://www.flaticon.com/packs/countrys-flags */ 18 | languages: [ 19 | { 20 | key: 'pt-br', 21 | title: 'Português', 22 | flag: '/portugal.svg', 23 | }, 24 | { 25 | key: 'en', 26 | title: 'English', 27 | flag: '/america.svg', 28 | }, 29 | { 30 | key: 'zh', 31 | title: '中文', 32 | flag: '/china.svg', 33 | }, 34 | ], 35 | defaultLanguage: 'en', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'umi-request'; 2 | import { message } from 'antd'; 3 | import config from './config'; 4 | 5 | message.config({ 6 | duration: 1.5, 7 | }); 8 | 9 | const time = Date.now(); 10 | const errorHandler = function (error: any) { 11 | if (error.response) { 12 | const message = error.data 13 | ? error.data.message || error.data 14 | : error.response.statusText; 15 | if (error.response.status !== 401 && error.response.status !== 502) { 16 | message.error(message); 17 | } else { 18 | console.log(error.response); 19 | } 20 | } else { 21 | console.log(error.message); 22 | } 23 | 24 | throw error; // 如果throw. 错误将继续抛出. 25 | }; 26 | 27 | const _request = extend({ timeout: 60000, params: { t: time }, errorHandler }); 28 | 29 | _request.interceptors.request.use((url, options) => { 30 | const token = localStorage.getItem(config.authKey); 31 | if (token) { 32 | const headers = { 33 | Authorization: `Bearer ${token}`, 34 | }; 35 | return { url, options: { ...options, headers } }; 36 | } 37 | return { url, options }; 38 | }); 39 | 40 | _request.interceptors.response.use(async (response) => { 41 | const res = await response.clone(); 42 | return response; 43 | }); 44 | 45 | export const request = _request; 46 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = 'v2.2.0-end'; 2 | export const changeLog = 'https://t.me/jiaolongwang/109'; 3 | -------------------------------------------------------------------------------- /tsconfig.back.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["es2017", "esnext.asynciterable", "DOM"], 5 | "typeRoots": [ 6 | "./node_modules/@types", 7 | "./src/types", 8 | "./node_modules/celebrate/lib/index.d.ts" 9 | ], 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "module": "commonjs", 16 | "pretty": true, 17 | "sourceMap": true, 18 | "outDir": "./build", 19 | "allowJs": true, 20 | "noEmit": false, 21 | "esModuleInterop": true 22 | }, 23 | "include": ["./back/**/*"], 24 | "exclude": ["node_modules", "tests"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react-jsx", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"] 15 | }, 16 | "lib": ["dom", "es2017", "esnext.asynciterable"], 17 | "typeRoots": [ 18 | "./node_modules/@types", 19 | "./back/types", 20 | "./node_modules/celebrate/lib/index.d.ts" 21 | ], 22 | "allowSyntheticDefaultImports": true, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "pretty": true, 27 | "allowJs": true, 28 | "noEmit": false 29 | }, 30 | "include": ["src/**/*", "config/**/*", ".umirc.ts", "typings.d.ts"], 31 | "exclude": [ 32 | "node_modules", 33 | "lib", 34 | "es", 35 | "dist", 36 | "typings", 37 | "**/__test__", 38 | "test", 39 | "docs", 40 | "tests" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | export function ReactComponent(props: React.SVGProps): React.ReactElement 6 | const url: string 7 | export default url 8 | } 9 | --------------------------------------------------------------------------------