Compare commits

..
37 Commits
Author SHA1 Message Date
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
56 changed files with 2782 additions and 2619 deletions
+4 -3
View File
@@ -85,8 +85,6 @@ jobs:
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build: build:
if: github.ref != 'refs/heads/master'
needs: build-static needs: build-static
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -131,11 +129,14 @@ jobs:
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes # generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1 # nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: | tags: |
type=schedule,pattern=nightly type=schedule,pattern=nightly
type=edge type=edge
type=ref,event=branch
type=ref,event=pr 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={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
+7 -4
View File
@@ -1,26 +1,28 @@
import { defineConfig } from '@umijs/max'; import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
antd: {}, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, fastRefresh: true,
favicons: ['./images/favicon.svg'], favicons: [`https://qn.whyour.cn/favicon.svg`],
mfsu: { mfsu: {
strategy: 'eager', strategy: 'eager',
}, },
publicPath: process.env.NODE_ENV === 'production' ? './' : '/', publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
'/api/public': { [`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/api/public': '/api/' }, pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
}, },
'/api': { [`${baseUrl}api`]: {
target: 'http://127.0.0.1:5600/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
pathRewrite: { [`^${baseUrl}api`]: '/api' },
}, },
}, },
chainWebpack: ((config: any) => { chainWebpack: ((config: any) => {
@@ -38,6 +40,7 @@ export default defineConfig({
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
}, },
headScripts: [ 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/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', 'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
], ],
+50 -91
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"> <div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<h1 align="center">Qinglong</h1>
[简体中文](./README.md) | English
Timed task management platform supporting Python3, JavaScript, Shell, Typescript Timed task management platform supporting 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] [![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-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [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> </div>
[![](https://user-images.githubusercontent.com/22700758/229290661-03aabe84-8780-4ef0-8e75-2146f4636130.jpeg)](https://whyour.cn) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
[简体中文](./README.md) | English
## Features ## Features
@@ -38,62 +37,10 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
## Deployment ## Deployment
### Local Deployment ### Docker (Recommended)
```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
```bash ```bash
# curl -sSL get.docker.com | sh
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
@@ -103,17 +50,10 @@ docker run -dit \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### Docker-compose Deployment ### Docker-compose (Recommended)
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
```bash ```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 mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -123,9 +63,32 @@ docker-compose up -d
docker-compose down 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 ## Use
@@ -181,16 +144,6 @@ task -l <file_path>
* account_number: Specify the account number of an environment variable to be executed when the task is executed * 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 * max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Links
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Development ## Development
```bash ```bash
@@ -205,9 +158,15 @@ $ pnpm start
Open your browser and visit http://127.0.0.1:5700 Open your browser and visit http://127.0.0.1:5700
## Communication ## Links
[telegram channel](https://t.me/jiao_long) - [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Name Origin ## Name Origin
+54 -89
View File
@@ -1,30 +1,31 @@
<p align="center"> <div align="center">
<a href="https://github.com/whyour/qinglong"> <img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<h1 align="center">青龙</h1> <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-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [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> </div>
[![](https://user-images.githubusercontent.com/22700758/229290661-03aabe84-8780-4ef0-8e75-2146f4636130.jpeg)](https://whyour.cn) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
简体中文 | [English](./README-en.md)
## 功能 ## 功能
@@ -38,82 +39,25 @@
## 部署 ## 部署
### 本机部署 ### docker (推荐)
```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. 配置国内镜像源
```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. 启动容器
```bash ```bash
# curl -sSL get.docker.com | sh
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \ --restart unless-stopped \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### docker-compose 部署 ### docker-compose (推荐)
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash ```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 mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -123,9 +67,34 @@ docker-compose up -d
docker-compose down 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
```
## 使用 ## 使用
@@ -180,16 +149,6 @@ task -l <file_path>
* account_number: 任务执行时指定某个环境变量需要执行的账号序号 * account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 * max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 链接
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发 ## 开发
```bash ```bash
@@ -204,9 +163,15 @@ $ pnpm start
打开你的浏览器,访问 http://127.0.0.1:5700 打开你的浏览器,访问 http://127.0.0.1:5700
## 交流 ## 链接
[telegram频道](https://t.me/jiao_long) - [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 名称来源 ## 名称来源
+1 -2
View File
@@ -47,8 +47,7 @@ export default (app: Router) => {
if ( if (
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin'
envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
+5
View File
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
import multer from 'multer'; import multer from 'multer';
import path from 'path'; import path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit';
import config from '../config'; import config from '../config';
const route = Router(); const route = Router();
@@ -26,6 +27,10 @@ export default (app: Router) => {
route.post( route.post(
'/login', '/login',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}),
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
+5
View File
@@ -92,6 +92,11 @@ export default {
'/api/system', '/api/system',
'/api/user/init', '/api/user/init',
'/api/user/notification/init', '/api/user/notification/init',
'/open/user/login',
'/open/user/two-factor/login',
'/open/system',
'/open/user/init',
'/open/user/notification/init',
], ],
versionFile, versionFile,
lastVersionFile, lastVersionFile,
+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'),
);
}
+1 -1
View File
@@ -15,7 +15,7 @@ export class Dependence {
this.status = this.status =
typeof options.status === 'number' && DependenceStatus[options.status] typeof options.status === 'number' && DependenceStatus[options.status]
? options.status ? options.status
: DependenceStatus.installing; : DependenceStatus.queued;
this.type = options.type || DependenceTypes.nodejs; this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.name = options.name; this.name = options.name;
+2
View File
@@ -72,10 +72,12 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
export class WeWorkBotNotification extends NotificationBaseInfo { export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = ''; public weWorkBotKey = '';
public weWorkOrigin = '';
} }
export class WeWorkAppNotification extends NotificationBaseInfo { export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = ''; public weWorkAppKey = '';
public weWorkOrigin = '';
} }
export class AibotkNotification extends NotificationBaseInfo { export class AibotkNotification extends NotificationBaseInfo {
-7
View File
@@ -26,13 +26,6 @@ export interface AppToken {
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs'; export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {} interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' }, name: { type: DataTypes.STRING, unique: 'name' },
+3 -37
View File
@@ -10,16 +10,16 @@ import Container from 'typedi';
import OpenService from '../services/open'; import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user'; import UserService from '../services/user';
import handler from 'serve-handler';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import path from 'path';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
app.use(cors()); app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use( app.use(
@@ -31,27 +31,6 @@ export default ({ app }: { app: Application }) => {
}), }),
); );
app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
next();
} else {
return handler(req, res, {
public: path.join(config.rootPath, 'static/dist'),
rewrites: [{ source: '**', destination: '/index.html' }],
headers: [
{
source: 'index.html',
headers: [
{
key: 'Cache-Control',
value: 'no-cache',
},
],
},
],
});
}
});
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -130,8 +109,7 @@ export default ({ app }: { app: Application }) => {
if ( if (
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin'
envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
@@ -192,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( app.use(
( (
err: Error & { status: number }, err: Error & { status: number },
+29 -8
View File
@@ -13,10 +13,15 @@ const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/'); const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json'); const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
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 homedir = os.homedir(); const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh'); const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d'); const sshdPath = path.join(dataPath, 'ssh.d');
@@ -31,19 +36,14 @@ export default async () => {
const sshDirExist = await fileExist(sshPath); const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath); const bakDirExist = await fileExist(bakPath);
const sshdDirExist = await fileExist(sshdPath); const sshdDirExist = await fileExist(sshdPath);
const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
if (!configDirExist) { if (!configDirExist) {
fs.mkdirSync(configPath); fs.mkdirSync(configPath);
} }
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) { if (!scriptDirExist) {
fs.mkdirSync(scriptPath); fs.mkdirSync(scriptPath);
} }
@@ -52,6 +52,10 @@ export default async () => {
fs.mkdirSync(logPath); fs.mkdirSync(logPath);
} }
if (!tmpDirExist) {
fs.mkdirSync(tmpPath);
}
if (!uploadDirExist) { if (!uploadDirExist) {
fs.mkdirSync(uploadPath); fs.mkdirSync(uploadPath);
} }
@@ -68,6 +72,23 @@ export default async () => {
fs.mkdirSync(sshdPath); 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));
}
dotenv.config({ path: confFile }); dotenv.config({ path: confFile });
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
+7 -1
View File
@@ -10,12 +10,18 @@ export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile); const { version } = await parseVersion(config.versionFile);
Sentry.init({ Sentry.init({
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1, tracesSampleRate: 0.8,
release: version, release: version,
}); });
+2 -2
View File
@@ -2,7 +2,7 @@ import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { AddCronRequest, AddCronResponse } from '../protos/cron'; import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data'; import { scheduleStacks } from './data';
import { exec } from 'child_process'; import { runCron } from '../shared/runCron';
const addCron = ( const addCron = (
call: ServerUnaryCall<AddCronRequest, AddCronResponse>, call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
@@ -16,7 +16,7 @@ const addCron = (
scheduleStacks.set( scheduleStacks.set(
id, id,
nodeSchedule.scheduleJob(id, schedule, async () => { nodeSchedule.scheduleJob(id, schedule, async () => {
exec(`ID=${id} ${command}`); runCron(`ID=${id} ${command}`)
}), }),
); );
} }
+9 -4
View File
@@ -1,6 +1,5 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js'; import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health'; import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
import { exec } from 'child_process';
import config from '../config'; import config from '../config';
import { promiseExec } from '../config/util'; import { promiseExec } from '../config/util';
@@ -11,16 +10,22 @@ const check = async (
switch (call.request.service) { switch (call.request.service) {
case 'cron': case 'cron':
const res = await promiseExec( const res = await promiseExec(
`curl -sf http://localhost:${config.port}/api/system`, `curl -s http://localhost:${config.port}/api/system`,
); );
if (res.includes('200')) { if (res.includes('200')) {
return callback(null, { status: 1 }); return callback(null, { status: 1 });
} }
const errLog = await promiseExec(
const panelErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/panel-error.log`, `tail -n 300 ~/.pm2/logs/panel-error.log`,
); );
return callback(new Error(errLog)); const scheduleErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
);
return callback(
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`),
);
default: default:
return callback(null, { status: 1 }); return callback(null, { status: 1 });
+4 -1
View File
@@ -13,7 +13,10 @@ server.addService(CronService, { addCron, delCron });
server.bindAsync( server.bindAsync(
`localhost:${config.cronPort}`, `localhost:${config.cronPort}`,
ServerCredentials.createInsecure(), ServerCredentials.createInsecure(),
() => { (err, port) => {
if (err) {
throw err;
}
server.start(); server.start();
Logger.debug(`✌️ 定时服务启动成功!`); Logger.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
+65 -66
View File
@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; 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 fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { import {
@@ -16,10 +16,12 @@ import { Op, where, col as colFn, FindOptions } from 'sequelize';
import path from 'path'; import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const'; import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client'; import cronClient from '../schedule/client';
import { runWithCpuLimit } from '../shared/pLimit';
import { spawn } from 'cross-spawn';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) { } constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@@ -192,17 +194,13 @@ export default class CronService {
{ {
[operate2]: [ [operate2]: [
{ [operate]: `%${value}%` }, { [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` }, { [operate]: `%${encodeURI(value)}%` },
], ],
}, },
{ {
[operate2]: [ [operate2]: [
where(colFn(property), operate, `%${value}%`), where(colFn(property), operate, `%${value}%`),
where( where(colFn(property), operate, `%${encodeURI(value)}%`),
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
], ],
}, },
], ],
@@ -229,7 +227,7 @@ export default class CronService {
q[column] = { q[column] = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${textArray[1]}%` }, { [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` }, { [Op.like]: `%${encodeURI(textArray[1])}%` },
], ],
}; };
break; break;
@@ -237,7 +235,7 @@ export default class CronService {
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` }, { [Op.like]: `%${encodeURI(searchText)}%` },
], ],
}; };
q[Op.or] = [ q[Op.or] = [
@@ -365,10 +363,9 @@ export default class CronService {
{ status: CrontabStatus.queued }, { status: CrontabStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
); );
concurrentRun( ids.forEach((id) => {
ids.map((id) => async () => await this.runSingle(id)), this.runSingle(id);
10, });
);
} }
public async stop(ids: number[]) { public async stop(ids: number[]) {
@@ -390,63 +387,65 @@ export default class CronService {
} }
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return runWithCpuLimit(() => {
const cron = await this.getDb({ id: cronId }); return new Promise(async (resolve: any) => {
if (cron.status !== CrontabStatus.queued) { const cron = await this.getDb({ id: cronId });
resolve(); if (cron.status !== CrontabStatus.queued) {
return; resolve();
} return;
let { id, command, log_path } = cron;
const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath));
this.logger.silly('Running job');
this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
{ where: { id } },
);
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
} }
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', async (code, signal) => { let { id, command, log_path } = cron;
this.logger.info( const absolutePath = path.resolve(config.logPath, `${log_path}`);
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`, const logFileExist = log_path && (await fileExist(absolutePath));
);
}); this.logger.silly('Running job');
cp.on('close', async (code) => { this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.running, pid: cp.pid },
{ where: { id } }, { where: { id } },
); );
resolve(); cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
}
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
}); });
}); });
} }
+92 -98
View File
@@ -9,18 +9,19 @@ import {
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
} from '../data/dependence'; } from '../data/dependence';
import { spawn } from 'child_process'; import { spawn } from 'cross-spawn';
import SockService from './sock'; import SockService from './sock';
import { FindOptions, Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import { concurrentRun } from '../config/util'; import { concurrentRun } from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { runOneByOne, runWithCpuLimit } from '../shared/pLimit';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
constructor( constructor(
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async create(payloads: Dependence[]): Promise<Dependence[]> { public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
@@ -78,7 +79,7 @@ export default class DependenceService {
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] }; let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) { if (searchValue) {
const encodeText = encodeURIComponent(searchValue); const encodeText = encodeURI(searchValue);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchValue}%` }, { [Op.like]: `%${searchValue}%` },
@@ -104,20 +105,9 @@ export default class DependenceService {
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
concurrentRun( docs.forEach((dep) => {
docs.map((dep) => async () => { this.installOrUninstallDependency(dep, isInstall, force);
const status = isInstall });
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: dep.id } });
return await this.installOrUninstallDependencies(
[dep],
isInstall,
force,
);
}),
1,
);
} }
public async reInstall(ids: number[]): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
@@ -152,77 +142,34 @@ export default class DependenceService {
await DependenceModel.update({ log: newLog }, { where: { id: ids } }); await DependenceModel.update({ log: newLog }, { where: { id: ids } });
} }
public installOrUninstallDependencies( public installOrUninstallDependency(
dependencies: Dependence[], dependency: Dependence,
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
return new Promise(async (resolve) => { return runOneByOne(() => {
if (dependencies.length === 0) { return new Promise(async (resolve) => {
resolve(null); const depIds = [dependency.id!];
return; const status = isInstall
} ? DependenceStatus.installing
const socketMessageType = !force : DependenceStatus.removing;
? 'installDependence' await DependenceModel.update({ status }, { where: { id: depIds } });
: 'uninstallDependence';
const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[];
const startTime = dayjs();
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format( const socketMessageType = !force
'YYYY-MM-DD HH:mm:ss', ? 'installDependence'
)}\n\n`; : 'uninstallDependence';
this.sockService.sendMessage({ const depName = dependency.name;
type: socketMessageType, const depRunCommand = (
message, isInstall
references: depIds, ? InstallDependenceCommandTypes
}); : unInstallDependenceCommandTypes
await this.updateLog(depIds, message); )[dependency.type as any];
const actionText = isInstall ? '安装' : '删除';
const startTime = dayjs();
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`; )}\n\n`;
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: socketMessageType,
message, message,
@@ -230,24 +177,71 @@ export default class DependenceService {
}); });
await this.updateLog(depIds, message); await this.updateLog(depIds, message);
let status = null; const cp = spawn(`${depRunCommand} ${depName}`, {
if (isSucceed) { shell: '/bin/bash',
status = isInstall });
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
// 如果删除依赖成功或者强制删除 cp.stdout.on('data', async (data) => {
if ((isSucceed || force) && !isInstall) { this.sockService.sendMessage({
this.removeDb(depIds); type: socketMessageType,
} message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
resolve(null); cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
await this.updateLog(depIds, message);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
// 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) {
this.removeDb(depIds);
}
resolve(null);
});
}); });
}); });
} }
+1 -1
View File
@@ -121,7 +121,7 @@ export default class EnvService {
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> { public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURI(searchText);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
+88 -24
View File
@@ -93,7 +93,11 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
return typeof res.id === 'number'; if (typeof res.id === 'number') {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -109,7 +113,11 @@ export default class NotificationService {
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
}) })
.json(); .json();
return res.retcode === 0; if (res.retcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -128,7 +136,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .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) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -147,9 +159,14 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
return ( if (
res.content.result.length !== undefined && res.content.result.length > 0 res.content.result.length !== undefined &&
); res.content.result.length > 0
) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -166,7 +183,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
return res.success; if (res.success) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -190,7 +211,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
return res.code === 200; if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -234,7 +259,11 @@ export default class NotificationService {
agent, agent,
}) })
.json(); .json();
return !!res.ok; if (res.ok) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -263,15 +292,19 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
return res.errcode === 0; if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
} }
private async weWorkBot() { private async weWorkBot() {
const { weWorkBotKey } = this.params; const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`; const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
try { try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
@@ -284,17 +317,21 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
return res.errcode === 0; if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
} }
private async weWorkApp() { 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'] = const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(','); weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; const url = `${weWorkOrigin}/cgi-bin/gettoken`;
const tokenRes: any = await got const tokenRes: any = await got
.post(url, { .post(url, {
...this.gotOption, ...this.gotOption,
@@ -346,7 +383,7 @@ export default class NotificationService {
try { try {
const res: any = await got const res: any = await got
.post( .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, ...this.gotOption,
json: { json: {
@@ -359,7 +396,11 @@ export default class NotificationService {
) )
.json(); .json();
return res.errcode === 0; if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -403,8 +444,11 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
if (res.code === 0) {
return res.code === 0; return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -422,7 +466,11 @@ export default class NotificationService {
}) })
.json(); .json();
return res.ret === 0; if (res.ret === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -444,7 +492,11 @@ export default class NotificationService {
}) })
.json(); .json();
return res.code === 200; if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -468,7 +520,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
.json(); .json();
return res.StatusCode === 0; if (res.StatusCode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -495,7 +551,11 @@ export default class NotificationService {
transporter.close(); transporter.close();
return !!info.messageId; if (info.messageId) {
return true;
} else {
throw new Error(JSON.stringify(info));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
@@ -529,7 +589,11 @@ export default class NotificationService {
}; };
try { try {
const res = await got(formatUrl, options); 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) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
+1 -1
View File
@@ -75,7 +75,7 @@ export default class OpenService {
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURI(searchText);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
+60 -57
View File
@@ -1,7 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process'; import { ChildProcessWithoutNullStreams } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, LongIntervalJob,
@@ -9,6 +9,8 @@ import {
Task, Task,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { runWithCpuLimit } from '../shared/pLimit';
import { spawn } from 'cross-spawn';
interface ScheduleTaskType { interface ScheduleTaskType {
id: number; id: number;
@@ -40,73 +42,74 @@ export default class ScheduleService {
private maxBuffer = 200 * 1024 * 1024; private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
async runTask( async runTask(
command: string, command: string,
callbacks: TaskCallbacks = {}, callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end', completionTime: 'start' | 'end' = 'end',
) { ) {
return new Promise(async (resolve, reject) => { return runWithCpuLimit(() => {
try { return new Promise(async (resolve, reject) => {
const startTime = dayjs(); try {
await callbacks.onBefore?.(startTime); const startTime = dayjs();
await callbacks.onBefore?.(startTime);
const cp = spawn(command, { shell: '/bin/bash' }); const cp = spawn(command, { shell: '/bin/bash' });
// TODO: callbacks.onStart?.(cp, startTime);
callbacks.onStart?.(cp, startTime); completionTime === 'start' && resolve(cp.pid);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString()); await callbacks.onLog?.(data.toString());
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.info( this.logger.info(
'[执行任务失败] %s,时间:%s, 错误信息:%j', '[执行任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
);
await callbacks.onError?.(data.toString());
});
cp.on('error', async (err) => {
this.logger.error(
'[创建任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command, command,
new Date().toLocaleString(), new Date().toLocaleString(),
data.toString(), error,
); );
await callbacks.onError?.(data.toString()); await callbacks.onError?.(JSON.stringify(error));
}); }
});
cp.on('error', async (err) => { })
this.logger.error(
'[创建任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
await callbacks.onError?.(JSON.stringify(error));
}
});
} }
async createCronTask( async createCronTask(
@@ -126,12 +129,12 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
}), }),
); );
if (runImmediately) { if (runImmediately) {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
} }
} }
+3 -1
View File
@@ -60,7 +60,9 @@ export default class SshKeyService {
if (host === 'github.com') { if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`; 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` : ''; 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( const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
this.sshPath, this.sshPath,
alias, alias,
+9 -29
View File
@@ -6,12 +6,7 @@ import {
SubscriptionModel, SubscriptionModel,
SubscriptionStatus, SubscriptionStatus,
} from '../data/subscription'; } from '../data/subscription';
import { import { ChildProcessWithoutNullStreams } from 'child_process';
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs'; import fs from 'fs';
import { import {
getFileContentByName, getFileContentByName,
@@ -20,6 +15,7 @@ import {
createFile, createFile,
killTask, killTask,
handleLogPath, handleLogPath,
promiseExec,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { FindOptions, Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
@@ -47,7 +43,7 @@ export default class SubscriptionService {
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` }, { [Op.like]: `%${encodeURI(searchText)}%` },
], ],
}; };
query = { query = {
@@ -110,18 +106,6 @@ export default class SubscriptionService {
this.sshKeyService.setSshConfig(docs); 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 taskCallbacks(doc: Subscription): TaskCallbacks { private taskCallbacks(doc: Subscription): TaskCallbacks {
return { return {
onBefore: async (startTime) => { onBefore: async (startTime) => {
@@ -144,7 +128,7 @@ export default class SubscriptionService {
try { try {
if (doc.sub_before) { if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`); fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before); beforeStr = await promiseExec(doc.sub_before);
} }
} catch (error: any) { } catch (error: any) {
beforeStr = beforeStr =
@@ -171,7 +155,7 @@ export default class SubscriptionService {
try { try {
if (sub.sub_after) { if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`); fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after); afterStr = await promiseExec(sub.sub_after);
} }
} catch (error: any) { } catch (error: any) {
afterStr = afterStr =
@@ -290,10 +274,9 @@ export default class SubscriptionService {
{ status: SubscriptionStatus.queued }, { status: SubscriptionStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
); );
concurrentRun( ids.forEach((id) => {
ids.map((id) => async () => await this.runSingle(id)), this.runSingle(id);
10, });
);
} }
public async stop(ids: number[]) { public async stop(ids: number[]) {
@@ -330,10 +313,7 @@ export default class SubscriptionService {
const command = formatCommand(subscription); const command = formatCommand(subscription);
await this.scheduleService.runTask( this.scheduleService.runTask(command, this.taskCallbacks(subscription));
command,
this.taskCallbacks(subscription),
);
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
+1 -1
View File
@@ -6,7 +6,7 @@ import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'cross-spawn';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; import got from 'got';
import { import {
-1
View File
@@ -15,7 +15,6 @@ import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import { Request } from 'express'; import { Request } from 'express';
import ScheduleService from './schedule'; import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
+17
View File
@@ -0,0 +1,17 @@
import pLimit from "p-limit";
import os from 'os';
const cpuLimit = pLimit(os.cpus().length);
const oneLimit = pLimit(1);
export function runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
return cpuLimit(() => {
return fn();
});
}
export function runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
return oneLimit(() => {
return fn();
});
}
+37
View File
@@ -0,0 +1,37 @@
import { spawn } from 'cross-spawn';
import { runWithCpuLimit } from "./pLimit";
import Logger from '../loaders/logger';
export function runCron(cmd: string): Promise<number> {
return 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();
});
});
})
}
+6 -5
View File
@@ -1,14 +1,15 @@
version: '2' version: '2'
services: services:
web: web:
# alpine 基础镜像版本
image: whyour/qinglong:latest image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes: volumes:
- ./data:/ql/data - ./data:/ql/data
ports: ports:
- "0.0.0.0:5700:5700" - "0.0.0.0:5700:5700"
environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:5400/api/health", "||", "exit", "1"]
interval: 2m
timeout: 10s
retries: 3
+7 -6
View File
@@ -14,10 +14,9 @@ map $http_upgrade $connection_upgrade {
server { server {
listen 5700; listen 5700;
IPV6_CONFIG IPV6_CONFIG
root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location QL_BASE_URL/api/public/ { location QL_BASE_URLapi/public/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -25,7 +24,7 @@ server {
proxy_buffering off; proxy_buffering off;
} }
location QL_BASE_URL/api/ { location QL_BASE_URLapi/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -36,7 +35,7 @@ server {
proxy_set_header Connection $connection_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 Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -52,10 +51,12 @@ server {
gzip_comp_level 6; gzip_comp_level 6;
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.0; 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; index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html; try_files $uri QL_BASE_URLindex.html;
} }
location ~ .*\.(html)$ { location ~ .*\.(html)$ {
+3
View File
@@ -5,6 +5,7 @@ module.exports = {
max_restarts: 10, max_restarts: 10,
kill_timeout: 15000, kill_timeout: 15000,
wait_ready: true, wait_ready: true,
listen_timeout: 10000,
source_map_support: true, source_map_support: true,
time: true, time: true,
script: 'static/build/schedule/index.js', script: 'static/build/schedule/index.js',
@@ -14,6 +15,7 @@ module.exports = {
max_restarts: 10, max_restarts: 10,
kill_timeout: 15000, kill_timeout: 15000,
wait_ready: true, wait_ready: true,
listen_timeout: 10000,
source_map_support: true, source_map_support: true,
time: true, time: true,
script: 'static/build/public.js', script: 'static/build/public.js',
@@ -23,6 +25,7 @@ module.exports = {
max_restarts: 10, max_restarts: 10,
kill_timeout: 15000, kill_timeout: 15000,
wait_ready: true, wait_ready: true,
listen_timeout: 10000,
source_map_support: true, source_map_support: true,
time: true, time: true,
script: 'static/build/app.js', script: 'static/build/app.js',
+4
View File
@@ -64,10 +64,12 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.2.1", "cron-parser": "^4.2.1",
"cross-spawn": "^7.0.3",
"dayjs": "^1.11.2", "dayjs": "^1.11.2",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-rate-limit": "^6.7.0",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
@@ -81,6 +83,7 @@
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-limit": "3.1.0",
"protobufjs": "^7.2.3", "protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@@ -102,6 +105,7 @@
"@sentry/react": "^7.12.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
+1802 -1861
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

+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_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE 'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_ORIGIN': '', # 企业微信代理地址
'QYWX_AM': '', # 企业微信应用 'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人 'QYWX_KEY': '', # 企业微信机器人
@@ -164,8 +166,7 @@ def dingding_bot(title: str, content: str) -> None:
timestamp = str(round(time.time() * 1000)) timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8") secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format( string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8") string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new( hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256 secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
@@ -231,8 +232,11 @@ def gotify(title: str, content: str) -> None:
print("gotify 服务启动") print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}' url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title, "message": content, data = {
"priority": push_config.get("GOTIFY_PRIORITY")} "title": title,
"message": content,
"priority": push_config.get("GOTIFY_PRIORITY"),
}
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if response.get("id"): if response.get("id"):
@@ -291,9 +295,13 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送") print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return return
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", data = {
"pushkey": push_config.get("DEER_KEY")} "text": title,
url = 'https://api2.pushdeer.com/message/push' "desp": content,
"type": "markdown",
"pushkey": push_config.get("DEER_KEY"),
}
url = "https://api2.pushdeer.com/message/push"
if push_config.get("DEER_URL"): if push_config.get("DEER_URL"):
url = 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取消推送") print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return return
print("chat 服务启动") 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") url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data) response = requests.post(url, data=data)
@@ -347,11 +355,9 @@ def pushplus_bot(title: str, content: str) -> None:
print("PUSHPLUS 推送成功!") print("PUSHPLUS 推送成功!")
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json" headers["Accept"] = "application/json"
response = requests.post( response = requests.post(url=url_old, data=body, headers=headers).json()
url=url_old, data=body, headers=headers).json()
if response["code"] == 200: if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!") print("PUSHPLUS(hxtrip) 推送成功!")
@@ -370,8 +376,7 @@ def qmsg_bot(title: str, content: str) -> None:
print("qmsg 服务启动") print("qmsg 服务启动")
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}' url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
payload = { payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
response = requests.post(url=url, params=payload).json() response = requests.post(url=url, params=payload).json()
if response["code"] == 0: if response["code"] == 0:
@@ -420,9 +425,12 @@ class WeCom:
self.CORPID = corpid self.CORPID = corpid
self.CORPSECRET = corpsecret self.CORPSECRET = corpsecret
self.AGENTID = agentid 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): def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" url = f"{self.ORIGIN}/cgi-bin/gettoken"
values = { values = {
"corpid": self.CORPID, "corpid": self.CORPID,
"corpsecret": self.CORPSECRET, "corpsecret": self.CORPSECRET,
@@ -432,10 +440,7 @@ class WeCom:
return data["access_token"] return data["access_token"]
def send_text(self, message, touser="@all"): def send_text(self, message, touser="@all"):
send_url = ( send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = { send_values = {
"touser": touser, "touser": touser,
"msgtype": "text", "msgtype": "text",
@@ -449,10 +454,7 @@ class WeCom:
return respone["errmsg"] return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"): def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = ( send_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}"
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = { send_values = {
"touser": touser, "touser": touser,
"msgtype": "mpnews", "msgtype": "mpnews",
@@ -485,7 +487,11 @@ def wecom_bot(title: str, content: str) -> None:
return return
print("企业微信机器人服务启动") 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"} headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}} data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post( 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取消推送") print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return return
print("智能微秘书 服务启动") 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" url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = { data = {
"apiKey": push_config.get("AIBOTK_KEY"), "apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"), "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: else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact" url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = { data = {
"apiKey": push_config.get("AIBOTK_KEY"), "apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"), "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") body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
@@ -580,29 +590,52 @@ def smtp(title: str, content: str) -> None:
""" """
使用 SMTP 邮件 推送消息。 使用 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"): if (
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送") 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 return
print("SMTP 邮件 服务启动") print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8') message = MIMEText(content, "plain", "utf-8")
message['From'] = formataddr((Header(push_config.get( message["From"] = formataddr(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL"))) (
message['To'] = formataddr((Header(push_config.get( Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL"))) push_config.get("SMTP_EMAIL"),
message['Subject'] = Header(title, 'utf-8') )
)
message["To"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["Subject"] = Header(title, "utf-8")
try: try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get( smtp_server = (
"SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER")) smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"), if push_config.get("SMTP_SSL") == "true"
push_config.get("SMTP_PASSWORD")) else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), )
push_config.get("SMTP_EMAIL"), message.as_bytes()) 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() smtp_server.close()
print("SMTP 邮件 推送成功!") print("SMTP 邮件 推送成功!")
except Exception as e: except Exception as e:
print(f'SMTP 邮件 推送失败!{e}') print(f"SMTP 邮件 推送失败!{e}")
def one() -> str: def one() -> str:
@@ -645,9 +678,19 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot) notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) 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) 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) notify_function.append(smtp)
@@ -659,7 +702,7 @@ def send(title: str, content: str) -> None:
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔 # 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE") skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle: if skipTitle:
if (title in re.split("\n", skipTitle)): if title in re.split("\n", skipTitle):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!") print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return return
@@ -669,8 +712,7 @@ def send(title: str, content: str) -> None:
content += "\n\n" + text content += "\n\n" + text
ts = [ ts = [
threading.Thread(target=mode, args=( threading.Thread(target=mode, args=(title, content), name=mode.__name__)
title, content), name=mode.__name__)
for mode in notify_function for mode in notify_function
] ]
[t.start() for t in ts] [t.start() for t in ts]
+15 -15
View File
@@ -30,10 +30,10 @@ add_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}') local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule="$1"
local command=$2 local command="$2"
local name=$3 local name="$3"
local sub_id=$4 local sub_id="$4"
fi fi
if [[ ! $sub_id ]];then if [[ ! $sub_id ]];then
@@ -69,10 +69,10 @@ update_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}') local id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule="$1"
local command=$2 local command="$2"
local name=$3 local name="$3"
local id=$4 local id="$4"
fi fi
local api=$( local api=$(
@@ -103,8 +103,8 @@ update_cron_command_api() {
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
local command=$1 local command="$1"
local id=$2 local id="$2"
fi fi
local api=$( local api=$(
@@ -130,7 +130,7 @@ update_cron_command_api() {
} }
del_cron_api() { del_cron_api() {
local ids=$1 local ids="$1"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
@@ -183,8 +183,8 @@ update_cron() {
} }
notify_api() { notify_api() {
local title=$1 local title="$1"
local content=$2 local content="$2"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \ 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) code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功🎉"
else else
echo -e "通知失败(${message})" echo -e "通知失败(${message})"
fi fi
} }
find_cron_api() { find_cron_api() {
local params=$1 local params="$1"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
+2 -2
View File
@@ -38,7 +38,7 @@ gen_array_scripts() {
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++ let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g") 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]="<未识别出活动名称>" [[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi fi
done done
@@ -193,7 +193,7 @@ run_else() {
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//.\/}
fi fi
shift shift
+15 -9
View File
@@ -35,7 +35,6 @@ file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js 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_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py dep_notify_py=$dir_dep/notify.py
@@ -68,7 +67,7 @@ import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""} ql_base_url=${QlBaseUrl:-"/"}
command_timeout_time=${CommandTimeoutTime:-""} command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
@@ -468,7 +467,19 @@ patch_version() {
init_nginx() { init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.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) ipv6=$(ip a | grep inet6)
ipv6Str="" ipv6Str=""
@@ -485,11 +496,6 @@ handle_task_before() {
[[ $is_macos -eq 0 ]] && check_server [[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
cat $task_error_log_path
echo -e "加载 config.sh 出错,请手动检查"
fi
. $file_task_before "$@" . $file_task_before "$@"
} }
@@ -513,4 +519,4 @@ detect_termux
detect_macos detect_macos
define_cmd define_cmd
import_config $1 2>$task_error_log_path import_config $1
+12 -19
View File
@@ -31,7 +31,7 @@ output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有${type}的定时任务:\n"
cat $list cat $list
echo echo
fi fi
@@ -44,7 +44,7 @@ del_cron() {
local path=$2 local path=$2
local detail="" local detail=""
local ids="" local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n" echo -e "开始尝试自动删除失效的定时任务..."
for cron in $(cat $list_drop); do 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 | 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 if [[ $ids ]]; then
@@ -54,7 +54,7 @@ del_cron() {
fi fi
cron_file="$dir_scripts/${cron}" cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then 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 rm -f $cron_file
fi fi
[[ -z $cron_name ]] && cron_name="$cron" [[ -z $cron_name ]] && cron_name="$cron"
@@ -75,7 +75,7 @@ del_cron() {
add_cron() { add_cron() {
local list_add=$1 local list_add=$1
local path=$2 local path=$2
echo -e "开始尝试自动添加定时任务...\n" echo -e "开始尝试自动添加定时任务..."
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
@@ -92,7 +92,7 @@ add_cron() {
s| | |g; s| | |g;
}" | sort -u | head -1 }" | 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_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 ":" '{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) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
@@ -193,7 +193,7 @@ update_raw() {
s| | |g; s| | |g;
}" | sort -u | head -1 }" | 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_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 ":" '{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) [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
@@ -243,10 +243,9 @@ update_qinglong() {
echo -e "使用 ${mirror} 源更新...\n" echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a)
local primary_branch="master" local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="${current_branch}" primary_branch="develop"
fi fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json) [[ -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} reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
@@ -364,10 +363,10 @@ gen_list_repo() {
done done
files=$(eval $cmd | sed 's/^..//') files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then if [[ $path ]]; then
files=$(echo "$files" | egrep $path) files=$(echo "$files" | egrep "$path")
fi fi
if [[ $blackword ]]; then if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword) files=$(echo "$files" | egrep -v "$blackword")
fi fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}" cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
@@ -375,7 +374,7 @@ gen_list_repo() {
if [[ $dependence ]]; then if [[ $dependence ]]; then
cd ${repo_path} cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence) results=$(eval $cmd | sed 's/^..//' | egrep "$dependence")
for _file in ${results}; do for _file in ${results}; do
file_path=$(dirname $_file) file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}" make_dir "${dir_scripts}/${uniq_path}/${file_path}"
@@ -388,7 +387,7 @@ gen_list_repo() {
fi fi
for file in ${files}; do for file in ${files}; do
filename=$(basename $file) filename=$(basename "$file")
cp -f $file "$dir_scripts/${uniq_path}/${filename}" cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list" 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}') # 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}')
@@ -457,12 +456,6 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi 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 if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd eval echo -e "请移除 -l 参数" $cmd
exit 1 exit 1
+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,
};
}
+34 -2
View File
@@ -332,8 +332,20 @@ select:-webkit-autofill:focus {
} }
.ant-pro-sider-logo { .ant-pro-sider-logo {
h1 { .title {
margin-left: 5px !important; 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;
a {
display: inline-flex;
align-items: center;
}
} }
img { img {
@@ -347,6 +359,10 @@ select:-webkit-autofill:focus {
// 移动端logo被拉伸 // 移动端logo被拉伸
width: auto !important; width: auto !important;
} }
.ant-image {
display: inline-flex;
}
} }
pre { pre {
@@ -386,3 +402,19 @@ pre {
.ant-table-filter-column { .ant-table-filter-column {
justify-content: unset; 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;
}
+31 -29
View File
@@ -280,39 +280,41 @@ export default function () {
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />} logo={
// @ts-ignore
title={
<> <>
<span style={{ fontSize: 16, marginRight: 5 }}></span> <Image preview={false} src="https://qn.whyour.cn/logo.png" />
<a <div className="title">
href={systemInfo?.changeLogLink} <span className="title"></span>
target="_blank" <a
rel="noopener noreferrer" href={systemInfo?.changeLogLink}
onClick={(e) => { target="_blank"
e.stopPropagation(); rel="noopener noreferrer"
}} onClick={(e) => {
> e.stopPropagation();
<Tooltip }}
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
> >
<Badge size="small" dot={systemInfo?.branch === 'develop'}> <Tooltip
<span title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
style={{ >
fontSize: isFirefox ? 9 : 12, <Badge size="small" dot={systemInfo?.branch === 'develop'}>
color: '#666', <span
marginLeft: 2, style={{
zoom: isSafari ? 0.66 : 0.8, fontSize: isFirefox ? 9 : 12,
letterSpacing: isQQBrowser ? -2 : 0, color: '#666',
}} marginLeft: 2,
> zoom: isSafari ? 0.66 : 0.8,
v{systemInfo?.version} letterSpacing: isQQBrowser ? -2 : 0,
</span> }}
</Badge> >
</Tooltip> v{systemInfo?.version}
</a> </span>
</Badge>
</Tooltip>
</a>
</div>
</> </>
} }
title={false}
menuItemRender={(menuItemProps: any, defaultDom: any) => { menuItemRender={(menuItemProps: any, defaultDom: any) => {
if ( if (
menuItemProps.isUrl || menuItemProps.isUrl ||
+11 -7
View File
@@ -824,12 +824,6 @@ const Crontab = () => {
}, [viewConf, enabledCronViews]); }, [viewConf, enabledCronViews]);
useEffect(() => { useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
getCronViews(); getCronViews();
}, []); }, []);
@@ -889,7 +883,17 @@ const Crontab = () => {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setCronViews(data); 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(() => { .finally(() => {
+2 -2
View File
@@ -253,9 +253,9 @@ const ViewCreateModal = ({
name={[name, 'value']} name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]} rules={[{ required: true, message: '请输入内容' }]}
> >
{EOperation[filtersValue[name]['operation']] === {EOperation[filtersValue?.[name]['operation']] ===
'select' ? ( 'select' ? (
statusElement(filtersValue[name]['property']) statusElement(filtersValue?.[name]['property'])
) : ( ) : (
<Input placeholder="请输入内容" /> <Input placeholder="请输入内容" />
)} )}
+10 -3
View File
@@ -75,6 +75,10 @@ const ViewManageModal = ({
handleCancel: () => void; handleCancel: () => void;
cronViewChange: (data?: any) => void; cronViewChange: (data?: any) => void;
}) => { }) => {
const islastEnableView = (record) => {
return list.filter((x) => !x.isDisabled).length <= 1 && !record.isDisabled;
};
const columns: any = [ const columns: any = [
{ {
title: '名称', title: '名称',
@@ -95,6 +99,7 @@ const ViewManageModal = ({
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
<Switch <Switch
disabled={islastEnableView(record)}
checked={!record.isDisabled} checked={!record.isDisabled}
onChange={(checked) => onShowChange(checked, record, index)} onChange={(checked) => onShowChange(checked, record, index)}
/> />
@@ -111,9 +116,11 @@ const ViewManageModal = ({
<a onClick={() => editView(record, index)}> <a onClick={() => editView(record, index)}>
<EditOutlined /> <EditOutlined />
</a> </a>
<a onClick={() => deleteView(record, index)}> {!islastEnableView(record) && (
<DeleteOutlined /> <a onClick={() => deleteView(record, index)}>
</a> <DeleteOutlined />
</a>
)}
</Space> </Space>
) : ( ) : (
'-' '-'
+11 -10
View File
@@ -17,9 +17,9 @@ const Error = () => {
needLoading && setLoading(true); needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}public/health`) .get(`${config.apiPrefix}public/health`)
.then(({ error, status }) => { .then(({ error, data }) => {
if (status === 1) { if (data?.status === 1) {
return reloadUser(); return;
} }
if (retryTimes.current > 3) { if (retryTimes.current > 3) {
setData(error?.details); setData(error?.details);
@@ -59,18 +59,19 @@ const Error = () => {
} }
description={ description={
<Typography.Text type="danger"> <Typography.Text type="danger">
<div> <div></div>
<Typography.Link href="https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml">
issue
</Typography.Link>
</div>
<div> <div>
1. 宿 docker run --rm -v 1. 宿 docker run --rm -v
/var/run/docker.sock:/var/run/docker.sock /var/run/docker.sock:/var/run/docker.sock
containrrr/watchtower -cR &lt;&gt; containrrr/watchtower -cR &lt;&gt;
</div> </div>
<div>2. ql -l checkql -l update</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> </Typography.Text>
} }
banner banner
@@ -80,7 +81,7 @@ const Error = () => {
</Typography.Paragraph> </Typography.Paragraph>
</div> </div>
) : ( ) : (
<PageLoading tip="启动中,请稍后..." /> <PageLoading style={{ paddingTop: 0 }} tip="启动中,请稍后..." />
)} )}
</div> </div>
); );
+2 -1
View File
@@ -57,10 +57,11 @@
display: flex; display: flex;
max-width: 500px; max-width: 500px;
width: 90%; width: 90%;
height: 350px; height: 400px;
.ant-steps { .ant-steps {
width: 35%; width: 35%;
min-width: 110px;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
+47 -47
View File
@@ -100,53 +100,6 @@ const Initialization = () => {
</div> </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: '账户设置', title: '账户设置',
content: ( content: (
@@ -202,6 +155,53 @@ const Initialization = () => {
</Form> </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: '完成安装', title: '完成安装',
content: ( content: (
+13 -2
View File
@@ -18,6 +18,7 @@ import { SharedContext } from '@/layouts';
const FormItem = Form.Item; const FormItem = Form.Item;
const { Countdown } = Statistic; const { Countdown } = Statistic;
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
const Login = () => { const Login = () => {
const { reloadUser } = useOutletContext<SharedContext>(); const { reloadUser } = useOutletContext<SharedContext>();
@@ -104,6 +105,10 @@ const Login = () => {
password: values.password, password: values.password,
}); });
setTwoFactor(true); setTwoFactor(true);
} else if (code === 100) {
setTimeout(() => {
location.reload();
}, 1000);
} }
}; };
@@ -169,10 +174,16 @@ const Login = () => {
) : ( ) : (
<Form layout="vertical" onFinish={handleOk}> <Form layout="vertical" onFinish={handleOk}>
<FormItem name="username" label="用户名" hasFeedback> <FormItem name="username" label="用户名" hasFeedback>
<Input placeholder="用户名" autoFocus /> <Input
placeholder={`用户名${isDemoEnv ? ': admin' : ''}`}
autoFocus
/>
</FormItem> </FormItem>
<FormItem name="password" label="密码" hasFeedback> <FormItem name="password" label="密码" hasFeedback>
<Input type="password" placeholder="密码" /> <Input
type="password"
placeholder={`密码${isDemoEnv ? ': 123' : ''}`}
/>
</FormItem> </FormItem>
<Row> <Row>
{waitTime ? ( {waitTime ? (
+18 -5
View File
@@ -33,6 +33,7 @@ import { SharedContext } from '@/layouts';
import './index.less'; import './index.less';
const { Text } = Typography; const { Text } = Typography;
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
const Setting = () => { const Setting = () => {
const { const {
@@ -252,6 +253,12 @@ const Setting = () => {
}); });
}; };
useEffect(() => {
if (isDemoEnv) {
getApps();
}
}, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container" className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
@@ -275,11 +282,17 @@ const Setting = () => {
tabPosition="top" tabPosition="top"
onChange={tabChange} onChange={tabChange}
items={[ items={[
{ ...(!isDemoEnv
key: 'security', ? [
label: '安全设置', {
children: <SecuritySettings user={user} userChange={reloadUser} />, key: 'security',
}, label: '安全设置',
children: (
<SecuritySettings user={user} userChange={reloadUser} />
),
},
]
: []),
{ {
key: 'app', key: 'app',
label: '应用设置', label: '应用设置',
+11 -1
View File
@@ -1,6 +1,8 @@
const baseUrl = window.__ENV__QlBaseUrl || '/';
export default { export default {
siteName: '青龙', siteName: '青龙',
apiPrefix: '/api/', apiPrefix: `${baseUrl}api/`,
authKey: 'token', authKey: 'token',
/* Layout configuration, specify which layout to use for route. */ /* Layout configuration, specify which layout to use for route. */
@@ -194,6 +196,10 @@ export default {
tip: '企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa', tip: '企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa',
required: true, required: true,
}, },
{
label: 'weWorkOrigin',
tip: '企业微信代理地址',
},
], ],
weWorkApp: [ weWorkApp: [
{ {
@@ -201,6 +207,10 @@ export default {
tip: 'corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat', tip: 'corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat',
required: true, required: true,
}, },
{
label: 'weWorkOrigin',
tip: '企业微信代理地址',
},
], ],
aibotk: [ aibotk: [
{ {
+10 -3
View File
@@ -4,7 +4,7 @@ import config from './config';
import { history } from '@umijs/max'; import { history } from '@umijs/max';
message.config({ message.config({
duration: 1.5, duration: 2,
}); });
const time = Date.now(); const time = Date.now();
@@ -23,7 +23,10 @@ const errorHandler = function (error: any) {
history.push('/login'); history.push('/login');
} }
} else { } else {
message.error(msg); message.error({
content: msg,
style: { maxWidth: 500, margin: '0 auto' },
});
} }
} else { } else {
console.log(error.message); console.log(error.message);
@@ -66,7 +69,11 @@ _request.interceptors.response.use(async (response) => {
const res = await response.clone().json(); const res = await response.clone().json();
if (res.code !== 200) { if (res.code !== 200) {
const msg = res.message || res.data; const msg = res.message || res.data;
msg && message.error(msg); msg &&
message.error({
content: msg,
style: { maxWidth: 500, margin: '0 auto' },
});
} }
return res; return res;
} }
+5
View File
@@ -10,3 +10,8 @@ declare module '*.svg' {
} }
declare module 'pstree.remy'; declare module 'pstree.remy';
interface Window {
__ENV__QlBaseUrl: string;
__ENV__DeployEnv: string;
}
+10 -4
View File
@@ -1,5 +1,11 @@
version: 2.15.14 version: 2.15.16
changeLogLink: https://t.me/jiao_long/374 changeLogLink: https://t.me/jiao_long/378
changeLog: | changeLog: |
1. 接口 system/command-run 返回响应头 QL-Task-Pidsystem/command-stop 支持 pid 参数 1. 企业微信通知增加代理地址配置 QYWX_ORIGIN
2. 其他 bug 修复 2. 重构任务并发执行逻辑,依赖并发安装逻辑
3. 修复关闭全部任务视图,默认视图筛选错误
4. 增加初始化文件写入,修复参数含有空格影响 task/ql 命令
5. 修复初始化界面、侧边栏、错误页样式
6. 修复拉取订阅文件包含空格出错
7. 修改 api 限流策略,修复检查检查日志
8. 修复环境变量列表搜索字符转码