Compare commits

..
117 Commits
Author SHA1 Message Date
whyour 56eb0c5408 更新版本 v2.15.18 2023-07-19 21:17:22 +08:00
whyour fe55929959 更新 readme 2023-07-19 21:02:52 +08:00
whyour 01e2bd007d 修复重启提示持续时间 2023-07-18 21:39:03 +08:00
whyour 4e091b0c3e 修改 nginx body 限制,服务重启延迟时间 2023-07-18 15:30:43 +08:00
whyour db94cd3799 修复备份数据超时时间 2023-07-18 13:43:02 +08:00
whyour a15192b9b2 升级 @umijs/max 版本 2023-07-17 23:18:48 +08:00
whyour efd4f1d5ab umi-request 替换为 axios 2023-07-17 23:13:06 +08:00
whyour bd166ee794 修复 pm2 重启逻辑 2023-07-17 13:25:56 +08:00
whyour 93e94ea94c 系统设置增加数据恢复功能 2023-07-16 22:02:30 +08:00
whyour 88b87de391 增加数据备份功能 2023-07-16 00:23:29 +08:00
whyour 8affff96f3 修改系统内更新系统逻辑 2023-07-13 22:09:45 +08:00
whyour 936b565fb1 修改系统更新时间和版本获取 2023-07-12 00:05:46 +08:00
whyour b69ff2895e 增加依赖是否已经安装判断 2023-07-10 23:48:05 +08:00
whyour f3791cbb62 修复编译错误 2023-07-09 17:46:02 +08:00
whyour b0f3b51736 修复服务启动时定时删除日志失效 2023-07-08 20:58:36 +08:00
whyour 3aa112e373 修改 node 进程错误监控 2023-07-08 00:27:05 +08:00
whyour 683482e067 修改 ql update 逻辑 2023-07-08 00:18:09 +08:00
whyour 5a2caeb66b 更新版本 v2.15.17 2023-07-02 16:09:09 +08:00
whyour ee6e5bd8b4 修改任务状态值 2023-07-02 16:09:01 +08:00
whyour f970322f0a 修复定时任务状态筛选 2023-07-02 12:19:34 +08:00
whyour 1718120623 修改更新仓库逻辑 2023-07-01 20:48:28 +08:00
whyour 702c3160ec 修改并发逻辑,系统设置增加定时任务并发设置 2023-07-01 15:26:20 +08:00
whyour db227e56bf 更新版本 v2.15.16 2023-06-18 15:51:42 +08:00
whyour 84828865f7 修复依赖安装流程 2023-06-18 15:51:35 +08:00
whyour 87455cebc0 修改应用设置 loading 默认值 2023-06-17 20:26:40 +08:00
whyour 9f4435b237 修改初始化逻辑 2023-06-16 02:12:25 +08:00
whyour e5bc4b3b04 修复关闭全部任务视图,默认视图筛选错误 2023-06-14 22:38:15 +08:00
whyour 11e1b50000 修改订阅通知 2023-06-11 20:05:39 +08:00
whyour bec226ae13 增加初始化文件写入,修复 shell 变量 2023-06-11 19:58:30 +08:00
whyour c9ecdc6c97 企业微信通知增加代理地址配置 QYWX_ORIGIN 2023-06-11 15:03:07 +08:00
whyour 4cf64e7af0 增加 demo 环境变量 2023-06-10 22:20:19 +08:00
whyour 4ce01c5f0e 修复初始化界面样式 2023-06-10 16:16:36 +08:00
whyour 2eff99abe1 修复侧边栏样式 2023-06-10 13:27:21 +08:00
whyour 5bf57185d9 更新 readme 2023-06-10 12:52:53 +08:00
whyour c59dac19ae 修复 shell 变量无引号报错 2023-06-09 19:50:38 +08:00
whyour f1ef8e72ae 修复解析脚本名称逻辑 2023-06-09 19:45:25 +08:00
whyour 5b1accffb7 修改 api 限流策略,修复检查检查日志 2023-06-07 22:45:37 +08:00
whyour f64ff83414 修复列表查询字符转码 2023-06-04 22:11:48 +08:00
whyour 26c914d1db 修复侧边栏样式 2023-06-03 01:24:52 +08:00
whyour 8f75a54110 修改 textarea 样式 2023-06-03 00:48:04 +08:00
whyour 874b6e3a56 修复错误页样式 2023-06-01 23:02:36 +08:00
whyour f305d45225 修复依赖安装状态流转 2023-05-31 15:07:40 +08:00
whyour a7f97bd729 替换 spawn 为 cross-spawn 2023-05-31 14:25:20 +08:00
whyour 0bbb7c7b89 修复获取 cpu 核数 2023-05-30 17:45:47 +08:00
whyour f7465759c7 降级 p-limit,修复 esm 问题 2023-05-30 17:31:31 +08:00
whyour 998e82d323 增加 api 限流配置 2023-05-30 16:41:19 +08:00
whyour f8dfee8945 重构任务并发执行逻辑 2023-05-30 16:32:00 +08:00
whyour 86e3d8736b 更新版本 v2.15.15 2023-05-28 20:06:15 +08:00
whyour 85d32d00e1 修改打包 latest 镜像分支 2023-05-28 20:06:05 +08:00
whyour 6a52e2f804 修复 env.js 路径 2023-05-27 23:05:46 +08:00
whyour b4cb72c5d0 修改错误页面提示 2023-05-27 11:35:30 +08:00
whyour 79fd4ec58f 修复 nginx 变量替换 2023-05-26 17:08:40 +08:00
jzksnsjswkwandGitHub 49ed5c6632 修复 可执行文件 提示找不到文件的问题 (#1943) 2023-05-26 15:17:47 +08:00
whyour ffba7ee4a1 增加定时任务服务启动失败日志 2023-05-25 21:45:28 +08:00
whyour 91d080472f 修复环境变量接口前缀 2023-05-24 23:46:07 +08:00
whyour 826f214f0f 修复设置通知错误时提示不清晰 2023-05-24 23:37:44 +08:00
whyour ca150dc6a6 修复 docker-compose 配置 2023-05-22 20:20:46 +08:00
whyour ae5db60211 修复筛选可能报错 2023-05-21 12:29:29 +08:00
whyour 490bdc15f6 支持非根目录部署 2023-05-19 01:10:33 +08:00
whyour 1377fe31aa 更新版本 v2.15.14 2023-05-15 23:44:42 +08:00
whyour 3d1cf6bf2e 修改群晖名称 2023-05-15 23:44:32 +08:00
whyour 6ac755b478 修复后端 html 缓存 2023-05-15 22:34:06 +08:00
whyour e70fa83074 修复获取配置文件参数 2023-05-14 20:53:45 +08:00
whyour 41521f1d08 修复 token 任务脚本路径,容器启动 bot 2023-05-13 01:10:49 +08:00
whyour 8b7bef097c 修复拉取仓库逻辑 2023-05-12 18:28:01 +08:00
whyour 618756d4a9 修复设置代理时,服务启动失败 2023-05-10 23:17:05 +08:00
whyour 70d0c71414 修复错误日志样式 2023-05-10 23:01:51 +08:00
whyour 65eb9c806d 更新 sqlite 2023-05-10 21:58:11 +08:00
whyour 50718ba714 修复 public 服务转发 2023-05-10 01:07:46 +08:00
whyour 7c3bb90d12 修复删除依赖和移动环境变量接口数据结构 2023-05-08 21:29:25 +08:00
whyour f66390eabd 修改初始系统状态逻辑已经错误页面 2023-05-07 23:40:37 +08:00
whyour 481646501c 修复订阅更新仓库失败 2023-05-06 22:58:05 +08:00
whyour 5baa8e4391 增加 pm2 平滑重启 2023-05-06 00:09:03 +08:00
whyour 3d11210aeb pnpm 版本降至 8.3.1 2023-05-05 00:26:23 +08:00
whyour e1863407f2 修复 npm 源判断 2023-05-04 22:23:55 +08:00
whyour 0c28b63ab7 修改容器启动流程 2023-05-04 22:07:39 +08:00
whyour 8b8bd279c6 修复 nginx 转发stream接口失效,停止运行命令支持 pid 参数 2023-05-04 21:28:14 +08:00
whyour 01b404765e 更新版本 v2.15.13 2023-05-04 17:13:18 +08:00
whyour c231a871d7 修复 check 异常 2023-05-04 17:13:04 +08:00
whyour 598c6b4e57 修复 task 打印日志逻辑 2023-05-04 15:17:38 +08:00
whyour 2295fabcc5 修复 task 命令日志目录判断 2023-05-04 13:11:18 +08:00
whyour be94d796ac 增加 procps 依赖 2023-05-04 13:05:48 +08:00
whyour c84908d7fa 修复运行命令api任务日志路径 2023-05-04 10:25:04 +08:00
whyour d8ae039b92 修复内容区头部样式 2023-05-03 21:23:13 +08:00
whyour bce1431d03 修复错误界面重试次数判断 2023-05-03 19:59:06 +08:00
whyour 9db0095e29 修复系统通知错误提示,gotifyPriority 配置参数 2023-05-03 18:18:28 +08:00
whyour 77dc7817fb 修复 pnpm 8.4.0 重新安装依赖会默认进行询问 2023-05-03 01:58:41 +08:00
whyour d0dd97631e 修复 task 命令任务 ID 查询参数 2023-05-03 01:27:14 +08:00
whyour d11d6d0c18 增加容器健康检查 2023-05-02 22:11:50 +08:00
whyour 0af687f781 增加停止指定命令接口 2023-05-01 22:56:47 +08:00
whyour 8db997abe8 修改 check 逻辑 2023-05-01 16:16:47 +08:00
whyour aab6bbeb15 增加运行指定命令接口 2023-05-01 14:28:58 +08:00
whyour df1addc1ff 修改 task 命令超时时间判断 2023-05-01 11:05:36 +08:00
whyour 1f8f35476a 移除任务默认超时时间 2023-04-30 20:02:14 +08:00
whyour 5354fc76db 修复 task 命令任务ID查询,系统依赖安装逻辑 2023-04-30 19:35:14 +08:00
whyour b29a9e012c 修复停止任务无效 2023-04-29 22:49:50 +08:00
whyour 6e5d89c197 修改 task 命令生成日志逻辑和关联任务查询 2023-04-28 23:15:07 +08:00
whyour d6cfb18f06 修改仓库拉取状态判断逻辑 2023-04-24 15:37:58 +08:00
whyour 9a3e38051d 克隆仓库增加 depth 2023-04-24 14:47:17 +08:00
whyour b705ad6ee8 修复更新任务、环境变量、依赖、订阅状态丢失 2023-04-21 21:15:28 +08:00
whyour db564d1add 更新版本 v2.15.12 2023-04-18 15:38:06 +08:00
whyour 4f422c0658 修改 dockerfile LANG 环境变量 2023-04-18 15:37:43 +08:00
whyour f267720af4 修复初始化定时服务运行非禁用任务 2023-04-17 13:36:37 +08:00
whyour 4a0c66bcc0 修复初始化 ssh 配置文件不存在 2023-04-15 12:14:56 +08:00
whyour ef7283a9fd 修改依赖安装初始状态 2023-04-12 00:45:06 +08:00
whyour 4498449eea 修复容器启动逻辑 2023-04-11 14:15:34 +08:00
whyour ebb9676a51 修复定时服务启动路径 2023-04-08 21:58:34 +08:00
whyour 6fb39ce835 重构六位定时任务服务 2023-04-08 17:07:06 +08:00
whyour 59c26d90d3 修改手机端网页样式 2023-04-06 21:49:42 +08:00
whyour 1d2df860e8 修复 ssh 配置文件目录 2023-04-06 14:23:02 +08:00
whyour ce22cad5b2 修改列表页更多菜单弹窗 2023-04-06 14:08:08 +08:00
whyour dc44ce6b1a 修复 workflow pnpm 版本 2023-04-06 13:41:18 +08:00
whyour b27ee23cc3 重构私有仓库ssh配置逻辑 2023-04-06 13:38:55 +08:00
whyour 083c8869aa 修复更新环境变量、定时任务、订阅等操作 2023-04-06 13:00:28 +08:00
whyour 6a971a0d6e 更新 readme 2023-04-02 14:49:30 +08:00
whyour a25bfb6912 修复定时任务状态筛选 2023-04-02 14:42:18 +08:00
whyour f19dd21155 更新 readme 2023-04-01 21:05:21 +08:00
118 changed files with 9027 additions and 7135 deletions
+6 -5
View File
@@ -45,7 +45,7 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 7
version: '8.3.1'
- uses: actions/setup-node@v3
with:
@@ -85,8 +85,6 @@ jobs:
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build:
if: github.ref != 'refs/heads/master'
needs: build-static
runs-on: ubuntu-latest
@@ -99,7 +97,7 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 7
version: '8.3.1'
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
@@ -131,11 +129,14 @@ jobs:
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: |
type=schedule,pattern=nightly
type=edge
type=ref,event=branch
type=ref,event=pr
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
+1
View File
@@ -29,3 +29,4 @@
/manual_log
/scripts
/bak
/.tmp
+7 -3
View File
@@ -1,25 +1,28 @@
import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({
hash: true,
antd: {},
outputPath: 'static/dist',
fastRefresh: true,
favicons: ['./images/favicon.svg'],
favicons: [`https://qn.whyour.cn/favicon.svg`],
mfsu: {
strategy: 'eager',
},
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: {
'/api/public': {
[`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
},
'/api': {
[`${baseUrl}api`]: {
target: 'http://127.0.0.1:5600/',
changeOrigin: true,
ws: true,
pathRewrite: { [`^${baseUrl}api`]: '/api' },
},
},
chainWebpack: ((config: any) => {
@@ -37,6 +40,7 @@ export default defineConfig({
'react-dom': 'window.ReactDOM',
},
headScripts: [
`./api/env.js`,
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
+75 -98
View File
@@ -1,30 +1,29 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
Timed task management panel with python3, javaScript, shell, typescript support
<h1 align="center">Qinglong</h1>
[![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]
[简体中文](./README.md) | English
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[![npm version][npm-version-image]][npm-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]
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
[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
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
[简体中文](./README.md) | English
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
## Features
@@ -36,64 +35,30 @@ Timed task management panel with python3, javaScript, shell, typescript support
- Support dark mode
- Support cell phone operation
## Version
### docker
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
```bash
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### npm
The npm version supports `debian/ubuntu/centos/alpine` systems and requires `node/python3` to be installed.
```bash
npm i @whyour/qinglong
```
## Deployment
### Local Deployment
```bash
# To be refined, see the development steps first (not supported on windows yet)
```
### Podman Deployment
1. podman installation
```bash
https://podman.io/getting-started/installation
```
2. start the container
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### Docker Deployment
1. docker installation
```bash
sudo curl -sSL get.docker.com | sh
```
2. configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. start the container
### Docker (Recommended)
```bash
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
@@ -103,17 +68,10 @@ docker run -dit \
whyour/qinglong:latest
```
### Docker-compose Deployment
1. docker-compose installation
```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. start the container
### Docker-compose (Recommended)
```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -123,9 +81,32 @@ docker-compose up -d
docker-compose down
```
3. access
### Podman (Recommended)
Open your browser and visit http://{ip}:5700
```bash
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### Local
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
```bash
npm install -g @whyour/qinglong
qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
export QL_DIR=""
export QL_DATA_DIR=""
# Run again
qinglong
```
## Use
@@ -181,6 +162,20 @@ task -l <file_path>
* account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Development
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Links
- [nevinee](https://gitee.com/evine)
@@ -191,24 +186,6 @@ task -l <file_path>
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Development
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Communication
[telegram channel](https://t.me/jiao_long)
## Name Origin
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
+79 -97
View File
@@ -1,30 +1,31 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<h1 align="center">青龙</h1>
<div align="center">
简体中文 | [English](./README-en.md)
支持python3、javaScript、shell、typescript 的定时任务管理面板
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
[![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]
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[![npm version][npm-version-image]][npm-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]
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
[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
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
简体中文 | [English](./README-en.md)
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
## 功能
@@ -36,85 +37,45 @@
- 支持暗黑模式
- 支持手机端操作
## 版本
### docker
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
```bash
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### npm
npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node/python3`
```bash
npm i @whyour/qinglong
```
## 部署
### 本机部署
```bash
# 待完善,可先参考开发步骤 (windows暂时不支持)
```
### podman 部署
1. podman 安装
```bash
https://podman.io/getting-started/installation
```
2. 启动容器
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 配置国内镜像源
Configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. 启动容器
### docker (推荐)
```bash
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--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. 启动容器
### docker-compose (推荐)
```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -124,9 +85,34 @@ docker-compose up -d
docker-compose down
```
3. 访问
### podman (推荐)
打开你的浏览器,访问 http://{ip}:5700
```bash
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### 本机
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
```bash
npm install -g @whyour/qinglong
qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
export QL_DIR=""
export QL_DATA_DIR=""
# 再次执行
qinglong
```
## 使用
@@ -181,6 +167,20 @@ task -l <file_path>
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 开发
```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@8.3.1
$ pnpm install
$ pnpm start
```
打开你的浏览器,访问 http://127.0.0.1:5700
## 链接
- [nevinee](https://gitee.com/evine)
@@ -191,24 +191,6 @@ task -l <file_path>
- [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)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
+4 -3
View File
@@ -5,6 +5,7 @@ import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
const route = Router();
export default (app: Router) => {
@@ -41,11 +42,11 @@ export default (app: Router) => {
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
`${config.samplePath}${req.params.file}`,
join(config.samplePath, req.params.file),
);
} else {
content = getFileContentByName(
`${config.configPath}${req.params.file}`,
join(config.configPath, req.params.file),
);
}
res.send({ code: 200, data: content });
@@ -70,7 +71,7 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
const path = `${config.configPath}${name}`;
const path = join(config.configPath, name);
fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (e) {
+3 -3
View File
@@ -94,7 +94,7 @@ export default (app: Router) => {
path += '/';
}
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
path = join(config.scriptPath, path);
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
@@ -124,7 +124,7 @@ export default (app: Router) => {
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
join(config.bakPath, originFilename.replace(/\//g, '')),
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
@@ -207,7 +207,7 @@ export default (app: Router) => {
let { filename } = req.body as {
filename: string;
};
const filePath = `${config.scriptPath}${filename}`;
const filePath = join(config.scriptPath, filename);
// const stats = fs.statSync(filePath);
// res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
+131 -28
View File
@@ -7,10 +7,25 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
import { parseVersion, promiseExec } from '../config/util';
import {
getUniqPath,
handleLogPath,
parseVersion,
promiseExec,
} from '../config/util';
import dayjs from 'dayjs';
import multer from 'multer';
const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.tmpPath);
},
filename: function (req, file, cb) {
cb(null, 'data.tgz');
},
});
const upload = multer({ storage: storage });
export default (app: Router) => {
app.use('/system', route);
@@ -20,30 +35,15 @@ export default (app: Router) => {
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
const { version, changeLog, changeLogLink } = await parseVersion(
const { version, changeLog, changeLogLink, publishTime } = await parseVersion(
config.versionFile,
);
const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
)
).replace('\n', '');
const lastCommitId = (
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
).replace('\n', '');
const branch = (
await promiseExec(
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
)
).replace('\n', '');
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
authInfo.password === 'admin'
) {
isInitialized = false;
}
@@ -52,9 +52,8 @@ export default (app: Router) => {
data: {
isInitialized,
version,
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
publishTime: dayjs(publishTime).unix(),
branch: process.env.QL_BRANCH || 'master',
changeLog,
changeLogLink,
},
@@ -66,12 +65,12 @@ export default (app: Router) => {
});
route.get(
'/log/remove',
'/config',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
const data = await systemService.getSystemConfig();
res.send({ code: 200, data });
} catch (e) {
return next(e);
@@ -80,19 +79,18 @@ export default (app: Router) => {
);
route.put(
'/log/remove',
'/config',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
logRemoveFrequency: Joi.number().optional().allow(null),
cronConcurrency: Joi.number().optional().allow(null),
}),
}),
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,
);
const result = await systemService.updateSystemConfig(req.body);
res.send(result);
} catch (e) {
return next(e);
@@ -128,6 +126,25 @@ export default (app: Router) => {
},
);
route.put(
'/reload',
celebrate({
body: Joi.object({
type: 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.reloadSystem(req.body.type);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/notify',
celebrate({
@@ -147,4 +164,90 @@ export default (app: Router) => {
}
},
);
route.put(
'/command-run',
celebrate({
body: Joi.object({
command: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const uniqPath = await getUniqPath(req.body.command);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logPath = `${uniqPath}/${logTime}.log`;
res.setHeader('Content-type', 'application/octet-stream');
await systemService.run(
{ ...req.body, logPath },
{
onStart: async (cp, startTime) => {
res.setHeader('QL-Task-Pid', `${cp.pid}`);
},
onEnd: async (cp, endTime, diff) => {
res.end();
},
onError: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
},
},
);
} catch (e) {
return next(e);
}
},
);
route.put(
'/command-stop',
celebrate({
body: Joi.object({
command: Joi.string().optional(),
pid: Joi.number().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.stop(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/data/export',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.exportData(res);
} catch (e) {
return next(e);
}
},
);
route.put(
'/data/import',
upload.single('data'),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.importData();
res.send(result);
} catch (e) {
return next(e);
}
},
);
};
+5
View File
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import path from 'path';
import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit';
import config from '../config';
const route = Router();
@@ -26,6 +27,10 @@ export default (app: Router) => {
route.post(
'/login',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}),
celebrate({
body: Joi.object({
username: Joi.string().required(),
+1
View File
@@ -18,6 +18,7 @@ async function startServer() {
const server = app
.listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
+12
View File
@@ -20,6 +20,7 @@ const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const dataPath = path.join(rootPath, 'data/');
const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
@@ -27,6 +28,7 @@ const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/');
const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh');
@@ -41,6 +43,7 @@ const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'version.yaml');
const dataTgzFile = path.join(tmpPath, 'data.tgz');
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
@@ -58,6 +61,9 @@ export default {
prefix: '/api',
},
rootPath,
tmpPath,
dataPath,
dataTgzFile,
configString,
loginFaild,
authError,
@@ -91,8 +97,14 @@ export default {
'/api/system',
'/api/user/init',
'/api/user/notification/init',
'/open/user/login',
'/open/user/two-factor/login',
'/open/system',
'/open/user/init',
'/open/user/notification/init',
],
versionFile,
lastVersionFile,
sqliteFile,
sshdPath,
};
+20
View File
@@ -0,0 +1,20 @@
import { Request, Response } from 'express';
import { pick } from 'lodash';
let pickedEnv: Record<string, string>;
function getPickedEnv() {
if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
pickedEnv = picked as Record<string, string>;
return picked;
}
export function serveEnv(_req: Request, res: Response) {
res.type('.js');
res.send(
Object.entries(getPickedEnv())
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
.join('\n'),
);
}
+74 -7
View File
@@ -7,6 +7,8 @@ import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
import config from './index';
import { TASK_COMMAND } from './const';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
@@ -245,6 +247,18 @@ export async function createFile(file: string, data: string = '') {
});
}
export async function handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
@@ -385,6 +399,18 @@ export function promiseExec(command: string): Promise<string> {
});
}
export function promiseExecSuccess(command: string): Promise<string> {
return new Promise((resolve) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || '');
},
);
});
}
export function parseHeaders(headers: string) {
if (!headers) return {};
@@ -469,25 +495,29 @@ export function psTree(pid: number): Promise<number[]> {
export async function killTask(pid: number) {
const pids = await psTree(pid);
// SIGALRM 14 时钟信号
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
if (pids.length) {
process.kill(pids[0], 14);
try {
[pid, ...pids].forEach((x) => {
process.kill(x, 2);
});
} catch (error) {}
} else {
process.kill(pid, 14);
process.kill(pid, 2);
}
}
export async function getPid(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
let pid = (await execAsync(taskCommand)).stdout;
return Number(pid);
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
const pid = await promiseExec(taskCommand);
return pid ? Number(pid) : undefined;
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
publishTime: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
@@ -497,3 +527,40 @@ export async function parseVersion(path: string): Promise<IVersion> {
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
export async function getUniqPath(command: string): Promise<string> {
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
let id = await promiseExec(idStr);
if (/^\d\d*\d$/.test(id)) {
id = `_${id}`;
} else {
id = '';
}
const items = command.split(/ +/);
let str = items[0];
if (items[0] === TASK_COMMAND) {
str = items[1];
}
const dotIndex = str.lastIndexOf('.');
if (dotIndex !== -1) {
str = str.slice(0, dotIndex);
}
const slashIndex = str.lastIndexOf('/');
let tempStr = '';
if (slashIndex !== -1) {
tempStr = str.slice(0, slashIndex);
const _slashIndex = tempStr.lastIndexOf('/');
if (_slashIndex !== -1) {
tempStr = tempStr.slice(_slashIndex + 1);
}
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
}
return `${str}${id}`;
}
+19 -2
View File
@@ -1,10 +1,11 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { NotificationInfo } from './notify';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: any;
info?: AuthModelInfo;
id?: number;
constructor(options: AuthInfo) {
@@ -25,9 +26,25 @@ export enum AuthDataType {
'authToken' = 'authToken',
'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig',
}
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
export interface SystemConfigInfo {
logRemoveFrequency?: number;
cronConcurrency?: number;
}
export interface LoginLogInfo {
timestamp?: number;
address?: string;
ip?: string;
platform?: string;
status?: LoginStatus,
}
export type AuthModelInfo = SystemConfigInfo & Partial<NotificationInfo> & LoginLogInfo;
export interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo { }
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
+5 -5
View File
@@ -26,7 +26,7 @@ export class Crontab {
this.saved = options.saved;
this.id = options.id;
this.status =
options.status && CrontabStatus[options.status]
typeof options.status === 'number' && CrontabStatus[options.status]
? options.status
: CrontabStatus.idle;
this.timestamp = new Date().toString();
@@ -43,13 +43,13 @@ export class Crontab {
}
export enum CrontabStatus {
'running',
'idle',
'running' = 0,
'queued' = 0.5,
'idle' = 1,
'disabled',
'queued',
}
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: {
unique: 'compositeIndex',
+1 -1
View File
@@ -39,7 +39,7 @@ export class CrontabView {
}
}
interface CronViewInstance
export interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
+25 -9
View File
@@ -4,18 +4,21 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence {
timestamp?: string;
id?: number;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
status: DependenceStatus;
type: DependenceTypes;
name: string;
log?: string[];
remark?: string;
constructor(options: Dependence) {
this.id = options.id;
this.status = options.status || DependenceStatus.installing;
this.status =
typeof options.status === 'number' && DependenceStatus[options.status]
? options.status
: DependenceStatus.queued;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
this.name = options.name;
this.name = options.name.trim();
this.log = options.log || [];
this.remark = options.remark || '';
}
@@ -28,6 +31,7 @@ export enum DependenceStatus {
'removing',
'removed',
'removeFailed',
'queued',
}
export enum DependenceTypes {
@@ -38,19 +42,31 @@ export enum DependenceTypes {
export enum InstallDependenceCommandTypes {
'pnpm add -g',
'pip3 install',
'pip3 install --disable-pip-version-check --root-user-action=ignore',
'apk add',
}
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 list --disable-pip-version-check',
'apk info',
}
export enum versionDependenceCommandTypes {
'@',
'==',
'=',
}
export enum unInstallDependenceCommandTypes {
'pnpm remove -g',
'pip3 uninstall -y',
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
'apk del',
}
interface DependenceInstance
export interface DependenceInstance
extends Model<Dependence, Dependence>,
Dependence {}
Dependence { }
export const DependenceModel = sequelize.define<DependenceInstance>(
'Dependence',
{
+5 -2
View File
@@ -13,7 +13,10 @@ export class Env {
constructor(options: Env) {
this.value = options.value;
this.id = options.id;
this.status = options.status || EnvStatus.normal;
this.status =
typeof options.status === 'number' && EnvStatus[options.status]
? options.status
: EnvStatus.normal;
this.timestamp = new Date().toString();
this.position = options.position;
this.name = options.name;
@@ -31,7 +34,7 @@ export const initPosition = 4500000000000000;
export const stepPosition = 10000000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {}
export interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING,
+2 -1
View File
@@ -1,9 +1,10 @@
import { Sequelize, Transaction } from 'sequelize';
import config from '../config/index';
import { join } from 'path';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`,
storage: join(config.dbPath, 'database.sqlite'),
logging: false,
retry: {
max: 10,
+2
View File
@@ -72,10 +72,12 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = '';
public weWorkOrigin = '';
}
export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
public weWorkOrigin = '';
}
export class AibotkNotification extends NotificationBaseInfo {
+1 -8
View File
@@ -26,14 +26,7 @@ export interface AppToken {
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {}
export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON,
+2 -1
View File
@@ -16,4 +16,5 @@ export type SockMessageType =
| 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd';
| 'runSubscriptionEnd'
| 'reloadSystem';
+3 -3
View File
@@ -37,8 +37,8 @@ export class Subscription {
this.name = options.name || options.alias;
this.type = options.type;
this.schedule = options.schedule;
this.status =
options.status && SubscriptionStatus[options.status]
this.status = this.status =
typeof options.status === 'number' && SubscriptionStatus[options.status]
? options.status
: SubscriptionStatus.idle;
this.url = options.url;
@@ -70,7 +70,7 @@ export enum SubscriptionStatus {
'queued',
}
interface SubscriptionInstance
export interface SubscriptionInstance
extends Model<Subscription, Subscription>,
Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
+13 -25
View File
@@ -10,26 +10,27 @@ 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';
import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
next();
} else {
return handler(req, res, {
public: 'static/dist',
rewrites: [{ source: '**', destination: '/index.html' }],
});
}
});
app.use(
'/api/public',
createProxyMiddleware({
target: `http://localhost:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
}),
);
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -108,8 +109,7 @@ export default ({ app }: { app: Application }) => {
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
authInfo.password === 'admin'
) {
isInitialized = false;
}
@@ -170,18 +170,6 @@ export default ({ app }: { app: Application }) => {
},
);
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use(
(
err: Error & { status: number },
+2 -2
View File
@@ -32,7 +32,7 @@ export default async () => {
// 初始化更新所有任务状态为空闲
await CrontabModel.update(
{ status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
{ where: {} },
);
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
@@ -42,7 +42,7 @@ export default async () => {
raw: true,
}).then(async (docs) => {
await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
dependenceService.installDependenceOneByOne(docs);
+48 -8
View File
@@ -13,12 +13,21 @@ const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/');
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');
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
const TaskAfterFile = path.join(configPath, 'task_after.sh');
const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
@@ -29,19 +38,17 @@ export default async () => {
const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath);
const sshdDirExist = await fileExist(sshdPath);
const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
const TaskAfterFileExist = await fileExist(TaskAfterFile);
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);
}
@@ -50,6 +57,10 @@ export default async () => {
fs.mkdirSync(logPath);
}
if (!tmpDirExist) {
fs.mkdirSync(tmpPath);
}
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
}
@@ -62,6 +73,35 @@ export default async () => {
fs.mkdirSync(bakPath);
}
if (!sshdDirExist) {
fs.mkdirSync(sshdPath);
}
// 初始化文件
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptNotifyJsFileExist) {
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
}
if (!scriptNotifyPyFileExist) {
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
}
if (!TaskBeforeFileExist) {
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
}
if (!TaskAfterFileExist) {
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
+9 -7
View File
@@ -4,6 +4,7 @@ import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
import { join } from 'path';
export default async () => {
const systemService = Container.get(SystemService);
@@ -11,8 +12,9 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `tsx ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}static/build/token.js`;
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
const tokenFile = join(config.rootPath, 'static/build/token.js');
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
@@ -27,16 +29,16 @@ export default async () => {
});
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const data = await systemService.getSystemConfig();
if (data && data.info && data.info.logRemoveFrequency) {
const rmlogCron = {
id: data.id,
id: data.id as number,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
command: `ql rmlog ${data.info.logRemoveFrequency}`,
};
await scheduleService.cancelIntervalTask(rmlogCron);
scheduleService.createIntervalTask(rmlogCron, {
days: data.info.frequency,
days: data.info.logRemoveFrequency,
});
}
+7 -1
View File
@@ -10,12 +10,18 @@ export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
Sentry.init({
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 0.1,
tracesSampleRate: 0.8,
release: version,
});
+26 -4
View File
@@ -5,12 +5,34 @@ import Sock from './sock';
export default async ({ server }: { server: Server }) => {
await Sock({ server });
Logger.info('✌️ Sock loaded');
let exitTime = 0;
let timer: NodeJS.Timeout;
process.on('SIGINT', () => {
Logger.info('✌️ Server need close');
process.on('SIGINT', (singal) => {
Logger.warn(`Server need close, singal ${singal}`);
exitTime++;
if (exitTime >= 3) {
Logger.warn('Forcing server close');
clearTimeout(timer);
process.exit(1);
}
server.close(() => {
Logger.info('✌️ Server closed');
process.exit(0);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
process.exit();
}, 15000);
});
});
process.on('uncaughtException', (error) => {
Logger.error('Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled rejection:', reason);
process.exit(1);
});
};
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package com.ql.cron;
service Cron {
rpc addCron(AddCronRequest) returns (AddCronResponse);
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
}
message ICron {
string id = 1;
string schedule = 2;
string command = 3;
}
message AddCronRequest { repeated ICron crons = 1; }
message AddCronResponse {}
message DeleteCronRequest { repeated string ids = 1; }
message DeleteCronResponse {}
+482
View File
@@ -0,0 +1,482 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientUnaryCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
export const protobufPackage = 'com.ql.cron';
export interface ICron {
id: string;
schedule: string;
command: string;
}
export interface AddCronRequest {
crons: ICron[];
}
export interface AddCronResponse {}
export interface DeleteCronRequest {
ids: string[];
}
export interface DeleteCronResponse {}
function createBaseICron(): ICron {
return { id: '', schedule: '', command: '' };
}
export const ICron = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== '') {
writer.uint32(10).string(message.id);
}
if (message.schedule !== '') {
writer.uint32(18).string(message.schedule);
}
if (message.command !== '') {
writer.uint32(26).string(message.command);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.id = reader.string();
continue;
case 2:
if (tag != 18) {
break;
}
message.schedule = reader.string();
continue;
case 3:
if (tag != 26) {
break;
}
message.command = reader.string();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): ICron {
return {
id: isSet(object.id) ? String(object.id) : '',
schedule: isSet(object.schedule) ? String(object.schedule) : '',
command: isSet(object.command) ? String(object.command) : '',
};
},
toJSON(message: ICron): unknown {
const obj: any = {};
message.id !== undefined && (obj.id = message.id);
message.schedule !== undefined && (obj.schedule = message.schedule);
message.command !== undefined && (obj.command = message.command);
return obj;
},
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
return ICron.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron();
message.id = object.id ?? '';
message.schedule = object.schedule ?? '';
message.command = object.command ?? '';
return message;
},
};
function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] };
}
export const AddCronRequest = {
encode(
message: AddCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.crons.push(ICron.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): AddCronRequest {
return {
crons: Array.isArray(object?.crons)
? object.crons.map((e: any) => ICron.fromJSON(e))
: [],
};
},
toJSON(message: AddCronRequest): unknown {
const obj: any = {};
if (message.crons) {
obj.crons = message.crons.map((e) => (e ? ICron.toJSON(e) : undefined));
} else {
obj.crons = [];
}
return obj;
},
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
base?: I,
): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
object: I,
): AddCronRequest {
const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
return message;
},
};
function createBaseAddCronResponse(): AddCronResponse {
return {};
}
export const AddCronResponse = {
encode(
_: AddCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): AddCronResponse {
return {};
},
toJSON(_: AddCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
base?: I,
): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
_: I,
): AddCronResponse {
const message = createBaseAddCronResponse();
return message;
},
};
function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] };
}
export const DeleteCronRequest = {
encode(
message: DeleteCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.ids) {
writer.uint32(10).string(v!);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.ids.push(reader.string());
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): DeleteCronRequest {
return {
ids: Array.isArray(object?.ids)
? object.ids.map((e: any) => String(e))
: [],
};
},
toJSON(message: DeleteCronRequest): unknown {
const obj: any = {};
if (message.ids) {
obj.ids = message.ids.map((e) => e);
} else {
obj.ids = [];
}
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
base?: I,
): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
object: I,
): DeleteCronRequest {
const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || [];
return message;
},
};
function createBaseDeleteCronResponse(): DeleteCronResponse {
return {};
}
export const DeleteCronResponse = {
encode(
_: DeleteCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): DeleteCronResponse {
return {};
},
toJSON(_: DeleteCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
base?: I,
): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
_: I,
): DeleteCronResponse {
const message = createBaseDeleteCronResponse();
return message;
},
};
export type CronService = typeof CronService;
export const CronService = {
addCron: {
path: '/com.ql.cron.Cron/addCron',
requestStream: false,
responseStream: false,
requestSerialize: (value: AddCronRequest) =>
Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) =>
Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
},
delCron: {
path: '/com.ql.cron.Cron/delCron',
requestStream: false,
responseStream: false,
requestSerialize: (value: DeleteCronRequest) =>
Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) =>
Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
},
} as const;
export interface CronServer extends UntypedServiceImplementation {
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
}
export interface CronClient extends Client {
addCron(
request: AddCronRequest,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
}
export const CronClient = makeGenericClientConstructor(
CronService,
'com.ql.cron.Cron',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): CronClient;
service: typeof CronService;
};
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package com.ql.health;
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
+317
View File
@@ -0,0 +1,317 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientReadableStream,
ClientUnaryCall,
handleServerStreamingCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
export const protobufPackage = 'com.ql.health';
export interface HealthCheckRequest {
service: string;
}
export interface HealthCheckResponse {
status: HealthCheckResponse_ServingStatus;
}
export enum HealthCheckResponse_ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
SERVICE_UNKNOWN = 3,
UNRECOGNIZED = -1,
}
export function healthCheckResponse_ServingStatusFromJSON(
object: any,
): HealthCheckResponse_ServingStatus {
switch (object) {
case 0:
case 'UNKNOWN':
return HealthCheckResponse_ServingStatus.UNKNOWN;
case 1:
case 'SERVING':
return HealthCheckResponse_ServingStatus.SERVING;
case 2:
case 'NOT_SERVING':
return HealthCheckResponse_ServingStatus.NOT_SERVING;
case 3:
case 'SERVICE_UNKNOWN':
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
case -1:
case 'UNRECOGNIZED':
default:
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
}
}
export function healthCheckResponse_ServingStatusToJSON(
object: HealthCheckResponse_ServingStatus,
): string {
switch (object) {
case HealthCheckResponse_ServingStatus.UNKNOWN:
return 'UNKNOWN';
case HealthCheckResponse_ServingStatus.SERVING:
return 'SERVING';
case HealthCheckResponse_ServingStatus.NOT_SERVING:
return 'NOT_SERVING';
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
return 'SERVICE_UNKNOWN';
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
default:
return 'UNRECOGNIZED';
}
}
function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: '' };
}
export const HealthCheckRequest = {
encode(
message: HealthCheckRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.service !== '') {
writer.uint32(10).string(message.service);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.service = reader.string();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? String(object.service) : '' };
},
toJSON(message: HealthCheckRequest): unknown {
const obj: any = {};
message.service !== undefined && (obj.service = message.service);
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
base?: I,
): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
object: I,
): HealthCheckRequest {
const message = createBaseHealthCheckRequest();
message.service = object.service ?? '';
return message;
},
};
function createBaseHealthCheckResponse(): HealthCheckResponse {
return { status: 0 };
}
export const HealthCheckResponse = {
encode(
message: HealthCheckResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.status !== 0) {
writer.uint32(8).int32(message.status);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 8) {
break;
}
message.status = reader.int32() as any;
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HealthCheckResponse {
return {
status: isSet(object.status)
? healthCheckResponse_ServingStatusFromJSON(object.status)
: 0,
};
},
toJSON(message: HealthCheckResponse): unknown {
const obj: any = {};
message.status !== undefined &&
(obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
base?: I,
): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
object: I,
): HealthCheckResponse {
const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0;
return message;
},
};
export type HealthService = typeof HealthService;
export const HealthService = {
check: {
path: '/com.ql.health.Health/Check',
requestStream: false,
responseStream: false,
requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
watch: {
path: '/com.ql.health.Health/Watch',
requestStream: false,
responseStream: true,
requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
} as const;
export interface HealthServer extends UntypedServiceImplementation {
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
}
export interface HealthClient extends Client {
check(
request: HealthCheckRequest,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall;
watch(
request: HealthCheckRequest,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
watch(
request: HealthCheckRequest,
metadata?: Metadata,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
}
export const HealthClient = makeGenericClientConstructor(
HealthService,
'com.ql.health.Health',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): HealthClient;
service: typeof HealthService;
};
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
+12 -6
View File
@@ -1,16 +1,21 @@
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import config from './config';
import { HealthClient } from './protos/health';
import { credentials } from '@grpc/grpc-js';
const app = express();
const client = new HealthClient(
`localhost:${config.cronPort}`,
credentials.createInsecure(),
);
app.get('/api/public/panel/log', (req, res) => {
exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr });
app.get('/api/health', (req, res) => {
client.check({ service: 'cron' }, (err, response) => {
if (err) {
return res.status(500).send({ code: 500, error: err });
}
return res.send({ code: 200, data: stdout });
return res.status(200).send({ code: 200, data: response });
});
});
@@ -20,6 +25,7 @@ app
await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
-56
View File
@@ -1,56 +0,0 @@
import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
import { QL_PREFIX, TASK_PREFIX } from './config/const';
const app = express();
const run = async () => {
CrontabModel.findAll({ where: {} })
.then((docs) => {
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
const _schedule = task.schedule && task.schedule.split(/ +/);
if (
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled &&
task.schedule
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (
!command.startsWith(TASK_PREFIX) &&
!command.startsWith(QL_PREFIX)
) {
command = `${TASK_PREFIX}${command}`;
}
exec(`ID=${task.id} ${command}`);
});
}
}
}
})
.catch((err) => {
Logger.error(err);
process.exit(1);
});
};
app
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.debug('定时任务服务启动成功!');
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+27
View File
@@ -0,0 +1,27 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data';
import { runCron } from '../shared/runCron';
const addCron = (
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
callback: sendUnaryData<AddCronResponse>,
) => {
for (const item of call.request.crons) {
const { id, schedule, command } = item;
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
}
scheduleStacks.set(
id,
nodeSchedule.scheduleJob(id, schedule, async () => {
runCron(`ID=${id} ${command}`)
}),
);
}
callback(null, null);
};
export { addCron };
+40
View File
@@ -0,0 +1,40 @@
import { credentials } from '@grpc/grpc-js';
import {
AddCronRequest,
AddCronResponse,
CronClient,
DeleteCronRequest,
DeleteCronResponse,
} from '../protos/cron';
import config from '../config';
class Client {
private client = new CronClient(
`localhost:${config.cronPort}`,
credentials.createInsecure(),
);
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
return new Promise((resolve, reject) => {
this.client.addCron({ crons: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
return new Promise((resolve, reject) => {
this.client.delCron({ ids: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
}
export default new Client();
+6
View File
@@ -0,0 +1,6 @@
import nodeSchedule from 'node-schedule';
import { ToadScheduler } from 'toad-scheduler';
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
export const intervalSchedule = new ToadScheduler();
+19
View File
@@ -0,0 +1,19 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
import { scheduleStacks } from './data';
const delCron = (
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
callback: sendUnaryData<DeleteCronResponse>,
) => {
for (const id of call.request.ids) {
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
scheduleStacks.delete(id);
}
}
callback(null, null);
};
export { delCron };
+35
View File
@@ -0,0 +1,35 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
import config from '../config';
import { promiseExec } from '../config/util';
const check = async (
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
callback: sendUnaryData<HealthCheckResponse>,
) => {
switch (call.request.service) {
case 'cron':
const res = await promiseExec(
`curl -s http://localhost:${config.port}/api/system`,
);
if (res.includes('200')) {
return callback(null, { status: 1 });
}
const panelErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/panel-error.log`,
);
const scheduleErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
);
return callback(
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`),
);
default:
return callback(null, { status: 1 });
}
};
export { check };
+24
View File
@@ -0,0 +1,24 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronService } from '../protos/cron';
import { addCron } from './addCron';
import { delCron } from './delCron';
import { HealthService } from '../protos/health';
import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server();
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.bindAsync(
`localhost:${config.cronPort}`,
ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
throw err;
}
server.start();
Logger.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
+72 -24
View File
@@ -2,19 +2,21 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import { exec, execSync } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import {
getFileContentByName,
concurrentRun,
fileExist,
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { Op, where, col as colFn } from 'sequelize';
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
@Service()
export default class CronService {
@@ -32,6 +34,11 @@ export default class CronService {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
if (this.isSixCron(doc)) {
await cronClient.addCron([
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
]);
}
await this.set_crontab();
return doc;
}
@@ -41,9 +48,25 @@ export default class CronService {
}
public async update(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload);
const doc = await this.getDb({ id: payload.id });
const tab = new Crontab({ ...doc, ...payload });
tab.saved = false;
const newDoc = await this.updateDb(tab);
if (doc.isDisabled === 1) {
return newDoc;
}
if (this.isSixCron(doc)) {
await cronClient.delCron([String(newDoc.id)]);
}
if (this.isSixCron(newDoc)) {
await cronClient.addCron([
{
id: String(newDoc.id),
schedule: newDoc.schedule!,
command: newDoc.command,
},
]);
}
await this.set_crontab();
return newDoc;
}
@@ -83,6 +106,7 @@ export default class CronService {
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await cronClient.delCron(ids.map(String));
await this.set_crontab();
}
@@ -169,17 +193,13 @@ export default class CronService {
{
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
{ [operate]: `%${encodeURI(value)}%` },
],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
where(colFn(property), operate, `%${encodeURI(value)}%`),
],
},
],
@@ -206,7 +226,7 @@ export default class CronService {
q[column] = {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
],
};
break;
@@ -214,7 +234,7 @@ export default class CronService {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` },
],
};
q[Op.or] = [
@@ -246,8 +266,12 @@ export default class CronService {
for (const key of filterKeys) {
let q: any = {};
if (!filterQuery[key]) continue;
if (key === 'status' && filterQuery[key].includes(2)) {
if (key === 'status') {
if (filterQuery[key].includes(CrontabStatus.disabled)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
}
} else {
q[key] = filterQuery[key];
}
@@ -264,7 +288,11 @@ export default class CronService {
}
}
public async find({ log_path }: { log_path: string }): Promise<Crontab | null> {
public async find({
log_path,
}: {
log_path: string;
}): Promise<Crontab | null> {
try {
const result = await CrontabModel.findOne({ where: { log_path } });
return result;
@@ -324,7 +352,7 @@ export default class CronService {
}
}
public async getDb(query: any): Promise<Crontab> {
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
const doc: any = await CrontabModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Crontab);
}
@@ -334,10 +362,9 @@ export default class CronService {
{ status: CrontabStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
ids.forEach((id) => {
this.runSingle(id);
});
}
public async stop(ids: number[]) {
@@ -359,6 +386,7 @@ export default class CronService {
}
private async runSingle(cronId: number): Promise<number> {
return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId });
if (cron.status !== CrontabStatus.queued) {
@@ -418,15 +446,26 @@ export default class CronService {
resolve();
});
});
});
}
public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await cronClient.delCron(ids.map(String));
await this.set_crontab();
}
public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs
.filter((x) => this.isSixCron(x))
.map((doc) => ({
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
}
@@ -478,8 +517,8 @@ export default class CronService {
return crontab_job_string;
}
private async set_crontab() {
const tabs = await this.crontabs();
private async set_crontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs());
var crontab_string = '';
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
@@ -501,7 +540,6 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
exec(`pm2 reload schedule`);
await CrontabModel.update({ saved: true }, { where: {} });
}
@@ -539,7 +577,17 @@ export default class CronService {
});
}
public autosave_crontab() {
return this.set_crontab();
public async autosave_crontab() {
const tabs = await this.crontabs();
this.set_crontab(tabs);
const sixCron = tabs.data
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
.map((doc) => ({
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
}));
await cronClient.addCron(sixCron);
}
}
+5 -2
View File
@@ -7,6 +7,7 @@ import {
minPosition,
stepPosition,
} from '../data/env';
import { FindOptions } from 'sequelize';
@Service()
export default class CronViewService {
@@ -31,7 +32,9 @@ export default class CronViewService {
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(new CrontabView(payload));
const doc = await this.getDb({ id: payload.id })
const tab = new CrontabView({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
return newDoc;
}
@@ -56,7 +59,7 @@ export default class CronViewService {
}
}
public async getDb(query: any): Promise<CrontabView> {
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
+81 -36
View File
@@ -8,12 +8,15 @@ import {
DependenceTypes,
unInstallDependenceCommandTypes,
DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes,
} from '../data/dependence';
import { spawn } from 'child_process';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import { Op } from 'sequelize';
import { concurrentRun } from '../config/util';
import { FindOptions, Op } from 'sequelize';
import { promiseExecSuccess } from '../config/util';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
@Service()
export default class DependenceService {
@@ -24,7 +27,7 @@ export default class DependenceService {
public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
return tab;
});
const docs = await this.insert(tabs);
@@ -45,7 +48,7 @@ export default class DependenceService {
const tab = new Dependence({
...doc,
...other,
status: DependenceStatus.installing,
status: DependenceStatus.queued,
});
const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]);
@@ -59,7 +62,7 @@ export default class DependenceService {
public async remove(ids: number[], force = false): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
@@ -78,7 +81,7 @@ export default class DependenceService {
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) {
const encodeText = encodeURIComponent(searchValue);
const encodeText = encodeURI(searchValue);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchValue}%` },
@@ -104,18 +107,14 @@ export default class DependenceService {
isInstall: boolean = true,
force: boolean = false,
) {
concurrentRun(
docs.map(
(dep) => async () =>
await this.installOrUninstallDependencies([dep], isInstall, force),
),
1,
);
docs.forEach((dep) => {
this.installOrUninstallDependency(dep, isInstall, force);
});
}
public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
@@ -132,41 +131,53 @@ export default class DependenceService {
return docs;
}
public async getDb(query: any): Promise<Dependence> {
public async getDb(
query: FindOptions<Dependence>['where'],
): Promise<Dependence> {
const doc: any = await DependenceModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Dependence);
}
private async updateLog(ids: number[], log: string): Promise<void> {
const doc = await DependenceModel.findOne({ where: { id: ids } });
taskLimit.updateDepLog(async () => {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
await DependenceModel.update(
{ log: newLog },
{ where: { id: doc.id } },
);
}
return null;
});
}
public installOrUninstallDependencies(
dependencies: Dependence[],
public installOrUninstallDependency(
dependency: Dependence,
isInstall: boolean = true,
force: boolean = false,
) {
return taskLimit.runOneByOne(() => {
return new Promise(async (resolve) => {
if (dependencies.length === 0) {
resolve(null);
return;
}
const socketMessageType = !force
const depIds = [dependency.id!];
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: depIds } });
const socketMessageType = isInstall
? 'installDependence'
: 'uninstallDependence';
const depNames = dependencies.map((x) => x.name).join(' ');
const depName = dependency.name.trim();
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
)[dependency.type];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[];
const startTime = dayjs();
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n`;
this.sockService.sendMessage({
@@ -174,9 +185,43 @@ export default class DependenceService {
message,
references: depIds,
});
await this.updateLog(depIds, message);
this.updateLog(depIds, message);
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
// 判断是否已经安装过依赖
if (isInstall) {
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
const depVersionStr = versionDependenceCommandTypes[dependency.type];
const [_depName] = dependency.name.trim().split(depVersionStr);
const depInfo = (
await promiseExecSuccess(
dependency.type === DependenceTypes.linux
? `${getCommandPrefix} ${_depName}`
: `${getCommandPrefix} | grep "${_depName}"`,
)
).replace(/\s{2,}/, ' ');
if (depInfo) {
const endTime = dayjs();
const _message = `检测到已经安装 ${_depName}\n\n${depInfo}\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message: _message,
references: depIds,
});
this.updateLog(depIds, _message);
await DependenceModel.update(
{ status: DependenceStatus.installed },
{ where: { id: depIds } },
);
return resolve(null);
}
}
const cp = spawn(`${depRunCommand} ${depName}`, {
shell: '/bin/bash',
});
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
@@ -184,7 +229,7 @@ export default class DependenceService {
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
@@ -193,7 +238,7 @@ export default class DependenceService {
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
@@ -202,8 +247,7 @@ export default class DependenceService {
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', async (code) => {
@@ -219,7 +263,7 @@ export default class DependenceService {
message,
references: depIds,
});
await this.updateLog(depIds, message);
this.updateLog(depIds, message);
let status = null;
if (isSucceed) {
@@ -241,5 +285,6 @@ export default class DependenceService {
resolve(null);
});
});
});
}
}
+6 -5
View File
@@ -12,7 +12,7 @@ import {
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize';
import { FindOptions, Op } from 'sequelize';
@Service()
export default class EnvService {
@@ -49,7 +49,9 @@ export default class EnvService {
}
public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(new Env(payload));
const doc = await this.getDb({ id: payload.id });
const tab = new Env({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.set_envs();
return newDoc;
}
@@ -119,7 +121,7 @@ export default class EnvService {
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const encodeText = encodeURI(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
@@ -144,7 +146,6 @@ export default class EnvService {
}
try {
const result = await this.find(condition, [
['status', 'ASC'],
['position', 'DESC'],
['createdAt', 'ASC'],
]);
@@ -162,7 +163,7 @@ export default class EnvService {
return docs;
}
public async getDb(query: any): Promise<Env> {
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
const doc: any = await EnvModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Env);
}
+164 -29
View File
@@ -36,7 +36,7 @@ export default class NotificationService {
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 30000,
timeout: 10000,
retry: 1,
};
@@ -78,7 +78,8 @@ export default class NotificationService {
}
private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
try {
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
...this.gotOption,
@@ -92,11 +93,19 @@ export default class NotificationService {
},
})
.json();
return typeof res.id === 'number';
if (typeof res.id === 'number') {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
try {
const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
...this.gotOption,
@@ -104,7 +113,14 @@ export default class NotificationService {
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
.json();
return res.retcode === 0;
if (res.retcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async serverChan() {
@@ -112,6 +128,7 @@ export default class NotificationService {
const url = serverChanKey.startsWith('SCT')
? `https://sctapi.ftqq.com/${serverChanKey}.send`
: `https://sc.ftqq.com/${serverChanKey}.send`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -119,12 +136,20 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.errno === 0 || res.data.errno === 0;
if (res.errno === 0 || res.data.errno === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async pushDeer() {
const { pushDeerKey, pushDeerUrl } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -134,14 +159,23 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return (
res.content.result.length !== undefined && res.content.result.length > 0
);
if (
res.content.result.length !== undefined &&
res.content.result.length > 0
) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -149,12 +183,19 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
if (res.success) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`;
}
const url = `${barkPush}/${encodeURIComponent(
@@ -162,13 +203,22 @@ export default class NotificationService {
)}/${encodeURIComponent(
this.content,
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
try {
const res: any = await got
.get(url, {
...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.code === 200;
if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async telegramBot() {
@@ -200,6 +250,7 @@ export default class NotificationService {
https: httpsAgent,
};
}
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -208,7 +259,14 @@ export default class NotificationService {
agent,
})
.json();
return !!res.ok;
if (res.ok) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async dingtalkBot() {
@@ -222,6 +280,7 @@ export default class NotificationService {
secretParam = `&timestamp=${dateNow}&sign=${result}`;
}
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -233,12 +292,20 @@ export default class NotificationService {
},
})
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async weWorkBot() {
const { weWorkBotKey } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -250,14 +317,21 @@ export default class NotificationService {
},
})
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async weWorkApp() {
const { weWorkAppKey } = this.params;
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
const tokenRes: any = await got
.post(url, {
...this.gotOption,
@@ -306,9 +380,10 @@ export default class NotificationService {
break;
}
try {
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
...this.gotOption,
json: {
@@ -321,7 +396,14 @@ export default class NotificationService {
)
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async aibotk() {
@@ -353,6 +435,7 @@ export default class NotificationService {
break;
}
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -361,13 +444,20 @@ export default class NotificationService {
},
})
.json();
return res.code === 0;
if (res.code === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async iGot() {
const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -376,12 +466,20 @@ export default class NotificationService {
})
.json();
return res.ret === 0;
if (res.ret === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params;
const url = `https://www.pushplus.plus/send`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
@@ -394,13 +492,26 @@ export default class NotificationService {
})
.json();
return res.code === 200;
if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async lark() {
const { larkKey } = this.params;
let { larkKey } = this.params;
if (!larkKey.startsWith('http')) {
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
}
try {
const res: any = await got
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
.post(larkKey, {
...this.gotOption,
json: {
msg_type: 'text',
@@ -409,11 +520,20 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/json' },
})
.json();
return res.StatusCode === 0;
if (res.StatusCode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async email() {
const { emailPass, emailService, emailUser } = this.params;
try {
const transporter = nodemailer.createTransport({
service: emailService,
auth: {
@@ -431,7 +551,14 @@ export default class NotificationService {
transporter.close();
return !!info.messageId;
if (info.messageId) {
return true;
} else {
throw new Error(JSON.stringify(info));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async webhook() {
@@ -460,8 +587,16 @@ export default class NotificationService {
allowGetBody: true,
...bodyParam,
};
try {
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
if (String(res.statusCode).startsWith('20')) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private formatBody(contentType: string, body: any): object {
+1 -1
View File
@@ -75,7 +75,7 @@ export default class OpenService {
): Promise<App[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const encodeText = encodeURI(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
+12 -6
View File
@@ -1,7 +1,7 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { ChildProcessWithoutNullStreams } from 'child_process';
import {
ToadScheduler,
LongIntervalJob,
@@ -9,6 +9,8 @@ import {
Task,
} from 'toad-scheduler';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
interface ScheduleTaskType {
id: number;
@@ -40,13 +42,14 @@ export default class ScheduleService {
private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
async runTask(
command: string,
callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end',
) {
return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
@@ -54,7 +57,6 @@ export default class ScheduleService {
const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
@@ -107,6 +109,7 @@ export default class ScheduleService {
await callbacks.onError?.(JSON.stringify(error));
}
});
})
}
async createCronTask(
@@ -126,19 +129,22 @@ export default class ScheduleService {
this.scheduleStacks.set(
_id,
nodeSchedule.scheduleJob(_id, schedule, async () => {
await this.runTask(command, callbacks);
this.runTask(command, callbacks);
}),
);
if (runImmediately) {
await this.runTask(command, callbacks);
this.runTask(command, callbacks);
}
}
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
if (this.scheduleStacks.has(_id)) {
this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id);
}
}
async createIntervalTask(
+42 -44
View File
@@ -5,18 +5,38 @@ import os from 'os';
import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
import config from '../config';
@Service()
export default class SshKeyService {
private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
private sshPath = config.sshdPath;
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) {
this.initSshConfigFile();
}
private initSshConfigFile() {
let config = '';
if (existsSync(this.sshConfigFilePath)) {
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
} else {
fs.writeFileSync(this.sshConfigFilePath, '');
}
if (!config.includes(this.sshConfigHeader)) {
fs.writeFileSync(
this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
);
}
}
private generatePrivateKeyFile(alias: string, key: string): void {
try {
fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
encoding: 'utf8',
mode: '400',
});
@@ -25,56 +45,39 @@ export default class SshKeyService {
}
}
private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void {
try {
const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(`${this.sshPath}/${alias}`);
fs.unlinkSync(filePath);
}
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(
alias: string,
host: string,
proxy?: string,
): string {
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
return `Host ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
}
private generateSshConfig(configs: string[]) {
try {
fs.writeFileSync(this.sshConfigFilePath, configs.join('\n'), {
const proxyStr = proxy
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
: '';
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
this.sshPath,
alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`;
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
encoding: 'utf8',
});
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
}
private removeSshConfig(alias: string) {
try {
const configRegx = this.getConfigRegx(alias);
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(configRegx, '')
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
const filePath = path.join(this.sshPath, `${alias}.config`);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
@@ -87,32 +90,27 @@ export default class SshKeyService {
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
this.generateSingleSshConfig(alias, host, proxy);
}
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config);
this.removeSshConfig(alias);
}
public setSshConfig(docs: Subscription[]) {
let result = [];
for (const doc of docs) {
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
const { alias, proxy } = doc;
const { host } = formatUrl(doc);
this.removePrivateKeyFile(alias);
this.removeSshConfig(alias);
this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
const config = this.generateSingleSshConfig(alias, host, proxy);
result.push(config);
this.generateSingleSshConfig(alias, host, proxy);
}
}
this.generateSshConfig(result);
}
}
+22 -50
View File
@@ -6,12 +6,7 @@ import {
SubscriptionModel,
SubscriptionStatus,
} from '../data/subscription';
import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import { ChildProcessWithoutNullStreams } from 'child_process';
import fs from 'fs';
import {
getFileContentByName,
@@ -19,9 +14,11 @@ import {
fileExist,
createFile,
killTask,
handleLogPath,
promiseExec,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { Op } from 'sequelize';
import { FindOptions, Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
@@ -46,7 +43,7 @@ export default class SubscriptionService {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` },
],
};
query = {
@@ -109,30 +106,6 @@ export default class SubscriptionService {
this.sshKeyService.setSshConfig(docs);
}
private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
private async handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
return {
onBefore: async (startTime) => {
@@ -145,7 +118,7 @@ export default class SubscriptionService {
},
{ where: { id: doc.id } },
);
const absolutePath = await this.handleLogPath(
const absolutePath = await handleLogPath(
logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
);
@@ -155,7 +128,7 @@ export default class SubscriptionService {
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before);
beforeStr = await promiseExec(doc.sub_before);
}
} catch (error: any) {
beforeStr =
@@ -175,14 +148,14 @@ export default class SubscriptionService {
},
onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
const absolutePath = await handleLogPath(sub.log_path as string);
// 执行 sub_after
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after);
afterStr = await promiseExec(sub.sub_after);
}
} catch (error: any) {
afterStr =
@@ -212,12 +185,12 @@ export default class SubscriptionService {
},
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
};
@@ -236,7 +209,8 @@ export default class SubscriptionService {
}
public async update(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload);
const doc = await this.getDb({ id: payload.id });
const tab = new Subscription({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
@@ -288,7 +262,9 @@ export default class SubscriptionService {
await this.setSshConfig();
}
public async getDb(query: any): Promise<Subscription> {
public async getDb(
query: FindOptions<Subscription>['where'],
): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription);
}
@@ -298,10 +274,9 @@ export default class SubscriptionService {
{ status: SubscriptionStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
ids.forEach((id) => {
this.runSingle(id);
});
}
public async stop(ids: number[]) {
@@ -314,7 +289,7 @@ export default class SubscriptionService {
this.logger.silly(error);
}
}
const absolutePath = await this.handleLogPath(doc.log_path as string);
const absolutePath = await handleLogPath(doc.log_path as string);
fs.appendFileSync(
`${absolutePath}`,
@@ -338,10 +313,7 @@ export default class SubscriptionService {
const command = formatCommand(subscription);
await this.scheduleService.runTask(
command,
this.taskCallbacks(subscription),
);
this.scheduleService.runTask(command, this.taskCallbacks(subscription));
}
public async disabled(ids: number[]) {
@@ -368,7 +340,7 @@ export default class SubscriptionService {
return '';
}
const absolutePath = await this.handleLogPath(doc.log_path as string);
const absolutePath = await handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
}
+124 -22
View File
@@ -1,15 +1,31 @@
import { Response } from 'express';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import {
AuthDataType,
AuthInfo,
AuthInstance,
AuthModel,
AuthModelInfo,
} from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import ScheduleService, { TaskCallbacks } from './schedule';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import got from 'got';
import { parseContentVersion, parseVersion } from '../config/util';
import {
getPid,
killTask,
parseContentVersion,
parseVersion,
promiseExec,
} from '../config/util';
import { TASK_COMMAND } from '../config/const';
import taskLimit from '../shared/pLimit';
import tar from 'tar';
import path from 'path';
@Service()
export default class SystemService {
@@ -20,22 +36,22 @@ export default class SystemService {
@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 || {};
public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig });
return doc || ({} as AuthInstance);
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
private async updateAuthDb(payload: AuthInfo): Promise<AuthInstance> {
await AuthModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type });
return doc;
}
public async getDb(query: any): Promise<any> {
public async getDb(query: any): Promise<AuthInstance> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
return doc && doc.get({ plain: true });
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
@@ -56,25 +72,30 @@ export default class SystemService {
}
}
public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getLogRemoveFrequency();
public async updateSystemConfig(info: AuthModelInfo) {
const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({
...oDoc,
type: AuthDataType.removeLogFrequency,
info: { frequency },
type: AuthDataType.systemConfig,
info,
});
if (info.logRemoveFrequency) {
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
command: `ql rmlog ${info.logRemoveFrequency}`,
};
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
if (info.logRemoveFrequency > 0) {
this.scheduleService.createIntervalTask(cron, {
days: frequency,
days: info.logRemoveFrequency,
});
}
return { code: 200, data: { ...cron } };
}
if (info.cronConcurrency) {
await taskLimit.setCustomLimit(info.cronConcurrency);
}
return { code: 200, data: info };
}
public async checkUpdate() {
@@ -90,7 +111,7 @@ export default class SystemService {
},
);
lastVersionContent = await parseContentVersion(result.body);
} catch (error) {}
} catch (error) { }
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
@@ -136,7 +157,7 @@ export default class SystemService {
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
const cp = spawn('ql -l update false', { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
@@ -162,6 +183,33 @@ export default class SystemService {
return { code: 200 };
}
public async reloadSystem(target: 'system' | 'data') {
const cp = spawn(`ql -l reload ${target || ''}`, { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'reloadSystem',
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) {
@@ -170,4 +218,58 @@ export default class SystemService {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
}
}
public async run(
{ command, logPath }: { command: string; logPath: string },
callback: TaskCallbacks,
) {
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
this.scheduleService.runTask(
`real_log_path=${logPath} real_time=true ${command}`,
callback,
);
}
public async stop({ command, pid }: { command: string; pid: number }) {
if (!pid && !command) {
return { code: 400, message: '参数错误' };
}
if (pid) {
await killTask(pid);
return { code: 200 };
}
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
const _pid = await getPid(command);
if (_pid) {
await killTask(_pid);
return { code: 200 };
} else {
return { code: 400, message: '任务未找到' };
}
}
public async exportData(res: Response) {
try {
await tar.create({ gzip: true, file: config.dataTgzFile, cwd: config.rootPath }, ['data'])
res.download(config.dataTgzFile);
} catch (error: any) {
return res.send({ code: 400, message: error.message });
}
}
public async importData() {
try {
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
await tar.x({ file: config.dataTgzFile, cwd: config.tmpPath });
return { code: 200 };
} catch (error: any) {
return { code: 400, message: error.message };
}
}
}
+26 -20
View File
@@ -1,16 +1,20 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util';
import {
createRandomString,
fileExist,
getNetIp,
getPlatform,
} from '../config/util';
import config from '../config';
import * as fs from 'fs';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import { Request } from 'express';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import dayjs from 'dayjs';
@@ -23,7 +27,7 @@ export default class UserService {
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
) { }
public async login(
payloads: {
@@ -115,8 +119,7 @@ export default class UserService {
});
await this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
} ip地址 ${ip}`,
);
await this.getLoginLog();
@@ -144,8 +147,7 @@ export default class UserService {
});
await this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
} ip地址 ${ip}`,
);
await this.getLoginLog();
@@ -183,12 +185,12 @@ export default class UserService {
});
}
public async getLoginLog(): Promise<AuthInfo[]> {
public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
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);
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 },
@@ -205,17 +207,16 @@ export default class UserService {
}
private initAuthInfo() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: newPassword,
password: 'admin',
}),
);
return {
code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录',
message: '未找到认证文件,重新初始化',
};
}
@@ -240,13 +241,18 @@ export default class UserService {
return { code: 200, data: avatar, message: '更新成功' };
}
public getUserInfo(): Promise<any> {
return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
resolve(JSON.parse(data));
});
});
public async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
}
return this.getAuthInfo();
}
public initTwoFactor() {
+38
View File
@@ -0,0 +1,38 @@
import pLimit from "p-limit";
import os from 'os';
import { AuthDataType, AuthModel } from "../data/auth";
class TaskLimit {
private oneLimit = pLimit(1);
private updateLogLimit = pLimit(1);
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
constructor() {
this.setCustomLimit();
}
public async setCustomLimit(limit?: number) {
if (limit) {
this.cpuLimit = pLimit(limit);
return;
}
const doc = await AuthModel.findOne({ where: { type: AuthDataType.systemConfig } });
if (doc?.info?.cronConcurrency) {
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
}
}
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
return this.cpuLimit(fn);
}
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
return this.oneLimit(fn);
}
public updateDepLog<T>(fn: () => Promise<T>): Promise<T> {
return this.updateLogLimit(fn);
}
}
export default new TaskLimit();
+37
View File
@@ -0,0 +1,37 @@
import { spawn } from 'cross-spawn';
import taskLimit from "./pLimit";
import Logger from '../loaders/logger';
export function runCron(cmd: string): Promise<number> {
return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve: any) => {
Logger.silly('运行命令: ' + cmd);
const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stderr.on('data', (data) => {
Logger.info(
'[执行任务失败] %s,时间:%s, 错误信息:%j',
cmd,
new Date().toLocaleString(),
data.toString(),
);
});
cp.on('error', (err) => {
Logger.error(
'[创建任务失败] %s,时间:%s, 错误信息:%j',
cmd,
new Date().toLocaleString(),
err,
);
});
cp.on('close', async (code) => {
Logger.info(
`[任务退出] ${cmd} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
resolve();
});
});
})
}
+12 -7
View File
@@ -3,9 +3,9 @@ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm \
&& npm i -g pnpm@8.3.1 \
&& cd /tmp/build \
&& pnpm install --prod
&& pnpm --registry https://registry.npmmirror.com install --prod
FROM python:3.10-alpine
@@ -17,7 +17,7 @@ ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=zh_CN.UTF-8 \
LANG=C.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
@@ -29,7 +29,6 @@ RUN set -x \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
@@ -40,6 +39,8 @@ RUN set -x \
nodejs \
jq \
openssh \
procps \
netcat-openbsd \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
@@ -48,7 +49,8 @@ RUN set -x \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \
&& npm install -g pnpm@8.3.1 \
&& cd && pnpm config set registry https://registry.npmmirror.com \
&& pnpm add -g pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
@@ -56,12 +58,12 @@ RUN set -x \
&& rm -rf /root/.npm
ARG SOURCE_COMMIT
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
RUN git clone --depth=1 -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 \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
@@ -70,4 +72,7 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+6
View File
@@ -1,9 +1,15 @@
version: '2'
services:
web:
# alpine 基础镜像版本
image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes:
- ./data:/ql/data
ports:
- "0.0.0.0:5700:5700"
environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped
+19 -23
View File
@@ -4,56 +4,52 @@ dir_shell=/ql/shell
. $dir_shell/share.sh
link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
init_nginx
fix_config
pm2 l &>/dev/null
patch_version &>/dev/null
echo
echo -e "======================2. 安装依赖========================\n"
update_depend
patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
current_npm_registry=$(cd && pnpm config get registry)
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
if [[ "$is_equal_registry" == "" ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
echo
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"
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_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n"
echo -e "======================6. 启动定时任务========================\n"
pm2 delete schedule &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n"
echo -e "======================4. 启动pm2服务========================\n"
reload_pm2
if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n"
echo -e "======================5. 启动bot========================\n"
nohup ql -l bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n"
echo -e "====================6. 执行自定义脚本========================\n"
nohup ql -l extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
echo -e "############################################################\n"
echo -e "容器启动成功..."
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
echo -e "############################################################\n"
crond -f >/dev/null
+11 -7
View File
@@ -14,31 +14,33 @@ map $http_upgrade $connection_upgrade {
server {
listen 5700;
IPV6_CONFIG
root /ql/static/dist;
ssl_session_timeout 5m;
location QL_BASE_URL/api/public/ {
location QL_BASE_URLapi/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://publicApi/api/public/;
proxy_pass http://publicApi/api/;
proxy_buffering off;
}
location QL_BASE_URL/api/ {
location QL_BASE_URLapi/ {
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://baseApi/api/;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location QL_BASE_URL/open/ {
location QL_BASE_URLopen/ {
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://baseApi/open/;
proxy_buffering off;
}
gzip on;
@@ -49,10 +51,12 @@ server {
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
QL_ROOT_CONFIG
location QL_BASE_URL/ {
location QL_BASE_URL_LOCATION {
QL_ALIAS_CONFIG
index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html;
try_files $uri QL_BASE_URLindex.html;
}
location ~ .*\.(html)$ {
+1 -1
View File
@@ -14,7 +14,7 @@ http {
server_tokens off;
client_max_body_size 20m;
client_max_body_size 4096m;
client_body_buffer_size 20m;
keepalive_timeout 65;
+34
View File
@@ -0,0 +1,34 @@
module.exports = {
apps: [
{
name: 'schedule',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/schedule/index.js',
},
{
name: 'public',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/public.js',
},
{
name: 'panel',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/app.js',
},
],
};
+18 -5
View File
@@ -5,12 +5,13 @@
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node --transpile-only ./back/public.ts",
"start:schedule": "ts-node --transpile-only ./back/schedule.ts",
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
"build:front": "max 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",
"schedule": "npm run build:back && node static/build/schedule/index.js",
"public": "npm run build:back && node static/build/public.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true",
"test": "umi-test",
@@ -54,6 +55,7 @@
}
},
"dependencies": {
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
@@ -62,14 +64,17 @@
"chokidar": "^3.5.3",
"cors": "^2.8.5",
"cron-parser": "^4.2.1",
"cross-spawn": "^7.0.3",
"dayjs": "^1.11.2",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"express-rate-limit": "^6.7.0",
"express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
@@ -78,12 +83,15 @@
"nedb": "^1.8.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"p-limit": "3.1.0",
"protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.2",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"tar": "^6.1.15",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2",
@@ -98,8 +106,10 @@
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/file-saver": "^2.0.5",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185",
@@ -115,15 +125,18 @@
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.55",
"@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6",
"antd": "^4.24.8",
"antd-img-crop": "^4.2.3",
"axios": "^1.4.0",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"file-saver": "^2.0.5",
"lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15",
@@ -142,10 +155,10 @@
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"ts-proto": "^1.146.0",
"tslib": "^2.4.0",
"tsx": "^3.12.3",
"typescript": "4.8.4",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0",
+5137 -5274
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

+6 -11
View File
@@ -1,9 +1,3 @@
## Version: v2.8.0
## Date: 2021-06-20
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
AutoDelCron="true"
@@ -24,11 +18,8 @@ CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h"
## 设置批量执行任务时的并发数,默认同时执行5个任务
MaxConcurrentNum="5"
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime=""
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
@@ -100,6 +91,10 @@ export TG_API_HOST=""
export DD_BOT_TOKEN=""
export DD_BOT_SECRET=""
## 企业微信反向代理地址
## (环境变量名 QYWX_ORIGIN)
export QYWX_ORIGIN=""
## 5. 企业微信机器人
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
## 下方填写密钥,企业微信推送 webhook 后面的 key
+19 -9
View File
File diff suppressed because one or more lines are too long
+89 -47
View File
@@ -76,6 +76,8 @@ push_config = {
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_ORIGIN': '', # 企业微信代理地址
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
@@ -164,8 +166,7 @@ def dingding_bot(title: str, content: str) -> None:
timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format(
timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
@@ -231,8 +232,11 @@ def gotify(title: str, content: str) -> None:
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")}
data = {
"title": title,
"message": content,
"priority": push_config.get("GOTIFY_PRIORITY"),
}
response = requests.post(url, data=data).json()
if response.get("id"):
@@ -291,9 +295,13 @@ def pushdeer(title: str, content: str) -> None:
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'
data = {
"text": title,
"desp": content,
"type": "markdown",
"pushkey": push_config.get("DEER_KEY"),
}
url = "https://api2.pushdeer.com/message/push"
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
@@ -313,7 +321,7 @@ def chat(title: str, content: str) -> None:
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
data = "payload=" + json.dumps({"text": title + "\n" + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
@@ -347,11 +355,9 @@ def pushplus_bot(title: str, content: str) -> None:
print("PUSHPLUS 推送成功!")
else:
url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(
url=url_old, data=body, headers=headers).json()
response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!")
@@ -370,8 +376,7 @@ def qmsg_bot(title: str, content: str) -> None:
print("qmsg 服务启动")
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
payload = {
"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
response = requests.post(url=url, params=payload).json()
if response["code"] == 0:
@@ -420,9 +425,12 @@ class WeCom:
self.CORPID = corpid
self.CORPSECRET = corpsecret
self.AGENTID = agentid
self.ORIGIN = "https://qyapi.weixin.qq.com"
if push_config.get("QYWX_ORIGIN"):
self.ORIGIN = push_config.get("QYWX_ORIGIN")
def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
url = f"{self.ORIGIN}/cgi-bin/gettoken"
values = {
"corpid": self.CORPID,
"corpsecret": self.CORPSECRET,
@@ -432,10 +440,7 @@ class WeCom:
return data["access_token"]
def send_text(self, message, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
send_values = {
"touser": touser,
"msgtype": "text",
@@ -449,10 +454,7 @@ class WeCom:
return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}"
send_values = {
"touser": touser,
"msgtype": "mpnews",
@@ -485,7 +487,11 @@ def wecom_bot(title: str, content: str) -> None:
return
print("企业微信机器人服务启动")
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
origin = "https://qyapi.weixin.qq.com"
if push_config.get("QYWX_ORIGIN"):
origin = push_config.get("QYWX_ORIGIN")
url = f"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
@@ -547,24 +553,28 @@ def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息
"""
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
if (
not push_config.get("AIBOTK_KEY")
or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME")
):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room':
if push_config.get("AIBOTK_TYPE") == "room":
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
@@ -580,29 +590,52 @@ def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息
"""
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
if (
not push_config.get("SMTP_SERVER")
or not push_config.get("SMTP_SSL")
or not push_config.get("SMTP_EMAIL")
or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME")
):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['Subject'] = Header(title, 'utf-8')
message = MIMEText(content, "plain", "utf-8")
message["From"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["To"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["Subject"] = Header(title, "utf-8")
try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get(
"SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server = (
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
if push_config.get("SMTP_SSL") == "true"
else smtplib.SMTP(push_config.get("SMTP_SERVER"))
)
smtp_server.login(
push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
)
smtp_server.sendmail(
push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"),
message.as_bytes(),
)
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f'SMTP 邮件 推送失败!{e}')
print(f"SMTP 邮件 推送失败!{e}")
def one() -> str:
@@ -645,9 +678,19 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
if (
push_config.get("AIBOTK_KEY")
and push_config.get("AIBOTK_TYPE")
and push_config.get("AIBOTK_NAME")
):
notify_function.append(aibotk)
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
if (
push_config.get("SMTP_SERVER")
and push_config.get("SMTP_SSL")
and push_config.get("SMTP_EMAIL")
and push_config.get("SMTP_PASSWORD")
and push_config.get("SMTP_NAME")
):
notify_function.append(smtp)
@@ -659,7 +702,7 @@ def send(title: str, content: str) -> None:
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle:
if (title in re.split("\n", skipTitle)):
if title in re.split("\n", skipTitle):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return
@@ -669,8 +712,7 @@ def send(title: str, content: str) -> None:
content += "\n\n" + text
ts = [
threading.Thread(target=mode, args=(
title, content), name=mode.__name__)
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function
]
[t.start() for t in ts]
-5
View File
@@ -3,13 +3,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"got": "^11.5.1",
"http-server": "^0.12.3",
"nodemailer": "^6.8.0",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.2",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
+17 -17
View File
@@ -2,7 +2,7 @@
create_token() {
local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
local token_file="${dir_root}/static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
@@ -30,10 +30,10 @@ add_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local sub_id=$4
local schedule="$1"
local command="$2"
local name="$3"
local sub_id="$4"
fi
if [[ ! $sub_id ]];then
@@ -69,10 +69,10 @@ update_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
local schedule="$1"
local command="$2"
local name="$3"
local id="$4"
fi
local api=$(
@@ -103,8 +103,8 @@ update_cron_command_api() {
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
local command="$1"
local id="$2"
fi
local api=$(
@@ -130,7 +130,7 @@ update_cron_command_api() {
}
del_cron_api() {
local ids=$1
local ids="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
@@ -178,13 +178,13 @@ update_cron() {
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
echo -e "\n## 更新任务状态失败(${message})\n"
fi
}
notify_api() {
local title=$1
local content=$2
local title="$1"
local content="$2"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
@@ -202,14 +202,14 @@ notify_api() {
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功"
echo -e "通知发送成功🎉"
else
echo -e "通知失败(${message})"
fi
}
find_cron_api() {
local params=$1
local params="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
+1 -1
View File
@@ -42,6 +42,6 @@ echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n"
make_dir $dir_log/bot
cd $dir_data
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
ps -eo pid,command | 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"
+17 -17
View File
@@ -33,12 +33,12 @@ pm2_log() {
echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log"
tail -n 100 "$panelOut"
tail -n 100 "$panelError"
tail -n 300 "$panelOut"
tail -n 300 "$panelError"
}
check_nginx() {
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
local nginxPid=$(ps -eo pid,command | grep nginx | grep -v grep)
echo -e "=====> 检测nginx服务\n$nginxPid"
if [[ $nginxPid ]]; then
echo -e "\n=====> nginx服务正常\n"
@@ -54,9 +54,6 @@ check_ql() {
echo -e "\n=====> 检测面板\n\n$api\n"
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
echo -e "=====> 面板服务启动正常\n"
else
echo -e "=====> 面板服务异常,重置基础环境\n"
reset_env
fi
}
@@ -74,23 +71,26 @@ check_pm2() {
echo -e "\n=====> 检测后台\n\n$api\n"
if [[ $api =~ "{\"code\"" ]]; then
echo -e "=====> 后台服务启动正常\n"
else
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
reset_env
fi
}
start_public() {
echo -e "=====> 启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
main() {
echo -e "=====> 开始检测"
npm i -g pnpm
npm i -g pnpm@8.3.1
patch_version
start_public
apk add procps netcat-openbsd
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
pnpm add -g pm2 tsx
reset_env
copy_dep
check_ql
check_nginx
+4 -42
View File
@@ -1,11 +1,5 @@
#!/usr/bin/env bash
trap "single_hanle" 2 20 15 14
single_hanle() {
handle_task_after "$@"
exit 1
}
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
@@ -30,7 +24,7 @@ random_delay() {
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\"随机延迟 $delay_second后执行\n"
echo -e "任务随机延迟 $delay_second,配置文件参数 RandomDelay 置空可取消延迟 \n"
sleep $delay_second
fi
}
@@ -44,7 +38,7 @@ gen_array_scripts() {
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
array_scripts_name[i]=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
@@ -92,42 +86,10 @@ check_server() {
fi
}
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
random_delay "$file_param"
fi
@@ -231,7 +193,7 @@ run_else() {
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
file_param=${file_param/$relative_path\//.\/}
fi
shift
+61 -107
View File
@@ -2,6 +2,7 @@
## 目录
dir_root=$QL_DIR
dir_tmp=$dir_root/.tmp
dir_data=$dir_root/data
dir_shell=$dir_root/shell
dir_sample=$dir_root/sample
@@ -18,6 +19,7 @@ dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_ecosystem_js=$dir_root/ecosystem.config.js
file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
@@ -34,7 +36,6 @@ file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py
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
@@ -67,8 +68,8 @@ import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
ql_base_url=${QlBaseUrl:-"/"}
command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
@@ -98,6 +99,9 @@ set_proxy() {
unset_proxy() {
unset http_proxy
unset https_proxy
unset ftp_proxy
unset all_proxy
unset no_proxy
}
make_dir() {
@@ -172,6 +176,7 @@ define_cmd() {
}
fix_config() {
make_dir $dir_tmp
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
@@ -260,17 +265,7 @@ npm_install_sub() {
else
pnpm install --loglevel error --production
fi
}
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
exit_status=$?
}
npm_install_2() {
@@ -278,12 +273,8 @@ npm_install_2() {
local dir_work=$1
cd $dir_work
echo -e "检测到 $dir_work 依赖包有变化,运行 npm install...\n"
echo -e "安装 $dir_work 依赖包...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
}
@@ -295,77 +286,22 @@ diff_and_copy() {
fi
}
update_depend() {
local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
cd $dir_current
}
git_clone_scripts() {
local url="$1"
local dir="$2"
local branch="$3"
local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
echo -e "开始拉取仓库 ${uniq_path}$dir\n"
set_proxy "$proxy"
git clone $part_cmd $url $dir
git clone --depth=1 $part_cmd $url $dir
exit_status=$?
unset_proxy
}
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
local proxy="$3"
cd $dir_work
echo -e "开始更新仓库:$dir_work"
set_proxy "$proxy"
git fetch --all
git pull 1>/dev/null
exit_status=$?
unset_proxy
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
cd $dir_work
if [[ -d "$dir_work/.git" ]]; then
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
else
git init
git remote add origin $url &>/dev/null
fi
reset_branch "$branch"
cd $dir_current
}
reset_branch() {
local branch="$1"
local part_cmd="HEAD"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() {
local beg=$1
local end=$2
@@ -373,15 +309,12 @@ random_range() {
}
reload_pm2() {
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
cd $dir_root
# 代理会影响 grpc 服务
unset_proxy
pm2 flush &>/dev/null
pm2 startOrGracefulReload $file_ecosystem_js --update-env
pm2 sendSignal SIGKILL panel &>/dev/null
}
diff_time() {
@@ -431,18 +364,9 @@ format_timestamp() {
}
patch_version() {
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
git config --global pull.rebase false
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
pnpm config set registry $NpmMirror
fi
pnpm install -g &>/dev/null
cp -f $dir_root/.env.example $dir_root/.env
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
@@ -451,12 +375,6 @@ patch_version() {
echo
fi
pnpm add -g pm2 tsx
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
@@ -485,7 +403,19 @@ patch_version() {
init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
local location_url="/"
local aliasStr=""
local rootStr=""
if [[ $ql_base_url != "/" ]]; then
location_url="^~${ql_base_url%*/}"
aliasStr="alias ${dir_static}/dist;"
else
rootStr="root ${dir_static}/dist;"
fi
sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6)
ipv6Str=""
@@ -495,10 +425,34 @@ init_nginx() {
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
}
handle_task_before() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
}
init_env
detect_termux
detect_macos
define_cmd
fix_config
import_config $1 2>$task_error_log_path
import_config $1
+36 -6
View File
@@ -5,6 +5,12 @@ dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 3 20 15 14
single_hanle() {
eval handle_task_after "$@" "$cmd"
exit 1
}
## 选择python3还是node
define_program() {
local file_param=$1
@@ -28,14 +34,22 @@ define_program() {
handle_log_path() {
local file_param=$1
if [[ -z $file_param ]];then
if [[ -z $file_param ]]; then
file_param="task"
fi
if [[ -z $ID ]]; then
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}')
fi
local suffix=""
if [[ ! -z $ID ]]; then
if [[ "$ID" -gt 0 ]] 2>/dev/null; then
suffix="_${ID}"
else
ID=""
fi
fi
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
log_dir_tmp="${file_param##*/}"
@@ -51,11 +65,19 @@ handle_log_path() {
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
if [[ $real_log_path ]]; then
log_path="$real_log_path"
fi
cmd=">> $dir_log/$log_path 2>&1"
if [[ "$show_log" == "true" ]]; then
cmd=""
else
make_dir "$dir_log/$log_dir"
if [[ "$show_log" == "true" ]]; then
cmd="2>&1 | tee -a $dir_log/$log_path"
fi
if [[ "$real_time" == "true" ]]; then
cmd=""
fi
}
@@ -67,12 +89,19 @@ format_params() {
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
fi
timeoutCmd=""
if [[ $command_timeout_time ]]; then
if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
timeoutCmd="timeout --foreground -s 2 -k 10s $command_timeout_time "
fi
fi
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
}
init_begin_time() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
}
while getopts ":lm:" opt; do
case $opt in
l)
@@ -92,8 +121,9 @@ fi
format_params "$@"
define_program "$@"
handle_log_path "$@"
init_begin_time
eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
exit 0
+81 -53
View File
@@ -31,7 +31,7 @@ output_list_add_drop() {
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
echo -e "检测到有${type}的定时任务:\n"
cat $list
echo
fi
@@ -44,9 +44,9 @@ del_cron() {
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
echo -e "开始尝试自动删除失效的定时任务..."
for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
ids="$ids,\"$id\""
else
@@ -54,7 +54,7 @@ del_cron() {
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
@@ -75,7 +75,7 @@ del_cron() {
add_cron() {
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
echo -e "开始尝试自动添加定时任务..."
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
@@ -92,7 +92,7 @@ add_cron() {
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
@@ -130,13 +130,9 @@ update_repo() {
make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" "${proxy}"
else
rm -rf ${repo_path} &>/dev/null
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
@@ -193,7 +189,7 @@ update_raw() {
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
@@ -233,65 +229,98 @@ usage() {
echo -e "9. $cmd_update resettfa # 禁用两步登录"
}
reload_qinglong() {
local reload_target="${1}"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="develop"
fi
if [[ "$reload_target" == 'system' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
fi
if [[ "$reload_target" == 'data' ]]; then
cp -rf ${dir_tmp}/data ${dir_root}/
fi
reload_pm2
}
## 更新qinglong
update_qinglong() {
rm -rf ${dir_tmp}/*
local mirror="gitee"
local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
mirror="github"
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
fi
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="develop"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
exit_status=$?
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n"
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
cp -f $file_config_sample $dir_config/config.sample.sh
update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
update_qinglong_static "$1" "$primary_branch"
update_qinglong_static
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
}
update_qinglong_static() {
local no_restart="$1"
local primary_branch="$2"
local url="https://${mirror}.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
rm -rf ${ql_static_repo} &>/dev/null
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
wget -cqO "${dir_tmp}/static.zip" "${downloadStaticUrl}/${primary_branch}.zip"
exit_status=$?
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
check_update_dep
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
fi
}
check_update_dep() {
echo -e "\n开始检测依赖...\n"
if [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n依赖检测安装成功...\n"
echo -e "\n更新包下载成功...\n"
if [[ "$needRestart" == 'true' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${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 "重启面板中..."
sleep 3
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
reload_pm2
fi
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
echo -e "\n依赖检测安装失败,请检查网络...\n"
fi
}
@@ -366,10 +395,10 @@ gen_list_repo() {
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
files=$(echo "$files" | egrep "$path")
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
files=$(echo "$files" | egrep -v "$blackword")
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
@@ -377,7 +406,7 @@ gen_list_repo() {
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
results=$(eval $cmd | sed 's/^..//' | egrep "$dependence")
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
@@ -390,7 +419,7 @@ gen_list_repo() {
fi
for file in ${files}; do
filename=$(basename $file)
filename=$(basename "$file")
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
@@ -459,12 +488,6 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
@@ -472,7 +495,12 @@ main() {
case $p1 in
update)
eval update_qinglong "$2" $cmd
fix_config
local needRestart=${p2:-"true"}
eval update_qinglong $cmd
;;
reload)
eval reload_qinglong "$p2" $cmd
;;
extra)
eval run_extra_shell $cmd
+16
View File
@@ -0,0 +1,16 @@
const baseUrl = window.__ENV__QlBaseUrl || '/';
export function modifyClientRenderOpts(memo: any) {
return {
...memo,
publicPath: baseUrl,
basename: baseUrl,
};
}
export function modifyContextOpts(memo: any) {
return {
...memo,
basename: baseUrl,
};
}
+36 -15
View File
@@ -26,8 +26,8 @@ body {
}
.ant-modal-body {
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
max-height: calc(90vh - 110px);
max-height: calc(90vh - var(--vh-offset, 110px));
overflow-y: auto;
}
@@ -327,24 +327,25 @@ select:-webkit-autofill:focus {
.side-menu-user-drop-menu {
position: relative;
text-align: left;
outline: none;
padding: 4px 0;
border: 1px solid fade(@component-background, 0.12);
border-radius: 4px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
padding: 2px 10px;
overflow: auto;
background-color: @component-background;
}
[data-dark='true'] .side-menu-user-drop-menu {
background-color: #373739;
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
.title {
display: flex;
height: 32px;
margin: 0 5px;
font-weight: 600;
font-size: 16px;
line-height: 32px;
vertical-align: middle;
animation: pro-layout-title-hide 0.3s;
h1 {
margin-left: 5px !important;
a {
display: inline-flex;
align-items: center;
}
}
img {
@@ -358,6 +359,10 @@ select:-webkit-autofill:focus {
// 移动端logo被拉伸
width: auto !important;
}
.ant-image {
display: inline-flex;
}
}
pre {
@@ -397,3 +402,19 @@ pre {
.ant-table-filter-column {
justify-content: unset;
}
textarea.ant-input {
word-break: break-all;
}
.ant-pro-sider-collapsed,
body[data-mode='phone'] header {
.title {
display: none;
}
}
.ant-design-pro .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
box-shadow: none;
border-right: 1px solid #eee;
}
+27 -13
View File
@@ -49,8 +49,7 @@ export interface SharedContext {
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: number;
publishTime: number;
version: string;
changeLog: string;
changeLogLink: string;
@@ -98,8 +97,7 @@ export default function () {
})
.catch((error) => {
console.log(error);
})
.finally(() => setInitLoading(false));
});
};
const getUser = (needLoading = true) => {
@@ -120,6 +118,22 @@ export default function () {
});
};
const getHealthStatus = () => {
request
.get(`${config.apiPrefix}public/health`)
.then((res) => {
if (res?.data?.status === 1) {
getSystemInfo();
} else {
history.push('/error');
}
})
.catch((error) => {
history.push('/error');
})
.finally(() => setInitLoading(false));
};
const reloadUser = (needLoading = false) => {
getUser(needLoading);
};
@@ -131,10 +145,8 @@ export default function () {
}, [location.pathname]);
useEffect(() => {
if (!systemInfo) {
getSystemInfo();
}
}, [systemInfo]);
getHealthStatus();
}, []);
useEffect(() => {
if (theme === 'vs-dark') {
@@ -267,11 +279,11 @@ export default function () {
selectedKeys={[location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
// @ts-ignore
title={
logo={
<>
<span style={{ fontSize: 16 }}></span>
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
<div className="title">
<span className="title"></span>
<a
href={systemInfo?.changeLogLink}
target="_blank"
@@ -298,8 +310,10 @@ export default function () {
</Badge>
</Tooltip>
</a>
</div>
</>
}
title={false}
menuItemRender={(menuItemProps: any, defaultDom: any) => {
if (
menuItemProps.isUrl ||
@@ -313,7 +327,7 @@ export default function () {
pageTitleRender={(props, pageName, info) => {
const title =
(config.documentTitleMap as any)[location.pathname] || '未找到';
return `${title} - 控制面板`;
return `${title} - 青龙`;
}}
onCollapse={setCollapsed}
collapsed={collapsed}
+1 -3
View File
@@ -51,9 +51,7 @@ const Config = () => {
: value;
request
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: select },
})
.post(`${config.apiPrefix}configs/save`, { content, name: select })
.then(({ code, data }) => {
if (code === 200) {
message.success('保存成功');
+5 -11
View File
@@ -22,7 +22,7 @@ import {
PauseCircleOutlined,
FullscreenOutlined,
} from '@ant-design/icons';
import { CrontabStatus } from './index';
import { CrontabStatus } from './type';
import { diffTime } from '@/utils/date';
import { request } from '@/utils/http';
import config from '@/utils/config';
@@ -184,11 +184,9 @@ const CronDetailModal = ({
return new Promise((resolve, reject) => {
request
.put(`${config.apiPrefix}scripts`, {
data: {
filename: scriptInfo.filename,
path: scriptInfo.parent || '',
content,
},
})
.then(({ code, data }) => {
if (code === 200) {
@@ -220,7 +218,7 @@ const CronDetailModal = ({
),
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.put(`${config.apiPrefix}crons/run`, [currentCron.id])
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
@@ -250,7 +248,7 @@ const CronDetailModal = ({
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.put(`${config.apiPrefix}crons/stop`, [currentCron.id] )
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
@@ -282,9 +280,7 @@ const CronDetailModal = ({
`${config.apiPrefix}crons/${
currentCron.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [currentCron.id],
},
[currentCron.id],
)
.then(({ code, data }) => {
if (code === 200) {
@@ -320,9 +316,7 @@ const CronDetailModal = ({
`${config.apiPrefix}crons/${
currentCron.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [currentCron.id],
},
[currentCron.id],
)
.then(({ code, data }) => {
if (code === 200) {
+6 -3
View File
@@ -6,8 +6,9 @@
.card-wrapper {
.ant-card:last-child {
.ant-card-body {
height: calc(80vh - 367px);
height: calc(80vh - var(--vh-offset, 0px) - 367px);
min-height: 300px;
height: calc(90vh - 367px);
height: calc(90vh - var(--vh-offset, 0px) - 367px);
overflow-y: auto;
}
}
@@ -193,8 +194,10 @@ tr.drop-over-upward td {
}
}
.crontab-wrapper {
body[data-mode='desktop'] {
.crontab-wrapper {
tbody .ant-table-cell {
height: 69px !important;
}
}
}
+26 -73
View File
@@ -54,51 +54,11 @@ import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { useVT } from 'virtualizedtableforantd4';
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
const { Text, Paragraph } = Typography;
const { Search } = Input;
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 };
enum OperationName {
'启用',
'禁用',
'运行',
'停止',
'置顶',
'取消置顶',
}
enum OperationPath {
'enable',
'disable',
'run',
'stop',
'pin',
'unpin',
}
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}
const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: ColumnProps<ICrontab>[] = [
@@ -264,28 +224,21 @@ const Crontab = () => {
filters: [
{
text: '运行中',
value: 0,
value: CrontabStatus.running,
},
{
text: '空闲中',
value: 1,
value: CrontabStatus.idle,
},
{
text: '已禁用',
value: 2,
value: CrontabStatus.disabled,
},
{
text: '队列中',
value: 3,
value: CrontabStatus.queued,
},
],
onFilter: (value, record) => {
if (record.isDisabled && record.status !== 0) {
return value === 2;
} else {
return record.status === value;
}
},
render: (text, record) => (
<>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
@@ -321,7 +274,7 @@ const Crontab = () => {
{
title: '操作',
key: 'action',
width: 100,
width: 130,
render: (text, record, index) => {
const isPc = !isPhone;
return (
@@ -504,7 +457,7 @@ const Crontab = () => {
),
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.put(`${config.apiPrefix}crons/run`, [record.id])
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
@@ -539,7 +492,7 @@ const Crontab = () => {
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.put(`${config.apiPrefix}crons/stop`, [record.id])
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
@@ -580,9 +533,7 @@ const Crontab = () => {
`${config.apiPrefix}crons/${
record.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [record.id],
},
[record.id],
)
.then(({ code, data }) => {
if (code === 200) {
@@ -624,9 +575,7 @@ const Crontab = () => {
`${config.apiPrefix}crons/${
record.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [record.id],
},
[record.id],
)
.then(({ code, data }) => {
if (code === 200) {
@@ -672,7 +621,6 @@ const Crontab = () => {
index: number;
}> = ({ record, index }) => (
<Dropdown
arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']}
menu={{
@@ -773,9 +721,10 @@ const Crontab = () => {
content: <>{OperationName[operationStatus]}</>,
onOk() {
request
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.put(
`${config.apiPrefix}crons/${OperationPath[operationStatus]}`,
selectedRowIds,
)
.then(({ code, data }) => {
if (code === 200) {
getCrons();
@@ -832,12 +781,6 @@ const Crontab = () => {
}, [viewConf, enabledCronViews]);
useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
getCronViews();
}, []);
@@ -897,7 +840,17 @@ const Crontab = () => {
.then(({ code, data }) => {
if (code === 200) {
setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
const firstEnableView = data.filter((x) => !x.isDisabled);
setEnabledCronViews(firstEnableView);
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
setViewConf({
...firstEnableView[0],
});
}
})
.finally(() => {
@@ -1051,7 +1004,7 @@ const Crontab = () => {
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
components={isPhone ? undefined : vt}
// components={isPhone ? undefined : vt}
/>
</div>
<CronLogModal
+2 -33
View File
@@ -8,13 +8,8 @@ import {
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
import { CrontabStatus } from './type';
enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
const { Countdown } = Statistic;
const CronLogModal = ({
@@ -51,10 +46,7 @@ const CronLogModal = ({
const log = data as string;
setValue(log || '暂无日志');
const hasNext = Boolean(
log &&
!logEnded(log) &&
!log.includes('重启面板') &&
!log.includes('任务未运行'),
log && !logEnded(log) && !log.includes('任务未运行'),
);
setExecuting(hasNext);
autoScroll();
@@ -63,29 +55,6 @@ const CronLogModal = ({
getCronLog();
}, 2000);
}
if (
log &&
log.includes('重启面板') &&
cron.status === CrontabStatus.running
) {
message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
</span>
),
duration: 10,
});
setTimeout(() => {
window.location.reload();
}, 30000);
}
}
})
.finally(() => {
+5 -6
View File
@@ -25,9 +25,10 @@ const CronModal = ({
payload.id = cron.id;
}
try {
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
const { code, data } = await request[method](
`${config.apiPrefix}crons`,
payload,
);
if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功');
@@ -130,9 +131,7 @@ const CronLabelModal = ({
try {
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
payload,
);
if (code === 200) {
+38
View File
@@ -0,0 +1,38 @@
export enum CrontabStatus {
'running' = 0,
'queued' = 0.5,
'idle' = 1,
'disabled',
}
export enum OperationName {
'启用',
'禁用',
'运行',
'停止',
'置顶',
'取消置顶',
}
export enum OperationPath {
'enable',
'disable',
'run',
'stop',
'pin',
'unpin',
}
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}
+7 -8
View File
@@ -14,6 +14,7 @@ import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
import get from 'lodash/get';
import { CrontabStatus } from './type';
const PROPERTIES = [
{ name: '命令', value: 'command' },
@@ -47,9 +48,9 @@ const SORTTYPES = [
const STATUS_MAP = {
status: [
{ name: '运行中', value: 0 },
{ name: '空闲中', value: 1 },
{ name: '已禁用', value: 2 },
{ name: '运行中', value: CrontabStatus.running },
{ name: '空闲中', value: CrontabStatus.idle },
{ name: '已禁用', value: CrontabStatus.disabled },
],
};
@@ -79,9 +80,7 @@ const ViewCreateModal = ({
try {
const { code, data } = await request[method](
`${config.apiPrefix}crons/views`,
{
data: view ? { ...values, id: view.id } : values,
},
view ? { ...values, id: view.id } : values,
);
if (code === 200) {
@@ -253,9 +252,9 @@ const ViewCreateModal = ({
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{EOperation[filtersValue[name]['operation']] ===
{EOperation[filtersValue?.[name]['operation']] ===
'select' ? (
statusElement(filtersValue[name]['property'])
statusElement(filtersValue?.[name]['property'])
) : (
<Input placeholder="请输入内容" />
)}
+13 -4
View File
@@ -75,6 +75,10 @@ const ViewManageModal = ({
handleCancel: () => void;
cronViewChange: (data?: any) => void;
}) => {
const islastEnableView = (record) => {
return list.filter((x) => !x.isDisabled).length <= 1 && !record.isDisabled;
};
const columns: any = [
{
title: '名称',
@@ -95,6 +99,7 @@ const ViewManageModal = ({
render: (text: string, record: any, index: number) => {
return (
<Switch
disabled={islastEnableView(record)}
checked={!record.isDisabled}
onChange={(checked) => onShowChange(checked, record, index)}
/>
@@ -111,9 +116,11 @@ const ViewManageModal = ({
<a onClick={() => editView(record, index)}>
<EditOutlined />
</a>
{!islastEnableView(record) && (
<a onClick={() => deleteView(record, index)}>
<DeleteOutlined />
</a>
)}
</Space>
) : (
'-'
@@ -161,9 +168,9 @@ const ViewManageModal = ({
const onShowChange = (checked: boolean, record: any, index: number) => {
request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id],
})
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, [
record.id,
])
.then(({ code, data }) => {
if (code === 200) {
const _list = [...list];
@@ -188,7 +195,9 @@ const ViewManageModal = ({
const dragRow = list[dragIndex];
request
.put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
fromIndex: dragIndex,
toIndex: hoverIndex,
id: dragRow.id,
})
.then(({ code, data }) => {
if (code === 200) {
+69 -27
View File
@@ -19,6 +19,8 @@ import {
DeleteFilled,
BugOutlined,
FileTextOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -31,6 +33,7 @@ import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import dayjs from 'dayjs';
const { Text } = Typography;
const { Search } = Input;
@@ -42,6 +45,7 @@ enum Status {
'删除中',
'已删除',
'删除失败',
'队列中',
}
enum StatusColor {
@@ -50,6 +54,37 @@ enum StatusColor {
'error',
}
const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
0: {
icon: <SyncOutlined spin />,
color: 'processing',
},
1: {
icon: <CheckCircleOutlined />,
color: 'success',
},
2: {
icon: <CloseCircleOutlined />,
color: 'error',
},
3: {
icon: <SyncOutlined spin />,
color: 'processing',
},
4: {
icon: <CheckCircleOutlined />,
color: 'success',
},
5: {
icon: <CloseCircleOutlined />,
color: 'error',
},
6: {
icon: <ClockCircleOutlined />,
color: 'default',
},
};
const Dependence = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
@@ -74,7 +109,8 @@ const Dependence = () => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
color={StatusColor[record.status % 3]}
color={StatusMap[record.status].color}
icon={StatusMap[record.status].icon}
style={{ marginRight: 0 }}
>
{Status[record.status]}
@@ -88,27 +124,20 @@ const Dependence = () => {
dataIndex: 'remark',
key: 'remark',
},
{
title: '更新时间',
key: 'updatedAt',
dataIndex: 'updatedAt',
render: (text: string) => {
return <span>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</span>;
},
},
{
title: '创建时间',
key: 'timestamp',
dataIndex: 'timestamp',
render: (text: string, record: any) => {
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>
);
key: 'createdAt',
dataIndex: 'createdAt',
render: (text: string) => {
return <span>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</span>;
},
},
{
@@ -211,7 +240,7 @@ const Dependence = () => {
})
.then(({ code, data }) => {
if (code === 200 && force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
const i = value.findIndex((x) => x.id === data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
@@ -240,9 +269,7 @@ const Dependence = () => {
),
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id],
})
.put(`${config.apiPrefix}dependencies/reinstall`, [record.id])
.then(({ code, data }) => {
if (code === 200) {
handleDependence(data[0]);
@@ -313,9 +340,7 @@ const Dependence = () => {
content: <></>,
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.put(`${config.apiPrefix}dependencies/reinstall`, selectedRowIds)
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]);
@@ -366,6 +391,23 @@ const Dependence = () => {
useEffect(() => {
if (!socketMessage) return;
const { type, message, references } = socketMessage;
if (
type === 'installDependence' &&
message.includes('开始时间') &&
references.length > 0
) {
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...value[index],
status: message.includes('安装') ? Status.安装中 : Status.删除中,
});
}
}
setValue(result);
}
if (
type === 'installDependence' &&
message.includes('结束时间') &&
+6 -4
View File
@@ -48,9 +48,7 @@ const DependenceModal = ({
try {
const { code, data } = await request[method](
`${config.apiPrefix}dependencies`,
{
data: payload,
},
payload,
);
if (code === 200) {
@@ -122,7 +120,11 @@ const DependenceModal = ({
name="name"
label="名称"
rules={[
{ required: true, message: '请输入依赖名称', whitespace: true },
{
required: true,
message: '请输入依赖名称,支持指定版本',
whitespace: true,
},
]}
>
<Input.TextArea
+2 -1
View File
@@ -48,7 +48,8 @@ const Diff = () => {
request
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: current },
content,
name: current,
})
.then(({ code, data }) => {
if (code === 200) {
-2
View File
@@ -19,10 +19,8 @@ const EditNameModal = ({
setLoading(true);
try {
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: {
ids,
name: values.name,
},
});
if (code === 200) {
+9 -11
View File
@@ -248,9 +248,7 @@ const Env = () => {
`${config.apiPrefix}envs/${
record.status === Status. ? 'enable' : 'disable'
}`,
{
data: [record.id],
},
[record.id],
)
.then(({ code, data }) => {
if (code === 200) {
@@ -388,13 +386,14 @@ const Env = () => {
const dragRow = value[dragIndex];
request
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
fromIndex: dragIndex,
toIndex: hoverIndex,
})
.then(({ code, data }) => {
if (code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setValue([...newData]);
}
});
@@ -438,9 +437,10 @@ const Env = () => {
content: <>{OperationName[operationStatus]}</>,
onOk() {
request
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.put(
`${config.apiPrefix}envs/${OperationPath[operationStatus]}`,
selectedRowIds,
)
.then(({ code, data }) => {
if (code === 200) {
getEnvs();
@@ -477,9 +477,7 @@ const Env = () => {
try {
const { code, data } = await request.post(
`${config.apiPrefix}envs/upload`,
{
data: formData,
},
formData,
);
if (code === 200) {
+4 -3
View File
@@ -37,9 +37,10 @@ const EnvModal = ({
payload = { ...values, id: env.id };
}
try {
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
const { code, data } = await request[method](
`${config.apiPrefix}envs`,
payload,
);
if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功');
+42 -1
View File
@@ -1,6 +1,5 @@
.error-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
@@ -9,4 +8,46 @@
height: calc(100vh - 80px);
overflow-y: auto;
}
.code-box {
position: relative;
display: inline-block;
width: 80vw;
height: 90vh;
margin: 16px;
background-color: #ffffff;
border: 1px solid rgba(5, 5, 5, 0.06);
border-radius: 6px;
-webkit-transition: all 0.2s;
transition: all 0.2s;
border-radius: 6px 6px 0 0;
color: rgba(0, 0, 0, 0.88);
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
.browser-markup {
position: relative;
border-top: 2em solid rgba(230, 230, 230, 0.7);
border-radius: 3px 3px 0 0;
&::before {
position: absolute;
top: -1.25em;
left: 1em;
display: block;
width: 0.5em;
height: 0.5em;
background-color: #f44;
border-radius: 50%;
box-shadow: 0 0 0 2px #f44, 1.5em 0 0 2px #9b3, 3em 0 0 2px #fb5;
content: '';
}
}
.log {
height: calc(90vh - 150px);
overflow-y: auto;
padding: 12px;
white-space: pre-line;
}
}
}
+43 -37
View File
@@ -1,43 +1,34 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } 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, useOutletContext } from '@umijs/max';
import Ansi from 'ansi-to-react';
import './index.less';
import { SharedContext } from '@/layouts';
import { Alert, Typography } from 'antd';
const Error = () => {
const { user, theme, reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志');
const getTimes = () => {
return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
};
let times = getTimes();
const retryTimes = useRef(1);
const getLog = (needLoading: boolean = true) => {
needLoading && setLoading(true);
request
.get(`${config.apiPrefix}public/panel/log`)
.then(({ code, data }) => {
if (code === 200) {
setData(data);
if (!data) {
times = getTimes();
if (times > 5) {
.get(`${config.apiPrefix}public/health`)
.then(({ error, data }) => {
if (data?.status === 1) {
return;
}
localStorage.setItem('error_retry_times', `${times + 1}`);
if (retryTimes.current > 3) {
setData(error?.details);
return;
}
retryTimes.current += 1;
setTimeout(() => {
reloadUser();
getLog(false);
}, 3000);
}
}
})
.finally(() => needLoading && setLoading(false));
};
@@ -56,24 +47,39 @@ const Error = () => {
<div className="error-wrapper">
{loading ? (
<PageLoading />
) : data ? (
<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>
),
},
]}
) : retryTimes.current > 3 ? (
<div className="code-box">
<div className="browser-markup"></div>
<Alert
type="error"
message={
<Typography.Title level={5} type="danger">
</Typography.Title>
}
description={
<Typography.Text type="danger">
<div></div>
<div>
1. 宿 docker run --rm -v
/var/run/docker.sock:/var/run/docker.sock
containrrr/watchtower -cR &lt;&gt;
</div>
<div>2. ql -l checkql -l update</div>
<div>
3. pm2 logs
<Typography.Link href="https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml">
issue
</Typography.Link>
</div>
</Typography.Text>
}
banner
/>
) : times > 5 ? (
<> ql -l check </>
<Typography.Paragraph code className="log">
{data}
</Typography.Paragraph>
</div>
) : (
<PageLoading tip="启动中,请稍后..." />
)}
+2 -1
View File
@@ -57,10 +57,11 @@
display: flex;
max-width: 500px;
width: 90%;
height: 350px;
height: 400px;
.ant-steps {
width: 35%;
min-width: 110px;
display: flex;
align-items: center;
position: relative;
+56 -55
View File
@@ -36,10 +36,8 @@ const Initialization = () => {
setLoading(true);
request
.put(`${config.apiPrefix}user/init`, {
data: {
username: values.username,
password: values.password,
},
})
.then(({ code, data }) => {
if (code === 200) {
@@ -53,9 +51,7 @@ const Initialization = () => {
setLoading(true);
request
.put(`${config.apiPrefix}user/notification/init`, {
data: {
...values,
},
values,
})
.then(({ code, data }) => {
if (code === 200) {
@@ -78,9 +74,14 @@ const Initialization = () => {
{
title: '欢迎使用',
content: (
<div className={styles.top} style={{ marginTop: 100 }}>
<div className={styles.top} style={{ marginTop: 30 }}>
<div className={styles.header}>
<span className={styles.title}>使</span>
<span className={styles.title}>使</span>
<span className={styles.desc}>
python3javaScriptshelltypescript A
timed task management panel that supports typescript, javaScript,
python3, and shell.
</span>
</div>
<div className={styles.action}>
<Button
@@ -95,53 +96,6 @@ const Initialization = () => {
</div>
),
},
{
title: '通知设置',
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label="通知方式"
name="type"
rules={[{ required: true, message: '请选择通知方式' }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder="请选择通知方式"
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={true}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
</Button>
</Form>
),
},
{
title: '账户设置',
content: (
@@ -197,6 +151,53 @@ const Initialization = () => {
</Form>
),
},
{
title: '通知设置',
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label="通知方式"
name="type"
rules={[{ required: true, message: '请选择通知方式' }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder="请选择通知方式"
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={true}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
</Button>
</Form>
),
},
{
title: '完成安装',
content: (
@@ -214,7 +215,7 @@ const Initialization = () => {
<Button
type="primary"
onClick={() => {
history.push('/login');
window.location.reload();
}}
>
+15 -5
View File
@@ -18,6 +18,7 @@ import { SharedContext } from '@/layouts';
const FormItem = Form.Item;
const { Countdown } = Statistic;
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
const Login = () => {
const { reloadUser } = useOutletContext<SharedContext>();
@@ -34,10 +35,8 @@ const Login = () => {
setWaitTime(null);
request
.post(`${config.apiPrefix}user/login`, {
data: {
username: values.username,
password: values.password,
},
})
.then((data) => {
checkResponse(data, values);
@@ -53,7 +52,8 @@ const Login = () => {
setVerifying(true);
request
.put(`${config.apiPrefix}user/two-factor/login`, {
data: { ...loginInfo, code: values.code },
...loginInfo,
code: values.code,
})
.then((data: any) => {
checkResponse(data);
@@ -104,6 +104,10 @@ const Login = () => {
password: values.password,
});
setTwoFactor(true);
} else if (code === 100) {
setTimeout(() => {
location.reload();
}, 1000);
}
};
@@ -169,10 +173,16 @@ const Login = () => {
) : (
<Form layout="vertical" onFinish={handleOk}>
<FormItem name="username" label="用户名" hasFeedback>
<Input placeholder="用户名" autoFocus />
<Input
placeholder={`用户名${isDemoEnv ? ': admin' : ''}`}
autoFocus
/>
</FormItem>
<FormItem name="password" label="密码" hasFeedback>
<Input type="password" placeholder="密码" />
<Input
type="password"
placeholder={`密码${isDemoEnv ? ': 123' : ''}`}
/>
</FormItem>
<Row>
{waitTime ? (
-4
View File
@@ -86,11 +86,9 @@ const EditModal = ({
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
request
.put(`${config.apiPrefix}scripts/run`, {
data: {
filename: cNode.title,
path: cNode.parent || '',
content,
},
})
.then(({ code, data }) => {
if (code === 200) {
@@ -106,11 +104,9 @@ const EditModal = ({
}
request
.put(`${config.apiPrefix}scripts/stop`, {
data: {
filename: cNode.title,
path: cNode.parent || '',
pid: currentPid,
},
})
.then(({ code, data }) => {
if (code === 200) {
+1 -3
View File
@@ -44,9 +44,7 @@ const EditScriptNameModal = ({
formData.append('content', '');
formData.append('directory', directory);
request
.post(`${config.apiPrefix}scripts`, {
data: formData,
})
.post(`${config.apiPrefix}scripts`, formData)
.then(({ code, data }) => {
if (code === 200) {
message.success(directory ? '新建文件夹成功' : '新建文件成功');
-4
View File
@@ -223,11 +223,9 @@ const Script = () => {
return new Promise((resolve, reject) => {
request
.put(`${config.apiPrefix}scripts`, {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
content,
},
})
.then(({ code, data }) => {
if (code === 200) {
@@ -341,9 +339,7 @@ const Script = () => {
const downloadFile = () => {
request
.post(`${config.apiPrefix}scripts/download`, {
data: {
filename: currentNode.title,
},
})
.then(({ code, data }) => {
if (code === 200) {

Some files were not shown because too many files have changed in this diff Show More