├── .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 |
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 |
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 |

51 |
{config.siteName}
52 |
53 |
54 |
55 |
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 |
120 |
121 |
122 |
129 |
130 |
131 |
134 |
135 |
136 |
137 |
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 |
--------------------------------------------------------------------------------