mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 11:22:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa6d57d809 | ||
|
|
1060fc7476 | ||
|
|
56bc38e9e7 | ||
|
|
18be65d6fe | ||
|
|
3409085195 | ||
|
|
17e17ac077 | ||
|
|
c655e1da38 | ||
|
|
0ea158724f | ||
|
|
fb521498ff | ||
|
|
399550ccad | ||
|
|
c7c30c86f2 | ||
|
|
5f6c0882d6 | ||
|
|
6e05a4f3a5 | ||
|
|
ea8aa6a231 | ||
|
|
ae2570c677 | ||
|
|
c76d952d41 | ||
|
|
cdf9fb24be | ||
|
|
2131e16781 | ||
|
|
b0c8425639 | ||
|
|
f0869567de | ||
|
|
be2a19949b | ||
|
|
8627cccb58 | ||
|
|
4a8eaebf17 | ||
|
|
e7edd0a5a4 | ||
|
|
9ccbea0a04 | ||
|
|
ab715ef840 | ||
|
|
117822fe7c | ||
|
|
da922678a1 | ||
|
|
133e9e0e1f | ||
|
|
7cc6081d15 | ||
|
|
5a24f62a62 | ||
|
|
be597355a7 | ||
|
|
51afcb6c5b | ||
|
|
92ebb70eaa | ||
|
|
ab19f30342 | ||
|
|
fc9bc7bea2 | ||
|
|
793da29f22 | ||
|
|
5afe4da607 | ||
|
|
a7a56c03ce | ||
|
|
7639b588d8 | ||
|
|
1d536cdd53 | ||
|
|
9f08245b6d | ||
|
|
ff9bf20e57 | ||
|
|
b201730100 | ||
|
|
6ca098f9e2 | ||
|
|
e0eef05f5c | ||
|
|
6398e5ec70 | ||
|
|
6f93219c44 | ||
|
|
9b957ac019 | ||
|
|
e0d1b127aa | ||
|
|
64072b3435 | ||
|
|
f81f7720f0 | ||
|
|
69fab610e6 | ||
|
|
d693e1f9ea | ||
|
|
2a9ca67355 | ||
|
|
3fb01860d4 | ||
|
|
d92a46d09c | ||
|
|
ec605bfba4 | ||
|
|
d2f999a5e2 | ||
|
|
627f541f17 | ||
|
|
c457dca555 | ||
|
|
ff3ec7395e | ||
|
|
fdaba146ca | ||
|
|
31a6d30c23 | ||
|
|
fa3b185fd5 | ||
|
|
101d4fb497 | ||
|
|
ccd308f5b0 | ||
|
|
999d9f8e01 | ||
|
|
313db0d74e | ||
|
|
634e9705a7 | ||
|
|
c959cec738 | ||
|
|
95330e04a3 | ||
|
|
a099d09af7 | ||
|
|
a0d0197ad7 | ||
|
|
d49e711a34 | ||
|
|
e3b8cd75b9 | ||
|
|
7a240eaabb | ||
|
|
01564e96d3 | ||
|
|
03f4b5c219 | ||
|
|
99e1d44f7d | ||
|
|
8c55659c18 | ||
|
|
3ea980b2f8 | ||
|
|
f5fc987b51 | ||
|
|
9cfa4af2b2 | ||
|
|
3efdf1fa87 | ||
|
|
25fc832605 | ||
|
|
a7e404c5d5 | ||
|
|
90b27f0564 | ||
|
|
632440965d | ||
|
|
aedfac446b | ||
|
|
3372b0025a | ||
|
|
0112b92e77 | ||
|
|
0caf53b147 | ||
|
|
4d1354d5f4 | ||
|
|
fc4562eca2 | ||
|
|
5b048a5510 | ||
|
|
d23049b2fe | ||
|
|
caac5607b9 | ||
|
|
fb2e69b12d | ||
|
|
fc1838ff39 | ||
|
|
25689cf40f | ||
|
|
c746a3d215 | ||
|
|
548d924fa0 | ||
|
|
18a6be3ecd | ||
|
|
afef0cb72f | ||
|
|
1221b47eca | ||
|
|
a356eeb5da | ||
|
|
25bb0972b9 | ||
|
|
0914075cf6 | ||
|
|
74b258e1b6 | ||
|
|
27eb0a21e4 | ||
|
|
d77c6d50d8 | ||
|
|
6a64c18a7b | ||
|
|
b97ff714ae | ||
|
|
1f23eea805 | ||
|
|
cb0cb7df21 | ||
|
|
f3e8628664 | ||
|
|
4758471f9f | ||
|
|
c10bd5a549 | ||
|
|
d53faf96eb | ||
|
|
2307b8ed1a | ||
|
|
711af8d4a5 | ||
|
|
458c9c456b | ||
|
|
29069df48c | ||
|
|
300e84ae2c | ||
|
|
bf11768b92 | ||
|
|
91bd1d9263 | ||
|
|
ac40f9ff12 | ||
|
|
f930837220 | ||
|
|
1d5e3acc93 | ||
|
|
2aec1f9846 | ||
|
|
b56a681b98 | ||
|
|
2713419b32 | ||
|
|
764a3492cc | ||
|
|
29fc471ac4 | ||
|
|
54a597aad3 | ||
|
|
778961d0b6 | ||
|
|
14b01ac761 | ||
|
|
f87848c05b | ||
|
|
153e5a806d | ||
|
|
ee1b8a906f | ||
|
|
734685d45a | ||
|
|
e3b540c212 | ||
|
|
201f578bbc | ||
|
|
ce9aa369d2 | ||
|
|
c5c41c0859 | ||
|
|
d3009f82dd | ||
|
|
0eb9a1fdb0 | ||
|
|
83f6d9926a | ||
|
|
14134842b0 | ||
|
|
4c094c4710 | ||
|
|
3ac41075fd | ||
|
|
fdc97773d1 | ||
|
|
917a739595 | ||
|
|
21475f81ae | ||
|
|
a2a9a0560c | ||
|
|
b879846cdb | ||
|
|
a24011cd72 | ||
|
|
410dd2ecdc | ||
|
|
96b111b894 | ||
|
|
630572c8f9 | ||
|
|
6a381a6366 | ||
|
|
fd04f12677 | ||
|
|
86b8cd17a4 | ||
|
|
bb8ee01693 | ||
|
|
5fb0bca3af | ||
|
|
1e4d1f373d | ||
|
|
87c78d0fcc | ||
|
|
041d2b7947 | ||
|
|
5e7b90d995 | ||
|
|
fadc3dcb55 | ||
|
|
4eea091944 | ||
|
|
9f8d46f395 | ||
|
|
2c54a057c9 | ||
|
|
e1e5cacc6d | ||
|
|
edf9066967 | ||
|
|
7097d12b87 | ||
|
|
08aaeb6372 | ||
|
|
4310f80ec0 | ||
|
|
01577f6494 | ||
|
|
9aa570b2c6 | ||
|
|
d611d7d698 | ||
|
|
f05549b06c | ||
|
|
8c4db8dd40 | ||
|
|
ddee69d291 | ||
|
|
28960c9fab | ||
|
|
449301e519 | ||
|
|
28dab3dc2f | ||
|
|
7995d98db5 | ||
|
|
1f319f3533 | ||
|
|
7d2dbaa62d | ||
|
|
ed5999526b | ||
|
|
2409f75491 | ||
|
|
0c840095d1 | ||
|
|
3eaf0b4856 | ||
|
|
394735a5d9 | ||
|
|
5bfa10bfe7 | ||
|
|
3f12d9cbd5 | ||
|
|
54d89754df | ||
|
|
9470524615 | ||
|
|
b5faa6eaaa | ||
|
|
eab476cfe8 | ||
|
|
b773def012 | ||
|
|
a9f8b23a7a | ||
|
|
61bb6535fb | ||
|
|
eb0bdffe06 | ||
|
|
e129485285 | ||
|
|
aac7de172e | ||
|
|
e19d2412fc | ||
|
|
7cba2cf76c | ||
|
|
55d7a49dbc | ||
|
|
f695864fd8 | ||
|
|
ae4883fbe7 | ||
|
|
836dfd2861 | ||
|
|
6e0523c6d7 | ||
|
|
cd9ba084ae | ||
|
|
c8a0c0de67 | ||
|
|
5dd8d9df91 | ||
|
|
2dbee94e85 | ||
|
|
9001797037 | ||
|
|
5e7104d5a3 | ||
|
|
7c33b22d63 | ||
|
|
b7cb1e379c | ||
|
|
274a6ce52e | ||
|
|
8079e4e7b8 | ||
|
|
920efb8fab | ||
|
|
92aeb2b9da | ||
|
|
ced76cf343 | ||
|
|
3750d6ecd5 | ||
|
|
7444f30384 | ||
|
|
b0fd5ba47f | ||
|
|
cc098bc2e5 | ||
|
|
1cfcf8d194 | ||
|
|
d1e9f741d0 | ||
|
|
0cde9d07ee | ||
|
|
a266425348 | ||
|
|
6dbe02e321 | ||
|
|
39979879c9 | ||
|
|
ba3f604a5c | ||
|
|
9572582401 | ||
|
|
2bcef0ba75 | ||
|
|
31cd7c4007 | ||
|
|
1c4c1799b0 | ||
|
|
2d558df15e | ||
|
|
7b769da4e2 |
+7
-4
@@ -1,9 +1,12 @@
|
||||
MONGODB_URI='mongodb://'
|
||||
YIYAN_MONGODB_URI=''
|
||||
|
||||
PUBLIC_PORT=5400
|
||||
CRON_PORT=5500
|
||||
PORT=5600
|
||||
BACK_PORT=5600
|
||||
PORT=5700
|
||||
|
||||
LOG_LEVEL='debug'
|
||||
|
||||
SECRET='whyour'
|
||||
|
||||
QINIU_AK = ''
|
||||
QINIU_SK = ''
|
||||
QINIU_SCOPE = ''
|
||||
@@ -27,18 +27,19 @@ jobs:
|
||||
|
||||
- name: build front and back
|
||||
run: |
|
||||
yarn install
|
||||
yarn build
|
||||
yarn build-back
|
||||
npm i -g pnpm
|
||||
pnpm install
|
||||
pnpm build:front
|
||||
pnpm build:back
|
||||
|
||||
- name: copy to static repo
|
||||
env:
|
||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
mkdir -p static
|
||||
cd ./static
|
||||
cp -rf ../dist ./ && cp -rf ../build ./
|
||||
mkdir -p tmp
|
||||
cd ./tmp
|
||||
cp -rf ../static/* ./
|
||||
git init -b ${GITHUB_BRANCH} && git add .
|
||||
git config --local user.name 'github-actions[bot]'
|
||||
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
@@ -56,6 +57,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Set time zone
|
||||
uses: szenius/set-timezone@v1.0
|
||||
@@ -105,8 +109,10 @@ jobs:
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: MAINTAINER=${{ github.repository_owner }}, QL_BRANCH=${{ github.ref_name }}
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
||||
build-args: |
|
||||
MAINTAINER=${{ github.repository_owner }}
|
||||
QL_BRANCH=${{ github.ref_name }}
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
context: docker/
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
+3
-2
@@ -4,12 +4,13 @@
|
||||
/node_modules
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/pnpm-lock.yaml
|
||||
/package-lock.json
|
||||
|
||||
# production
|
||||
/dist
|
||||
/build
|
||||
/static
|
||||
/data
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -4,15 +4,22 @@ const CompressionPlugin = require('compression-webpack-plugin');
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
layout: false,
|
||||
outputPath: 'static/dist',
|
||||
nodeModulesTransform: {
|
||||
type: 'none',
|
||||
},
|
||||
fastRefresh: {},
|
||||
esbuild: {},
|
||||
webpack5: {},
|
||||
dynamicImport: {
|
||||
loading: '@/components/pageLoading',
|
||||
},
|
||||
favicon: '/images/g5.ico',
|
||||
proxy: {
|
||||
'/api/public': {
|
||||
target: 'http://127.0.0.1:5400/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5600/',
|
||||
changeOrigin: true,
|
||||
@@ -32,18 +39,9 @@ export default defineConfig({
|
||||
externals: {
|
||||
react: 'window.React',
|
||||
'react-dom': 'window.ReactDOM',
|
||||
darkreader: 'window.DarkReader',
|
||||
codemirror: 'window.CodeMirror',
|
||||
'sockjs-client': 'window.SockJS',
|
||||
},
|
||||
scripts: [
|
||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
||||
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
|
||||
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
|
||||
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
|
||||
],
|
||||
});
|
||||
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
# 安装说明
|
||||
|
||||
## 前置要求
|
||||
```bash
|
||||
1.已安装docker-ce
|
||||
2.选装docker-compose
|
||||
```
|
||||
## 安装方式1
|
||||
```bash
|
||||
1. 新建一个文件夹,用于存放相关数据
|
||||
2. 下载本仓库中的`docker-compose.yml`至本地,或是复制文件内容后在本地自行建立并粘贴内容
|
||||
3. 使用docker-compose启动
|
||||
4. 浏览器输入ip:5700即可进入面板
|
||||
|
||||
# 新建数据文件夹
|
||||
mkdir qinglong
|
||||
cd qinglong
|
||||
# 下载docker-compose.yml文件
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/develop/docker-compose.yml
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 安装方式2
|
||||
```bash
|
||||
# 复制下列命令在ssh执行(先安装docker)
|
||||
# 注:$PWD请修改为实际你想安装的路径
|
||||
docker run -dit \
|
||||
-v $PWD/ql/config:/ql/config \
|
||||
-v $PWD/ql/log:/ql/log \
|
||||
-v $PWD/ql/db:/ql/db \
|
||||
-v $PWD/ql/repo:/ql/repo \
|
||||
-v $PWD/ql/raw:/ql/raw \
|
||||
-v $PWD/ql/scripts:/ql/scripts \
|
||||
-v $PWD/ql/jbot:/ql/jbot \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
## 登录
|
||||
```bash
|
||||
|
||||
打开浏览器访问宿主机ip的5700端口即可
|
||||
例如http://192.168.100.123:5700即ip:5700
|
||||
|
||||
首次登录
|
||||
账号:admin 密码:admin
|
||||
会生成`auth.json`
|
||||
|
||||
在ssh输入
|
||||
1.docker exec -it qinglong bash
|
||||
2.cat /ql/config/auth.json
|
||||
|
||||
cat查看之后返回的结果类似如下字段
|
||||
|
||||
{"username":"admin","password":"Xb-ZYP526wmg4_h6q1WqIO"}
|
||||
# admin即为登录名;Xb-ZYP526wmg4_h6q1WqIO为登录密码
|
||||
```
|
||||
|
||||
输入此处记录的`username`及`password`,即可成功登录qinglong面板,登录后即可正常使用
|
||||
|
||||
## 拉取脚本
|
||||
```bash
|
||||
示例
|
||||
ql repo https://github.com/xxx.git #拉取仓库
|
||||
ql raw https://raw.githubusercontent.com/xxx #拉取单个脚本
|
||||
```
|
||||
## 备份
|
||||
|
||||
所有数据都将保存在`docker-compose.yml`所在的同级目录的`data`文件夹中,如需要备份,请直接备份`docker-compose.yml`及`data`文件夹即可
|
||||
|
||||
```bash
|
||||
root@debian:/opt/qinglong# ls -lah
|
||||
总用量 8.0K
|
||||
drwxr-xr-x 3 root root 4.0K 8月 30 01:29 .
|
||||
drwxr-xr-x 4 root root 4.0K 8月 30 00:51 ..
|
||||
drwxr-xr-x 8 root root 4.0K 8月 30 01:30 data
|
||||
-rw-r--r-- 1 root root 386 8月 30 01:29 docker-compose.yml
|
||||
```
|
||||
## 更新
|
||||
|
||||
在面板执行"更新面板"任务即可
|
||||
|
||||
或者
|
||||
```bash
|
||||
cd qinglong
|
||||
docker-compose down
|
||||
docker pull whyour/qinglong:latest
|
||||
docker-compose up -d
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="/public/images/qinglong.png">
|
||||
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
A timed task management panel that supports typescript, javaScript, python3, and shell.(支持python3、javaScript、shell、typescript 的定时任务管理面板)
|
||||
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.)
|
||||
|
||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url]
|
||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
|
||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
|
||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||
@@ -24,31 +22,168 @@ A timed task management panel that supports typescript, javaScript, python3, and
|
||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<img width="49%" src="/public/images/login.png">
|
||||
<img width="49%" src="/public/images/home.png">
|
||||
</p>
|
||||
[](https://whyour.cn)
|
||||
|
||||
简体中文 | [English](./README-en.md)
|
||||
|
||||
## 功能
|
||||
|
||||
- 支持多种脚本语言(python3、javaScript、shell、typescript)
|
||||
- 支持在线管理脚本、环境变量、配置文件
|
||||
- 支持在线查看任务日志
|
||||
- 支持秒级任务设置
|
||||
- 支持系统级通知
|
||||
- 支持暗黑模式
|
||||
- 支持手机端操作
|
||||
|
||||
## 部署
|
||||
|
||||
### 本机部署
|
||||
|
||||
```bash
|
||||
# 待完善
|
||||
```
|
||||
|
||||
### podman 部署
|
||||
|
||||
1. podman 安装
|
||||
|
||||
```bash
|
||||
https://podman.io/getting-started/installation
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
|
||||
```bash
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### docker 部署
|
||||
|
||||
1. docker 安装
|
||||
|
||||
```bash
|
||||
sudo curl -sSL get.docker.com | sh
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
|
||||
```bash
|
||||
docker run -dit \
|
||||
-v $PWD/ql:/ql/data \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### docker-compose 部署
|
||||
|
||||
1. docker-compose 安装
|
||||
|
||||
```bash
|
||||
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
|
||||
```bash
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
# 停止
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
1. 内置命令
|
||||
|
||||
```bash
|
||||
# 更新并重启青龙
|
||||
ql update
|
||||
# 运行自定义脚本extra.sh
|
||||
ql extra
|
||||
# 添加单个脚本文件
|
||||
ql raw <file_url>
|
||||
# 添加单个仓库的指定脚本
|
||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||
# 删除旧日志
|
||||
ql rmlog <days>
|
||||
# 启动tg-bot
|
||||
ql bot
|
||||
# 检测青龙环境并修复
|
||||
ql check
|
||||
# 重置登录错误次数
|
||||
ql resetlet
|
||||
# 禁用两步登录
|
||||
ql resettfa
|
||||
|
||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||
task <file_path>
|
||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||
task <file_path> now
|
||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||
task <file_path> conc <env_name> <account_number>(可选的)
|
||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||
task <file_path> desi <env_name> <account_number>
|
||||
```
|
||||
|
||||
2. 参数说明
|
||||
|
||||
* file_url: 脚本地址
|
||||
* repo_url: 仓库地址
|
||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
|
||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
|
||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
|
||||
* branch: 拉取仓库的分支
|
||||
* days: 需要保留的日志的天数
|
||||
* file_path: 任务执行时的文件路径
|
||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||
|
||||
## 链接
|
||||
|
||||
- [nevinee](https://gitee.com/evine)
|
||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
- [Ant Design](https://ant.design)
|
||||
- [Ant Design Pro](https://pro.ant.design/)
|
||||
- [Umijs3.0](https://umijs.org)
|
||||
- [darkreader](https://github.com/darkreader/darkreader)
|
||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:whyour/qinglong.git
|
||||
$ cd qinglong
|
||||
$ cp .env.example .env
|
||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||
$ npm install -g pnpm
|
||||
$ pnpm install
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
打开你的浏览器,访问 http://127.0.0.1:5700
|
||||
|
||||
## 交流
|
||||
|
||||
[telegram频道](https://t.me/jiao_long)
|
||||
|
||||
## 名称来源
|
||||
|
||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||
|
||||
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
|
||||
|
||||
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
||||
|
||||
## 如何安装
|
||||
|
||||
请查看[INSTALL.md](INSTALL.md)
|
||||
|
||||
## 多谢
|
||||
|
||||
* [nevinee](https://gitee.com/evine)
|
||||
|
||||
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
|
||||
* [Ant Design](https://ant.design)
|
||||
|
||||
* [Ant Design Pro](https://pro.ant.design/)
|
||||
|
||||
* [Umijs3.0](https://umijs.org)
|
||||
|
||||
* [darkreader](https://github.com/darkreader/darkreader)
|
||||
|
||||
+4
-4
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
app.use('/configs', route);
|
||||
|
||||
route.get(
|
||||
'/configs/files',
|
||||
'/files',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -32,7 +32,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/configs/:file',
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -58,7 +58,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/configs/save',
|
||||
'/save',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
|
||||
+108
-45
@@ -7,31 +7,28 @@ import cron_parser from 'cron-parser';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.get(
|
||||
'/crons',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.crontabs(
|
||||
req.query.searchValue as string,
|
||||
);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
app.use('/crons', route);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.crontabs(req.query.searchValue as string);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
route.post(
|
||||
'/crons',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().optional(),
|
||||
labels: Joi.array().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -52,9 +49,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/run',
|
||||
'/run',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -70,9 +67,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/stop',
|
||||
'/stop',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -87,10 +84,55 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/disable',
|
||||
route.delete(
|
||||
'/labels',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.object({
|
||||
ids: Joi.array().items(Joi.number().required()),
|
||||
labels: Joi.array().items(Joi.string().required()),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.removeLabels(
|
||||
req.body.ids,
|
||||
req.body.labels,
|
||||
);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/labels',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
ids: Joi.array().items(Joi.number().required()),
|
||||
labels: Joi.array().items(Joi.string().required()),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.addLabels(req.body.ids, req.body.labels);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/disable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -106,9 +148,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/enable',
|
||||
'/enable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -124,13 +166,13 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/log',
|
||||
'/:id/log',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
@@ -144,13 +186,14 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
labels: Joi.array().optional().allow(null),
|
||||
command: Joi.string().optional(),
|
||||
schedule: Joi.string().optional(),
|
||||
name: Joi.string().optional(),
|
||||
_id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -174,9 +217,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/crons',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -192,9 +235,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/pin',
|
||||
'/pin',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -210,9 +253,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/unpin',
|
||||
'/unpin',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -228,7 +271,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/import',
|
||||
'/import',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -243,17 +286,17 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id',
|
||||
'/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.get(req.params.id);
|
||||
const data = await cronService.getDb({ id: req.params.id });
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -263,10 +306,10 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons/status',
|
||||
'/status',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
ids: Joi.array().items(Joi.string().required()),
|
||||
ids: Joi.array().items(Joi.number().required()),
|
||||
status: Joi.string().required(),
|
||||
pid: Joi.string().optional(),
|
||||
log_path: Joi.string().optional(),
|
||||
@@ -290,4 +333,24 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:id/logs',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.logs(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+28
-30
@@ -6,30 +6,28 @@ import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.get(
|
||||
'/dependencies',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const data = await dependenceService.dependencies(req.query as any);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
app.use('/dependencies', route);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const data = await dependenceService.dependencies(req.query as any);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
route.post(
|
||||
'/dependencies',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
type: Joi.number().required(),
|
||||
remark: Joi.number().optional().allow(''),
|
||||
remark: Joi.string().optional().allow(''),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
@@ -47,13 +45,13 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/dependencies',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
_id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
type: Joi.number().required(),
|
||||
remark: Joi.number().optional().allow(''),
|
||||
remark: Joi.string().optional().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -70,9 +68,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/dependencies',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -88,9 +86,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/dependencies/force',
|
||||
'/force',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -106,17 +104,17 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/dependencies/:id',
|
||||
'/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const data = await dependenceService.get(req.params.id);
|
||||
const data = await dependenceService.getDb({ id: req.params.id });
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -126,9 +124,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/dependencies/reinstall',
|
||||
'/reinstall',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
|
||||
+32
-34
@@ -6,24 +6,22 @@ import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.get(
|
||||
'/envs',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.envs(req.query.searchValue as string);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
app.use('/envs', route);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.envs(req.query.searchValue as string);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
route.post(
|
||||
'/envs',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(
|
||||
Joi.object({
|
||||
@@ -47,13 +45,13 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
value: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
remarks: Joi.string().optional().allow(''),
|
||||
_id: Joi.string().required(),
|
||||
remarks: Joi.string().optional().allow('').allow(null),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -70,9 +68,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/envs',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -88,17 +86,17 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/:id/move',
|
||||
'/:id/move',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
body: Joi.object({
|
||||
fromIndex: Joi.number().required(),
|
||||
toIndex: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
@@ -112,9 +110,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/disable',
|
||||
'/disable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -130,9 +128,9 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/enable',
|
||||
'/enable',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -148,10 +146,10 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/envs/name',
|
||||
'/name',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
ids: Joi.array().items(Joi.string().required()),
|
||||
ids: Joi.array().items(Joi.number().required()),
|
||||
name: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
@@ -169,17 +167,17 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/envs/:id',
|
||||
'/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.get(req.params.id);
|
||||
const data = await envService.getDb(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
|
||||
@@ -7,6 +7,7 @@ import cron from './cron';
|
||||
import script from './script';
|
||||
import open from './open';
|
||||
import dependence from './dependence';
|
||||
import system from './system';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
@@ -18,6 +19,7 @@ export default () => {
|
||||
script(app);
|
||||
open(app);
|
||||
dependence(app);
|
||||
system(app);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
+33
-35
@@ -7,44 +7,42 @@ import { getFileContentByName } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.get(
|
||||
'/logs',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const fileList = fs.readdirSync(config.logPath, 'utf-8');
|
||||
const dirs = [];
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const stat = fs.lstatSync(config.logPath + fileList[i]);
|
||||
if (stat.isDirectory()) {
|
||||
const fileListTmp = fs.readdirSync(
|
||||
`${config.logPath}/${fileList[i]}`,
|
||||
'utf-8',
|
||||
);
|
||||
dirs.push({
|
||||
name: fileList[i],
|
||||
isDir: true,
|
||||
files: fileListTmp.reverse(),
|
||||
});
|
||||
} else {
|
||||
dirs.push({
|
||||
name: fileList[i],
|
||||
isDir: false,
|
||||
files: [],
|
||||
});
|
||||
}
|
||||
app.use('/logs', route);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const fileList = fs.readdirSync(config.logPath, 'utf-8');
|
||||
const dirs = [];
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const stat = fs.lstatSync(config.logPath + fileList[i]);
|
||||
if (stat.isDirectory()) {
|
||||
const fileListTmp = fs.readdirSync(
|
||||
`${config.logPath}/${fileList[i]}`,
|
||||
'utf-8',
|
||||
);
|
||||
dirs.push({
|
||||
name: fileList[i],
|
||||
isDir: true,
|
||||
files: fileListTmp.reverse(),
|
||||
});
|
||||
} else {
|
||||
dirs.push({
|
||||
name: fileList[i],
|
||||
isDir: false,
|
||||
files: [],
|
||||
});
|
||||
}
|
||||
res.send({ code: 200, dirs });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
res.send({ code: 200, dirs });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/logs/:dir/:file',
|
||||
'/:dir/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -61,7 +59,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/logs/:file',
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
|
||||
+4
-4
@@ -49,7 +49,7 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
name: Joi.string().optional().allow(''),
|
||||
scopes: Joi.array().items(Joi.string()),
|
||||
_id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -68,7 +68,7 @@ export default (app: Router) => {
|
||||
route.delete(
|
||||
'/apps',
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.string().required()),
|
||||
body: Joi.array().items(Joi.number().required()),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
@@ -87,10 +87,10 @@ export default (app: Router) => {
|
||||
'/apps/:id/reset-secret',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const openService = Container.get(OpenService);
|
||||
|
||||
+77
-17
@@ -9,14 +9,15 @@ import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import path from 'path';
|
||||
import path, { join } from 'path';
|
||||
import ScriptService from '../services/script';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
app.use('/scripts', route);
|
||||
|
||||
route.get(
|
||||
'/scripts/files',
|
||||
'/files',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -43,7 +44,7 @@ export default (app: Router) => {
|
||||
children.push({
|
||||
title: childFile,
|
||||
value: childFile,
|
||||
key: childFile,
|
||||
key: `${fileOrDir}/${childFile}`,
|
||||
mtime: statObj.mtimeMs,
|
||||
parent: fileOrDir,
|
||||
});
|
||||
@@ -79,14 +80,16 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/scripts/:file',
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const path = req.query.path ? `${req.query.path}/` : '';
|
||||
const content = getFileContentByName(
|
||||
`${config.scriptPath}${path}${req.params.file}`,
|
||||
const filePath = join(
|
||||
config.scriptPath,
|
||||
req.query.path as string,
|
||||
req.params.file,
|
||||
);
|
||||
const content = getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -96,11 +99,11 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/scripts',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
content: Joi.string().allow(''),
|
||||
originFilename: Joi.string().allow(''),
|
||||
}),
|
||||
@@ -120,6 +123,9 @@ export default (app: Router) => {
|
||||
if (!path.endsWith('/')) {
|
||||
path += '/';
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
path = `${config.scriptPath}${path}`;
|
||||
}
|
||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||
return res.send({
|
||||
code: 430,
|
||||
@@ -153,11 +159,11 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/scripts',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
content: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
@@ -169,7 +175,7 @@ export default (app: Router) => {
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${path}/${filename}`;
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
fs.writeFileSync(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
@@ -180,19 +186,21 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/scripts',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${filename}`;
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
fs.unlinkSync(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
@@ -203,7 +211,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/scripts/download',
|
||||
'/download',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
@@ -232,4 +240,56 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/run',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().optional().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(path, filename);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.runScript(filePath);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/stop',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().optional().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(path, filename);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.stopScript(filePath);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import SystemService from '../services/system';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import UserService from '../services/user';
|
||||
import { EnvModel } from '../data/env';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/system', route);
|
||||
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envCount = await EnvModel.count();
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const version = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envCount === 0
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
isInitialized,
|
||||
version,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/log/remove',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const data = await systemService.getLogRemoveFrequency();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/log/remove',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
frequency: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateLogRemoveFrequency(
|
||||
req.body.frequency,
|
||||
);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/update-check',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.checkUpdate();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/update',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateSystem();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/notify',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
title: Joi.string().required(),
|
||||
content: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.notify(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
+29
-130
@@ -9,7 +9,8 @@ import { getFileContentByName } from '../config/util';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
app.use('/user', route);
|
||||
|
||||
route.post(
|
||||
'/login',
|
||||
celebrate({
|
||||
@@ -47,7 +48,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user',
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
username: Joi.string().required(),
|
||||
@@ -67,29 +68,26 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
username: authInfo.username,
|
||||
twoFactorActivated: authInfo.twoFactorActivated,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
username: authInfo.username,
|
||||
twoFactorActivated: authInfo.twoFactorActivated,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
route.get(
|
||||
'/user/two-factor/init',
|
||||
'/two-factor/init',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -104,7 +102,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/two-factor/active',
|
||||
'/two-factor/active',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
code: Joi.string().required(),
|
||||
@@ -124,7 +122,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/two-factor/deactive',
|
||||
'/two-factor/deactive',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -139,7 +137,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/two-factor/login',
|
||||
'/two-factor/login',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
code: Joi.string().required(),
|
||||
@@ -161,7 +159,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/login-log',
|
||||
'/login-log',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -176,7 +174,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/notification',
|
||||
'/notification',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -191,7 +189,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/notification',
|
||||
'/notification',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -205,40 +203,8 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/system',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envDbContent = getFileContentByName(config.envDbFile);
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envDbContent.length === 0
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
isInitialized,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 初始化api
|
||||
route.put(
|
||||
'/init/user',
|
||||
'/init',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
username: Joi.string().required(),
|
||||
@@ -259,7 +225,7 @@ export default (app: Router) => {
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/init/notification',
|
||||
'/notification/init',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
@@ -272,71 +238,4 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/system/log/remove',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const data = await userService.getLogRemoveFrequency();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/system/log/remove',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
frequency: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const result = await userService.updateLogRemoveFrequency(
|
||||
req.body.frequency,
|
||||
);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/system/update-check',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const result = await userService.checkUpdate();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/system/update',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const result = await userService.updateSystem();
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import Logger from './loaders/logger';
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
|
||||
await require('./loaders/initFile').default();
|
||||
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
|
||||
await require('./loaders/db').default();
|
||||
|
||||
+23
-34
@@ -4,47 +4,40 @@ import { createRandomString } from './util';
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
const lastVersionFile =
|
||||
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
||||
const lastVersionFile = 'http://qn.whyour.cn/version.ts?v=2.12.1';
|
||||
|
||||
const envFound = dotenv.config();
|
||||
const rootPath = process.cwd();
|
||||
const envFile = path.join(rootPath, 'config/env.sh');
|
||||
const confFile = path.join(rootPath, 'config/config.sh');
|
||||
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
||||
const crontabFile = path.join(rootPath, 'config/crontab.list');
|
||||
const confBakDir = path.join(rootPath, 'config/bak/');
|
||||
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
||||
const extraFile = path.join(rootPath, 'config/extra.sh');
|
||||
const configPath = path.join(rootPath, 'config/');
|
||||
const scriptPath = path.join(rootPath, 'scripts/');
|
||||
const bakPath = path.join(rootPath, 'bak/');
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const logPath = path.join(rootPath, 'log/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
const bakPath = path.join(dataPath, 'bak/');
|
||||
const logPath = path.join(dataPath, 'log/');
|
||||
const dbPath = path.join(dataPath, 'db/');
|
||||
|
||||
const envFile = path.join(configPath, 'env.sh');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const crontabFile = path.join(configPath, 'crontab.list');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
const extraFile = path.join(configPath, 'extra.sh');
|
||||
const confBakDir = path.join(dataPath, 'config/bak/');
|
||||
const sampleFile = path.join(samplePath, 'config.sample.sh');
|
||||
const sqliteFile = path.join(samplePath, 'database.sqlite');
|
||||
|
||||
const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
const configString = 'config sample crontab shareCode diy';
|
||||
const dbPath = path.join(rootPath, 'db/');
|
||||
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
||||
const envDbFile = path.join(rootPath, 'db/env.db');
|
||||
const appDbFile = path.join(rootPath, 'db/app.db');
|
||||
const authDbFile = path.join(rootPath, 'db/auth.db');
|
||||
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
|
||||
const versionFile = path.join(rootPath, 'src/version.ts');
|
||||
|
||||
const configFound = dotenv.config({ path: confFile });
|
||||
|
||||
if (envFound.error) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
}
|
||||
|
||||
if (configFound.error) {
|
||||
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
|
||||
}
|
||||
|
||||
export default {
|
||||
port: parseInt(process.env.PORT as string, 10),
|
||||
port: parseInt(process.env.BACK_PORT as string, 10),
|
||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
|
||||
secret: process.env.SECRET || createRandomString(16, 32),
|
||||
logs: {
|
||||
level: process.env.LOG_LEVEL || 'silly',
|
||||
@@ -65,11 +58,6 @@ export default {
|
||||
confFile,
|
||||
envFile,
|
||||
dbPath,
|
||||
cronDbFile,
|
||||
envDbFile,
|
||||
appDbFile,
|
||||
authDbFile,
|
||||
dependenceDbFile,
|
||||
configPath,
|
||||
scriptPath,
|
||||
samplePath,
|
||||
@@ -83,13 +71,14 @@ export default {
|
||||
writePathList: [configPath, scriptPath],
|
||||
bakPath,
|
||||
apiWhiteList: [
|
||||
'/api/login',
|
||||
'/api/user/login',
|
||||
'/open/auth/token',
|
||||
'/api/user/two-factor/login',
|
||||
'/api/system',
|
||||
'/api/init/user',
|
||||
'/api/init/notification',
|
||||
'/api/user/init',
|
||||
'/api/user/notification/init',
|
||||
],
|
||||
versionFile,
|
||||
lastVersionFile,
|
||||
sqliteFile,
|
||||
};
|
||||
|
||||
+38
-1
@@ -107,7 +107,7 @@ export function createRandomString(min: number, max: number): string {
|
||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||
}
|
||||
|
||||
function getOne(arr) {
|
||||
function getOne(arr: any[]) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
@@ -231,3 +231,40 @@ export async function fileExist(file: any) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function concurrentRun(
|
||||
fnList: Array<() => Promise<any>> = [],
|
||||
max = 5,
|
||||
) {
|
||||
if (!fnList.length) return;
|
||||
|
||||
const replyList: any[] = []; // 收集任务执行结果
|
||||
const startTime = new Date().getTime(); // 记录任务执行开始时间
|
||||
|
||||
// 任务执行程序
|
||||
const schedule = async (index: number) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const fn = fnList[index];
|
||||
if (!fn) return resolve(null);
|
||||
|
||||
// 执行当前异步任务
|
||||
const reply = await fn();
|
||||
replyList[index] = reply;
|
||||
|
||||
// 执行完当前任务后,继续执行任务池的剩余任务
|
||||
await schedule(index + max);
|
||||
resolve(null);
|
||||
});
|
||||
};
|
||||
|
||||
// 任务池执行程序
|
||||
const scheduleList = new Array(max)
|
||||
.fill(0)
|
||||
.map((_, index) => schedule(index));
|
||||
|
||||
// 使用 Promise.all 批量执行
|
||||
const r = await Promise.all(scheduleList);
|
||||
const cost = (new Date().getTime() - startTime) / 1000;
|
||||
|
||||
return replyList;
|
||||
}
|
||||
|
||||
+15
-2
@@ -1,14 +1,17 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
|
||||
export class AuthInfo {
|
||||
ip?: string;
|
||||
type: AuthDataType;
|
||||
info?: any;
|
||||
_id?: string;
|
||||
id?: number;
|
||||
|
||||
constructor(options: AuthInfo) {
|
||||
this.ip = options.ip;
|
||||
this.info = options.info;
|
||||
this.type = options.type;
|
||||
this._id = options._id;
|
||||
this.id = options.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +26,13 @@ export enum AuthDataType {
|
||||
'notification' = 'notification',
|
||||
'removeLogFrequency' = 'removeLogFrequency',
|
||||
}
|
||||
|
||||
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
|
||||
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
|
||||
ip: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
info: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
},
|
||||
});
|
||||
|
||||
+43
-8
@@ -1,34 +1,42 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
|
||||
export class Crontab {
|
||||
name?: string;
|
||||
command: string;
|
||||
schedule: string;
|
||||
schedule?: string;
|
||||
timestamp?: string;
|
||||
created?: number;
|
||||
saved?: boolean;
|
||||
_id?: string;
|
||||
id?: number;
|
||||
status?: CrontabStatus;
|
||||
isSystem?: 1 | 0;
|
||||
pid?: number;
|
||||
isDisabled?: 1 | 0;
|
||||
log_path?: string;
|
||||
isPinned?: 1 | 0;
|
||||
labels?: string[];
|
||||
last_running_time?: number;
|
||||
last_execution_time?: number;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
this.command = options.command;
|
||||
this.schedule = options.schedule;
|
||||
this.saved = options.saved;
|
||||
this._id = options._id;
|
||||
this.created = options.created;
|
||||
this.status = CrontabStatus[options.status]
|
||||
? options.status
|
||||
: CrontabStatus.idle;
|
||||
this.id = options.id;
|
||||
this.status =
|
||||
options.status && CrontabStatus[options.status]
|
||||
? options.status
|
||||
: CrontabStatus.idle;
|
||||
this.timestamp = new Date().toString();
|
||||
this.isSystem = options.isSystem || 0;
|
||||
this.pid = options.pid;
|
||||
this.isDisabled = options.isDisabled || 0;
|
||||
this.log_path = options.log_path || '';
|
||||
this.isPinned = options.isPinned || 0;
|
||||
this.labels = options.labels || [''];
|
||||
this.last_running_time = options.last_running_time || 0;
|
||||
this.last_execution_time = options.last_execution_time || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,3 +46,30 @@ export enum CrontabStatus {
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||
name: {
|
||||
unique: 'compositeIndex',
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
command: {
|
||||
unique: 'compositeIndex',
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
schedule: {
|
||||
unique: 'compositeIndex',
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
timestamp: DataTypes.STRING,
|
||||
saved: DataTypes.BOOLEAN,
|
||||
status: DataTypes.NUMBER,
|
||||
isSystem: DataTypes.NUMBER,
|
||||
pid: DataTypes.NUMBER,
|
||||
isDisabled: DataTypes.NUMBER,
|
||||
isPinned: DataTypes.NUMBER,
|
||||
log_path: DataTypes.STRING,
|
||||
labels: DataTypes.JSON,
|
||||
last_running_time: DataTypes.NUMBER,
|
||||
last_execution_time: DataTypes.NUMBER,
|
||||
});
|
||||
|
||||
+20
-4
@@ -1,7 +1,9 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
|
||||
export class Dependence {
|
||||
timestamp?: string;
|
||||
created?: number;
|
||||
_id?: string;
|
||||
id?: number;
|
||||
status?: DependenceStatus;
|
||||
type?: DependenceTypes;
|
||||
name?: number;
|
||||
@@ -9,8 +11,7 @@ export class Dependence {
|
||||
remark?: string;
|
||||
|
||||
constructor(options: Dependence) {
|
||||
this._id = options._id;
|
||||
this.created = options.created || new Date().valueOf();
|
||||
this.id = options.id;
|
||||
this.status = options.status || DependenceStatus.installing;
|
||||
this.type = options.type || DependenceTypes.nodejs;
|
||||
this.timestamp = new Date().toString();
|
||||
@@ -46,3 +47,18 @@ export enum unInstallDependenceCommandTypes {
|
||||
'pip3 uninstall -y',
|
||||
'apk del -f',
|
||||
}
|
||||
|
||||
interface DependenceInstance
|
||||
extends Model<Dependence, Dependence>,
|
||||
Dependence {}
|
||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
||||
'Dependence',
|
||||
{
|
||||
name: DataTypes.STRING,
|
||||
type: DataTypes.NUMBER,
|
||||
timestamp: DataTypes.STRING,
|
||||
status: DataTypes.NUMBER,
|
||||
log: DataTypes.JSON,
|
||||
remark: DataTypes.STRING,
|
||||
},
|
||||
);
|
||||
|
||||
+17
-6
@@ -1,17 +1,18 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
|
||||
export class Env {
|
||||
value?: string;
|
||||
timestamp?: string;
|
||||
created?: number;
|
||||
_id?: string;
|
||||
id?: number;
|
||||
status?: EnvStatus;
|
||||
position?: number;
|
||||
name?: number;
|
||||
remarks?: number;
|
||||
name?: string;
|
||||
remarks?: string;
|
||||
|
||||
constructor(options: Env) {
|
||||
this.value = options.value;
|
||||
this._id = options._id;
|
||||
this.created = options.created || new Date().valueOf();
|
||||
this.id = options.id;
|
||||
this.status = options.status || EnvStatus.normal;
|
||||
this.timestamp = new Date().toString();
|
||||
this.position = options.position;
|
||||
@@ -26,3 +27,13 @@ export enum EnvStatus {
|
||||
}
|
||||
|
||||
export const initEnvPosition = 9999999999;
|
||||
|
||||
interface EnvInstance extends Model<Env, Env>, Env {}
|
||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||
timestamp: DataTypes.STRING,
|
||||
status: DataTypes.NUMBER,
|
||||
position: DataTypes.NUMBER,
|
||||
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||
remarks: DataTypes.STRING,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import config from '../config/index';
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: `${config.dbPath}database.sqlite`,
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 6,
|
||||
min: 0,
|
||||
idle: 30000,
|
||||
},
|
||||
});
|
||||
|
||||
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
||||
@@ -1,6 +1,8 @@
|
||||
export enum NotificationMode {
|
||||
'gotify' = 'gotify',
|
||||
'goCqHttpBot' = 'goCqHttpBot',
|
||||
'serverChan' = 'serverChan',
|
||||
'pushDeer' = 'pushDeer',
|
||||
'bark' = 'bark',
|
||||
'telegramBot' = 'telegramBot',
|
||||
'dingtalkBot' = 'dingtalkBot',
|
||||
@@ -15,6 +17,12 @@ abstract class NotificationBaseInfo {
|
||||
public type!: NotificationMode;
|
||||
}
|
||||
|
||||
export class GotifyNotification extends NotificationBaseInfo {
|
||||
public gotifyUrl = '';
|
||||
public gotifyToken = '';
|
||||
public gotifyPriority = 0;
|
||||
}
|
||||
|
||||
export class GoCqHttpBotNotification extends NotificationBaseInfo {
|
||||
public goCqHttpBotUrl = '';
|
||||
public goCqHttpBotToken = '';
|
||||
@@ -25,8 +33,13 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
||||
public serverChanKey = '';
|
||||
}
|
||||
|
||||
export class PushDeerNotification extends NotificationBaseInfo {
|
||||
public pushDeerKey = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkPush = '';
|
||||
public barkIcon = 'http://qn.whyour.cn/logo.png';
|
||||
public barkSound = '';
|
||||
public barkGroup = 'qinglong';
|
||||
}
|
||||
@@ -70,7 +83,9 @@ export class EmailNotification extends NotificationBaseInfo {
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
|
||||
+15
-3
@@ -1,23 +1,26 @@
|
||||
import { sequelize } from '.';
|
||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
||||
|
||||
export class App {
|
||||
name: string;
|
||||
scopes: AppScope[];
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
tokens?: AppToken[];
|
||||
_id?: string;
|
||||
id?: number;
|
||||
|
||||
constructor(options: App) {
|
||||
this.name = options.name;
|
||||
this.scopes = options.scopes;
|
||||
this.client_id = options.client_id;
|
||||
this.client_secret = options.client_secret;
|
||||
this._id = options._id;
|
||||
this.id = options.id;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppToken {
|
||||
value: string;
|
||||
type: 'Bearer';
|
||||
type?: 'Bearer';
|
||||
expiration: number;
|
||||
}
|
||||
|
||||
@@ -29,3 +32,12 @@ export enum CrontabStatus {
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
interface AppInstance extends Model<App, App>, App {}
|
||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||
name: { type: DataTypes.STRING, unique: 'name' },
|
||||
scopes: DataTypes.JSON,
|
||||
client_id: DataTypes.STRING,
|
||||
client_secret: DataTypes.STRING,
|
||||
tokens: DataTypes.JSON,
|
||||
});
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
export class SockMessage {
|
||||
message?: string;
|
||||
type?: SockMessageType;
|
||||
references?: string[];
|
||||
references?: number[];
|
||||
|
||||
constructor(options: SockMessage) {
|
||||
this.type = options.type;
|
||||
@@ -13,4 +13,5 @@ export class SockMessage {
|
||||
export type SockMessageType =
|
||||
| 'ping'
|
||||
| 'installDependence'
|
||||
| 'updateSystemVersion';
|
||||
| 'updateSystemVersion'
|
||||
| 'manuallyRunScript';
|
||||
|
||||
@@ -3,6 +3,7 @@ import dependencyInjectorLoader from './dependencyInjector';
|
||||
import Logger from './logger';
|
||||
import initData from './initData';
|
||||
import { Application } from 'express';
|
||||
import linkDeps from './deps';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
await dependencyInjectorLoader({
|
||||
@@ -15,4 +16,7 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps');
|
||||
};
|
||||
|
||||
+92
-49
@@ -1,57 +1,102 @@
|
||||
import DataStore from 'nedb';
|
||||
import config from '../config';
|
||||
import Logger from './logger';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import DataStore from 'nedb';
|
||||
import { EnvModel } from '../data/env';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { AppModel } from '../data/open';
|
||||
import { AuthModel } from '../data/auth';
|
||||
import { sequelize } from '../data';
|
||||
import { fileExist } from '../config/util';
|
||||
import config from '../config';
|
||||
|
||||
interface Dbs {
|
||||
cronDb: DataStore;
|
||||
dependenceDb: DataStore;
|
||||
envDb: DataStore;
|
||||
appDb: DataStore;
|
||||
authDb: DataStore;
|
||||
}
|
||||
|
||||
const db: Dbs = {} as any;
|
||||
|
||||
async function truncateDb() {
|
||||
return new Promise(async (resolve) => {
|
||||
const files = [
|
||||
config.cronDbFile,
|
||||
config.dependenceDbFile,
|
||||
config.envDbFile,
|
||||
config.appDbFile,
|
||||
config.authDbFile,
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const _fileExist = await fileExist(file);
|
||||
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
|
||||
fs.truncateSync(file, 1024 * 1024 * 500);
|
||||
}
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
export default async () => {
|
||||
try {
|
||||
await truncateDb();
|
||||
await CrontabModel.sync();
|
||||
await DependenceModel.sync();
|
||||
await AppModel.sync();
|
||||
await AuthModel.sync();
|
||||
await EnvModel.sync();
|
||||
await sequelize.sync();
|
||||
|
||||
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true });
|
||||
db.dependenceDb = new DataStore({
|
||||
filename: config.dependenceDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true });
|
||||
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true });
|
||||
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true });
|
||||
// try {
|
||||
// const queryInterface = sequelize.getQueryInterface();
|
||||
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
|
||||
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
|
||||
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
|
||||
// } catch (error) { }
|
||||
|
||||
// compaction data file
|
||||
db.cronDb.persistence.compactDatafile();
|
||||
db.envDb.persistence.compactDatafile();
|
||||
db.dependenceDb.persistence.compactDatafile();
|
||||
db.appDb.persistence.compactDatafile();
|
||||
db.authDb.persistence.compactDatafile();
|
||||
// 2.10-2.11 升级
|
||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||
const envDbFile = path.join(config.rootPath, 'db/env.db');
|
||||
const appDbFile = path.join(config.rootPath, 'db/app.db');
|
||||
const authDbFile = path.join(config.rootPath, 'db/auth.db');
|
||||
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
|
||||
const crondbExist = await fileExist(cronDbFile);
|
||||
const dependenceDbExist = await fileExist(dependenceDbFile);
|
||||
const envDbExist = await fileExist(envDbFile);
|
||||
const appDbExist = await fileExist(appDbFile);
|
||||
const authDbExist = await fileExist(authDbFile);
|
||||
|
||||
const cronCount = await CrontabModel.count();
|
||||
const dependenceCount = await DependenceModel.count();
|
||||
const envCount = await EnvModel.count();
|
||||
const appCount = await AppModel.count();
|
||||
const authCount = await AuthModel.count();
|
||||
if (crondbExist && cronCount === 0) {
|
||||
const cronDb = new DataStore({
|
||||
filename: cronDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
cronDb.persistence.compactDatafile();
|
||||
cronDb.find({}).exec(async (err, docs) => {
|
||||
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (dependenceDbExist && dependenceCount === 0) {
|
||||
const dependenceDb = new DataStore({
|
||||
filename: dependenceDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
dependenceDb.persistence.compactDatafile();
|
||||
dependenceDb.find({}).exec(async (err, docs) => {
|
||||
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (envDbExist && envCount === 0) {
|
||||
const envDb = new DataStore({
|
||||
filename: envDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
envDb.persistence.compactDatafile();
|
||||
envDb.find({}).exec(async (err, docs) => {
|
||||
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (appDbExist && appCount === 0) {
|
||||
const appDb = new DataStore({
|
||||
filename: appDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
appDb.persistence.compactDatafile();
|
||||
appDb.find({}).exec(async (err, docs) => {
|
||||
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (authDbExist && authCount === 0) {
|
||||
const authDb = new DataStore({
|
||||
filename: authDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
authDb.persistence.compactDatafile();
|
||||
authDb.find({}).exec(async (err, docs) => {
|
||||
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
Logger.info('✌️ DB loaded');
|
||||
} catch (error) {
|
||||
@@ -59,5 +104,3 @@ export default async () => {
|
||||
Logger.info(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const dbs: Dbs = db;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import chokidar from 'chokidar';
|
||||
import config from '../config/index';
|
||||
|
||||
function linkToNodeModule(src: string, dst?: string) {
|
||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||
const source = path.join(config.rootPath, src);
|
||||
|
||||
fs.lstat(target, (err, stat) => {
|
||||
if (!stat) {
|
||||
fs.symlink(source, target, 'dir', (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default async (src: string = 'deps') => {
|
||||
linkToNodeModule(src);
|
||||
|
||||
const source = path.join(config.rootPath, src);
|
||||
const watcher = chokidar.watch(source, {
|
||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
watcher
|
||||
.on('add', (path) => linkToNodeModule(src))
|
||||
.on('change', (path) => linkToNodeModule(src));
|
||||
};
|
||||
+11
-9
@@ -5,13 +5,14 @@ import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt from 'express-jwt';
|
||||
import fs from 'fs';
|
||||
import { getFileContentByName, getPlatform, getToken } from '../config/util';
|
||||
import { getPlatform, getToken } from '../config/util';
|
||||
import Container from 'typedi';
|
||||
import OpenService from '../services/open';
|
||||
import rewrite from 'express-urlrewrite';
|
||||
import UserService from '../services/user';
|
||||
import handler from 'serve-handler';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { EnvModel } from '../data/env';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.enable('trust proxy');
|
||||
@@ -22,7 +23,7 @@ export default ({ app }: { app: Application }) => {
|
||||
next();
|
||||
} else {
|
||||
return handler(req, res, {
|
||||
public: 'dist',
|
||||
public: 'static/dist',
|
||||
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||
});
|
||||
}
|
||||
@@ -68,18 +69,19 @@ export default ({ app }: { app: Application }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
|
||||
if (
|
||||
!headerToken &&
|
||||
req.path &&
|
||||
config.apiWhiteList.includes(req.path) &&
|
||||
req.path !== '/api/crons/status'
|
||||
originPath &&
|
||||
config.apiWhiteList.includes(originPath) &&
|
||||
originPath !== '/api/crons/status'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
const remoteAddress = req.socket.remoteAddress;
|
||||
if (
|
||||
remoteAddress === '::ffff:127.0.0.1' &&
|
||||
req.path === '/api/crons/status'
|
||||
originPath === '/api/crons/status'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
@@ -98,19 +100,19 @@ export default ({ app }: { app: Application }) => {
|
||||
});
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
|
||||
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envDbContent = getFileContentByName(config.envDbFile);
|
||||
const envCount = await EnvModel.count();
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
Object.keys(authInfo).length === 2 &&
|
||||
authInfo.username === 'admin' &&
|
||||
authInfo.password === 'admin' &&
|
||||
envDbContent.length === 0
|
||||
envCount === 0
|
||||
) {
|
||||
isInitialized = false;
|
||||
}
|
||||
|
||||
+102
-41
@@ -1,77 +1,138 @@
|
||||
import DependenceService from '../services/dependence';
|
||||
import { exec } from 'child_process';
|
||||
import { Container } from 'typedi';
|
||||
import { Crontab, CrontabStatus } from '../data/cron';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import CronService from '../services/cron';
|
||||
import EnvService from '../services/env';
|
||||
import _ from 'lodash';
|
||||
import { dbs } from '../loaders/db';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { Op } from 'sequelize';
|
||||
import SystemService from '../services/system';
|
||||
import ScheduleService from '../services/schedule';
|
||||
import config from '../config';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
const envService = Container.get(EnvService);
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const cronDb = dbs.cronDb;
|
||||
const dependenceDb = dbs.dependenceDb;
|
||||
const systemService = Container.get(SystemService);
|
||||
const scheduleService = Container.get(ScheduleService);
|
||||
|
||||
// 初始化更新所有任务状态为空闲
|
||||
cronDb.update(
|
||||
{ status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||
{ $set: { status: CrontabStatus.idle } },
|
||||
{ multi: true },
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle },
|
||||
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||
);
|
||||
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => {
|
||||
DependenceModel.findAll({ where: {} }).then(async (docs) => {
|
||||
const groups = _.groupBy(docs, 'type');
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
const depIds = group.map((x) => x._id);
|
||||
const depIds = group.map((x) => x.id);
|
||||
for (const dep of depIds) {
|
||||
dependenceService.reInstall([dep]);
|
||||
if (dep) {
|
||||
await dependenceService.reInstall([dep]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时执行一次所有的ql repo 任务
|
||||
cronDb
|
||||
.find({
|
||||
command: /ql (repo|raw)/,
|
||||
isDisabled: { $ne: 1 },
|
||||
})
|
||||
.exec((err, docs) => {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
if (doc) {
|
||||
exec(doc.command);
|
||||
CrontabModel.findAll({
|
||||
where: {
|
||||
isDisabled: { [Op.ne]: 1 },
|
||||
command: {
|
||||
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
|
||||
},
|
||||
},
|
||||
}).then((docs) => {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
if (doc) {
|
||||
exec(doc.command);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新2.11.3以前的脚本路径
|
||||
CrontabModel.findAll({
|
||||
where: {
|
||||
command: {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
|
||||
{ [Op.like]: `%\/${config.rootPath}\/config\/%` },
|
||||
{ [Op.like]: `%\/${config.rootPath}\/log\/%` },
|
||||
{ [Op.like]: `%\/${config.rootPath}\/db\/%` },
|
||||
],
|
||||
},
|
||||
},
|
||||
}).then(async (docs) => {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
if (doc) {
|
||||
if (doc.command.includes(`${config.rootPath}/scripts/`)) {
|
||||
await CrontabModel.update(
|
||||
{ command: doc.command.replace(`${config.rootPath}/scripts/`, '') },
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.rootPath}/data/log/${doc.command.replace(
|
||||
`${config.rootPath}/log/`,
|
||||
'',
|
||||
)}`,
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.rootPath}/data/config/${doc.command.replace(
|
||||
`${config.rootPath}/config/`,
|
||||
'',
|
||||
)}`,
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
command: `${config.rootPath}/data/db/${doc.command.replace(
|
||||
`${config.rootPath}/db/`,
|
||||
'',
|
||||
)}`,
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// patch 禁用状态字段改变
|
||||
cronDb
|
||||
.find({
|
||||
status: CrontabStatus.disabled,
|
||||
})
|
||||
.exec((err, docs) => {
|
||||
if (docs.length > 0) {
|
||||
const ids = docs.map((x) => x._id);
|
||||
cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
|
||||
{ multi: true },
|
||||
(err) => {
|
||||
cronService.autosave_crontab();
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化保存一次ck和定时任务数据
|
||||
await cronService.autosave_crontab();
|
||||
await envService.set_envs();
|
||||
|
||||
// 运行删除日志任务
|
||||
const data = await systemService.getLogRemoveFrequency();
|
||||
if (data && data.info && data.info.frequency) {
|
||||
const cron = {
|
||||
id: data.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.frequency}`,
|
||||
};
|
||||
await scheduleService.createIntervalTask(cron, {
|
||||
days: data.info.frequency,
|
||||
runImmediately: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function randomSchedule(from: number, to: number) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
import Logger from './logger';
|
||||
import { fileExist } from '../config/util';
|
||||
|
||||
const rootPath = process.cwd();
|
||||
const dataPath = path.join(rootPath, 'data/');
|
||||
const configPath = path.join(dataPath, 'config/');
|
||||
const scriptPath = path.join(dataPath, 'scripts/');
|
||||
const logPath = path.join(dataPath, 'log/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
||||
|
||||
export default async () => {
|
||||
const authFileExist = await fileExist(authConfigFile);
|
||||
const confFileExist = await fileExist(confFile);
|
||||
const scriptDirExist = await fileExist(scriptPath);
|
||||
const logDirExist = await fileExist(logPath);
|
||||
const configDirExist = await fileExist(configPath);
|
||||
|
||||
if (!configDirExist) {
|
||||
fs.mkdirSync(configPath);
|
||||
}
|
||||
|
||||
if (!authFileExist) {
|
||||
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||
}
|
||||
|
||||
if (!confFileExist) {
|
||||
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||
}
|
||||
|
||||
if (!scriptDirExist) {
|
||||
fs.mkdirSync(scriptPath);
|
||||
}
|
||||
|
||||
if (!logDirExist) {
|
||||
fs.mkdirSync(logPath);
|
||||
}
|
||||
|
||||
dotenv.config({ path: confFile });
|
||||
|
||||
Logger.info('✌️ Init file down');
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import Logger from './logger';
|
||||
|
||||
export default ({ expressApp }: { expressApp: Application }) => {
|
||||
Sentry.init({
|
||||
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906',
|
||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Tracing.Integrations.Express({ app: expressApp }),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import Logger from './loaders/logger';
|
||||
import config from './config';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get('/api/public/panel/log', (req, res) => {
|
||||
exec(
|
||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
||||
(err, stdout, stderr) => {
|
||||
if (err || stderr) {
|
||||
return res.send({ code: 400, data: err || stderr });
|
||||
}
|
||||
return res.send({ code: 200, data: stdout });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
app
|
||||
.listen(config.publicPort, async () => {
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
Logger.info(`
|
||||
################################################
|
||||
🛡️ Public listening on port: ${config.publicPort} 🛡️
|
||||
################################################
|
||||
`);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
+12
-15
@@ -2,24 +2,14 @@ import schedule from 'node-schedule';
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import Logger from './loaders/logger';
|
||||
import { CrontabStatus } from './data/cron';
|
||||
import { CrontabModel, CrontabStatus } from './data/cron';
|
||||
import config from './config';
|
||||
import { dbs } from './loaders/db';
|
||||
|
||||
const app = express();
|
||||
|
||||
const run = async () => {
|
||||
const cronDb = dbs.cronDb;
|
||||
|
||||
cronDb
|
||||
.find({})
|
||||
.sort({ created: 1 })
|
||||
.exec((err, docs) => {
|
||||
if (err) {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
CrontabModel.findAll({ where: {} })
|
||||
.then((docs) => {
|
||||
if (docs && docs.length > 0) {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const task = docs[i];
|
||||
@@ -40,12 +30,19 @@ const run = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
app
|
||||
.listen(config.cronPort, () => {
|
||||
run();
|
||||
.listen(config.cronPort, async () => {
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
await require('./loaders/db').default();
|
||||
|
||||
await run();
|
||||
Logger.info(`
|
||||
################################################
|
||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||
|
||||
+276
-245
@@ -1,30 +1,23 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import DataStore from 'nedb';
|
||||
import config from '../config';
|
||||
import { Crontab, CrontabStatus } from '../data/cron';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import { exec, execSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import PQueue from 'p-queue';
|
||||
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { dbs } from '../loaders/db';
|
||||
import { Op } from 'sequelize';
|
||||
import path from 'path';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
private cronDb = dbs.cronDb;
|
||||
|
||||
private queue = new PQueue({
|
||||
concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
|
||||
});
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private isSixCron(cron: Crontab) {
|
||||
const { schedule } = cron;
|
||||
if (schedule.split(/ +/).length === 6) {
|
||||
if (schedule?.split(/ +/).length === 6) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -32,7 +25,6 @@ export default class CronService {
|
||||
|
||||
public async create(payload: Crontab): Promise<Crontab> {
|
||||
const tab = new Crontab(payload);
|
||||
tab.created = new Date().valueOf();
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
await this.set_crontab(this.isSixCron(doc));
|
||||
@@ -40,42 +32,19 @@ export default class CronService {
|
||||
}
|
||||
|
||||
public async insert(payload: Crontab): Promise<Crontab> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.insert(payload, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
return await CrontabModel.create(payload, { returning: true });
|
||||
}
|
||||
|
||||
public async update(payload: Crontab): Promise<Crontab> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new Crontab({ ...doc, ...other });
|
||||
tab.saved = false;
|
||||
const newDoc = await this.updateDb(tab);
|
||||
payload.saved = false;
|
||||
const newDoc = await this.updateDb(payload);
|
||||
await this.set_crontab(this.isSixCron(newDoc));
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
public async updateDb(payload: Crontab): Promise<Crontab> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, num, docs: any) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
await CrontabModel.update(payload, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async status({
|
||||
@@ -86,156 +55,166 @@ export default class CronService {
|
||||
last_running_time = 0,
|
||||
last_execution_time = 0,
|
||||
}: {
|
||||
ids: string[];
|
||||
ids: number[];
|
||||
status: CrontabStatus;
|
||||
pid: number;
|
||||
log_path: string;
|
||||
last_running_time: number;
|
||||
last_execution_time: number;
|
||||
}) {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
const options: any = {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
last_execution_time,
|
||||
};
|
||||
if (last_running_time > 0) {
|
||||
options.last_running_time = last_running_time;
|
||||
}
|
||||
|
||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
await CrontabModel.destroy({ where: { id: ids } });
|
||||
await this.set_crontab(true);
|
||||
}
|
||||
|
||||
public async pin(ids: number[]) {
|
||||
await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async unPin(ids: number[]) {
|
||||
await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async addLabels(ids: string[], labels: string[]) {
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
$set: {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
last_running_time,
|
||||
last_execution_time,
|
||||
},
|
||||
},
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
(err) => {
|
||||
resolve(null);
|
||||
labels: Array.from(new Set((doc.labels || []).concat(labels))),
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.remove(
|
||||
{ _id: { $in: ids } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_crontab(true);
|
||||
resolve();
|
||||
public async removeLabels(ids: string[], labels: string[]) {
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await CrontabModel.update(
|
||||
{
|
||||
labels: (doc.labels || []).filter((label) => !labels.includes(label)),
|
||||
},
|
||||
{ where: { id: doc.id } },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async pin(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isPinned: 1 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async unPin(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isPinned: 0 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||
let query = {};
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText, 'i');
|
||||
query = {
|
||||
$or: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
const textArray = searchText.split(':');
|
||||
switch (textArray[0]) {
|
||||
case 'name':
|
||||
case 'command':
|
||||
case 'schedule':
|
||||
case 'label':
|
||||
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
|
||||
query = {
|
||||
[column]: {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${textArray[1]}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
|
||||
],
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
|
||||
],
|
||||
};
|
||||
query = {
|
||||
[Op.or]: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
command: reg,
|
||||
},
|
||||
{
|
||||
schedule: reg,
|
||||
},
|
||||
{
|
||||
labels: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb
|
||||
.find(query)
|
||||
.sort({ created: -1 })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Crontab> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
try {
|
||||
const result = await CrontabModel.findAll({
|
||||
where: query,
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async run(ids: string[]) {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: CrontabStatus.queued } },
|
||||
{ multi: true },
|
||||
);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
this.queue.add(() => this.runSingle(id));
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb
|
||||
.find({ _id: { $in: ids } })
|
||||
.exec(async (err, docs: Crontab[]) => {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
}
|
||||
}
|
||||
const err = await this.killTask(doc.command);
|
||||
if (doc.log_path) {
|
||||
const str = err ? `\n${err}` : '';
|
||||
fs.appendFileSync(
|
||||
`${doc.log_path}`,
|
||||
`${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')} `,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||
{ multi: true },
|
||||
);
|
||||
this.queue.clear();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
public async getDb(query: any): Promise<Crontab> {
|
||||
const doc: any = await CrontabModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Crontab);
|
||||
}
|
||||
|
||||
private async killTask(name: string) {
|
||||
public async run(ids: number[]) {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.queued },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
concurrentRun(
|
||||
ids.map((id) => async () => await this.runSingle(id)),
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
public async stop(ids: number[]) {
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
} catch (error) {
|
||||
this.logger.silly(error);
|
||||
}
|
||||
}
|
||||
const err = await this.killTask(doc.command);
|
||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||
if (logFileExist) {
|
||||
const str = err ? `\n${err}` : '';
|
||||
fs.appendFileSync(
|
||||
`${absolutePath}`,
|
||||
`${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')} `,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
}
|
||||
|
||||
public async killTask(name: string) {
|
||||
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||
const execAsync = promisify(exec);
|
||||
try {
|
||||
@@ -245,16 +224,23 @@ export default class CronService {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const pids = pid.match(/\(\d+/g);
|
||||
let pids = pid.match(/\(\d+/g);
|
||||
const killLogs = [];
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
if (pids && pids.length > 0) {
|
||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
}
|
||||
} catch (error: any) {
|
||||
killLogs.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||
@@ -263,18 +249,20 @@ export default class CronService {
|
||||
}
|
||||
}
|
||||
|
||||
private async runSingle(id: string): Promise<number> {
|
||||
private async runSingle(cronId: number): Promise<number> {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.get(id);
|
||||
const cron = await this.getDb({ id: cronId });
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let { _id, command, log_path } = cron;
|
||||
let { id, command, log_path } = cron;
|
||||
const absolutePath = path.resolve(config.logPath, `${log_path}`);
|
||||
const logFileExist = log_path && (await fileExist(absolutePath));
|
||||
|
||||
this.logger.silly('Running job');
|
||||
this.logger.silly('ID: ' + _id);
|
||||
this.logger.silly('ID: ' + id);
|
||||
this.logger.silly('Original command: ' + command);
|
||||
|
||||
let cmdStr = command;
|
||||
@@ -286,78 +274,63 @@ export default class CronService {
|
||||
}
|
||||
|
||||
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
||||
this.cronDb.update(
|
||||
{ _id },
|
||||
{ $set: { status: CrontabStatus.running, pid: cp.pid } },
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.running, pid: cp.pid },
|
||||
{ where: { id } },
|
||||
);
|
||||
cp.stderr.on('data', (data) => {
|
||||
if (log_path) {
|
||||
fs.appendFileSync(`${log_path}`, `${data}`);
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${data}`);
|
||||
}
|
||||
});
|
||||
cp.on('error', (err) => {
|
||||
if (log_path) {
|
||||
fs.appendFileSync(`${log_path}`, `${JSON.stringify(err)}`);
|
||||
if (logFileExist) {
|
||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
cp.on('exit', (code, signal) => {
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||
);
|
||||
this.cronDb.update(
|
||||
{ _id },
|
||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
cp.on('close', (code) => {
|
||||
cp.on('close', async (code) => {
|
||||
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||
this.cronDb.update(
|
||||
{ _id },
|
||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isDisabled: 1 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_crontab(true);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
public async disabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
await this.set_crontab(true);
|
||||
}
|
||||
|
||||
public async enabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { isDisabled: 0 } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_crontab(true);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
public async enabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||
await this.set_crontab(true);
|
||||
}
|
||||
|
||||
public async log(_id: string) {
|
||||
const doc = await this.get(_id);
|
||||
public async log(id: number) {
|
||||
const doc = await this.getDb({ id });
|
||||
if (!doc) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (doc.log_path) {
|
||||
return getFileContentByName(`${doc.log_path}`);
|
||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||
if (logFileExist) {
|
||||
return getFileContentByName(`${absolutePath}`);
|
||||
}
|
||||
const [, commandStr, url] = doc.command.split(/ +/);
|
||||
let logPath = this.getKey(commandStr);
|
||||
@@ -380,18 +353,78 @@ export default class CronService {
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(command: string) {
|
||||
public async logs(id: number) {
|
||||
const doc = await this.getDb({ id });
|
||||
if (!doc) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (doc.log_path) {
|
||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
||||
const dir = path.resolve(config.logPath, relativeDir);
|
||||
if (existsSync(dir)) {
|
||||
let files = await promises.readdir(dir);
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: relativeDir.replace(config.logPath, ''),
|
||||
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
}
|
||||
}
|
||||
|
||||
const [, commandStr, url] = doc.command.split(/ +/);
|
||||
let logPath = this.getKey(commandStr);
|
||||
const isQlCommand = doc.command.startsWith('ql ');
|
||||
const key =
|
||||
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
|
||||
logPath;
|
||||
if (isQlCommand) {
|
||||
logPath = 'update';
|
||||
}
|
||||
let logDir = `${config.logPath}${logPath}`;
|
||||
if (existsSync(logDir)) {
|
||||
let files = await promises.readdir(logDir);
|
||||
if (isQlCommand) {
|
||||
files = files.filter((x) => x.includes(key));
|
||||
}
|
||||
return files
|
||||
.map((x) => ({
|
||||
filename: x,
|
||||
directory: logPath,
|
||||
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => b.time - a.time);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(command: string): string {
|
||||
const start =
|
||||
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
|
||||
const end =
|
||||
command.lastIndexOf('.') !== -1
|
||||
? command.lastIndexOf('.')
|
||||
: command.length;
|
||||
return command.substring(start, end);
|
||||
|
||||
const tmpStr = command.substring(0, start - 1);
|
||||
let index = 0;
|
||||
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
|
||||
index = tmpStr.lastIndexOf('/');
|
||||
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
|
||||
index = tmpStr.lastIndexOf(':');
|
||||
}
|
||||
if (index) {
|
||||
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
|
||||
} else {
|
||||
return command.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
private make_command(tab: Crontab) {
|
||||
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
|
||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
@@ -400,7 +433,7 @@ export default class CronService {
|
||||
var crontab_string = '';
|
||||
tabs.forEach((tab) => {
|
||||
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||
if (tab.isDisabled === 1 || _schedule.length !== 5) {
|
||||
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
|
||||
crontab_string += '# ';
|
||||
crontab_string += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
@@ -421,40 +454,38 @@ export default class CronService {
|
||||
if (needReloadSchedule) {
|
||||
exec(`pm2 reload schedule`);
|
||||
}
|
||||
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
||||
await CrontabModel.update({ saved: true }, { where: {} });
|
||||
}
|
||||
|
||||
public import_crontab() {
|
||||
exec('crontab -l', (error, stdout, stderr) => {
|
||||
var lines = stdout.split('\n');
|
||||
var namePrefix = new Date().getTime();
|
||||
const lines = stdout.split('\n');
|
||||
const namePrefix = new Date().getTime();
|
||||
|
||||
lines.reverse().forEach((line, index) => {
|
||||
lines.reverse().forEach(async (line, index) => {
|
||||
line = line.replace(/\t+/g, ' ');
|
||||
var regex =
|
||||
const regex =
|
||||
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
||||
var command = line.replace(regex, '').trim();
|
||||
var schedule = line.replace(command, '').trim();
|
||||
const command = line.replace(regex, '').trim();
|
||||
const schedule = line.replace(command, '').trim();
|
||||
|
||||
if (
|
||||
command &&
|
||||
schedule &&
|
||||
cron_parser.parseExpression(schedule).hasNext()
|
||||
) {
|
||||
var name = namePrefix + '_' + index;
|
||||
const name = namePrefix + '_' + index;
|
||||
|
||||
this.cronDb.findOne({ command, schedule }, (err, doc) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!doc) {
|
||||
this.create({ name, command, schedule });
|
||||
} else {
|
||||
doc.command = command;
|
||||
doc.schedule = schedule;
|
||||
this.update(doc);
|
||||
}
|
||||
const _crontab = await CrontabModel.findOne({
|
||||
where: { command, schedule },
|
||||
});
|
||||
if (!_crontab) {
|
||||
await this.create({ name, command, schedule });
|
||||
} else {
|
||||
_crontab.command = command;
|
||||
_crontab.schedule = schedule;
|
||||
await this.update(_crontab);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+142
-192
@@ -1,23 +1,21 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import DataStore from 'nedb';
|
||||
import {
|
||||
Dependence,
|
||||
InstallDependenceCommandTypes,
|
||||
DependenceStatus,
|
||||
DependenceTypes,
|
||||
unInstallDependenceCommandTypes,
|
||||
DependenceModel,
|
||||
} from '../data/dependence';
|
||||
import _ from 'lodash';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import { dbs } from '../loaders/db';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class DependenceService {
|
||||
private dependenceDb = dbs.dependenceDb;
|
||||
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private sockService: SockService,
|
||||
@@ -34,22 +32,15 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.insert(payloads, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
const docs = await DependenceModel.bulkCreate(payloads);
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async update(
|
||||
payload: Dependence & { _id: string },
|
||||
payload: Dependence & { id: string },
|
||||
): Promise<Dependence> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const { id, ...other } = payload;
|
||||
const doc = await this.getDb({ id });
|
||||
const tab = new Dependence({
|
||||
...doc,
|
||||
...other,
|
||||
@@ -61,46 +52,22 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
private async updateDb(payload: Dependence): Promise<Dependence> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, num, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(doc as Dependence);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
await DependenceModel.update(payload, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: DependenceStatus.removing, log: [] } },
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
async (err, num, docs: Dependence[]) => {
|
||||
this.installOrUninstallDependencies(docs, false);
|
||||
resolve(docs);
|
||||
},
|
||||
);
|
||||
});
|
||||
public async remove(ids: number[]) {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.removing, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
this.installOrUninstallDependencies(docs, false);
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async removeDb(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.dependenceDb.remove(
|
||||
{ _id: { $in: ids } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
public async removeDb(ids: number[]) {
|
||||
await DependenceModel.destroy({ where: { id: ids } });
|
||||
}
|
||||
|
||||
public async dependencies(
|
||||
@@ -110,171 +77,154 @@ export default class DependenceService {
|
||||
): Promise<Dependence[]> {
|
||||
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||
if (searchValue) {
|
||||
const reg = new RegExp(searchValue);
|
||||
condition = {
|
||||
...condition,
|
||||
$or: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
const encodeText = encodeURIComponent(searchValue);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchValue}%` },
|
||||
{ [Op.like]: `%${encodeText}%` },
|
||||
],
|
||||
};
|
||||
|
||||
condition = {
|
||||
...condition,
|
||||
name: reg,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition);
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const newDocs = await this.find(condition, sort);
|
||||
return newDocs;
|
||||
}
|
||||
|
||||
public async reInstall(ids: string[]): Promise<Dependence[]> {
|
||||
return new Promise((resolve: any) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: DependenceStatus.installing, log: [] } },
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
async (err, num, docs: Dependence[]) => {
|
||||
this.installOrUninstallDependencies(docs);
|
||||
resolve(docs);
|
||||
},
|
||||
);
|
||||
});
|
||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.installing, log: [] },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
|
||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
||||
await this.installOrUninstallDependencies(docs);
|
||||
return docs;
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any): Promise<Dependence[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
private async find(query: any, sort?: any): Promise<Dependence[]> {
|
||||
const docs = await DependenceModel.findAll({ where: { ...query } });
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Dependence> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
public async getDb(query: any): Promise<Dependence> {
|
||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Dependence);
|
||||
}
|
||||
|
||||
private async updateLog(ids: string[], log: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $push: { log } },
|
||||
{ multi: true },
|
||||
(err, num, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
||||
const doc = await DependenceModel.findOne({ where: { id: ids } });
|
||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
||||
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public installOrUninstallDependencies(
|
||||
dependencies: Dependence[],
|
||||
isInstall: boolean = true,
|
||||
) {
|
||||
if (dependencies.length === 0) {
|
||||
return;
|
||||
}
|
||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependencies[0].type as any];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const depIds = dependencies.map((x) => x._id) as string[];
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
const startTime = Date.now();
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(
|
||||
depIds,
|
||||
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}\n`,
|
||||
);
|
||||
cp.stdout.on('data', (data) => {
|
||||
return new Promise(async (resolve) => {
|
||||
if (dependencies.length === 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependencies[0].type as any];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const depIds = dependencies.map((x) => x.id) as number[];
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
const startTime = Date.now();
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('close', (code) => {
|
||||
const endTime = Date.now();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(
|
||||
await this.updateLog(
|
||||
depIds,
|
||||
`依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}\n`,
|
||||
);
|
||||
cp.stdout.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed;
|
||||
} else {
|
||||
status = isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: depIds } },
|
||||
{
|
||||
$set: { status },
|
||||
$unset: { pid: true },
|
||||
},
|
||||
{ multi: true },
|
||||
);
|
||||
cp.stderr.on('data', async (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
// 如果删除依赖成功,3秒后删除数据库记录
|
||||
if (isSucceed && !isInstall) {
|
||||
setTimeout(() => {
|
||||
this.removeDb(depIds);
|
||||
}, 5000);
|
||||
}
|
||||
cp.on('error', async (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(depIds, JSON.stringify(err));
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
const endTime = Date.now();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
references: depIds,
|
||||
});
|
||||
await this.updateLog(
|
||||
depIds,
|
||||
`依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
);
|
||||
|
||||
let status = null;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed;
|
||||
} else {
|
||||
status = isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
// 如果删除依赖成功,3秒后删除数据库记录
|
||||
if (isSucceed && !isInstall) {
|
||||
setTimeout(() => {
|
||||
this.removeDb(depIds);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+60
-109
@@ -3,15 +3,12 @@ import winston from 'winston';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import DataStore from 'nedb';
|
||||
import { Env, EnvStatus, initEnvPosition } from '../data/env';
|
||||
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
|
||||
import _ from 'lodash';
|
||||
import { dbs } from '../loaders/db';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class EnvService {
|
||||
private envDb = dbs.envDb;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async create(payloads: Env[]): Promise<Env[]> {
|
||||
@@ -31,54 +28,32 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb.insert(payloads, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
const result = [];
|
||||
for (const env of payloads) {
|
||||
const doc = await EnvModel.create(env, { returning: true });
|
||||
result.push(doc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async update(payload: Env): Promise<Env> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new Env({ ...doc, ...other });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
const newDoc = await this.updateDb(payload);
|
||||
await this.set_envs();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
private async updateDb(payload: Env): Promise<Env> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, num, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(doc as Env);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
await EnvModel.update({ ...payload }, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await EnvModel.destroy({ where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async move(
|
||||
_id: string,
|
||||
id: number,
|
||||
{
|
||||
fromIndex,
|
||||
toIndex,
|
||||
@@ -100,7 +75,7 @@ export default class EnvService {
|
||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
||||
}
|
||||
const newDoc = await this.update({
|
||||
_id,
|
||||
id,
|
||||
position: targetPosition,
|
||||
});
|
||||
return newDoc;
|
||||
@@ -113,103 +88,73 @@ export default class EnvService {
|
||||
): Promise<Env[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeText}%` },
|
||||
],
|
||||
};
|
||||
|
||||
condition = {
|
||||
$or: [
|
||||
{
|
||||
value: reg,
|
||||
},
|
||||
...condition,
|
||||
[Op.or]: [
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
value: reg,
|
||||
},
|
||||
{
|
||||
remarks: reg,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const newDocs = await this.find(condition, sort);
|
||||
return newDocs;
|
||||
try {
|
||||
const result = await this.find(condition, [['position', 'DESC']]);
|
||||
return result as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any): Promise<Env[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
private async find(query: any, sort: any = []): Promise<Env[]> {
|
||||
const docs = await EnvModel.findAll({
|
||||
where: { ...query },
|
||||
order: [...sort],
|
||||
});
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Env> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getBySort(sort: any): Promise<Env> {
|
||||
return new Promise((resolve) => {
|
||||
this.envDb
|
||||
.find({})
|
||||
.sort({ ...sort })
|
||||
.limit(1)
|
||||
.exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
public async getDb(query: any): Promise<Env> {
|
||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as Env);
|
||||
}
|
||||
|
||||
public async disabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: EnvStatus.disabled } },
|
||||
{ multi: true },
|
||||
async (err) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
await EnvModel.update(
|
||||
{ status: EnvStatus.disabled },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async enabled(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { status: EnvStatus.normal } },
|
||||
{ multi: true },
|
||||
async (err, num) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.envDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{ $set: { name } },
|
||||
{ multi: true },
|
||||
async (err, num) => {
|
||||
await this.set_envs();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
await EnvModel.update({ name }, { where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async set_envs() {
|
||||
const envs = await this.envs(
|
||||
'',
|
||||
{ position: -1 },
|
||||
{ name: { $exists: true } },
|
||||
{ name: { [Op.not]: null } },
|
||||
);
|
||||
const groups = _.groupBy(envs, 'name');
|
||||
let env_string = '';
|
||||
@@ -219,11 +164,17 @@ export default class EnvService {
|
||||
|
||||
// 忽略不符合bash要求的环境变量名称
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||
env_string += `export ${key}="${_(group)
|
||||
let value = _(group)
|
||||
.filter((x) => x.status !== EnvStatus.disabled)
|
||||
.map('value')
|
||||
.join('&')
|
||||
.replace(/ /g, '')}"\n`;
|
||||
.replace(/ /g, '');
|
||||
if (/"/.test(value)) {
|
||||
value = `'${value}'`;
|
||||
} else {
|
||||
value = `"${value}"`;
|
||||
}
|
||||
env_string += `export ${key}=${value}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-10
@@ -13,8 +13,10 @@ export default class NotificationService {
|
||||
private userService!: UserService;
|
||||
|
||||
private modeMap = new Map([
|
||||
['gotify', this.gotify],
|
||||
['goCqHttpBot', this.goCqHttpBot],
|
||||
['serverChan', this.serverChan],
|
||||
['pushDeer', this.pushDeer],
|
||||
['bark', this.bark],
|
||||
['telegramBot', this.telegramBot],
|
||||
['dingtalkBot', this.dingtalkBot],
|
||||
@@ -67,17 +69,34 @@ export default class NotificationService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async gotify() {
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||
const res: any = await got
|
||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&message=${encodeURIComponent(
|
||||
this.content,
|
||||
)}&priority=${gotifyPriority}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return typeof res.id === 'number';
|
||||
}
|
||||
|
||||
private async goCqHttpBot() {
|
||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||
const res: any = await got
|
||||
.post(
|
||||
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
|
||||
{
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: { message: `${this.title}\n${this.content}` },
|
||||
},
|
||||
)
|
||||
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
json: { message: `${this.title}\n${this.content}` },
|
||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
||||
})
|
||||
.json();
|
||||
return res.retcode === 0;
|
||||
}
|
||||
@@ -98,8 +117,26 @@ export default class NotificationService {
|
||||
return res.errno === 0 || res.data.errno === 0;
|
||||
}
|
||||
|
||||
private async pushDeer() {
|
||||
const { pushDeerKey } = this.params;
|
||||
const url = `https://api2.pushdeer.com/message/push`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
return (
|
||||
res.content.result.length !== undefined && res.content.result.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
private async bark() {
|
||||
let { barkPush, barkSound, barkGroup } = this.params;
|
||||
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
|
||||
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||
barkPush = `https://api.day.app/${barkPush}`;
|
||||
}
|
||||
@@ -107,7 +144,7 @@ export default class NotificationService {
|
||||
this.title,
|
||||
)}/${encodeURIComponent(
|
||||
this.content,
|
||||
)}?sound=${barkSound}&group=${barkGroup}`;
|
||||
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
|
||||
const res: any = await got
|
||||
.get(url, {
|
||||
timeout: this.timeout,
|
||||
|
||||
+89
-115
@@ -2,90 +2,68 @@ import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { createRandomString } from '../config/util';
|
||||
import config from '../config';
|
||||
import DataStore from 'nedb';
|
||||
import { App } from '../data/open';
|
||||
import { App, AppModel } from '../data/open';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { dbs } from '../loaders/db';
|
||||
import sequelize, { Op } from 'sequelize';
|
||||
|
||||
@Service()
|
||||
export default class OpenService {
|
||||
private appDb = dbs.appDb;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async findTokenByValue(token: string): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.find(
|
||||
{ tokens: { $elemMatch: { value: token } } },
|
||||
(err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs[0]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
public async findTokenByValue(token: string): Promise<App | null> {
|
||||
const docs = await this.find({});
|
||||
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
|
||||
return doc[0];
|
||||
}
|
||||
|
||||
public async create(payload: App): Promise<App> {
|
||||
const tab = new App({ ...payload });
|
||||
const tab = { ...payload };
|
||||
tab.client_id = createRandomString(12, 12);
|
||||
tab.client_secret = createRandomString(24, 24);
|
||||
const docs = await this.insert([tab]);
|
||||
return { ...docs[0], tokens: [] };
|
||||
const doc = await this.insert(tab);
|
||||
return { ...doc, tokens: [] };
|
||||
}
|
||||
|
||||
public async insert(payloads: App[]): Promise<App[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.insert(payloads, (err, docs) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(docs);
|
||||
}
|
||||
});
|
||||
});
|
||||
public async insert(payload: App): Promise<App> {
|
||||
const doc = await AppModel.create(payload, { returning: true });
|
||||
return doc.get({ plain: true }) as App;
|
||||
}
|
||||
|
||||
public async update(payload: App): Promise<App> {
|
||||
const { _id, client_id, client_secret, tokens, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new App({ ...doc, ...other });
|
||||
const newDoc = await this.updateDb(tab);
|
||||
const newDoc = await this.updateDb({
|
||||
name: payload.name,
|
||||
scopes: payload.scopes,
|
||||
id: payload.id,
|
||||
} as any);
|
||||
return { ...newDoc, tokens: [] };
|
||||
}
|
||||
|
||||
private async updateDb(payload: App): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.update(
|
||||
{ _id: payload._id },
|
||||
payload,
|
||||
{ returnUpdatedDocs: true },
|
||||
(err, num, doc, up) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(doc);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
await AppModel.update(payload, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
return new Promise((resolve: any) => {
|
||||
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
public async getDb(query: any): Promise<App> {
|
||||
const doc: any = await AppModel.findOne({ where: query });
|
||||
return doc && (doc.get({ plain: true }) as App);
|
||||
}
|
||||
|
||||
public async resetSecret(_id: string): Promise<App> {
|
||||
const doc = await this.get(_id);
|
||||
const tab = new App({ ...doc });
|
||||
tab.client_secret = createRandomString(24, 24);
|
||||
tab.tokens = [];
|
||||
public async remove(ids: number[]) {
|
||||
await AppModel.destroy({ where: { id: ids } });
|
||||
}
|
||||
|
||||
public async resetSecret(id: number): Promise<App> {
|
||||
const tab: any = {
|
||||
client_secret: createRandomString(24, 24),
|
||||
tokens: [],
|
||||
id,
|
||||
};
|
||||
// const doc = await this.get(id);
|
||||
// const tab = new App({ ...doc });
|
||||
// tab.client_secret = createRandomString(24, 24);
|
||||
// tab.tokens = [];
|
||||
// const newDoc = await this.updateDb(tab);
|
||||
// return newDoc;
|
||||
const newDoc = await this.updateDb(tab);
|
||||
return newDoc;
|
||||
}
|
||||
@@ -97,42 +75,30 @@ export default class OpenService {
|
||||
): Promise<App[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
condition = {
|
||||
$or: [
|
||||
{
|
||||
value: reg,
|
||||
},
|
||||
{
|
||||
name: reg,
|
||||
},
|
||||
{
|
||||
remarks: reg,
|
||||
},
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = {
|
||||
[Op.or]: [
|
||||
{ [Op.like]: `%${searchText}%` },
|
||||
{ [Op.like]: `%${encodeText}%` },
|
||||
],
|
||||
};
|
||||
|
||||
condition = {
|
||||
...condition,
|
||||
name: reg,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition);
|
||||
return result.map((x) => ({ ...x, tokens: [] }));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const newDocs = await this.find(condition, sort);
|
||||
return newDocs.map((x) => ({ ...x, tokens: [] }));
|
||||
}
|
||||
|
||||
private async find(query: any, sort: any): Promise<App[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb
|
||||
.find(query)
|
||||
.sort({ ...sort })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<App> {
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
private async find(query: any, sort?: any): Promise<App[]> {
|
||||
const docs = await AppModel.findAll({ where: { ...query } });
|
||||
return docs.map((x) => x.get({ plain: true }));
|
||||
}
|
||||
|
||||
public async authToken({
|
||||
@@ -142,30 +108,38 @@ export default class OpenService {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
}): Promise<any> {
|
||||
const token = uuidV4();
|
||||
let token = uuidV4();
|
||||
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
||||
return new Promise((resolve) => {
|
||||
this.appDb.find({ client_id, client_secret }).exec((err, docs) => {
|
||||
if (docs && docs[0]) {
|
||||
this.appDb.update(
|
||||
{ client_id, client_secret },
|
||||
{ $push: { tokens: { value: token, expiration } } },
|
||||
{},
|
||||
(err, num, doc) => {
|
||||
resolve({
|
||||
code: 200,
|
||||
data: {
|
||||
token,
|
||||
token_type: 'Bearer',
|
||||
expiration,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
resolve({ code: 400, message: 'client_id或client_seret有误' });
|
||||
}
|
||||
});
|
||||
});
|
||||
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
|
||||
if (doc) {
|
||||
const timestamp = Math.round(Date.now() / 1000);
|
||||
const invalidTokens = (doc.tokens || []).filter(
|
||||
(x) => x.expiration >= timestamp,
|
||||
);
|
||||
let tokens = invalidTokens;
|
||||
if (invalidTokens.length > 5) {
|
||||
tokens = [
|
||||
...invalidTokens.slice(0, 4),
|
||||
{ ...invalidTokens[4], expiration },
|
||||
];
|
||||
token = invalidTokens[4].value;
|
||||
} else {
|
||||
tokens = [...invalidTokens, { value: token, expiration }];
|
||||
}
|
||||
await AppModel.update(
|
||||
{ tokens },
|
||||
{ where: { client_id, client_secret } },
|
||||
);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
token,
|
||||
token_type: 'Bearer',
|
||||
expiration,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { code: 400, message: 'client_id或client_seret有误' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+97
-19
@@ -3,16 +3,27 @@ import winston from 'winston';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { Crontab } from '../data/cron';
|
||||
import { exec } from 'child_process';
|
||||
import {
|
||||
ToadScheduler,
|
||||
SimpleIntervalJob,
|
||||
Task,
|
||||
SimpleIntervalSchedule,
|
||||
} from 'toad-scheduler';
|
||||
|
||||
@Service()
|
||||
export default class ScheduleService {
|
||||
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||
|
||||
private intervalSchedule = new ToadScheduler();
|
||||
|
||||
private maxBuffer = 200 * 1024 * 1024;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
|
||||
async createCronTask({ id = 0, command, name, schedule = '' }: Crontab) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[创建定时任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||
'[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||
_id,
|
||||
schedule,
|
||||
name,
|
||||
@@ -21,9 +32,67 @@ export default class ScheduleService {
|
||||
|
||||
this.scheduleStacks.set(
|
||||
_id,
|
||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||
nodeSchedule.scheduleJob(id + '', schedule, async () => {
|
||||
try {
|
||||
exec(command, async (error, stdout, stderr) => {
|
||||
exec(
|
||||
command,
|
||||
{ maxBuffer: this.maxBuffer },
|
||||
async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
stderr,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async cancelCronTask({ id = 0, name }: Crontab) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||
}
|
||||
|
||||
async createIntervalTask(
|
||||
{ id = 0, command, name = '' }: Crontab,
|
||||
schedule: SimpleIntervalSchedule,
|
||||
) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info(
|
||||
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
|
||||
_id,
|
||||
name,
|
||||
command,
|
||||
);
|
||||
const task = new Task(name, async () => {
|
||||
try {
|
||||
exec(
|
||||
command,
|
||||
{ maxBuffer: this.maxBuffer },
|
||||
async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
@@ -41,22 +110,31 @@ export default class ScheduleService {
|
||||
stderr,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
await this.logger.info(
|
||||
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||
command,
|
||||
new Date().toLocaleString(),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
const job = new SimpleIntervalJob({ ...schedule }, task, _id);
|
||||
|
||||
this.intervalSchedule.addIntervalJob(job);
|
||||
}
|
||||
|
||||
async cancelSchedule({ _id = '', name }: Crontab) {
|
||||
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||
async cancelIntervalTask({ id = 0, name }: Crontab) {
|
||||
const _id = this.formatId(id);
|
||||
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
|
||||
this.intervalSchedule.removeById(_id);
|
||||
}
|
||||
|
||||
private formatId(id: number): string {
|
||||
return String(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import CronService from './cron';
|
||||
|
||||
@Service()
|
||||
export default class ScriptService {
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private sockService: SockService,
|
||||
private cronService: CronService,
|
||||
) {}
|
||||
|
||||
public async runScript(path: string) {
|
||||
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
|
||||
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: JSON.stringify(err),
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('close', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: `执行结束`,
|
||||
});
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async stopScript(path: string) {
|
||||
const err = await this.cronService.killTask(`task -l ${path} now`);
|
||||
const str = err ? `\n${err}` : '';
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: `${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')} `,
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import got from 'got';
|
||||
|
||||
@Service()
|
||||
export default class SystemService {
|
||||
@Inject((type) => NotificationService)
|
||||
private notificationService!: NotificationService;
|
||||
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
|
||||
public async getLogRemoveFrequency() {
|
||||
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
|
||||
return doc || {};
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
await AuthModel.upsert({ ...payload });
|
||||
const doc = await this.getDb({ type: payload.type });
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<any> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
const code = Math.random().toString().slice(-6);
|
||||
const isSuccess = await this.notificationService.testNotify(
|
||||
notificationInfo,
|
||||
'青龙',
|
||||
`【蛟龙】测试通知 https://t.me/jiao_long`,
|
||||
);
|
||||
if (isSuccess) {
|
||||
const result = await this.updateAuthDb({
|
||||
type: AuthDataType.notification,
|
||||
info: { ...notificationInfo },
|
||||
});
|
||||
return { code: 200, data: { ...result, code } };
|
||||
} else {
|
||||
return { code: 400, data: '通知发送失败,请检查参数' };
|
||||
}
|
||||
}
|
||||
|
||||
public async updateLogRemoveFrequency(frequency: number) {
|
||||
const result = await this.updateAuthDb({
|
||||
type: AuthDataType.removeLogFrequency,
|
||||
info: { frequency },
|
||||
});
|
||||
const cron = {
|
||||
id: result.id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${frequency}`,
|
||||
};
|
||||
await this.scheduleService.cancelIntervalTask(cron);
|
||||
if (frequency > 0) {
|
||||
await this.scheduleService.createIntervalTask(cron, {
|
||||
days: frequency,
|
||||
runImmediately: true,
|
||||
});
|
||||
}
|
||||
return { code: 200, data: { ...cron } };
|
||||
}
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
let lastVersion = '';
|
||||
let lastLog = '';
|
||||
try {
|
||||
const result = await got.get(config.lastVersionFile, {
|
||||
timeout: 6000,
|
||||
retry: 0,
|
||||
});
|
||||
const lastVersionFileContent = result.body;
|
||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
} catch (error) {}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||
lastVersion,
|
||||
lastLog,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 400,
|
||||
data: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private checkHasNewVersion(curVersion: string, lastVersion: string) {
|
||||
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
|
||||
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
|
||||
if (curArr[0] < lastArr[0]) {
|
||||
return true;
|
||||
}
|
||||
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
curArr[0] === lastArr[0] &&
|
||||
curArr[1] === lastArr[1] &&
|
||||
curArr[2] < lastArr[2]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async updateSystem() {
|
||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: `开始更新系统`,
|
||||
});
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: JSON.stringify(err),
|
||||
});
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async notify({ title, content }: { title: string; content: string }) {
|
||||
const isSuccess = await this.notificationService.notify(title, content);
|
||||
if (isSuccess) {
|
||||
return { code: 200, data: '通知发送成功' };
|
||||
} else {
|
||||
return { code: 400, data: '通知发送失败,请检查参数' };
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-166
@@ -6,8 +6,7 @@ import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
import DataStore from 'nedb';
|
||||
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
|
||||
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
import { Request } from 'express';
|
||||
@@ -15,13 +14,11 @@ import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import got from 'got';
|
||||
import { dbs } from '../loaders/db';
|
||||
|
||||
@Service()
|
||||
export default class UserService {
|
||||
@Inject((type) => NotificationService)
|
||||
private notificationService!: NotificationService;
|
||||
private authDb = dbs.authDb;
|
||||
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
@@ -174,33 +171,24 @@ export default class UserService {
|
||||
}
|
||||
|
||||
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => {
|
||||
if (err || docs.length === 0) {
|
||||
resolve([]);
|
||||
} else {
|
||||
const result = docs.sort(
|
||||
(a, b) => b.info.timestamp - a.info.timestamp,
|
||||
);
|
||||
if (result.length > 100) {
|
||||
this.authDb.remove({ _id: result[result.length - 1]._id });
|
||||
}
|
||||
resolve(result.map((x) => x.info));
|
||||
}
|
||||
});
|
||||
const docs = await AuthModel.findAll({
|
||||
where: { type: AuthDataType.loginLog },
|
||||
});
|
||||
if (docs && docs.length > 0) {
|
||||
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
|
||||
if (result.length > 100) {
|
||||
await AuthModel.destroy({
|
||||
where: { id: result[result.length - 1].id },
|
||||
});
|
||||
}
|
||||
return result.map((x) => x.info);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb.insert(payload, (err, doc) => {
|
||||
if (err) {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
resolve(doc);
|
||||
}
|
||||
});
|
||||
});
|
||||
const doc = await AuthModel.create({ ...payload }, { returning: true });
|
||||
return doc;
|
||||
}
|
||||
|
||||
private initAuthInfo() {
|
||||
@@ -315,48 +303,27 @@ export default class UserService {
|
||||
}
|
||||
|
||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb
|
||||
.find({ type: AuthDataType.notification })
|
||||
.exec((err, docs) => {
|
||||
if (err || docs.length === 0) {
|
||||
resolve({} as NotificationInfo);
|
||||
} else {
|
||||
resolve(docs[0].info);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getLogRemoveFrequency() {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb
|
||||
.find({ type: AuthDataType.removeLogFrequency })
|
||||
.exec((err, docs) => {
|
||||
if (err || docs.length === 0) {
|
||||
resolve({});
|
||||
} else {
|
||||
resolve(docs[0].info);
|
||||
}
|
||||
});
|
||||
});
|
||||
const doc = await this.getDb({ type: AuthDataType.notification });
|
||||
return (doc && doc.info) || {};
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.authDb.update(
|
||||
{ type: payload.type },
|
||||
{ ...payload },
|
||||
{ upsert: true, returnUpdatedDocs: true },
|
||||
(err, num, doc: any) => {
|
||||
if (err) {
|
||||
resolve({} as NotificationInfo);
|
||||
} else {
|
||||
resolve({ ...doc.info, _id: doc._id });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
let doc = await AuthModel.findOne({ type: payload.type });
|
||||
if (doc) {
|
||||
const updateResult = await AuthModel.update(payload, {
|
||||
where: { id: doc.id },
|
||||
returning: true,
|
||||
});
|
||||
doc = updateResult[1][0];
|
||||
} else {
|
||||
doc = await AuthModel.create(payload, { returning: true });
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<any> {
|
||||
const doc: any = await AuthModel.findOne({ where: { ...query } });
|
||||
return doc && (doc.get({ plain: true }) as any);
|
||||
}
|
||||
|
||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||
@@ -376,104 +343,4 @@ export default class UserService {
|
||||
return { code: 400, data: '通知发送失败,请检查参数' };
|
||||
}
|
||||
}
|
||||
|
||||
public async updateLogRemoveFrequency(frequency: number) {
|
||||
const result = await this.updateAuthDb({
|
||||
type: AuthDataType.removeLogFrequency,
|
||||
info: { frequency },
|
||||
});
|
||||
const cron = {
|
||||
_id: result._id,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${frequency}`,
|
||||
schedule: `5 23 */${frequency} * *`,
|
||||
};
|
||||
await this.scheduleService.cancelSchedule(cron);
|
||||
if (frequency > 0) {
|
||||
await this.scheduleService.generateSchedule(cron);
|
||||
}
|
||||
return { code: 200, data: { ...cron } };
|
||||
}
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
const lastVersionFileContent = await (
|
||||
await got.get(config.lastVersionFile)
|
||||
).body;
|
||||
const lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
const lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||
lastVersion,
|
||||
lastLog,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 400,
|
||||
data: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private checkHasNewVersion(curVersion: string, lastVersion: string) {
|
||||
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
|
||||
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
|
||||
if (curArr[0] < lastArr[0]) {
|
||||
return true;
|
||||
}
|
||||
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
curArr[0] === lastArr[0] &&
|
||||
curArr[1] === lastArr[1] &&
|
||||
curArr[2] < lastArr[2]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async updateSystem() {
|
||||
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: `开始更新系统`,
|
||||
});
|
||||
cp.stdout.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'updateSystemVersion',
|
||||
message: JSON.stringify(err),
|
||||
});
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CommandModule } from 'yargs';
|
||||
export const updateCommand: CommandModule = {
|
||||
command: 'update',
|
||||
describe: 'Update and restart qinglong',
|
||||
builder: (yargs) => {
|
||||
return yargs.option('repositority', {
|
||||
type: 'string',
|
||||
alias: 'r',
|
||||
describe: `Specify the release warehouse address of the package`,
|
||||
});
|
||||
},
|
||||
handler: async (argv) => {},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as yargs from 'yargs';
|
||||
import { green, red } from 'chalk';
|
||||
import { updateCommand } from './commands/update';
|
||||
|
||||
yargs
|
||||
.usage('Usage: ql [command] <options>')
|
||||
.command(updateCommand)
|
||||
.fail((err) => {
|
||||
console.error(`${red(err)}`);
|
||||
})
|
||||
.alias('h', 'help')
|
||||
.showHelp()
|
||||
.recommendCommands().argv;
|
||||
@@ -1,16 +0,0 @@
|
||||
version: '2'
|
||||
services:
|
||||
web:
|
||||
image: whyour/qinglong:latest
|
||||
volumes:
|
||||
- ./data/config:/ql/config
|
||||
- ./data/log:/ql/log
|
||||
- ./data/db:/ql/db
|
||||
- ./data/scripts:/ql/scripts
|
||||
- ./data/repo:/ql/repo
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
environment:
|
||||
- ENABLE_HANGUP=true
|
||||
- ENABLE_WEB_PANEL=true
|
||||
restart: always
|
||||
+15
-10
@@ -1,19 +1,21 @@
|
||||
FROM node:lts-alpine
|
||||
FROM node:alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
ARG QL_BRANCH=master
|
||||
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
LANG=zh_CN.UTF-8 \
|
||||
SHELL=/bin/bash \
|
||||
PS1="\u@\h:\w \$ " \
|
||||
QL_DIR=/ql \
|
||||
QL_MAINTAINER=${QL_MAINTAINER} \
|
||||
QL_URL=${QL_URL} \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
|
||||
WORKDIR ${QL_DIR}
|
||||
|
||||
RUN set -x \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
|
||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update -f \
|
||||
&& apk upgrade \
|
||||
&& apk --no-cache add -f bash \
|
||||
@@ -34,20 +36,23 @@ RUN set -x \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& touch ~/.bashrc \
|
||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||
&& git config --global user.name "qinglong" \
|
||||
&& npm install -g pnpm \
|
||||
&& pnpm install -g pm2 \
|
||||
&& pnpm install -g ts-node typescript tslib \
|
||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
&& cd ${QL_DIR} \
|
||||
&& cp -f .env.example .env \
|
||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||
&& npm install -g pnpm \
|
||||
&& pnpm install -g pm2 \
|
||||
&& pnpm install -g ts-node typescript tslib \
|
||||
&& rm -rf /root/.npm \
|
||||
&& pnpm install --prod \
|
||||
&& rm -rf /root/.pnpm-store \
|
||||
&& rm -rf /root/.cache \
|
||||
&& rm -rf /root/.npm \
|
||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||
&& cp -rf /static/* ${QL_DIR} \
|
||||
&& mkdir -p ${QL_DIR}/static \
|
||||
&& cp -rf /static/* ${QL_DIR}/static \
|
||||
&& rm -rf /static
|
||||
|
||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: '2'
|
||||
services:
|
||||
web:
|
||||
image: whyour/qinglong:latest
|
||||
volumes:
|
||||
- ./data:/ql/data
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
restart: unless-stopped
|
||||
@@ -6,6 +6,7 @@ link_shell
|
||||
|
||||
echo -e "======================1. 检测配置文件========================\n"
|
||||
make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
pm2 l &>/dev/null
|
||||
@@ -19,24 +20,29 @@ echo -e "======================3. 启动nginx========================\n"
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "nginx启动成功...\n"
|
||||
|
||||
echo -e "======================4. 启动控制面板========================\n"
|
||||
echo -e "======================4. 启动面板监控========================\n"
|
||||
pm2 delete public &>/dev/null
|
||||
pm2 start $dir_static/build/public.js -n public --source-map-support --time
|
||||
echo -e "监控服务启动成功...\n"
|
||||
|
||||
echo -e "======================5. 启动控制面板========================\n"
|
||||
pm2 delete panel &>/dev/null
|
||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
|
||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
|
||||
echo -e "控制面板启动成功...\n"
|
||||
|
||||
echo -e "======================5. 启动定时任务========================\n"
|
||||
echo -e "======================6. 启动定时任务========================\n"
|
||||
pm2 delete schedule &>/dev/null
|
||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
|
||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
|
||||
echo -e "定时任务启动成功...\n"
|
||||
|
||||
if [[ $AutoStartBot == true ]]; then
|
||||
echo -e "======================6. 启动bot========================\n"
|
||||
echo -e "======================7. 启动bot========================\n"
|
||||
nohup ql bot >>$dir_log/start.log 2>&1 &
|
||||
echo -e "bot后台启动中...\n"
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
echo -e "======================7. 执行自定义脚本========================\n"
|
||||
echo -e "======================8. 执行自定义脚本========================\n"
|
||||
nohup ql extra >>$dir_log/start.log 2>&1 &
|
||||
echo -e "自定义脚本后台执行中...\n"
|
||||
fi
|
||||
|
||||
+16
-1
@@ -2,6 +2,10 @@ upstream api {
|
||||
server 0.0.0.0:5600;
|
||||
}
|
||||
|
||||
upstream public {
|
||||
server 0.0.0.0:5400;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default keep-alive;
|
||||
'websocket' upgrade;
|
||||
@@ -9,9 +13,16 @@ map $http_upgrade $connection_upgrade {
|
||||
|
||||
server {
|
||||
listen 5700;
|
||||
root /ql/dist;
|
||||
root /ql/static/dist;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location /api/public {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://public;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -42,4 +53,8 @@ server {
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~ .*\.(html)$ {
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
}
|
||||
|
||||
+64
-52
@@ -1,14 +1,17 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "umi dev",
|
||||
"build": "umi build",
|
||||
"build-back": "tsc -p tsconfig.back.json",
|
||||
"start-back": "nodemon",
|
||||
"panel": "npm run build-back && node build/app.js",
|
||||
"schedule": "npm run build-back && node build/schedule.js",
|
||||
"prepare": "umi generate tmp",
|
||||
"start": "concurrently -n w: npm:start:*",
|
||||
"start:front": "umi dev",
|
||||
"start:back": "nodemon",
|
||||
"start:public": "ts-node back/public.ts",
|
||||
"build:front": "umi build",
|
||||
"build:back": "tsc -p tsconfig.back.json",
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
"schedule": "npm run build:back && node static/build/schedule.js",
|
||||
"public": "npm run build:back && node static/build/public.js",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"prepare": "umi generate tmp",
|
||||
"test": "umi-test",
|
||||
"test:coverage": "umi-test --coverage"
|
||||
},
|
||||
@@ -25,15 +28,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^6.14.0",
|
||||
"@sentry/tracing": "^6.14.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"celebrate": "^13.0.3",
|
||||
"@sentry/node": "^6.18.1",
|
||||
"@sentry/tracing": "^6.18.1",
|
||||
"body-parser": "^1.19.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^3.5.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"cron-parser": "^4.2.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express-jwt": "^6.1.1",
|
||||
"express-urlrewrite": "^1.4.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^0.1.2",
|
||||
@@ -41,48 +45,56 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nedb": "^1.8.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-schedule": "^2.0.0",
|
||||
"nodemailer": "^6.7.0",
|
||||
"p-queue": "6.6.2",
|
||||
"node-fetch": "^3.2.1",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"p-queue": "7.2.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.17.0",
|
||||
"serve-handler": "^6.1.3",
|
||||
"sockjs": "^0.3.21",
|
||||
"typedi": "^0.8.0",
|
||||
"sockjs": "^0.3.24",
|
||||
"sqlite3": "npm:@louislam/sqlite3@^6.0.1",
|
||||
"toad-scheduler": "^1.6.0",
|
||||
"typedi": "^0.10.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
"winston": "^3.6.0",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@ant-design/pro-layout": "^6.26.0",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/pro-layout": "^6.33.1",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@sentry/react": "^6.14.0",
|
||||
"@sentry/webpack-plugin": "^1.18.3",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/express-jwt": "^6.0.1",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/lodash": "^4.14.170",
|
||||
"@types/nedb": "^1.8.11",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@sentry/react": "^6.18.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-jwt": "^6.0.4",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/nedb": "^1.8.12",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/qrcode.react": "^1.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/serve-handler": "^6.1.1",
|
||||
"@types/sockjs": "^0.3.33",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@umijs/plugin-antd": "^0.11.0",
|
||||
"@umijs/test": "^3.3.9",
|
||||
"antd": "^4.17.0-alpha.6",
|
||||
"codemirror": "^5.62.2",
|
||||
"compression-webpack-plugin": "6.1.1",
|
||||
"darkreader": "^4.9.27",
|
||||
"lint-staged": "^10.0.7",
|
||||
"nodemon": "^2.0.4",
|
||||
"prettier": "^2.2.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@umijs/plugin-antd": "^0.15.0",
|
||||
"@umijs/plugin-esbuild": "^1.4.1",
|
||||
"@umijs/test": "^3.5.21",
|
||||
"ansi-to-react": "^6.1.6",
|
||||
"antd": "^4.18.9",
|
||||
"codemirror": "^5.65.2",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"darkreader": "4.9.44",
|
||||
"lint-staged": "^12.3.4",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"qiniu": "^7.4.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "17.x",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
@@ -91,13 +103,13 @@
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "17.x",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.5.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"umi": "^3.5.0",
|
||||
"umi-request": "^1.3.5",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"typescript": "^4.6.2",
|
||||
"umi": "^3.5.21",
|
||||
"umi-request": "^1.4.0",
|
||||
"vh-check": "^2.0.5",
|
||||
"webpack": "^5.28.0",
|
||||
"webpack": "^5.70.0",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB |
+23
-6
@@ -16,8 +16,8 @@ DefaultCronRule=""
|
||||
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
||||
RepoFileExtensions="js py"
|
||||
|
||||
## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除
|
||||
GithubProxyUrl="https://ghproxy.com/"
|
||||
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
|
||||
ProxyUrl=""
|
||||
|
||||
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||
CommandTimeoutTime="1h"
|
||||
@@ -35,6 +35,9 @@ EnableExtraShell="true"
|
||||
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
|
||||
AutoStartBot=""
|
||||
|
||||
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||
BotRepoUrl=""
|
||||
|
||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
|
||||
@@ -47,12 +50,14 @@ export PUSH_KEY=""
|
||||
## 2. BARK
|
||||
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
|
||||
export BARK_PUSH=""
|
||||
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
|
||||
export BARK_ICON="http://qn.whyour.cn/logo.png"
|
||||
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
|
||||
export BARK_SOUND=""
|
||||
## 下方填写推送消息分组,默认为"QingLong"
|
||||
export BARK_GROUP="QingLong"
|
||||
|
||||
## 3. Telegram
|
||||
## 3. Telegram
|
||||
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
|
||||
export TG_BOT_TOKEN=""
|
||||
## 下方填写 @getuseridbot 中获取到的纯数字ID
|
||||
@@ -73,7 +78,7 @@ export TG_PROXY_AUTH=""
|
||||
## 如需使用,请赋值代理地址链接,并自行解除下一行的注释
|
||||
export TG_API_HOST=""
|
||||
|
||||
## 4. 钉钉
|
||||
## 4. 钉钉
|
||||
## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access
|
||||
## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可
|
||||
export DD_BOT_TOKEN=""
|
||||
@@ -104,12 +109,24 @@ export PUSH_PLUS_TOKEN=""
|
||||
export PUSH_PLUS_USER=""
|
||||
|
||||
## 9. go-cqhttp
|
||||
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||
## go-cqhttp相关API https://docs.go-cqhttp.org/api
|
||||
export GOBOT_URL=""
|
||||
export GOBOT_TOKEN=""
|
||||
export GOBOT_QQ=""
|
||||
|
||||
## 10. gotify
|
||||
## gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||
## gotify_token 填写gotify的消息应用token
|
||||
## gotify_priority 填写推送消息优先级,默认为0
|
||||
export GOTIFY_URL=""
|
||||
export GOTIFY_TOKEN=""
|
||||
export GOTIFY_PRIORITY=0
|
||||
|
||||
## 11. PushDeer
|
||||
## deer_key 填写PushDeer的key
|
||||
export DEER_KEY=""
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
|
||||
+128
-7
@@ -13,23 +13,37 @@
|
||||
const querystring = require('querystring');
|
||||
const $ = new Env();
|
||||
const timeout = 15000; //超时时间(单位毫秒)
|
||||
// =======================================gotify通知设置区域==============================================
|
||||
//gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||
//gotify_token 填写gotify的消息应用token
|
||||
//gotify_priority 填写推送消息优先级,默认为0
|
||||
let GOTIFY_URL = '';
|
||||
let GOTIFY_TOKEN = '';
|
||||
let GOTIFY_PRIORITY = 0;
|
||||
// =======================================go-cqhttp通知设置区域===========================================
|
||||
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
||||
//gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||
//gobot_qq 填写推送到个人QQ或者QQ群号
|
||||
//go-cqhttp相关API https://docs.go-cqhttp.org/api
|
||||
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||
let GOBOT_TOKEN = ''; //访问密钥
|
||||
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||
|
||||
// =======================================微信server酱通知设置区域===========================================
|
||||
//此处填你申请的SCKEY.
|
||||
//(环境变量名 PUSH_KEY)
|
||||
let SCKEY = '';
|
||||
|
||||
// =======================================PushDeer通知设置区域===========================================
|
||||
//此处填你申请的PushDeer KEY.
|
||||
//(环境变量名 DEER_KEY)
|
||||
let PUSHDEER_KEY = '';
|
||||
|
||||
// =======================================Bark App通知设置区域===========================================
|
||||
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
|
||||
let BARK_PUSH = '';
|
||||
//BARK app推送图标,自定义推送图标(需iOS15或以上)
|
||||
let BARK_ICON = 'http://qn.whyour.cn/logo.png';
|
||||
//BARK app推送铃声,铃声列表去APP查看复制填写
|
||||
let BARK_SOUND = '';
|
||||
//BARK app推送消息的分组, 默认为"QingLong"
|
||||
@@ -84,6 +98,16 @@ let PUSH_PLUS_TOKEN = '';
|
||||
let PUSH_PLUS_USER = '';
|
||||
|
||||
//==========================云端环境变量的判断与接收=========================
|
||||
if (process.env.GOTIFY_URL) {
|
||||
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||
}
|
||||
if (process.env.GOTIFY_TOKEN) {
|
||||
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
|
||||
}
|
||||
if (process.env.GOTIFY_PRIORITY) {
|
||||
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
|
||||
}
|
||||
|
||||
if (process.env.GOBOT_URL) {
|
||||
GOBOT_URL = process.env.GOBOT_URL;
|
||||
}
|
||||
@@ -98,6 +122,10 @@ if (process.env.PUSH_KEY) {
|
||||
SCKEY = process.env.PUSH_KEY;
|
||||
}
|
||||
|
||||
if (process.env.DEER_KEY) {
|
||||
PUSHDEER_KEY = process.env.DEER_KEY;
|
||||
}
|
||||
|
||||
if (process.env.QQ_SKEY) {
|
||||
QQ_SKEY = process.env.QQ_SKEY;
|
||||
}
|
||||
@@ -116,6 +144,9 @@ if (process.env.BARK_PUSH) {
|
||||
} else {
|
||||
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
|
||||
}
|
||||
if (process.env.BARK_ICON) {
|
||||
BARK_ICON = process.env.BARK_ICON;
|
||||
}
|
||||
if (process.env.BARK_SOUND) {
|
||||
BARK_SOUND = process.env.BARK_SOUND;
|
||||
}
|
||||
@@ -199,16 +230,54 @@ async function sendNotify(
|
||||
qywxBotNotify(text, desp), //企业微信机器人
|
||||
qywxamNotify(text, desp), //企业微信应用消息推送
|
||||
iGotNotify(text, desp, params), //iGot
|
||||
gobotNotify(text, desp),//go-cqhttp
|
||||
gobotNotify(text, desp), //go-cqhttp
|
||||
gotifyNotify(text, desp), //gotify
|
||||
]);
|
||||
}
|
||||
|
||||
function gotifyNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
||||
const options = {
|
||||
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
|
||||
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
|
||||
desp,
|
||||
)}&priority=${GOTIFY_PRIORITY}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
};
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('gotify发送通知调用API失败!!\n');
|
||||
console.log(err);
|
||||
} else {
|
||||
data = JSON.parse(data);
|
||||
if (data.id) {
|
||||
console.log('gotify发送通知消息成功🎉\n');
|
||||
} else {
|
||||
console.log(`${data.message}\n`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function gobotNotify(text, desp, time = 2100) {
|
||||
return new Promise((resolve) => {
|
||||
if (GOBOT_URL) {
|
||||
const options = {
|
||||
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
|
||||
json: {message:`${text}\n${desp}`},
|
||||
json: { message: `${text}\n${desp}` },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -293,6 +362,52 @@ function serverNotify(text, desp, time = 2100) {
|
||||
});
|
||||
}
|
||||
|
||||
function PushDeerNotify(text, desp, time = 2100) {
|
||||
return new Promise((resolve) => {
|
||||
if (PUSHDEER_KEY) {
|
||||
// PushDeer 建议对消息内容进行 urlencode
|
||||
desp = encodeURI(desp);
|
||||
const options = {
|
||||
url: `https://api2.pushdeer.com/message/push`,
|
||||
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
setTimeout(() => {
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('发送通知调用API失败!!\n');
|
||||
console.log(err);
|
||||
} else {
|
||||
data = JSON.parse(data);
|
||||
// 通过返回的result的长度来判断是否成功
|
||||
if (
|
||||
data.content.result.length !== undefined &&
|
||||
data.content.result.length > 0
|
||||
) {
|
||||
console.log('PushDeer发送通知消息成功🎉\n');
|
||||
} else {
|
||||
console.log(
|
||||
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}, time);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function CoolPush(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (QQ_SKEY) {
|
||||
@@ -376,7 +491,9 @@ function BarkNotify(text, desp, params = {}) {
|
||||
const options = {
|
||||
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
|
||||
desp,
|
||||
)}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`,
|
||||
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
|
||||
params,
|
||||
)}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -412,9 +529,13 @@ function tgBotNotify(text, desp) {
|
||||
if (TG_BOT_TOKEN && TG_USER_ID) {
|
||||
const options = {
|
||||
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
||||
body: `chat_id=${TG_USER_ID}&text=${text}\n\n${desp}&disable_web_page_preview=true`,
|
||||
json: {
|
||||
chat_id: `${TG_USER_ID}`,
|
||||
text: `${text}\n\n${desp}`,
|
||||
disable_web_page_preview: true,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ push_config = {
|
||||
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||
'BARK_GROUP': '', # bark 推送分组
|
||||
'BARK_SOUND': '', # bark 推送声音
|
||||
'BARK_ICON': '', # bark 推送图标
|
||||
|
||||
'CONSOLE': True, # 控制台输出
|
||||
|
||||
@@ -51,10 +52,16 @@ push_config = {
|
||||
# /send_group_msg 时填入 group_id=QQ群
|
||||
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
|
||||
|
||||
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
|
||||
'GOTIFY_TOKEN': '', # gotify的消息应用token
|
||||
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
|
||||
|
||||
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
|
||||
|
||||
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||
|
||||
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
||||
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
|
||||
@@ -100,6 +107,7 @@ def bark(title: str, content: str) -> None:
|
||||
"BARK_ARCHIVE": "isArchive",
|
||||
"BARK_GROUP": "group",
|
||||
"BARK_SOUND": "sound",
|
||||
"BARK_ICON": "icon",
|
||||
}
|
||||
params = ""
|
||||
for pair in filter(
|
||||
@@ -194,6 +202,25 @@ def go_cqhttp(title: str, content: str) -> None:
|
||||
print("go-cqhttp 推送失败!")
|
||||
|
||||
|
||||
def gotify(title:str,content:str) -> None:
|
||||
"""
|
||||
使用 gotify 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("gotify 服务启动")
|
||||
|
||||
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
||||
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
|
||||
response = requests.post(url,data=data).json()
|
||||
|
||||
if response.get("id"):
|
||||
print("gotify 推送成功!")
|
||||
else:
|
||||
print("gotify 推送失败!")
|
||||
|
||||
|
||||
def iGot(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 iGot 推送消息。
|
||||
@@ -236,6 +263,24 @@ def serverJ(title: str, content: str) -> None:
|
||||
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
||||
|
||||
|
||||
def pushdeer(title: str, content: str) -> None:
|
||||
"""
|
||||
通过PushDeer 推送消息
|
||||
"""
|
||||
if not push_config.get("DEER_KEY"):
|
||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushDeer 服务启动")
|
||||
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
|
||||
url = 'https://api2.pushdeer.com/message/push'
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
if len(response.get("content").get("result")) > 0:
|
||||
print("PushDeer 推送成功!")
|
||||
else:
|
||||
print("PushDeer 推送失败!错误信息:", response)
|
||||
|
||||
|
||||
def pushplus_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 push+ 推送消息。
|
||||
@@ -262,6 +307,7 @@ def pushplus_bot(title: str, content: str) -> None:
|
||||
else:
|
||||
|
||||
url_old = "http://pushplus.hxtrip.com/send"
|
||||
headers["Accept"] = "application/json"
|
||||
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||
|
||||
if response["code"] == 200:
|
||||
@@ -473,10 +519,14 @@ if push_config.get("FSKEY"):
|
||||
notify_function.append(feishu_bot)
|
||||
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
|
||||
notify_function.append(go_cqhttp)
|
||||
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
|
||||
notify_function.append(gotify)
|
||||
if push_config.get("IGOT_PUSH_KEY"):
|
||||
notify_function.append(iGot)
|
||||
if push_config.get("PUSH_KEY"):
|
||||
notify_function.append(serverJ)
|
||||
if push_config.get("DEER_KEY"):
|
||||
notify_function.append(pushdeer)
|
||||
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||
notify_function.append(pushplus_bot)
|
||||
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as qiniu from 'qiniu';
|
||||
import dotenv from 'dotenv';
|
||||
const envFound = dotenv.config();
|
||||
|
||||
const accessKey = process.env.QINIU_AK;
|
||||
const secretKey = process.env.QINIU_SK;
|
||||
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
||||
const key = 'version.ts';
|
||||
const options = {
|
||||
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
||||
};
|
||||
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||
const uploadToken = putPolicy.uploadToken(mac);
|
||||
|
||||
const localFile = 'src/version.ts';
|
||||
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
|
||||
const formUploader = new qiniu.form_up.FormUploader(config);
|
||||
const putExtra = new qiniu.form_up.PutExtra(
|
||||
'',
|
||||
'',
|
||||
'text/plain; charset=utf-8',
|
||||
);
|
||||
// 文件上传
|
||||
formUploader.putFile(
|
||||
uploadToken,
|
||||
key,
|
||||
localFile,
|
||||
putExtra,
|
||||
function (respErr, respBody, respInfo) {
|
||||
if (respErr) {
|
||||
throw respErr;
|
||||
}
|
||||
if (respInfo.statusCode == 200) {
|
||||
console.log(respBody);
|
||||
} else {
|
||||
console.log(respInfo.statusCode);
|
||||
console.log(respBody);
|
||||
}
|
||||
},
|
||||
);
|
||||
+28
-2
@@ -61,7 +61,7 @@ update_cron_api() {
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \
|
||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
@@ -93,7 +93,7 @@ update_cron_command_api() {
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \
|
||||
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
@@ -176,4 +176,30 @@ update_cron() {
|
||||
fi
|
||||
}
|
||||
|
||||
notify_api() {
|
||||
local title=$1
|
||||
local content=$2
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system/notify?t=$currentTimeStamp" \
|
||||
-X 'PUT' \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-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" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo $api | jq -r .code)
|
||||
message=$(echo $api | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "通知发送成功"
|
||||
else
|
||||
echo -e "通知失败(${message})"
|
||||
fi
|
||||
}
|
||||
|
||||
get_token
|
||||
|
||||
+16
-10
@@ -1,10 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=/ql/shell
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
|
||||
repo_path="${dir_repo}/dockerbot"
|
||||
|
||||
if [[ -z ${BotRepoUrl} ]]; then
|
||||
url="https://github.com/SuMaiKaDe/bot.git"
|
||||
repo_path="${dir_repo}/dockerbot"
|
||||
else
|
||||
url=${BotRepoUrl}
|
||||
repo_path="${dir_repo}/diybot"
|
||||
fi
|
||||
|
||||
echo -e "\n1、安装bot依赖...\n"
|
||||
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
|
||||
@@ -16,9 +22,9 @@ if [[ ! -d ${repo_path}/.git ]]; then
|
||||
git_clone_scripts ${url} ${repo_path} "main"
|
||||
fi
|
||||
|
||||
cp -rf "$repo_path/jbot" $dir_root
|
||||
if [[ ! -f "$dir_root/config/bot.json" ]]; then
|
||||
cp -f "$repo_path/config/bot.json" "$dir_root/config"
|
||||
cp -rf "$repo_path/jbot" $dir_data
|
||||
if [[ ! -f "$dir_config/bot.json" ]]; then
|
||||
cp -f "$repo_path/config/bot.json" "$dir_config"
|
||||
fi
|
||||
echo -e "\nbot文件下载成功...\n"
|
||||
|
||||
@@ -26,12 +32,12 @@ echo -e "3、安装python3依赖...\n"
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
|
||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
|
||||
|
||||
cd $dir_root
|
||||
cd $dir_data
|
||||
cat requirements.txt | while read LREAD
|
||||
do
|
||||
if test ! -z "$(pip3 show "${LREAD%%=*}" 1>/dev/null)"; then
|
||||
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
||||
pip3 --default-timeout=100 install ${LREAD}
|
||||
fi
|
||||
done
|
||||
@@ -40,7 +46,7 @@ echo -e "\npython3依赖安装成功...\n"
|
||||
|
||||
echo -e "4、启动bot程序...\n"
|
||||
make_dir $dir_log/bot
|
||||
cd $dir_root
|
||||
cd $dir_data
|
||||
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
|
||||
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
|
||||
echo -e "bot启动成功...\n"
|
||||
|
||||
+8
-15
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir_shell=/ql/shell
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
@@ -10,13 +10,11 @@ reset_env() {
|
||||
echo -e "---> 配置文件检测完成\n"
|
||||
|
||||
echo -e "---> 2. 开始安装青龙依赖\n"
|
||||
rm -rf $dir_root/node_modules
|
||||
npm_install_2 $dir_root
|
||||
echo -e "---> 青龙依赖安装完成\n"
|
||||
|
||||
echo -e "---> 3. 开始安装脚本依赖\n"
|
||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||
rm -rf $dir_scripts/node_modules
|
||||
npm_install_2 $dir_scripts
|
||||
echo -e "---> 脚本依赖安装完成\n"
|
||||
}
|
||||
@@ -39,25 +37,19 @@ copy_dep() {
|
||||
reload_pm2() {
|
||||
pm2 l &>/dev/null
|
||||
|
||||
if test -z "$(pm2 info panel 1>/dev/null)"; then
|
||||
pm2 reload panel --source-map-support --time &>/dev/null
|
||||
else
|
||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
fi
|
||||
pm2 delete panel --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
|
||||
if test -z "$(pm2 info schedule 1>/dev/null)"; then
|
||||
pm2 reload schedule --source-map-support --time &>/dev/null
|
||||
else
|
||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
fi
|
||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
}
|
||||
|
||||
pm2_log() {
|
||||
echo -e "---> pm2日志"
|
||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
||||
local panelError="/root/.pm2/logs/panel-error.log"
|
||||
tail -n 20 "$panelOut"
|
||||
tail -n 20 "$panelError"
|
||||
tail -n 100 "$panelOut"
|
||||
tail -n 100 "$panelError"
|
||||
}
|
||||
|
||||
check_nginx() {
|
||||
@@ -106,6 +98,7 @@ check_pm2() {
|
||||
|
||||
main() {
|
||||
echo -e "=====> 开始检测"
|
||||
pnpm install -g pm2
|
||||
copy_dep
|
||||
check_ql
|
||||
check_nginx
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
const notify = require('/ql/scripts/sendNotify.js');
|
||||
const title = process.argv[2];
|
||||
const content = process.argv[3];
|
||||
|
||||
notify.sendNotify(`${title}`, `${content}`);
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
#author:spark thanks to: https://github.com/sparkssssssss/scripts
|
||||
|
||||
. /ql/config/config.sh
|
||||
title=$(echo $1|sed 's/-/_/g')
|
||||
msg=$(echo -e $2)
|
||||
|
||||
node /ql/shell/notify.js "$title" "$msg"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
echo -e "开始发布"
|
||||
|
||||
echo -e "切换master分支"
|
||||
git checkout master
|
||||
|
||||
echo -e "合并develop代码"
|
||||
git merge origin/develop
|
||||
|
||||
echo -e "提交master代码"
|
||||
git push
|
||||
|
||||
echo -e "更新cdn文件"
|
||||
ts-node sample/tool.ts
|
||||
|
||||
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
|
||||
version="v$string"
|
||||
echo -e "当前版本$version"
|
||||
|
||||
echo -e "删除已经存在的本地tag"
|
||||
git tag -d "$version" &>/dev/null
|
||||
|
||||
echo -e "删除已经存在的远程tag"
|
||||
git push origin :refs/tags/$version &>/dev/null
|
||||
|
||||
echo -e "创建新tag"
|
||||
git tag -a "$version" -m "release $version"
|
||||
|
||||
echo -e "提交tag"
|
||||
git push --tags
|
||||
|
||||
echo -e "完成发布"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=/ql/shell
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
|
||||
days=$1
|
||||
|
||||
+70
-17
@@ -1,15 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 目录
|
||||
dir_root=/ql
|
||||
dir_root=$QL_DIR
|
||||
dir_data=$dir_root/data
|
||||
dir_shell=$dir_root/shell
|
||||
dir_sample=$dir_root/sample
|
||||
dir_config=$dir_root/config
|
||||
dir_scripts=$dir_root/scripts
|
||||
dir_repo=$dir_root/repo
|
||||
dir_raw=$dir_root/raw
|
||||
dir_log=$dir_root/log
|
||||
dir_db=$dir_root/db
|
||||
dir_static=$dir_root/static
|
||||
dir_config=$dir_data/config
|
||||
dir_scripts=$dir_data/scripts
|
||||
dir_repo=$dir_data/repo
|
||||
dir_raw=$dir_data/raw
|
||||
dir_log=$dir_data/log
|
||||
dir_db=$dir_data/db
|
||||
dir_dep=$dir_data/deps
|
||||
dir_list_tmp=$dir_log/.tmp
|
||||
dir_code=$dir_log/code
|
||||
dir_update_log=$dir_log/update
|
||||
@@ -34,6 +37,8 @@ file_notify_js=$dir_scripts/sendNotify.js
|
||||
task_error_log_path=$dir_log/task_error.log
|
||||
nginx_app_conf=$dir_root/docker/front.conf
|
||||
nginx_conf=$dir_root/docker/nginx.conf
|
||||
dep_notify_py=$dir_dep/notify.py
|
||||
dep_notify_js=$dir_dep/sendNotify.js
|
||||
|
||||
## 清单文件
|
||||
list_crontab_user=$dir_config/crontab.list
|
||||
@@ -57,6 +62,7 @@ original_name=(
|
||||
|
||||
init_env() {
|
||||
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
|
||||
export PYTHONUNBUFFERED=1
|
||||
}
|
||||
|
||||
import_config() {
|
||||
@@ -64,8 +70,9 @@ import_config() {
|
||||
[[ -f $file_env ]] && . $file_env
|
||||
|
||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
||||
github_proxy_url=${GithubProxyUrl:-""}
|
||||
proxy_url=${ProxyUrl:-""}
|
||||
file_extensions=${RepoFileExtensions:-"js py"}
|
||||
current_branch=${QL_BRANCH}
|
||||
|
||||
if [[ -n "${DefaultCronRule}" ]]; then
|
||||
default_cron="${DefaultCronRule}"
|
||||
@@ -74,6 +81,18 @@ import_config() {
|
||||
fi
|
||||
}
|
||||
|
||||
set_proxy() {
|
||||
if [[ $proxy_url ]]; then
|
||||
export http_proxy="${proxy_url}"
|
||||
export https_proxy="${proxy_url}"
|
||||
fi
|
||||
}
|
||||
|
||||
unset_proxy() {
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
}
|
||||
|
||||
make_dir() {
|
||||
local dir=$1
|
||||
if [[ ! -d $dir ]]; then
|
||||
@@ -146,6 +165,8 @@ define_cmd() {
|
||||
}
|
||||
|
||||
fix_config() {
|
||||
make_dir $dir_static
|
||||
make_dir $dir_data
|
||||
make_dir $dir_config
|
||||
make_dir $dir_log
|
||||
make_dir $dir_db
|
||||
@@ -154,6 +175,7 @@ fix_config() {
|
||||
make_dir $dir_repo
|
||||
make_dir $dir_raw
|
||||
make_dir $dir_update_log
|
||||
make_dir $dir_dep
|
||||
|
||||
if [[ ! -s $file_config_user ]]; then
|
||||
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
||||
@@ -205,12 +227,26 @@ fix_config() {
|
||||
|
||||
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
|
||||
echo -e "检测到默认nginx配置文件,清空...\n"
|
||||
cat /dev/null > /etc/nginx/conf.d/default.conf
|
||||
cat /dev/null >/etc/nginx/conf.d/default.conf
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $dep_notify_js ]]; then
|
||||
echo -e "复制一份 $file_notify_js_sample 为 $dep_notify_js\n"
|
||||
cp -fv $file_notify_js_sample $dep_notify_js
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $dep_notify_py ]]; then
|
||||
echo -e "复制一份 $file_notify_py_sample 为 $dep_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $dep_notify_py
|
||||
echo
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
npm_install_sub() {
|
||||
set_proxy
|
||||
if [ $is_termux -eq 1 ]; then
|
||||
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
|
||||
elif ! type pnpm &>/dev/null; then
|
||||
@@ -218,6 +254,7 @@ npm_install_sub() {
|
||||
else
|
||||
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
|
||||
fi
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
npm_install_1() {
|
||||
@@ -270,21 +307,27 @@ git_clone_scripts() {
|
||||
local branch=$3
|
||||
[[ $branch ]] && local part_cmd="-b $branch "
|
||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
||||
|
||||
set_proxy
|
||||
git clone $part_cmd $url $dir
|
||||
exit_status=$?
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
git_pull_scripts() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work="$1"
|
||||
local branch="$2"
|
||||
[[ $branch ]] && local part_cmd="origin/${branch}"
|
||||
cd $dir_work
|
||||
echo -e "开始更新仓库:$dir_work\n"
|
||||
|
||||
set_proxy
|
||||
git fetch --all
|
||||
exit_status=$?
|
||||
git reset --hard $part_cmd
|
||||
git pull
|
||||
git pull &>/dev/null
|
||||
unset_proxy
|
||||
reset_branch "$branch"
|
||||
|
||||
cd $dir_current
|
||||
}
|
||||
|
||||
@@ -294,17 +337,27 @@ reset_romote_url() {
|
||||
local url=$2
|
||||
local branch="$3"
|
||||
|
||||
[[ $branch ]] && local part_cmd="origin/${branch}"
|
||||
|
||||
if [[ -d "$dir_work/.git" ]]; then
|
||||
cd $dir_work
|
||||
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||
git remote set-url origin $url >/dev/null
|
||||
git reset --hard $part_cmd >/dev/null
|
||||
git remote set-url origin $url &>/dev/null
|
||||
|
||||
local part_cmd=""
|
||||
reset_branch "$branch"
|
||||
cd $dir_current
|
||||
fi
|
||||
}
|
||||
|
||||
reset_branch() {
|
||||
local branch="$1"
|
||||
if [[ $branch ]]; then
|
||||
part_cmd="origin/${branch}"
|
||||
git checkout -B "$branch"
|
||||
git branch --set-upstream-to=$part_cmd $branch
|
||||
fi
|
||||
git reset --hard $part_cmd &>/dev/null
|
||||
}
|
||||
|
||||
random_range() {
|
||||
local beg=$1
|
||||
local end=$2
|
||||
@@ -317,4 +370,4 @@ detect_macos
|
||||
define_cmd
|
||||
fix_config
|
||||
|
||||
import_config $1 > $task_error_log_path 2>&1
|
||||
import_config $1 >$task_error_log_path 2>&1
|
||||
|
||||
+102
-63
@@ -1,20 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## 导入通用变量与函数
|
||||
dir_shell=/ql/shell
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
## 选择python3还是node
|
||||
define_program() {
|
||||
local first_param=$1
|
||||
if [[ $first_param == *.js ]]; then
|
||||
local file_param=$1
|
||||
if [[ $file_param == *.js ]]; then
|
||||
which_program="node"
|
||||
elif [[ $first_param == *.py ]]; then
|
||||
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
|
||||
which_program="python3"
|
||||
elif [[ $first_param == *.sh ]]; then
|
||||
elif [[ $file_param == *.sh ]]; then
|
||||
which_program="bash"
|
||||
elif [[ $first_param == *.ts ]]; then
|
||||
elif [[ $file_param == *.ts ]]; then
|
||||
which_program="ts-node-transpile-only"
|
||||
else
|
||||
which_program=""
|
||||
@@ -76,45 +76,52 @@ run_nohup() {
|
||||
|
||||
## 正常运行单个脚本,$1:传入参数
|
||||
run_normal() {
|
||||
local first_param=$1
|
||||
define_program "$first_param"
|
||||
if [[ $first_param == *.js ]]; then
|
||||
local file_param=$1
|
||||
define_program "$file_param"
|
||||
if [[ $file_param == *.js ]]; then
|
||||
if [[ $# -eq 1 ]]; then
|
||||
random_delay
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${first_param##*/}"
|
||||
log_dir_tmp_path="${first_param%%/*}"
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
cmd="&>> $dir_log/$log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$log_dir"
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
local begin_timestamp=$(date "+%s")
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${first_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]]; then
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
first_param=${first_param/$relative_path\//}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
eval timeout -k 10s $command_timeout_time $which_program $first_param $cmd
|
||||
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
@@ -122,7 +129,7 @@ run_normal() {
|
||||
|
||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||
run_concurrent() {
|
||||
local first_param="$1"
|
||||
local file_param="$1"
|
||||
local env_param="$2"
|
||||
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||
if [[ ! $env_param ]]; then
|
||||
@@ -141,59 +148,65 @@ run_concurrent() {
|
||||
array_run[n]=${array[$i - 1]}
|
||||
let n++
|
||||
done
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
|
||||
define_program "$first_param"
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
define_program "$file_param"
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${first_param##*/}"
|
||||
log_dir_tmp_path="${first_param%%/*}"
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
cmd="&>> $dir_log/$log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir $log_dir
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
local begin_timestamp=$(date "+%s")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${first_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]]; then
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
first_param=${first_param/$relative_path\//}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
for i in "${!array[@]}"; do
|
||||
export ${env_param}=${array[i]}
|
||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path &
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
timeout -k 10s $command_timeout_time $which_program $file_param &>$single_log_path &
|
||||
done
|
||||
|
||||
wait
|
||||
for i in "${!array[@]}"; do
|
||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval cat $single_log_path $cmd
|
||||
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||
done
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
@@ -211,17 +224,24 @@ run_designated() {
|
||||
define_program "$file_param"
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
log_dir_tmp_path="${file_param%%/*}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
cmd="&>> $dir_log/$log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir $log_dir
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
local begin_timestamp=$(date "+%s")
|
||||
|
||||
local envs=$(eval echo "\$${env_param}")
|
||||
local array=($(echo $envs | sed 's/&/ /g'))
|
||||
@@ -234,8 +254,9 @@ run_designated() {
|
||||
array_run[n]=${array[$i - 1]}
|
||||
let n++
|
||||
done
|
||||
|
||||
|
||||
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
@@ -244,11 +265,9 @@ run_designated() {
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]]; then
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
@@ -256,7 +275,7 @@ run_designated() {
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
@@ -264,29 +283,49 @@ run_designated() {
|
||||
|
||||
## 运行其他命令
|
||||
run_else() {
|
||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
local log_dir_tmp="${1##*/}"
|
||||
local log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||
local file_param="$1"
|
||||
define_program "$file_param"
|
||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||
log_dir_tmp="${file_param##*/}"
|
||||
if [[ $file_param =~ "/" ]]; then
|
||||
if [[ $file_param == /* ]]; then
|
||||
log_dir_tmp_path="${file_param:1}"
|
||||
else
|
||||
log_dir_tmp_path="${file_param}"
|
||||
fi
|
||||
fi
|
||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||
log_dir="${log_dir_tmp%.*}"
|
||||
log_path="$log_dir/$log_time.log"
|
||||
cmd="&>> $log_path"
|
||||
cmd="&>> $dir_log/$log_path"
|
||||
[[ "$show_log" == "true" ]] && cmd=""
|
||||
make_dir "$log_dir"
|
||||
make_dir "$dir_log/$log_dir"
|
||||
|
||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||
local begin_timestamp=$(date "+%s")
|
||||
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
eval . $file_task_before "$@" $cmd
|
||||
|
||||
eval timeout -k 10s $command_timeout_time "$@" $cmd
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
eval . $file_task_after "$@" $cmd
|
||||
shift
|
||||
eval timeout -k 10s $command_timeout_time $which_program "$file_param" "$@" $cmd
|
||||
|
||||
eval . $file_task_after "$file_param" "$@" $cmd
|
||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||
local end_timestamp=$(date "+%s")
|
||||
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||
@@ -327,7 +366,7 @@ main() {
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
[[ -f $log_path ]] && cat $log_path
|
||||
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
|
||||
elif [[ $# -eq 0 ]]; then
|
||||
echo
|
||||
usage
|
||||
|
||||
+72
-60
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dir_shell=/ql/shell
|
||||
dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
@@ -43,7 +43,7 @@ detect_config_version() {
|
||||
local notify_title="配置文件更新通知"
|
||||
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
||||
echo -e $notify_content
|
||||
notify "$notify_title" "$notify_content"
|
||||
notify_api "$notify_title" "$notify_content"
|
||||
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
||||
fi
|
||||
else
|
||||
@@ -91,7 +91,7 @@ del_cron() {
|
||||
done
|
||||
if [[ $ids ]]; then
|
||||
result=$(del_cron_api "$ids")
|
||||
notify "$path 删除任务${result}" "$detail"
|
||||
notify_api "$path 删除任务${result}" "$detail"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ add_cron() {
|
||||
fi
|
||||
fi
|
||||
done
|
||||
notify "$path 新增任务" "$detail"
|
||||
notify_api "$path 新增任务" "$detail"
|
||||
}
|
||||
|
||||
## 更新仓库
|
||||
@@ -151,11 +151,6 @@ update_repo() {
|
||||
make_dir "${dir_scripts}/${uniq_path}"
|
||||
|
||||
local formatUrl="$url"
|
||||
if [[ $github_proxy_url ]]; then
|
||||
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
|
||||
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
|
||||
fi
|
||||
fi
|
||||
if [[ -d ${repo_path}/.git ]]; then
|
||||
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
||||
git_pull_scripts ${repo_path} "${branch}"
|
||||
@@ -166,15 +161,7 @@ update_repo() {
|
||||
echo -e "\n更新${repo_path}成功...\n"
|
||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
||||
else
|
||||
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n"
|
||||
rm -rf ${repo_path}
|
||||
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新${repo_path}成功...\n"
|
||||
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
||||
else
|
||||
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||
fi
|
||||
echo -e "\n更新${repo_path}失败,请检查网络...\n"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -183,15 +170,14 @@ update_raw() {
|
||||
echo -e "--------------------------------------------------------------\n"
|
||||
local url="$1"
|
||||
local raw_url="$url"
|
||||
if [[ $github_proxy_url ]]; then
|
||||
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
|
||||
[[ ! $url =~ "https://ghproxy.com" ]] && raw_url="${github_proxy_url}${url}"
|
||||
fi
|
||||
fi
|
||||
local suffix="${raw_url##*.}"
|
||||
local raw_file_name="${uniq_path}.${suffix}"
|
||||
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
|
||||
|
||||
set_proxy
|
||||
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
|
||||
unset_proxy
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
|
||||
echo -e "下载 ${raw_file_name} 成功...\n"
|
||||
@@ -217,7 +203,7 @@ update_raw() {
|
||||
if [[ -z $cron_id ]]; then
|
||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
||||
echo -e "$result\n"
|
||||
notify "新增任务通知" "\n$result"
|
||||
notify_api "新增任务通知" "\n$result"
|
||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||
fi
|
||||
else
|
||||
@@ -258,12 +244,17 @@ update_qinglong() {
|
||||
patch_version
|
||||
|
||||
local no_restart="$1"
|
||||
local all_branch=$(git branch -a)
|
||||
local primary_branch="master"
|
||||
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
|
||||
primary_branch="${current_branch}"
|
||||
fi
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
|
||||
git_pull_scripts $dir_root "master"
|
||||
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
|
||||
git_pull_scripts $dir_root ${primary_branch}
|
||||
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新$dir_root成功...\n"
|
||||
echo -e "\n更新青龙源文件成功...\n"
|
||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||
detect_config_version
|
||||
update_depend
|
||||
@@ -271,23 +262,23 @@ update_qinglong() {
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
||||
else
|
||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
||||
echo -e "\n更新青龙源文件失败,请检查原因...\n"
|
||||
fi
|
||||
|
||||
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
|
||||
local url="https://github.com/whyour/qinglong-static.git"
|
||||
if [[ -d ${ql_static_repo}/.git ]]; then
|
||||
reset_romote_url ${ql_static_repo} ${url} "master"
|
||||
git_pull_scripts ${ql_static_repo} "master"
|
||||
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
||||
git_pull_scripts ${ql_static_repo} ${primary_branch}
|
||||
else
|
||||
git_clone_scripts ${url} ${ql_static_repo}
|
||||
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
|
||||
fi
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新$ql_static_repo成功...\n"
|
||||
local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
||||
echo -e "\n更新青龙静态资源成功...\n"
|
||||
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
||||
echo -e "\n当前版本 $static_version...\n"
|
||||
cd $dir_root
|
||||
rm -rf $dir_root/build && rm -rf $dir_root/dist
|
||||
cp -rf $ql_static_repo/* $dir_root
|
||||
|
||||
rm -rf $dir_static/*
|
||||
cp -rf $ql_static_repo/* $dir_static
|
||||
if [[ $no_restart != "no-restart" ]]; then
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
echo -e "重启面板中..."
|
||||
@@ -295,16 +286,16 @@ update_qinglong() {
|
||||
reload_pm2
|
||||
fi
|
||||
else
|
||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
||||
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
patch_version() {
|
||||
if [[ -f "/ql/db/cookie.db" ]]; then
|
||||
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
||||
mv /ql/db/cookie.db /ql/db/env.db
|
||||
rm -rf /ql/db/cookie.db
|
||||
mv $dir_root/db/cookie.db $dir_root/db/env.db
|
||||
rm -rf $dir_root/db/cookie.db
|
||||
echo
|
||||
fi
|
||||
|
||||
@@ -313,22 +304,43 @@ patch_version() {
|
||||
fi
|
||||
|
||||
git config --global pull.rebase false
|
||||
|
||||
cp -f $dir_root/.env.example $dir_root/.env
|
||||
|
||||
if [[ -d "$dir_root/db" ]]; then
|
||||
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/config $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/scripts" ]]; then
|
||||
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/scripts $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/log" ]]; then
|
||||
echo -e "检测到旧的log目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/log $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ -d "$dir_root/config" ]]; then
|
||||
echo -e "检测到旧的config目录,拷贝到data目录...\n"
|
||||
cp -rf $dir_root/config $dir_root/data
|
||||
echo
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
reload_pm2() {
|
||||
pm2 l &>/dev/null
|
||||
|
||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
||||
pm2 reload panel --source-map-support --time &>/dev/null
|
||||
else
|
||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
fi
|
||||
pm2 delete panel --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||
|
||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
||||
pm2 reload schedule --source-map-support --time &>/dev/null
|
||||
else
|
||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
fi
|
||||
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||
}
|
||||
|
||||
## 对比脚本
|
||||
@@ -391,16 +403,11 @@ gen_list_repo() {
|
||||
if [[ $blackword ]]; then
|
||||
files=$(echo "$files" | egrep -v $blackword)
|
||||
fi
|
||||
if [[ $dependence ]]; then
|
||||
cd ${dir_scripts}
|
||||
depInScripts=$(eval $cmd | sed 's/^..//' | egrep -v $uniq_path | egrep $dependence)
|
||||
for dep in ${depInScripts}; do
|
||||
file_path=$(dirname $dep)
|
||||
[[ ! $file_path =~ "/" ]] && file_path=""
|
||||
make_dir "${dir_scripts}/${uniq_path}/${file_path#*/}"
|
||||
cp -f $dep "${dir_scripts}/${uniq_path}/${file_path#*/}"
|
||||
done
|
||||
|
||||
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
|
||||
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
|
||||
|
||||
if [[ $dependence ]]; then
|
||||
cd ${repo_path}
|
||||
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
|
||||
for _file in ${results}; do
|
||||
@@ -409,6 +416,11 @@ gen_list_repo() {
|
||||
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -d $dir_dep ]]; then
|
||||
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
|
||||
fi
|
||||
|
||||
for file in ${files}; do
|
||||
filename=$(basename $file)
|
||||
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.react-terminal-wrapper {
|
||||
width: 100%;
|
||||
background: #252a33;
|
||||
color: #eee;
|
||||
font-size: 18px;
|
||||
font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier,
|
||||
monospace;
|
||||
border-radius: 4px;
|
||||
padding: 75px 45px 35px;
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.react-terminal {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.react-terminal-wrapper.react-terminal-light {
|
||||
background: #ddd;
|
||||
color: #1a1e24;
|
||||
}
|
||||
|
||||
.react-terminal-wrapper:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background: #d9515d;
|
||||
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
|
||||
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
|
||||
}
|
||||
|
||||
.react-terminal-wrapper:after {
|
||||
content: attr(data-terminal-name);
|
||||
position: absolute;
|
||||
color: #a2a2a2;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.react-terminal-wrapper.react-terminal-light:after {
|
||||
color: #d76d77;
|
||||
}
|
||||
|
||||
.react-terminal-line {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.react-terminal-line:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: #a2a2a2;
|
||||
}
|
||||
|
||||
.react-terminal-light .react-terminal-line:before {
|
||||
color: #d76d77;
|
||||
}
|
||||
|
||||
.react-terminal-input:before {
|
||||
margin-right: 0.75em;
|
||||
content: '$';
|
||||
}
|
||||
|
||||
.react-terminal-input[data-terminal-prompt]:before {
|
||||
content: attr(data-terminal-prompt);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import './index.less';
|
||||
|
||||
export enum LineType {
|
||||
Input,
|
||||
Output,
|
||||
}
|
||||
|
||||
export enum ColorMode {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
name?: string;
|
||||
prompt?: string;
|
||||
colorMode?: ColorMode;
|
||||
lineData: Array<{ type: LineType; value: string | React.ReactNode }>;
|
||||
startingInputValue?: string;
|
||||
}
|
||||
|
||||
const Terminal = ({
|
||||
name,
|
||||
prompt,
|
||||
colorMode,
|
||||
lineData,
|
||||
startingInputValue = '',
|
||||
}: Props) => {
|
||||
const lastLineRef = useRef<null | HTMLElement>(null);
|
||||
|
||||
// An effect that handles scrolling into view the last line of terminal input or output
|
||||
const performScrolldown = useRef(false);
|
||||
useEffect(() => {
|
||||
if (performScrolldown.current) {
|
||||
// skip scrolldown when the component first loads
|
||||
setTimeout(
|
||||
() =>
|
||||
lastLineRef?.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
}),
|
||||
500,
|
||||
);
|
||||
}
|
||||
performScrolldown.current = true;
|
||||
}, [lineData.length]);
|
||||
|
||||
const renderedLineData = lineData.map((ld, i) => {
|
||||
const classes = ['react-terminal-line'];
|
||||
if (ld.type === LineType.Input) {
|
||||
classes.push('react-terminal-input');
|
||||
}
|
||||
// `lastLineRef` is used to ensure the terminal scrolls into view to the last line; make sure to add the ref to the last
|
||||
if (lineData.length === i + 1) {
|
||||
return (
|
||||
<span className={classes.join(' ')} key={i} ref={lastLineRef}>
|
||||
{ld.value}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className={classes.join(' ')} key={i}>
|
||||
{ld.value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const classes = ['react-terminal-wrapper'];
|
||||
if (colorMode === ColorMode.Light) {
|
||||
classes.push('react-terminal-light');
|
||||
}
|
||||
return (
|
||||
<div className={classes.join(' ')} data-terminal-name={name}>
|
||||
<div className="react-terminal">{renderedLineData}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Terminal;
|
||||
@@ -25,6 +25,12 @@ export default {
|
||||
hideInMenu: true,
|
||||
component: '@/pages/initialization/index',
|
||||
},
|
||||
{
|
||||
name: '错误',
|
||||
path: '/error',
|
||||
hideInMenu: true,
|
||||
component: '@/pages/error/index',
|
||||
},
|
||||
{
|
||||
path: '/crontab',
|
||||
name: '定时任务',
|
||||
@@ -79,6 +85,5 @@ export default {
|
||||
fixSiderbar: true,
|
||||
contentWidth: 'Fixed',
|
||||
splitMenus: false,
|
||||
logo: '/images/qinglong.png',
|
||||
siderWidth: 180,
|
||||
} as any;
|
||||
|
||||
+43
-10
@@ -26,6 +26,7 @@
|
||||
.monaco-editor:not(.rename-box) {
|
||||
height: calc(100vh - 128px) !important;
|
||||
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
|
||||
|
||||
.view-overlays .current-line {
|
||||
border-width: 0;
|
||||
}
|
||||
@@ -33,6 +34,7 @@
|
||||
|
||||
.rename-box {
|
||||
height: 0;
|
||||
|
||||
.rename-input {
|
||||
height: 0;
|
||||
padding: 0 !important;
|
||||
@@ -43,6 +45,7 @@
|
||||
.ant-pro-grid-content.wide {
|
||||
max-width: unset !important;
|
||||
overflow: auto;
|
||||
|
||||
.ant-pro-page-container-children-content {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
@@ -57,6 +60,7 @@
|
||||
|
||||
.ant-tooltip {
|
||||
max-width: 500px !important;
|
||||
|
||||
.ant-tooltip-inner {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
@@ -72,6 +76,7 @@
|
||||
.log-select {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.ant-page-header-heading-left {
|
||||
min-width: 100px;
|
||||
}
|
||||
@@ -81,6 +86,7 @@
|
||||
.config-select {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.ant-page-header-heading-left {
|
||||
min-width: 100px;
|
||||
}
|
||||
@@ -119,10 +125,13 @@
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 48px);
|
||||
height: calc(100vh - var(--vh-offset, 0px) - 48px);
|
||||
|
||||
.ant-pro-grid-content.wide {
|
||||
flex: 1;
|
||||
|
||||
.ant-pro-grid-content-children {
|
||||
height: calc(100% - 36px);
|
||||
|
||||
> div,
|
||||
.log-container,
|
||||
.react-codemirror2,
|
||||
@@ -152,8 +161,8 @@
|
||||
|
||||
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
|
||||
margin-bottom: 0 !important;
|
||||
min-height: calc(100vh - 66px);
|
||||
min-height: calc(100vh - var(--vh-offset, 0px) - 66px);
|
||||
min-height: calc(100vh - 72px);
|
||||
min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
|
||||
}
|
||||
|
||||
.Resizer {
|
||||
@@ -197,6 +206,7 @@
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
@@ -209,6 +219,7 @@
|
||||
|
||||
.inline-countdown.ant-statistic {
|
||||
display: inline-block;
|
||||
|
||||
.ant-statistic-content {
|
||||
font-size: 14px;
|
||||
padding: 0 3px;
|
||||
@@ -235,6 +246,7 @@
|
||||
box-shadow: none;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
-webkit-text-fill-color: @text-color;
|
||||
caret-color: #e8e6f3 !important;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
@@ -252,6 +264,7 @@
|
||||
|
||||
.ant-pro-basicLayout-content {
|
||||
margin: 18px;
|
||||
|
||||
.ant-pro-page-container {
|
||||
margin: -18px -18px -18px;
|
||||
}
|
||||
@@ -279,16 +292,36 @@
|
||||
|
||||
.side-menu-user-drop-menu {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
text-align: left;
|
||||
background-clip: padding-box;
|
||||
outline: none;
|
||||
background-color: #fff;
|
||||
padding: 4px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%),
|
||||
0 12px 48px 16px rgb(0 0 0 / 3%);
|
||||
.ant-dropdown-menu-item:hover {
|
||||
color: #1890ff;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[data-dark='true'] .side-menu-user-drop-menu {
|
||||
background-color: #373739;
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo {
|
||||
padding: 16px 8px !important;
|
||||
|
||||
h1 {
|
||||
margin-left: 5px !important;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px !important;
|
||||
border-radius: 52% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-global-header-logo {
|
||||
a img {
|
||||
// 移动端logo被拉伸
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-25
@@ -20,18 +20,11 @@ import './index.less';
|
||||
import vhCheck from 'vh-check';
|
||||
import { version, changeLogLink, changeLog } from '../version';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd';
|
||||
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
|
||||
// @ts-ignore
|
||||
import SockJS from 'sockjs-client';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851',
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
release: version,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
import { init } from '../utils/init';
|
||||
|
||||
export default function (props: any) {
|
||||
const ctx = useCtx();
|
||||
@@ -44,7 +37,7 @@ export default function (props: any) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const logout = () => {
|
||||
request.post(`${config.apiPrefix}logout`).then(() => {
|
||||
request.post(`${config.apiPrefix}user/logout`).then(() => {
|
||||
localStorage.removeItem(config.authKey);
|
||||
history.push('/login');
|
||||
});
|
||||
@@ -107,10 +100,6 @@ export default function (props: any) {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
vhCheck();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||
getUser();
|
||||
@@ -128,6 +117,9 @@ export default function (props: any) {
|
||||
}, [theme.theme]);
|
||||
|
||||
useEffect(() => {
|
||||
vhCheck();
|
||||
init();
|
||||
|
||||
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||
setFetchMethod(window.fetch);
|
||||
if (_theme === 'dark') {
|
||||
@@ -187,7 +179,9 @@ export default function (props: any) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (['/login', '/initialization'].includes(props.location.pathname)) {
|
||||
if (
|
||||
['/login', '/initialization', '/error'].includes(props.location.pathname)
|
||||
) {
|
||||
document.title = `${
|
||||
(config.documentTitleMap as any)[props.location.pathname]
|
||||
} - 控制面板`;
|
||||
@@ -198,7 +192,7 @@ export default function (props: any) {
|
||||
history.push('/crontab');
|
||||
}
|
||||
|
||||
if (systemInfo) {
|
||||
if (systemInfo || props.location.pathname === '/error') {
|
||||
return React.Children.map(props.children, (child) => {
|
||||
return React.cloneElement(child, {
|
||||
...ctx,
|
||||
@@ -232,6 +226,7 @@ export default function (props: any) {
|
||||
selectedKeys={[props.location.pathname]}
|
||||
loading={loading}
|
||||
ErrorBoundary={Sentry.ErrorBoundary}
|
||||
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
|
||||
title={
|
||||
<>
|
||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||
@@ -275,14 +270,16 @@ export default function (props: any) {
|
||||
}}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
rightContentRender={() => (
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)}
|
||||
rightContentRender={() =>
|
||||
ctx.isPhone && (
|
||||
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
collapsedButtonRender={(collapsed) => (
|
||||
<span
|
||||
className="side-menu-container"
|
||||
@@ -292,7 +289,7 @@ export default function (props: any) {
|
||||
}}
|
||||
>
|
||||
{!collapsed && !ctx.isPhone && (
|
||||
<Dropdown overlay={menu} trigger={['hover']}>
|
||||
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
message,
|
||||
Input,
|
||||
Form,
|
||||
Button,
|
||||
Card,
|
||||
Tag,
|
||||
List,
|
||||
Divider,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
Loading3QuartersOutlined,
|
||||
FileOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CrontabStatus } from './index';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import CronLogModal from './logModal';
|
||||
import Editor from '@monaco-editor/react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: 'log',
|
||||
tab: '日志',
|
||||
},
|
||||
{
|
||||
key: 'script',
|
||||
tab: '脚本',
|
||||
},
|
||||
];
|
||||
|
||||
interface LogItem {
|
||||
directory: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
|
||||
const CronDetailModal = ({
|
||||
cron = {},
|
||||
handleCancel,
|
||||
visible,
|
||||
theme,
|
||||
isPhone,
|
||||
}: {
|
||||
cron?: any;
|
||||
visible: boolean;
|
||||
handleCancel: (needUpdate?: boolean) => void;
|
||||
theme: string;
|
||||
isPhone: boolean;
|
||||
}) => {
|
||||
const [activeTabKey, setActiveTabKey] = useState('log');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [logs, setLogs] = useState<LogItem[]>([]);
|
||||
const [log, setLog] = useState('');
|
||||
const [value, setValue] = useState('');
|
||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||
const editorRef = useRef<any>(null);
|
||||
const [scriptInfo, setScriptInfo] = useState<any>({});
|
||||
const [logUrl, setLogUrl] = useState('');
|
||||
const [validTabs, setValidTabs] = useState(tabList);
|
||||
|
||||
const contentList: any = {
|
||||
log: (
|
||||
<List
|
||||
dataSource={logs}
|
||||
loading={loading}
|
||||
renderItem={(item) => (
|
||||
<List.Item className="log-item" onClick={() => onClickItem(item)}>
|
||||
<FileOutlined style={{ marginRight: 10 }} />
|
||||
{item.directory}/{item.filename}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
script: (
|
||||
<Editor
|
||||
language="shell"
|
||||
theme={theme}
|
||||
value={value}
|
||||
options={{
|
||||
fontSize: 12,
|
||||
lineNumbersMinChars: 3,
|
||||
fontFamily: 'Source Code Pro',
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const onClickItem = (item: LogItem) => {
|
||||
localStorage.setItem('logCron', cron.id);
|
||||
setLogUrl(`${config.apiPrefix}logs/${item.directory}/${item.filename}`);
|
||||
request
|
||||
.get(`${config.apiPrefix}logs/${item.directory}/${item.filename}`)
|
||||
.then((data) => {
|
||||
setLog(data.data);
|
||||
setIsLogModalVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
const onTabChange = (key: string) => {
|
||||
setActiveTabKey(key);
|
||||
};
|
||||
|
||||
const getLogs = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
setLogs(data.data);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const getScript = () => {
|
||||
const cmd = cron.command.split(' ') as string[];
|
||||
if (cmd[0] === 'task') {
|
||||
setValidTabs(validTabs);
|
||||
if (cmd[1].startsWith('/ql/data/scripts')) {
|
||||
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
||||
}
|
||||
|
||||
let [p, s] = cmd[1].split('/');
|
||||
if (!s) {
|
||||
s = p;
|
||||
p = '';
|
||||
}
|
||||
setScriptInfo({ parent: p, filename: s });
|
||||
request
|
||||
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
|
||||
.then((data) => {
|
||||
setValue(data.data);
|
||||
});
|
||||
} else {
|
||||
setValidTabs([validTabs[0]]);
|
||||
}
|
||||
};
|
||||
|
||||
const saveFile = () => {
|
||||
Modal.confirm({
|
||||
title: `确认保存`,
|
||||
content: (
|
||||
<>
|
||||
确认保存文件
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{scriptInfo.filename}
|
||||
</Text>{' '}
|
||||
,保存后不可恢复
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
const content = editorRef.current
|
||||
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||
: value;
|
||||
return new Promise((resolve, reject) => {
|
||||
request
|
||||
.put(`${config.apiPrefix}scripts`, {
|
||||
data: {
|
||||
filename: scriptInfo.filename,
|
||||
path: scriptInfo.parent || '',
|
||||
content,
|
||||
},
|
||||
})
|
||||
.then((_data: any) => {
|
||||
if (_data.code === 200) {
|
||||
setValue(content);
|
||||
message.success(`保存成功`);
|
||||
} else {
|
||||
message.error(_data);
|
||||
}
|
||||
resolve(null);
|
||||
})
|
||||
.catch((e) => reject(e));
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cron && cron.id) {
|
||||
getLogs();
|
||||
getScript();
|
||||
}
|
||||
}, [cron]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<>
|
||||
<span>{cron.name}</span>
|
||||
{cron.labels?.length > 0 && cron.labels[0] !== '' && (
|
||||
<Divider type="vertical"></Divider>
|
||||
)}
|
||||
{cron.labels?.length > 0 &&
|
||||
cron.labels[0] !== '' &&
|
||||
cron.labels?.map((label: string, i: number) => (
|
||||
<Tag color="blue" style={{ marginRight: 5 }}>
|
||||
{label}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
centered
|
||||
visible={visible}
|
||||
forceRender
|
||||
footer={false}
|
||||
onCancel={() => handleCancel()}
|
||||
wrapClassName="crontab-detail"
|
||||
width={!isPhone ? '80vw' : ''}
|
||||
>
|
||||
<div className="card-wrapper">
|
||||
<Card>
|
||||
<div className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">任务</div>
|
||||
<div className="cron-detail-info-value">{cron.command}</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ marginTop: 10 }}>
|
||||
<div className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">状态</div>
|
||||
<div className="cron-detail-info-value">
|
||||
{(!cron.isDisabled || cron.status !== CrontabStatus.idle) && (
|
||||
<>
|
||||
{cron.status === CrontabStatus.idle && (
|
||||
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||
空闲中
|
||||
</Tag>
|
||||
)}
|
||||
{cron.status === CrontabStatus.running && (
|
||||
<Tag
|
||||
icon={<Loading3QuartersOutlined spin />}
|
||||
color="processing"
|
||||
>
|
||||
运行中
|
||||
</Tag>
|
||||
)}
|
||||
{cron.status === CrontabStatus.queued && (
|
||||
<Tag icon={<FieldTimeOutlined />} color="default">
|
||||
队列中
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{cron.isDisabled === 1 && cron.status === CrontabStatus.idle && (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
已禁用
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">定时</div>
|
||||
<div className="cron-detail-info-value">{cron.schedule}</div>
|
||||
</div>
|
||||
<div className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">最后运行时间</div>
|
||||
<div className="cron-detail-info-value">
|
||||
{cron.last_execution_time
|
||||
? new Date(cron.last_execution_time * 1000)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">最后运行时长</div>
|
||||
<div className="cron-detail-info-value">
|
||||
{cron.last_running_time ? diffTime(cron.last_running_time) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">下次运行时间</div>
|
||||
<div className="cron-detail-info-value">
|
||||
{cron.nextRunTime &&
|
||||
cron.nextRunTime
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
style={{ marginTop: 10 }}
|
||||
tabList={validTabs}
|
||||
activeTabKey={activeTabKey}
|
||||
onTabChange={(key) => {
|
||||
onTabChange(key);
|
||||
}}
|
||||
tabBarExtraContent={
|
||||
activeTabKey === 'script' && (
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={saveFile}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{contentList[activeTabKey]}
|
||||
</Card>
|
||||
</div>
|
||||
<CronLogModal
|
||||
visible={isLogModalVisible}
|
||||
handleCancel={() => {
|
||||
setIsLogModalVisible(false);
|
||||
}}
|
||||
cron={cron}
|
||||
data={log}
|
||||
logUrl={logUrl}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CronDetailModal;
|
||||
@@ -1,3 +1,87 @@
|
||||
.ant-table-pagination.ant-pagination {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.cron {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.crontab-detail {
|
||||
.card-wrapper {
|
||||
height: 80vh;
|
||||
height: calc(80vh - var(--vh-offset, 0px));
|
||||
|
||||
.ant-card:last-child {
|
||||
.ant-card-body {
|
||||
height: calc(80vh - 238px);
|
||||
height: calc(80vh - var(--vh-offset, 0px) - 238px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
background: #eee;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 18px;
|
||||
}
|
||||
.ant-card-head {
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.ant-card:first-child {
|
||||
max-height: 66px;
|
||||
overflow: auto;
|
||||
|
||||
.ant-card-body {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.cron-detail-info-item {
|
||||
display: flex;
|
||||
|
||||
.cron-detail-info-title {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.cron-detail-info-value {
|
||||
flex: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card:nth-child(2) {
|
||||
overflow-x: auto;
|
||||
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.cron-detail-info-item {
|
||||
flex: auto;
|
||||
|
||||
.cron-detail-info-title {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cron-detail-info-value {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-item {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
+159
-51
@@ -11,6 +11,7 @@ import {
|
||||
Menu,
|
||||
Typography,
|
||||
Input,
|
||||
Popover,
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
@@ -30,24 +31,26 @@ import {
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { request } from '@/utils/http';
|
||||
import CronModal from './modal';
|
||||
import CronModal, { CronLabelModal } from './modal';
|
||||
import CronLogModal from './logModal';
|
||||
import CronDetailModal from './detail';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { getTableScroll } from '@/utils/index';
|
||||
import { history } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Search } = Input;
|
||||
|
||||
enum CrontabStatus {
|
||||
export enum CrontabStatus {
|
||||
'running',
|
||||
'idle',
|
||||
'disabled',
|
||||
'queued',
|
||||
}
|
||||
|
||||
const CrontabSort: any = { 0: 0, 3: 1, 1: 2, 4: 3 };
|
||||
const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 };
|
||||
|
||||
enum OperationName {
|
||||
'启用',
|
||||
@@ -67,7 +70,7 @@ enum OperationPath {
|
||||
'unpin',
|
||||
}
|
||||
|
||||
const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const columns: any = [
|
||||
{
|
||||
title: '任务名',
|
||||
@@ -76,16 +79,47 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
width: 150,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => (
|
||||
<span>
|
||||
{record.name || record._id}{' '}
|
||||
{record.isPinned ? (
|
||||
<span>
|
||||
<PushpinOutlined />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
<>
|
||||
<a
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
}}
|
||||
>
|
||||
{record.labels?.length > 0 && record.labels[0] !== '' ? (
|
||||
<Popover
|
||||
placement="right"
|
||||
trigger={isPhone ? 'click' : 'hover'}
|
||||
content={
|
||||
<div>
|
||||
{record.labels?.map((label: string) => (
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ cursor: 'point' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchValue(`label:${label}`);
|
||||
}}
|
||||
>
|
||||
<a>{label}</a>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{record.name || '-'}
|
||||
</Popover>
|
||||
) : (
|
||||
record.name || '-'
|
||||
)}
|
||||
{record.isPinned ? (
|
||||
<span>
|
||||
<PushpinOutlined />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => a.name.localeCompare(b.name),
|
||||
@@ -100,16 +134,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
<span
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
display: 'inline-block',
|
||||
wordBreak: 'break-all',
|
||||
marginBottom: 0,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
sorter: {
|
||||
@@ -282,7 +316,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
{record.status === CrontabStatus.idle && (
|
||||
<Tooltip title={isPc ? '运行' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
runCron(record, index);
|
||||
}}
|
||||
>
|
||||
@@ -293,7 +328,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
{record.status !== CrontabStatus.idle && (
|
||||
<Tooltip title={isPc ? '停止' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
stopCron(record, index);
|
||||
}}
|
||||
>
|
||||
@@ -303,7 +339,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
)}
|
||||
<Tooltip title={isPc ? '日志' : ''}>
|
||||
<a
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setLogCron({ ...record, timestamp: Date.now() });
|
||||
}}
|
||||
>
|
||||
@@ -320,6 +357,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
const [value, setValue] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isLabelModalVisible, setIsLabelModalVisible] = useState(false);
|
||||
const [editedCron, setEditedCron] = useState();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||
@@ -328,6 +366,27 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
||||
const [detailCron, setDetailCron] = useState<any>();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const goToScriptManager = (record: any) => {
|
||||
const cmd = record.command.split(' ') as string[];
|
||||
if (cmd[0] === 'task') {
|
||||
if (cmd[1].startsWith('/ql/data/scripts')) {
|
||||
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
|
||||
}
|
||||
|
||||
let [p, s] = cmd[1].split('/');
|
||||
if (!s) {
|
||||
s = p;
|
||||
p = '';
|
||||
}
|
||||
history.push(`/script?p=${p}&s=${s}`);
|
||||
} else if (cmd[1] === 'repo') {
|
||||
location.href = cmd[2];
|
||||
}
|
||||
};
|
||||
|
||||
const getCrons = () => {
|
||||
setLoading(true);
|
||||
@@ -337,14 +396,19 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
setValue(
|
||||
data.data
|
||||
.sort((a: any, b: any) => {
|
||||
const sortA = a.isDisabled ? 4 : a.status;
|
||||
const sortB = b.isDisabled ? 4 : b.status;
|
||||
a.isPinned = a.isPinned ? a.isPinned : 0;
|
||||
b.isPinned = b.isPinned ? b.isPinned : 0;
|
||||
if (a.isPinned === b.isPinned) {
|
||||
return CrontabSort[sortA] - CrontabSort[sortB];
|
||||
}
|
||||
return b.isPinned - a.isPinned;
|
||||
const sortA =
|
||||
a.isPinned && a.status !== 0
|
||||
? 5
|
||||
: a.isDisabled && a.status !== 0
|
||||
? 4
|
||||
: a.status;
|
||||
const sortB =
|
||||
b.isPinned && b.status !== 0
|
||||
? 5
|
||||
: b.isDisabled && b.status !== 0
|
||||
? 4
|
||||
: b.status;
|
||||
return CrontabSort[sortA] - CrontabSort[sortB];
|
||||
})
|
||||
.map((x) => {
|
||||
return {
|
||||
@@ -385,12 +449,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}crons`, { data: [record._id] })
|
||||
.delete(`${config.apiPrefix}crons`, { data: [record.id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
message.success('删除成功');
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
const i = result.findIndex((x) => x.id === record.id);
|
||||
result.splice(i, 1);
|
||||
setValue(result);
|
||||
} else {
|
||||
@@ -418,11 +482,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.put(`${config.apiPrefix}crons/run`, { data: [record._id] })
|
||||
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
const i = result.findIndex((x) => x.id === record.id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
status: CrontabStatus.running,
|
||||
@@ -453,11 +517,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.put(`${config.apiPrefix}crons/stop`, { data: [record._id] })
|
||||
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
const i = result.findIndex((x) => x.id === record.id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
pid: null,
|
||||
@@ -495,14 +559,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: [record._id],
|
||||
data: [record.id],
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
const i = result.findIndex((x) => x.id === record.id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
isDisabled: newStatus,
|
||||
@@ -539,14 +603,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
}`,
|
||||
{
|
||||
data: [record._id],
|
||||
data: [record.id],
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
const newStatus = record.isPinned === 1 ? 0 : 1;
|
||||
const result = [...value];
|
||||
const i = result.findIndex((x) => x._id === record._id);
|
||||
const i = result.findIndex((x) => x.id === record.id);
|
||||
result.splice(i, 1, {
|
||||
...record,
|
||||
isPinned: newStatus,
|
||||
@@ -568,10 +632,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
index: number;
|
||||
}> = ({ record, index }) => (
|
||||
<Dropdown
|
||||
arrow
|
||||
arrow={{ pointAtCenter: true }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu onClick={({ key }) => action(key, record, index)}>
|
||||
<Menu
|
||||
onClick={({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
action(key, record, index);
|
||||
}}
|
||||
>
|
||||
<Menu.Item key="edit" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Menu.Item>
|
||||
@@ -601,7 +671,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a>
|
||||
<a onClick={(e) => e.stopPropagation()}>
|
||||
<EllipsisOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
@@ -638,7 +708,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
};
|
||||
|
||||
const handleCrons = (cron: any) => {
|
||||
const index = value.findIndex((x) => x._id === cron._id);
|
||||
const index = value.findIndex((x) => x.id === cron.id);
|
||||
const result = [...value];
|
||||
cron.nextRunTime = cron_parser
|
||||
.parseExpression(cron.schedule)
|
||||
@@ -656,9 +726,9 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
|
||||
const getCronDetail = (cron: any) => {
|
||||
request
|
||||
.get(`${config.apiPrefix}crons/${cron._id}`)
|
||||
.get(`${config.apiPrefix}crons/${cron.id}`)
|
||||
.then((data: any) => {
|
||||
const index = value.findIndex((x) => x._id === cron._id);
|
||||
const index = value.findIndex((x) => x.id === cron.id);
|
||||
const result = [...value];
|
||||
data.data.nextRunTime = cron_parser
|
||||
.parseExpression(data.data.schedule)
|
||||
@@ -746,12 +816,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
};
|
||||
|
||||
const getRowClassName = (record: any, index: number) => {
|
||||
return record.isPinned ? 'pinned-cron' : '';
|
||||
return record.isPinned ? 'pinned-cron cron' : 'cron';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (logCron) {
|
||||
localStorage.setItem('logCron', logCron._id);
|
||||
localStorage.setItem('logCron', logCron.id);
|
||||
setIsLogModalVisible(true);
|
||||
}
|
||||
}, [logCron]);
|
||||
@@ -776,11 +846,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
placeholder="请输入名称或者关键词"
|
||||
style={{ width: 'auto' }}
|
||||
enterButton
|
||||
allowClear
|
||||
loading={loading}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||
添加任务
|
||||
新建任务
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
@@ -830,6 +903,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
>
|
||||
批量取消置顶
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setIsLabelModalVisible(true)}
|
||||
style={{ marginLeft: 8, marginRight: 8 }}
|
||||
>
|
||||
批量修改标签
|
||||
</Button>
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
已选择
|
||||
<a>{selectedRowIds?.length}</a>项
|
||||
@@ -847,9 +927,18 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
defaultPageSize: 20,
|
||||
showTotal: (total: number, range: number[]) =>
|
||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||
pageSizeOptions: [20, 100, 500, 1000] as any,
|
||||
}}
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
setDetailCron(record);
|
||||
setIsDetailModalVisible(true);
|
||||
},
|
||||
};
|
||||
}}
|
||||
dataSource={value}
|
||||
rowKey="_id"
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
scroll={{ x: 1000, y: tableScrollHeight }}
|
||||
loading={loading}
|
||||
@@ -869,6 +958,25 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
handleCancel={handleCancel}
|
||||
cron={editedCron}
|
||||
/>
|
||||
<CronLabelModal
|
||||
visible={isLabelModalVisible}
|
||||
handleCancel={(needUpdate?: boolean) => {
|
||||
setIsLabelModalVisible(false);
|
||||
if (needUpdate) {
|
||||
getCrons();
|
||||
}
|
||||
}}
|
||||
ids={selectedRowIds}
|
||||
/>
|
||||
<CronDetailModal
|
||||
visible={isDetailModalVisible}
|
||||
handleCancel={() => {
|
||||
setIsDetailModalVisible(false);
|
||||
}}
|
||||
cron={detailCron}
|
||||
theme={theme}
|
||||
isPhone={isPhone}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,14 @@ const CronLogModal = ({
|
||||
cron,
|
||||
handleCancel,
|
||||
visible,
|
||||
data,
|
||||
logUrl,
|
||||
}: {
|
||||
cron?: any;
|
||||
visible: boolean;
|
||||
handleCancel: () => void;
|
||||
data?: string;
|
||||
logUrl?: string;
|
||||
}) => {
|
||||
const [value, setValue] = useState<string>('启动中...');
|
||||
const [loading, setLoading] = useState<any>(true);
|
||||
@@ -36,9 +40,9 @@ const CronLogModal = ({
|
||||
setLoading(true);
|
||||
}
|
||||
request
|
||||
.get(`${config.apiPrefix}crons/${cron._id}/log`)
|
||||
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
|
||||
.then((data: any) => {
|
||||
if (localStorage.getItem('logCron') === cron._id) {
|
||||
if (localStorage.getItem('logCron') === String(cron.id)) {
|
||||
const log = data.data as string;
|
||||
setValue(log || '暂无日志');
|
||||
setExecuting(
|
||||
@@ -61,7 +65,7 @@ const CronLogModal = ({
|
||||
<Countdown
|
||||
className="inline-countdown"
|
||||
format="ss"
|
||||
value={Date.now() + 1000 * 10}
|
||||
value={Date.now() + 1000 * 30}
|
||||
/>
|
||||
秒后自动刷新
|
||||
</span>
|
||||
@@ -70,7 +74,7 @@ const CronLogModal = ({
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 10000);
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -90,17 +94,23 @@ const CronLogModal = ({
|
||||
return (
|
||||
<>
|
||||
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
||||
{!executing && <CheckCircleOutlined />}
|
||||
{!executing && !loading && <CheckCircleOutlined />}
|
||||
<span style={{ marginLeft: 5 }}>日志-{cron && cron.name}</span>{' '}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cron) {
|
||||
if (cron && cron.id && visible) {
|
||||
getCronLog(true);
|
||||
}
|
||||
}, [cron]);
|
||||
}, [cron, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setValue(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsPhone(document.body.clientWidth < 768);
|
||||
|
||||
+99
-13
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, message, Input, Form } from 'antd';
|
||||
import { Modal, message, Input, Form, Button } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import cronParse from 'cron-parser';
|
||||
@@ -20,19 +20,28 @@ const CronModal = ({
|
||||
setLoading(true);
|
||||
const method = cron ? 'put' : 'post';
|
||||
const payload = { ...values };
|
||||
if (typeof payload.labels === 'string') {
|
||||
payload.labels = values.labels.split(/,|,/);
|
||||
} else if (!payload.labels) {
|
||||
payload.labels = [];
|
||||
}
|
||||
if (cron) {
|
||||
payload._id = cron._id;
|
||||
payload.id = cron.id;
|
||||
}
|
||||
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(cron ? '更新Cron成功' : '添加Cron成功');
|
||||
} else {
|
||||
message.error(data);
|
||||
try {
|
||||
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(cron ? '更新Cron成功' : '新建Cron成功');
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
} catch (error: any) {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,7 +50,7 @@ const CronModal = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cron ? '编辑定时' : '新建定时'}
|
||||
title={cron ? '编辑任务' : '新建任务'}
|
||||
visible={visible}
|
||||
forceRender
|
||||
onOk={() => {
|
||||
@@ -95,9 +104,86 @@ const CronModal = ({
|
||||
>
|
||||
<Input placeholder="秒(可选) 分 时 天 月 周" />
|
||||
</Form.Item>
|
||||
<Form.Item name="labels" label="标签">
|
||||
<Input placeholder="请输入任务标签" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CronModal;
|
||||
const CronLabelModal = ({
|
||||
ids,
|
||||
handleCancel,
|
||||
visible,
|
||||
}: {
|
||||
ids: Array<string>;
|
||||
visible: boolean;
|
||||
handleCancel: (needUpdate?: boolean) => void;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const update = async (action: 'delete' | 'post') => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(async (values) => {
|
||||
if (typeof values.labels === 'string') {
|
||||
values.labels = values.labels.split(/,|,/);
|
||||
}
|
||||
setLoading(true);
|
||||
const payload = { ids, labels: values.labels };
|
||||
const { code, data } = await request[action](
|
||||
`${config.apiPrefix}crons/labels`,
|
||||
{
|
||||
data: payload,
|
||||
},
|
||||
);
|
||||
if (code === 200) {
|
||||
message.success(
|
||||
action === 'post' ? '添加Labels成功' : '删除Labels成功',
|
||||
);
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(true);
|
||||
})
|
||||
.catch((info) => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [ids, visible]);
|
||||
|
||||
const buttons = [
|
||||
<Button onClick={() => handleCancel(false)}>取消</Button>,
|
||||
<Button type="primary" danger onClick={() => update('delete')}>
|
||||
删除
|
||||
</Button>,
|
||||
<Button type="primary" onClick={() => update('post')}>
|
||||
添加
|
||||
</Button>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="批量修改标签"
|
||||
visible={visible}
|
||||
footer={buttons}
|
||||
forceRender
|
||||
onCancel={() => handleCancel(false)}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form form={form} layout="vertical" name="form_in_label_modal">
|
||||
<Form.Item name="labels" label="标签">
|
||||
<Input placeholder="请输入任务标签" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { CronModal as default, CronLabelModal };
|
||||
|
||||
@@ -82,13 +82,34 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
key: 'remark',
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created',
|
||||
dataIndex: 'created',
|
||||
key: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
return <span>{new Date(record.created).toLocaleString()}</span>;
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
const time = record.createdAt || record.timestamp;
|
||||
const date = new Date(time)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:');
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={date}
|
||||
trigger={['hover', 'click']}
|
||||
>
|
||||
<span>{date}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -175,7 +196,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] })
|
||||
.delete(`${config.apiPrefix}dependencies`, { data: [record.id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
handleDependence(data.data[0]);
|
||||
@@ -205,7 +226,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
onOk() {
|
||||
request
|
||||
.put(`${config.apiPrefix}dependencies/reinstall`, {
|
||||
data: [record._id],
|
||||
data: [record.id],
|
||||
})
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
@@ -231,7 +252,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
if (Array.isArray(dependence)) {
|
||||
result.push(...dependence);
|
||||
} else {
|
||||
const index = value.findIndex((x) => x._id === dependence._id);
|
||||
const index = value.findIndex((x) => x.id === dependence.id);
|
||||
result.splice(index, 1, {
|
||||
...dependence,
|
||||
});
|
||||
@@ -278,9 +299,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
|
||||
const getDependenceDetail = (dependence: any) => {
|
||||
request
|
||||
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
|
||||
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
|
||||
.then((data: any) => {
|
||||
const index = value.findIndex((x) => x._id === dependence._id);
|
||||
const index = value.findIndex((x) => x.id === dependence.id);
|
||||
const result = [...value];
|
||||
result.splice(index, 1, {
|
||||
...dependence,
|
||||
@@ -307,7 +328,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (logDependence) {
|
||||
localStorage.setItem('logDependence', logDependence._id);
|
||||
localStorage.setItem('logDependence', logDependence.id);
|
||||
setIsLogModalVisible(true);
|
||||
}
|
||||
}, [logDependence]);
|
||||
@@ -328,7 +349,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
}
|
||||
const result = [...value];
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
const index = value.findIndex((x) => x._id === references[i]);
|
||||
const index = value.findIndex((x) => x.id === references[i]);
|
||||
result.splice(index, 1, {
|
||||
...result[index],
|
||||
status,
|
||||
@@ -340,7 +361,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
setTimeout(() => {
|
||||
const _result = [...value];
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
const index = value.findIndex((x) => x._id === references[i]);
|
||||
const index = value.findIndex((x) => x.id === references[i]);
|
||||
_result.splice(index, 1);
|
||||
}
|
||||
setValue(_result);
|
||||
@@ -372,7 +393,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
rowSelection={rowSelection}
|
||||
pagination={false}
|
||||
dataSource={value}
|
||||
rowKey="_id"
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
scroll={{ x: 768, y: tableScrollHeight }}
|
||||
loading={loading}
|
||||
@@ -398,7 +419,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="2" type="primary" onClick={() => addDependence()}>
|
||||
添加依赖
|
||||
新建依赖
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
@@ -432,11 +453,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||
handleCancel={(needRemove?: boolean) => {
|
||||
setIsLogModalVisible(false);
|
||||
if (needRemove) {
|
||||
const index = value.findIndex((x) => x._id === logDependence._id);
|
||||
const index = value.findIndex((x) => x.id === logDependence.id);
|
||||
const result = [...value];
|
||||
result.splice(index, 1);
|
||||
setValue(result);
|
||||
} else if ([...value].map((x) => x._id).includes(logDependence._id)) {
|
||||
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
|
||||
getDependenceDetail(logDependence);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -46,10 +46,10 @@ const DependenceLogModal = ({
|
||||
const getDependenceLog = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
|
||||
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
|
||||
.then((data: any) => {
|
||||
if (localStorage.getItem('logDependence') === dependence._id) {
|
||||
const log = (data.data.log || []).join('\n') as string;
|
||||
if (localStorage.getItem('logDependence') === String(dependence.id)) {
|
||||
const log = (data.data.log || []).join('') as string;
|
||||
setValue(log);
|
||||
setExecuting(!log.includes('结束时间'));
|
||||
setIsRemoveFailed(log.includes('删除失败'));
|
||||
@@ -64,7 +64,7 @@ const DependenceLogModal = ({
|
||||
setRemoveLoading(true);
|
||||
request
|
||||
.delete(`${config.apiPrefix}dependencies/force`, {
|
||||
data: [dependence._id],
|
||||
data: [dependence.id],
|
||||
})
|
||||
.then((data: any) => {
|
||||
cancel(true);
|
||||
@@ -99,7 +99,7 @@ const DependenceLogModal = ({
|
||||
setExecuting(false);
|
||||
setIsRemoveFailed(message.includes('删除失败'));
|
||||
}
|
||||
setValue(`${value} \n ${message}`);
|
||||
setValue(`${value}${message}`);
|
||||
}, [socketMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const DependenceModal = ({
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
const { name, split, type } = values;
|
||||
const { name, split, type, remark } = values;
|
||||
const method = dependence ? 'put' : 'post';
|
||||
let payload;
|
||||
if (!dependence) {
|
||||
@@ -36,13 +36,14 @@ const DependenceModal = ({
|
||||
return {
|
||||
name: x,
|
||||
type,
|
||||
remark,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
payload = [{ name, type }];
|
||||
payload = [{ name, type, remark }];
|
||||
}
|
||||
} else {
|
||||
payload = { ...values, _id: dependence._id };
|
||||
payload = { ...values, id: dependence.id };
|
||||
}
|
||||
try {
|
||||
const { code, data } = await request[method](
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
|
||||
.diff-switch-file {
|
||||
min-width: 768px;
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
Vendored
+40
-24
@@ -27,7 +27,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import './index.less';
|
||||
import { getTableScroll } from '@/utils/index';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Search } = Input;
|
||||
|
||||
enum Status {
|
||||
@@ -101,7 +101,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
{
|
||||
title: '序号',
|
||||
align: 'center' as const,
|
||||
width: 50,
|
||||
width: 60,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||
},
|
||||
@@ -118,15 +118,19 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
align: 'center' as const,
|
||||
width: '44%',
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
width: '35%',
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={text}>
|
||||
<span>{text}</span>
|
||||
</Tooltip>
|
||||
<Paragraph
|
||||
style={{
|
||||
wordBreak: 'break-all',
|
||||
marginBottom: 0,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
>
|
||||
{text}
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -141,19 +145,31 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
align: 'center' as const,
|
||||
width: 164,
|
||||
width: 165,
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();
|
||||
const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();
|
||||
return updatedAtA - updatedAtB;
|
||||
},
|
||||
},
|
||||
render: (text: string, record: any) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
const date = new Date(text)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
const time = record.updatedAt || record.timestamp;
|
||||
const date = new Date(time)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:');
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={date}>
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={date}
|
||||
trigger={['hover', 'click']}
|
||||
>
|
||||
<span>{date}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -189,7 +205,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
width: 120,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
const isPc = !isPhone;
|
||||
@@ -262,7 +278,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
record.status === Status.已禁用 ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: [record._id],
|
||||
data: [record.id],
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
@@ -313,7 +329,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}envs`, { data: [record._id] })
|
||||
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
message.success('删除成功');
|
||||
@@ -343,7 +359,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
|
||||
const handleEnv = (env: any) => {
|
||||
const result = [...value];
|
||||
const index = value.findIndex((x) => x._id === env._id);
|
||||
const index = value.findIndex((x) => x.id === env.id);
|
||||
if (index === -1) {
|
||||
env = Array.isArray(env) ? env : [env];
|
||||
result.push(...env);
|
||||
@@ -368,7 +384,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
}
|
||||
const dragRow = value[dragIndex];
|
||||
request
|
||||
.put(`${config.apiPrefix}envs/${dragRow._id}/move`, {
|
||||
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
|
||||
data: { fromIndex: dragIndex, toIndex: hoverIndex },
|
||||
})
|
||||
.then((data: any) => {
|
||||
@@ -477,7 +493,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="2" type="primary" onClick={() => addEnv()}>
|
||||
添加变量
|
||||
新建变量
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
@@ -526,9 +542,9 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||
rowSelection={rowSelection}
|
||||
pagination={false}
|
||||
dataSource={value}
|
||||
rowKey="_id"
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
scroll={{ x: 768, y: tableScrollHeight }}
|
||||
scroll={{ x: 1000, y: tableScrollHeight }}
|
||||
components={components}
|
||||
loading={loading}
|
||||
onRow={(record: any, index: number) => {
|
||||
|
||||
Vendored
+15
-11
@@ -34,18 +34,22 @@ const EnvModal = ({
|
||||
payload = [{ value, name, remarks }];
|
||||
}
|
||||
} else {
|
||||
payload = { ...values, _id: env._id };
|
||||
payload = { ...values, id: env.id };
|
||||
}
|
||||
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(env ? '更新变量成功' : '添加变量成功');
|
||||
} else {
|
||||
message.error(data);
|
||||
try {
|
||||
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(env ? '更新变量成功' : '新建变量成功');
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
} catch (error: any) {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +81,7 @@ const EnvModal = ({
|
||||
rules={[
|
||||
{ required: true, message: '请输入环境变量名称', whitespace: true },
|
||||
{
|
||||
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/,
|
||||
pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
|
||||
message: '只能输入字母数字下划线,且不能以数字开头',
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.error-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
|
||||
.react-terminal-wrapper {
|
||||
max-width: 90%;
|
||||
height: calc(100vh - 80px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import Terminal, { ColorMode, LineType } from '../../components/terminal';
|
||||
import { PageLoading } from '@ant-design/pro-layout';
|
||||
import { history } from 'umi';
|
||||
import Ansi from 'ansi-to-react';
|
||||
import './index.less';
|
||||
|
||||
const Error = ({ user, theme }: any) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState('暂无日志');
|
||||
|
||||
const getLog = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}public/panel/log`)
|
||||
.then((data: any) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
history.push('/crontab');
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
getLog();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="error-wrapper">
|
||||
{loading ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
<Terminal
|
||||
name="服务错误"
|
||||
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
|
||||
lineData={[
|
||||
{ type: LineType.Input, value: 'pm2 logs panel' },
|
||||
{
|
||||
type: LineType.Output,
|
||||
value: (
|
||||
<pre>
|
||||
<Ansi>{data}</Ansi>
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Error;
|
||||
@@ -35,7 +35,7 @@ const Initialization = () => {
|
||||
const submitAccountSetting = (values: any) => {
|
||||
setLoading(true);
|
||||
request
|
||||
.put(`${config.apiPrefix}init/user`, {
|
||||
.put(`${config.apiPrefix}user/init`, {
|
||||
data: {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
@@ -52,8 +52,9 @@ const Initialization = () => {
|
||||
};
|
||||
|
||||
const submitNotification = (values: any) => {
|
||||
setLoading(true);
|
||||
request
|
||||
.put(`${config.apiPrefix}init/notification`, {
|
||||
.put(`${config.apiPrefix}user/notification/init`, {
|
||||
data: {
|
||||
...values,
|
||||
},
|
||||
@@ -73,6 +74,10 @@ const Initialization = () => {
|
||||
setFields(_fields || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.removeItem(config.authKey);
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: '欢迎使用',
|
||||
@@ -225,7 +230,11 @@ const Initialization = () => {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.top}>
|
||||
<div className={styles.header}>
|
||||
<img alt="logo" className={styles.logo} src="/images/qinglong.png" />
|
||||
<img
|
||||
alt="logo"
|
||||
className={styles.logo}
|
||||
src="http://qn.whyour.cn/logo.png"
|
||||
/>
|
||||
<span className={styles.title}>初始化配置</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
&-scroller {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border-right: 1px dashed #f0f0f0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
@@ -22,3 +21,10 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:global {
|
||||
.Pane.vertical.Pane1 {
|
||||
padding-right: 5px;
|
||||
border-right: 1px dashed #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
+44
-22
@@ -1,12 +1,11 @@
|
||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||
import { TreeSelect, Tree, Input } from 'antd';
|
||||
import { TreeSelect, Tree, Input, Empty } from 'antd';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { request } from '@/utils/http';
|
||||
import styles from './index.module.less';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import SplitPane from 'react-split-pane';
|
||||
|
||||
function getFilterData(keyword: string, data: any) {
|
||||
@@ -16,7 +15,6 @@ function getFilterData(keyword: string, data: any) {
|
||||
data.forEach((item: any) => {
|
||||
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
||||
tree.push(item);
|
||||
expandedKeys.push(...item.children.map((x: any) => x.key));
|
||||
} else {
|
||||
const children: any[] = [];
|
||||
(item.children || []).forEach((subItem: any) => {
|
||||
@@ -29,7 +27,7 @@ function getFilterData(keyword: string, data: any) {
|
||||
...item,
|
||||
children,
|
||||
});
|
||||
expandedKeys.push(...children.map((x) => x.key));
|
||||
expandedKeys.push(item.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -47,6 +45,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [height, setHeight] = useState<number>();
|
||||
const treeDom = useRef<any>();
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
const getLogs = () => {
|
||||
setLoading(true);
|
||||
@@ -100,8 +99,12 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const onSearch = useCallback(
|
||||
(e) => {
|
||||
const keyword = e.target.value;
|
||||
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
|
||||
const { tree, expandedKeys } = getFilterData(
|
||||
keyword.toLocaleLowerCase(),
|
||||
data,
|
||||
);
|
||||
setFilterData(tree);
|
||||
setExpandedKeys(expandedKeys);
|
||||
},
|
||||
[data, setFilterData],
|
||||
);
|
||||
@@ -125,9 +128,8 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
||||
value={select}
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeData={data}
|
||||
placeholder="请选择日志文件"
|
||||
placeholder="请选择日志"
|
||||
showSearch
|
||||
key="value"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
]
|
||||
@@ -140,21 +142,41 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
|
||||
{!isPhone && (
|
||||
<SplitPane split="vertical" size={200} maxSize={-100}>
|
||||
<div className={styles['left-tree-container']}>
|
||||
<Input.Search
|
||||
className={styles['left-tree-search']}
|
||||
onChange={onSearch}
|
||||
></Input.Search>
|
||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||
<Tree
|
||||
className={styles['left-tree']}
|
||||
treeData={filterData}
|
||||
showIcon={true}
|
||||
height={height}
|
||||
selectedKeys={[select]}
|
||||
showLine={{ showLeafIcon: true }}
|
||||
onSelect={onTreeSelect}
|
||||
></Tree>
|
||||
</div>
|
||||
{data.length > 0 ? (
|
||||
<>
|
||||
<Input.Search
|
||||
className={styles['left-tree-search']}
|
||||
onChange={onSearch}
|
||||
placeholder="请输入日志名"
|
||||
allowClear
|
||||
></Input.Search>
|
||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||
<Tree
|
||||
className={styles['left-tree']}
|
||||
treeData={filterData}
|
||||
showIcon={true}
|
||||
height={height}
|
||||
selectedKeys={[select]}
|
||||
showLine={{ showLeafIcon: true }}
|
||||
onSelect={onTreeSelect}
|
||||
></Tree>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Empty
|
||||
description="暂无日志"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Editor
|
||||
language="shell"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const Login = () => {
|
||||
setTwoFactor(false);
|
||||
setWaitTime(null);
|
||||
request
|
||||
.post(`${config.apiPrefix}login`, {
|
||||
.post(`${config.apiPrefix}user/login`, {
|
||||
data: {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
@@ -113,6 +113,14 @@ const Login = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const codeInputChange = (e: React.ChangeEvent) => {
|
||||
const { value } = e.target as any;
|
||||
const regx = /^[0-9]{6}$/;
|
||||
if (regx.test(value)) {
|
||||
completeTowFactor({ code: value });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isAuth = localStorage.getItem(config.authKey);
|
||||
if (isAuth) {
|
||||
@@ -124,7 +132,11 @@ const Login = () => {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.top}>
|
||||
<div className={styles.header}>
|
||||
<img alt="logo" className={styles.logo} src="/images/qinglong.png" />
|
||||
<img
|
||||
alt="logo"
|
||||
className={styles.logo}
|
||||
src="http://qn.whyour.cn/logo.png"
|
||||
/>
|
||||
<span className={styles.title}>
|
||||
{twoFactor ? '两步验证' : config.siteName}
|
||||
</span>
|
||||
@@ -140,12 +152,15 @@ const Login = () => {
|
||||
{
|
||||
pattern: /^[0-9]{6}$/,
|
||||
message: '验证码为6位数字',
|
||||
validateTrigger: 'onBlur',
|
||||
},
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input placeholder="6位数字" autoFocus autoComplete="off" />
|
||||
<Input
|
||||
placeholder="6位数字"
|
||||
onChange={codeInputChange}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormItem>
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
+103
-24
@@ -24,72 +24,134 @@ const prefixMap: any = {
|
||||
|
||||
const EditModal = ({
|
||||
treeData,
|
||||
currentFile,
|
||||
currentNode,
|
||||
content,
|
||||
handleCancel,
|
||||
visible,
|
||||
socketMessage,
|
||||
}: {
|
||||
treeData?: any;
|
||||
currentFile?: string;
|
||||
content?: string;
|
||||
visible: boolean;
|
||||
socketMessage: any;
|
||||
currentNode: any;
|
||||
handleCancel: () => void;
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [language, setLanguage] = useState<string>('javascript');
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
const [cNode, setCNode] = useState<any>();
|
||||
const [selectedKey, setSelectedKey] = useState<string>('');
|
||||
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
|
||||
const [settingModalVisible, setSettingModalVisible] =
|
||||
useState<boolean>(false);
|
||||
const [log, setLog] = useState<string>('');
|
||||
const { theme } = useTheme();
|
||||
const editorRef = useRef<any>(null);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
const cancel = () => {
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
const onSelect = (value: any, node: any) => {
|
||||
if (node.key === selectedKey || !value) {
|
||||
return;
|
||||
}
|
||||
const newMode = LangMap[value.slice(-3)] || '';
|
||||
setFileName(value);
|
||||
setCNode(node);
|
||||
setLanguage(newMode);
|
||||
getDetail(node);
|
||||
setSelectedKey(node.key);
|
||||
};
|
||||
|
||||
const getDetail = (node: any) => {
|
||||
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => {
|
||||
setValue(data.data);
|
||||
});
|
||||
request
|
||||
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
|
||||
.then((data) => {
|
||||
setValue(data.data);
|
||||
});
|
||||
};
|
||||
|
||||
const run = () => {};
|
||||
const run = () => {
|
||||
setLog('');
|
||||
request
|
||||
.put(`${config.apiPrefix}scripts/run`, {
|
||||
data: {
|
||||
filename: cNode.value,
|
||||
path: cNode.parent || '',
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setIsRunning(true);
|
||||
});
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (!cNode || !cNode.value) {
|
||||
return;
|
||||
}
|
||||
request
|
||||
.put(`${config.apiPrefix}scripts/stop`, {
|
||||
data: {
|
||||
filename: cNode.value,
|
||||
path: cNode.parent || '',
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setIsRunning(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFile) {
|
||||
setFileName(currentFile);
|
||||
setValue(content as string);
|
||||
if (!socketMessage) {
|
||||
return;
|
||||
}
|
||||
}, [currentFile, content]);
|
||||
|
||||
let { type, message: _message, references } = socketMessage;
|
||||
|
||||
if (type !== 'manuallyRunScript') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_message === '执行结束') {
|
||||
_message = '';
|
||||
setTimeout(() => {
|
||||
setIsRunning(false);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
if (log) {
|
||||
_message = `\n${_message}`;
|
||||
}
|
||||
setLog(`${log}${_message}`);
|
||||
}, [socketMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentNode) {
|
||||
setCNode(currentNode);
|
||||
setValue(content as string);
|
||||
setSelectedKey(currentNode.key);
|
||||
}
|
||||
}, [content, currentNode]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="edit-modal"
|
||||
closable={false}
|
||||
title={
|
||||
<>
|
||||
<span style={{ marginRight: 8 }}>{fileName}</span>
|
||||
<TreeSelect
|
||||
style={{ marginRight: 8, width: 120 }}
|
||||
value={currentFile}
|
||||
style={{ marginRight: 8, width: 150 }}
|
||||
value={selectedKey}
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeData={treeData}
|
||||
placeholder="请选择脚本文件"
|
||||
showSearch
|
||||
key="value"
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<Select
|
||||
value={language}
|
||||
style={{ width: 120, marginRight: 8 }}
|
||||
style={{ width: 110, marginRight: 8 }}
|
||||
onChange={(e) => {
|
||||
setLanguage(e);
|
||||
}}
|
||||
@@ -99,8 +161,12 @@ const EditModal = ({
|
||||
<Option value="shell">shell</Option>
|
||||
<Option value="python">python</Option>
|
||||
</Select>
|
||||
<Button type="primary" style={{ marginRight: 8 }} onClick={run}>
|
||||
运行
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={isRunning ? stop : run}
|
||||
>
|
||||
{isRunning ? '停止' : '运行'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -129,6 +195,16 @@ const EditModal = ({
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => {
|
||||
stop();
|
||||
handleCancel();
|
||||
}}
|
||||
>
|
||||
退出
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={'100%'}
|
||||
@@ -136,7 +212,12 @@ const EditModal = ({
|
||||
onClose={cancel}
|
||||
visible={visible}
|
||||
>
|
||||
<SplitPane split="vertical" minSize={200} defaultSize="50%">
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
minSize={200}
|
||||
defaultSize="50%"
|
||||
style={{ height: 'calc(100vh - 55px)' }}
|
||||
>
|
||||
<Editor
|
||||
language={language}
|
||||
value={value}
|
||||
@@ -151,9 +232,7 @@ const EditModal = ({
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<pre>{log}</pre>
|
||||
</div>
|
||||
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
|
||||
</SplitPane>
|
||||
<SaveModal
|
||||
visible={saveModalVisible}
|
||||
@@ -164,7 +243,7 @@ const EditModal = ({
|
||||
content:
|
||||
editorRef.current &&
|
||||
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
||||
filename: fileName,
|
||||
filename: cNode?.value,
|
||||
}}
|
||||
/>
|
||||
<SettingModal
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, message, Input, Form } from 'antd';
|
||||
import { Modal, message, Input, Form, Select } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const EditScriptNameModal = ({
|
||||
handleCancel,
|
||||
treeData,
|
||||
visible,
|
||||
}: {
|
||||
visible: boolean;
|
||||
handleCancel: (file?: { filename: string }) => void;
|
||||
treeData: any[];
|
||||
handleCancel: (file?: {
|
||||
filename: string;
|
||||
path: string;
|
||||
key: string;
|
||||
}) => void;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dirs, setDirs] = useState<any[]>([]);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
values.path = values.path || '';
|
||||
request
|
||||
.post(`${config.apiPrefix}scripts`, {
|
||||
data: { filename: values.filename, content: '' },
|
||||
data: { filename: values.filename, path: values.path, content: '' },
|
||||
})
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
message.success('保存文件成功');
|
||||
handleCancel({ filename: values.filename });
|
||||
handleCancel({
|
||||
filename: values.filename,
|
||||
path: values.path,
|
||||
key: `${values.path}-${values.filename}`,
|
||||
});
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
@@ -33,6 +47,8 @@ const EditScriptNameModal = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
const originDirs = treeData.filter((x) => x.disabled);
|
||||
setDirs([{ key: '' }, ...originDirs]);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
@@ -56,10 +72,22 @@ const EditScriptNameModal = ({
|
||||
<Form form={form} layout="vertical" name="edit_name_modal">
|
||||
<Form.Item
|
||||
name="filename"
|
||||
label="文件名"
|
||||
rules={[{ required: true, message: '请输入文件名' }]}
|
||||
>
|
||||
<Input placeholder="请输入文件名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="父目录"
|
||||
name="path"
|
||||
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
|
||||
>
|
||||
<Select placeholder="请选择父目录">
|
||||
{dirs.map((x) => (
|
||||
<Option value={x.key}>{x.key || '根'}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
&-scroller {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border-right: 1px dashed #f0f0f0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
@@ -22,3 +21,10 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:global {
|
||||
.Pane.vertical.Pane1 {
|
||||
padding-right: 5px;
|
||||
border-right: 1px dashed #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
+156
-39
@@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Empty,
|
||||
} from 'antd';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
@@ -31,20 +32,37 @@ import {
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import EditScriptNameModal from './editNameModal';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { history } from 'umi';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function getFilterData(keyword: string, data: any) {
|
||||
const expandedKeys: string[] = [];
|
||||
if (keyword) {
|
||||
const tree: any = [];
|
||||
data.forEach((item: any) => {
|
||||
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
||||
tree.push(item);
|
||||
} else {
|
||||
const children: any[] = [];
|
||||
(item.children || []).forEach((subItem: any) => {
|
||||
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
|
||||
children.push(subItem);
|
||||
}
|
||||
});
|
||||
if (children.length > 0) {
|
||||
tree.push({
|
||||
...item,
|
||||
children,
|
||||
});
|
||||
expandedKeys.push(item.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { tree };
|
||||
return { tree, expandedKeys };
|
||||
}
|
||||
return { tree: data };
|
||||
return { tree: data, expandedKeys };
|
||||
}
|
||||
|
||||
const LangMap: any = {
|
||||
@@ -54,7 +72,7 @@ const LangMap: any = {
|
||||
'.ts': 'typescript',
|
||||
};
|
||||
|
||||
const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
||||
const [title, setTitle] = useState('请选择脚本文件');
|
||||
const [value, setValue] = useState('请选择脚本文件');
|
||||
const [select, setSelect] = useState<any>();
|
||||
@@ -70,6 +88,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const editorRef = useRef<any>(null);
|
||||
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
||||
const [currentNode, setCurrentNode] = useState<any>();
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
const getScripts = () => {
|
||||
setLoading(true);
|
||||
@@ -78,6 +97,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
setFilterData(data.data);
|
||||
initGetScript();
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
@@ -90,6 +110,23 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const initGetScript = () => {
|
||||
const { p, s } = history.location.query as any;
|
||||
if (s) {
|
||||
const vkey = `${p}/${s}`;
|
||||
const obj = {
|
||||
node: {
|
||||
title: s,
|
||||
value: s,
|
||||
key: p ? vkey : s,
|
||||
parent: p,
|
||||
},
|
||||
};
|
||||
setExpandedKeys([p]);
|
||||
onTreeSelect([vkey], obj);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = (value: any, node: any) => {
|
||||
if (node.key === select || !value) {
|
||||
return;
|
||||
@@ -97,12 +134,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
setValue('加载中...');
|
||||
const newMode = value ? LangMap[value.slice(-3)] : '';
|
||||
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
||||
setSelect(value);
|
||||
setSelect(node.key);
|
||||
setTitle(node.parent || node.value);
|
||||
setCurrentNode(node);
|
||||
getDetail(node);
|
||||
};
|
||||
|
||||
const onExpand = (expKeys: any) => {
|
||||
setExpandedKeys(expKeys);
|
||||
};
|
||||
|
||||
const onTreeSelect = useCallback(
|
||||
(keys: Key[], e: any) => {
|
||||
const content = editorRef.current
|
||||
@@ -131,13 +172,24 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const onSearch = useCallback(
|
||||
(e) => {
|
||||
const keyword = e.target.value;
|
||||
setSearchValue(keyword);
|
||||
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
|
||||
setFilterData(tree);
|
||||
debounceSearch(keyword);
|
||||
},
|
||||
[data, setFilterData],
|
||||
);
|
||||
|
||||
const debounceSearch = useCallback(
|
||||
debounce((keyword) => {
|
||||
setSearchValue(keyword);
|
||||
const { tree, expandedKeys } = getFilterData(
|
||||
keyword.toLocaleLowerCase(),
|
||||
data,
|
||||
);
|
||||
setExpandedKeys(expandedKeys);
|
||||
setFilterData(tree);
|
||||
}, 300),
|
||||
[data, setFilterData],
|
||||
);
|
||||
|
||||
const editFile = () => {
|
||||
setTimeout(() => {
|
||||
setIsEditing(true);
|
||||
@@ -147,7 +199,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const cancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setValue('加载中...');
|
||||
getDetail({ value: select });
|
||||
getDetail(currentNode);
|
||||
};
|
||||
|
||||
const saveFile = () => {
|
||||
@@ -157,7 +209,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
<>
|
||||
确认保存文件
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{select}
|
||||
{currentNode.value}
|
||||
</Text>{' '}
|
||||
,保存后不可恢复
|
||||
</>
|
||||
@@ -170,7 +222,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
request
|
||||
.put(`${config.apiPrefix}scripts`, {
|
||||
data: {
|
||||
filename: select,
|
||||
filename: currentNode.value,
|
||||
path: currentNode.parent || '',
|
||||
content,
|
||||
},
|
||||
@@ -210,15 +262,30 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
request
|
||||
.delete(`${config.apiPrefix}scripts`, {
|
||||
data: {
|
||||
filename: select,
|
||||
filename: currentNode.value,
|
||||
path: currentNode.parent || '',
|
||||
},
|
||||
})
|
||||
.then((_data: any) => {
|
||||
if (_data.code === 200) {
|
||||
message.success(`删除成功`);
|
||||
let newData = [...data];
|
||||
const index = newData.findIndex((x) => x.value === select);
|
||||
newData.splice(index, 1);
|
||||
if (currentNode.parent) {
|
||||
const parentNodeIndex = newData.findIndex(
|
||||
(x) => x.key === currentNode.parent,
|
||||
);
|
||||
const parentNode = newData[parentNodeIndex];
|
||||
const index = parentNode.children.findIndex(
|
||||
(y) => y.key === currentNode.key,
|
||||
);
|
||||
parentNode.children.splice(index, 1);
|
||||
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
||||
} else {
|
||||
const index = newData.findIndex(
|
||||
(x) => x.key === currentNode.key,
|
||||
);
|
||||
newData.splice(index, 1);
|
||||
}
|
||||
setData(newData);
|
||||
} else {
|
||||
message.error(_data);
|
||||
@@ -236,12 +303,27 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
};
|
||||
|
||||
const addFileModalClose = (
|
||||
{ filename }: { filename: string } = { filename: '' },
|
||||
{ filename, path, key }: { filename: string; path: string; key: string } = {
|
||||
filename: '',
|
||||
path: '',
|
||||
key: '',
|
||||
},
|
||||
) => {
|
||||
if (filename) {
|
||||
const newData = [...data];
|
||||
const _file = { title: filename, key: filename, value: filename };
|
||||
newData.unshift(_file);
|
||||
const _file = { title: filename, key, value: filename, parent: path };
|
||||
if (path) {
|
||||
const parentNodeIndex = newData.findIndex((x) => x.key === path);
|
||||
const parentNode = newData[parentNodeIndex];
|
||||
if (parentNode.children && parentNode.children.length > 0) {
|
||||
parentNode.children.unshift(_file);
|
||||
} else {
|
||||
parentNode.children = [_file];
|
||||
}
|
||||
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
||||
} else {
|
||||
newData.unshift(_file);
|
||||
}
|
||||
setData(newData);
|
||||
onSelect(_file.value, _file);
|
||||
setIsEditing(true);
|
||||
@@ -253,7 +335,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
request
|
||||
.post(`${config.apiPrefix}scripts/download`, {
|
||||
data: {
|
||||
filename: select,
|
||||
filename: currentNode.value,
|
||||
},
|
||||
})
|
||||
.then((_data: any) => {
|
||||
@@ -261,7 +343,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = select;
|
||||
a.download = currentNode.value;
|
||||
document.documentElement.appendChild(a);
|
||||
a.click();
|
||||
document.documentElement.removeChild(a);
|
||||
@@ -297,12 +379,22 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
) : (
|
||||
<Menu>
|
||||
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
|
||||
添加
|
||||
新建
|
||||
</Menu.Item>
|
||||
<Menu.Item key="edit" icon={<EditOutlined />} onClick={editFile}>
|
||||
<Menu.Item
|
||||
key="edit"
|
||||
icon={<EditOutlined />}
|
||||
onClick={editFile}
|
||||
disabled={!select}
|
||||
>
|
||||
编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item key="delete" icon={<DeleteOutlined />} onClick={deleteFile}>
|
||||
<Menu.Item
|
||||
key="delete"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={deleteFile}
|
||||
disabled={!select}
|
||||
>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
@@ -321,9 +413,8 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
value={select}
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeData={data}
|
||||
placeholder="请选择脚本文件"
|
||||
placeholder="请选择脚本"
|
||||
showSearch
|
||||
key="value"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
@@ -349,6 +440,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
</Tooltip>,
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
disabled={!select}
|
||||
type="primary"
|
||||
onClick={editFile}
|
||||
icon={<EditOutlined />}
|
||||
@@ -357,6 +449,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!select}
|
||||
onClick={deleteFile}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
@@ -379,21 +472,43 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
{!isPhone && (
|
||||
<SplitPane split="vertical" size={200} maxSize={-100}>
|
||||
<div className={styles['left-tree-container']}>
|
||||
<Input.Search
|
||||
className={styles['left-tree-search']}
|
||||
onChange={onSearch}
|
||||
></Input.Search>
|
||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||
<Tree
|
||||
className={styles['left-tree']}
|
||||
treeData={filterData}
|
||||
showIcon={true}
|
||||
height={height}
|
||||
selectedKeys={[select]}
|
||||
showLine={{ showLeafIcon: true }}
|
||||
onSelect={onTreeSelect}
|
||||
></Tree>
|
||||
</div>
|
||||
{data.length > 0 ? (
|
||||
<>
|
||||
<Input.Search
|
||||
className={styles['left-tree-search']}
|
||||
onChange={onSearch}
|
||||
placeholder="请输入脚本名"
|
||||
allowClear
|
||||
></Input.Search>
|
||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||
<Tree
|
||||
className={styles['left-tree']}
|
||||
treeData={filterData}
|
||||
showIcon={true}
|
||||
height={height}
|
||||
selectedKeys={[select]}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={onExpand}
|
||||
showLine={{ showLeafIcon: true }}
|
||||
onSelect={onTreeSelect}
|
||||
></Tree>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Empty
|
||||
description="暂无脚本"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Editor
|
||||
language={mode}
|
||||
@@ -432,14 +547,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
<EditModal
|
||||
visible={isLogModalVisible}
|
||||
treeData={data}
|
||||
currentFile={select}
|
||||
currentNode={currentNode}
|
||||
content={value}
|
||||
socketMessage={socketMessage}
|
||||
handleCancel={() => {
|
||||
setIsLogModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
<EditScriptNameModal
|
||||
visible={isAddFileModalVisible}
|
||||
treeData={data}
|
||||
handleCancel={addFileModalClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Typography, Input, Form, Button, message } from 'antd';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Link } = Typography;
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<img
|
||||
alt="logo"
|
||||
style={{ width: 140, marginRight: 20 }}
|
||||
src="http://qn.whyour.cn/logo.png"
|
||||
/>
|
||||
<div className={styles.right}>
|
||||
<span className={styles.title}>青龙</span>
|
||||
<span className={styles.desc}>
|
||||
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed
|
||||
task management panel that supports typescript, javaScript, python3,
|
||||
and shell.)
|
||||
</span>
|
||||
<div>
|
||||
<Link
|
||||
href="https://github.com/whyour/qinglong"
|
||||
target="_blank"
|
||||
style={{ marginRight: 15 }}
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
<Link
|
||||
href="https://t.me/jiao_long"
|
||||
target="_blank"
|
||||
style={{ marginRight: 15 }}
|
||||
>
|
||||
Telegram频道
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/whyour/qinglong/issues"
|
||||
target="_blank"
|
||||
>
|
||||
提交BUG
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -20,13 +20,13 @@ const AppModal = ({
|
||||
const method = app ? 'put' : 'post';
|
||||
const payload = { ...values };
|
||||
if (app) {
|
||||
payload._id = app._id;
|
||||
payload.id = app.id;
|
||||
}
|
||||
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(app ? '更新应用成功' : '添加应用成功');
|
||||
message.success(app ? '更新应用成功' : '新建应用成功');
|
||||
} else {
|
||||
message.error(data);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user