Compare commits

..
61 Commits
Author SHA1 Message Date
whyour ddee69d291 更新版本 v2.10.13 2021-12-25 21:31:52 +08:00
whyour 28960c9fab 添加sentry-cli cdn 2021-12-25 20:55:13 +08:00
whyour 449301e519 修改热更新目录 2021-12-25 20:13:49 +08:00
whyour 28dab3dc2f 修改两步验证界面逻辑 2021-12-25 20:09:02 +08:00
whyour 7995d98db5 修改批量运行任务逻辑 2021-12-24 23:31:11 +08:00
whyour 1f319f3533 调试脚本增加停止,修复调试交互 2021-12-23 23:25:39 +08:00
whyour 7d2dbaa62d 修改文本 2021-12-23 13:39:16 +08:00
whyour ed5999526b 优化初始化通知设置交互 2021-12-21 23:49:52 +08:00
whyour 2409f75491 修复登录退出接口路径 2021-12-21 23:42:05 +08:00
whyour 0c840095d1 增加系统openapi 2021-12-21 23:22:34 +08:00
whyour 3eaf0b4856 system接口增加版本号,应用权限增加依赖管理 2021-12-21 22:53:19 +08:00
琚涛涛andGitHub 394735a5d9 优化定时任务名称展示效果,提升用户体验 (#1017) 2021-12-21 22:39:17 +08:00
whyour 5bfa10bfe7 修复任务跳转脚本参数,task运行脚本支持更多参数 2021-12-21 00:23:20 +08:00
whyour 3f12d9cbd5 更新版本号 v2.10.12 2021-12-09 23:24:10 +08:00
whyour 54d89754df 修复darkreader最新版bug 2021-12-09 23:22:17 +08:00
whyour 9470524615 更新readme 2021-12-06 23:42:14 +08:00
whyour b5faa6eaaa 修复logo样式 2021-12-06 11:18:19 +08:00
whyour eab476cfe8 更新版本号 v2.10.11 2021-12-05 23:15:49 +08:00
whyour b773def012 修复搜索环境变量urlencode 2021-12-05 23:04:26 +08:00
whyour a9f8b23a7a 修复内容区高度和logo样式 2021-11-29 23:02:42 +08:00
whyour 61bb6535fb 修复最后运行时长被覆盖 2021-11-29 21:49:32 +08:00
雪狐andGitHub eb0bdffe06 避免环境变量中出现"字符的时候提前截断字符串 (#966) 2021-11-29 21:43:29 +08:00
whyour e129485285 更新版本 v2.10.10 2021-11-27 20:43:18 +08:00
colinxuandGitHub aac7de172e 修复使用旧pushplus推送后结果解析失败的问题 (#958) 2021-11-27 20:33:49 +08:00
whyour e19d2412fc 检测更新增加强制更新 2021-11-27 20:19:49 +08:00
whyour 7cba2cf76c 依赖目录通知文件没有时,拷贝示例的通知文件 2021-11-27 17:41:26 +08:00
whyour 55d7a49dbc 修复deps映射目录 2021-11-27 17:19:50 +08:00
whyour f695864fd8 修复build args 2021-11-27 17:12:45 +08:00
whyour ae4883fbe7 测试yml 2021-11-27 17:05:55 +08:00
whyour 836dfd2861 添加deps目录软链 2021-11-27 16:20:24 +08:00
whyour 6e0523c6d7 修复删除JavaScript进程,修改依赖重新安装逻辑 2021-11-27 13:41:38 +08:00
whyour cd9ba084ae 修复调试运行时文件路径 2021-11-23 23:36:43 +08:00
phoenixandGitHub c8a0c0de67 fix nginx can not run without /run/nginx (#944)
alpine3.12 和 alpine3.14 兼容性问题
2021-11-23 18:32:22 +08:00
whyour 5dd8d9df91 更新版本 v2.10.9 2021-11-22 22:27:15 +08:00
whyour 2dbee94e85 默认拷贝deps目录下所有文件 2021-11-22 22:20:13 +08:00
风之凌殇andGitHub 9001797037 表格选择pageSize增加200/500/1000 (#934) 2021-11-22 19:26:02 +08:00
kilo5hzandGitHub 5e7104d5a3 系统通知增加gotify (#936) 2021-11-22 19:23:40 +08:00
whyour 7c33b22d63 修改docker基础镜像为alpine3.12,解决arm32无法启动 2021-11-22 19:22:39 +08:00
whyour b7cb1e379c 更新readme 2021-11-21 22:26:22 +08:00
whyour 274a6ce52e 修复调试功能 2021-11-20 01:06:25 +08:00
kilo5hzandwhyour 8079e4e7b8 点击任务名跳转脚本管理编辑脚本 (#929)
* 增加捷径:点击任务名跳转脚本管理编辑脚本

* 修正树节点选中,父节点自动展开
2021-11-20 01:06:25 +08:00
whyour 920efb8fab 更新readme图片链接 2021-11-20 01:06:25 +08:00
whyour 92aeb2b9da 更新readme 2021-11-20 01:06:25 +08:00
whyour ced76cf343 更新readme 2021-11-20 01:06:25 +08:00
whyour 3750d6ecd5 增加初始化依赖目录及文件 2021-11-20 01:06:25 +08:00
风之凌殇andwhyour 7444f30384 移除dockerfile中不必要的环境变量 (#925)
* 移除上次pr新增的不必要的环境变量
2021-11-20 01:06:16 +08:00
whyour b0fd5ba47f 更新readme 2021-11-17 19:22:40 +08:00
whyour cc098bc2e5 修复拷贝deps已存在依赖 2021-11-17 10:23:45 +08:00
whyour 1cfcf8d194 更新版本 v2.10.8 2021-11-16 14:36:13 +08:00
whyour d1e9f741d0 修复脚本管理删除、新建增加选择父目录 2021-11-16 14:18:02 +08:00
kilo5hzandGitHub 0cde9d07ee 增加gotify推送 (#905)
* 增加gotify推送
2021-11-15 23:04:55 +08:00
whyour a266425348 修复拷贝deps 2021-11-13 22:05:54 +08:00
whyour 6dbe02e321 修复dep目录 2021-11-13 21:54:45 +08:00
whyour 39979879c9 更新版本 v2.10.7 2021-11-13 21:21:35 +08:00
whyour ba3f604a5c 修复bot安装依赖判断 2021-11-13 20:58:27 +08:00
whyour 9572582401 修改拷贝依赖 2021-11-13 19:05:34 +08:00
whyour 2bcef0ba75 修复脚本管理搜索 2021-11-13 17:42:06 +08:00
whyour 31cd7c4007 外部依赖改为系统直接分发通知文件js和py版 2021-11-13 17:21:27 +08:00
whyour 1c4c1799b0 修复环境变量表格样式 2021-11-13 16:44:21 +08:00
whyour 2d558df15e 修复schedule服务 2021-11-13 12:34:49 +08:00
whyour 7b769da4e2 修复脚本管理树结构 2021-11-13 11:14:43 +08:00
75 changed files with 1696 additions and 690 deletions
+3 -1
View File
@@ -105,7 +105,9 @@ jobs:
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
build-args: MAINTAINER=${{ github.repository_owner }}, QL_BRANCH=${{ github.ref_name }} build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
context: docker/ context: docker/
push: true push: true
+1
View File
@@ -0,0 +1 @@
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
+1 -1
View File
@@ -39,7 +39,7 @@ export default defineConfig({
scripts: [ scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js', 'https://cdn.jsdelivr.net/npm/darkreader@4.9.40/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
-92
View File
@@ -1,92 +0,0 @@
# 安装说明
## 前置要求
```bash
1.已安装docker-ce
2.选装docker-compose
```
## 安装方式1
```bash
1. 新建一个文件夹,用于存放相关数据
2. 下载本仓库中的`docker-compose.yml`至本地,或是复制文件内容后在本地自行建立并粘贴内容
3. 使用docker-compose启动
4. 浏览器输入ip:5700即可进入面板
# 新建数据文件夹
mkdir qinglong
cd qinglong
# 下载docker-compose.yml文件
wget https://raw.githubusercontent.com/whyour/qinglong/develop/docker-compose.yml
# 启动
docker-compose up -d
```
## 安装方式2
```bash
# 复制下列命令在ssh执行(先安装docker)
# 注:$PWD请修改为实际你想安装的路径
docker run -dit \
-v $PWD/ql/config:/ql/config \
-v $PWD/ql/log:/ql/log \
-v $PWD/ql/db:/ql/db \
-v $PWD/ql/repo:/ql/repo \
-v $PWD/ql/raw:/ql/raw \
-v $PWD/ql/scripts:/ql/scripts \
-v $PWD/ql/jbot:/ql/jbot \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
## 登录
```bash
打开浏览器访问宿主机ip的5700端口即可
例如http://192.168.100.123:5700即ip:5700
首次登录
账号:admin 密码:admin
会生成`auth.json`
在ssh输入
1.docker exec -it qinglong bash
2.cat /ql/config/auth.json
cat查看之后返回的结果类似如下字段
{"username":"admin","password":"Xb-ZYP526wmg4_h6q1WqIO"}
# admin即为登录名;Xb-ZYP526wmg4_h6q1WqIO为登录密码
```
输入此处记录的`username``password`,即可成功登录qinglong面板,登录后即可正常使用
## 拉取脚本
```bash
示例
ql repo https://github.com/xxx.git #拉取仓库
ql raw https://raw.githubusercontent.com/xxx #拉取单个脚本
```
## 备份
所有数据都将保存在`docker-compose.yml`所在的同级目录的`data`文件夹中,如需要备份,请直接备份`docker-compose.yml``data`文件夹即可
```bash
root@debian:/opt/qinglong# ls -lah
总用量 8.0K
drwxr-xr-x 3 root root 4.0K 8月 30 01:29 .
drwxr-xr-x 4 root root 4.0K 8月 30 00:51 ..
drwxr-xr-x 8 root root 4.0K 8月 30 01:30 data
-rw-r--r-- 1 root root 386 8月 30 01:29 docker-compose.yml
```
## 更新
在面板执行"更新面板"任务即可
或者
```bash
cd qinglong
docker-compose down
docker pull whyour/qinglong:latest
docker-compose up -d
```
+145 -27
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="/public/images/qinglong.png"> <img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a> </a>
</p> </p>
@@ -8,12 +8,10 @@
<div align="center"> <div align="center">
A timed task management panel that supports typescript, javaScript, python3, and shell.(支持python3、javaScript、shell、typescript 的定时任务管理面板 支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url] [![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
[donate-url]: https://qinglong.whyour.cn/nice.png
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat [docker-pulls-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-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
@@ -24,31 +22,151 @@ A timed task management panel that supports typescript, javaScript, python3, and
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
<p align="center"> [![](https://z3.ax1x.com/2021/11/18/I7KrTg.jpg)](https://whyour.cn)
<img width="49%" src="/public/images/login.png">
<img width="49%" src="/public/images/home.png"> 简体中文 | [English](./README-en.md)
</p>
## 功能
- 支持多种脚本语言(python3、javaScript、shell、typescript
- 支持在线管理脚本、环境变量、配置文件
- 支持在线查看任务日志
- 支持秒级任务设置
- 支持系统级通知
- 支持暗黑模式
- 支持手机端操作
## 部署
### 本机部署
```bash
# 待完善
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 启动容器
```bash
docker run -dit \
-v $PWD/ql/config:/ql/config \
-v $PWD/ql/log:/ql/log \
-v $PWD/ql/db:/ql/db \
-v $PWD/ql/repo:/ql/repo \
-v $PWD/ql/raw:/ql/raw \
-v $PWD/ql/scripts:/ql/scripts \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose 部署
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
## 使用
1. 内置命令
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
```
2. 参数说明
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
## 链接
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
$ yarn install
$ yarn start-back
$ yarn start
```
打开你的浏览器,访问 http://127.0.0.1:5601
## 交流
[telegram频道](https://t.me/jiao_long)
## 名称来源
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。 青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。 《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。 在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 如何安装
请查看[INSTALL.md](INSTALL.md)
## 多谢
* [nevinee](https://gitee.com/evine)
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
* [Ant Design](https://ant.design)
* [Ant Design Pro](https://pro.ant.design/)
* [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader)
+4 -4
View File
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/configs', route);
route.get( route.get(
'/configs/files', '/files',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -32,7 +32,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/configs/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -58,7 +58,7 @@ export default (app: Router) => {
); );
route.post( route.post(
'/configs/save', '/save',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
+26 -30
View File
@@ -7,26 +7,22 @@ import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/crons', route);
route.get(
'/crons', route.get('/', async (req: Request, res: Response, next: NextFunction) => {
async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger');
const logger: Logger = Container.get('logger'); try {
try { const cronService = Container.get(CronService);
const cronService = Container.get(CronService); const data = await cronService.crontabs(req.query.searchValue as string);
const data = await cronService.crontabs( return res.send({ code: 200, data });
req.query.searchValue as string, } catch (e) {
); logger.error('🔥 error: %o', e);
return res.send({ code: 200, data }); return next(e);
} catch (e) { }
logger.error('🔥 error: %o', e); });
return next(e);
}
},
);
route.post( route.post(
'/crons', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
command: Joi.string().required(), command: Joi.string().required(),
@@ -52,7 +48,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/run', '/run',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -70,7 +66,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/stop', '/stop',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -88,7 +84,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/disable', '/disable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -106,7 +102,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/enable', '/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -124,7 +120,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/:id/log', '/:id/log',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -144,7 +140,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons', '',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
command: Joi.string().optional(), command: Joi.string().optional(),
@@ -174,7 +170,7 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/crons', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -192,7 +188,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/pin', '/pin',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -210,7 +206,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/unpin', '/unpin',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -228,7 +224,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/import', '/import',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -243,7 +239,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -263,7 +259,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/status', '/status',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.string().required()),
+19 -21
View File
@@ -6,24 +6,22 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/dependencies', route);
route.get(
'/dependencies', route.get('/', async (req: Request, res: Response, next: NextFunction) => {
async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger');
const logger: Logger = Container.get('logger'); try {
try { const dependenceService = Container.get(DependenceService);
const dependenceService = Container.get(DependenceService); const data = await dependenceService.dependencies(req.query as any);
const data = await dependenceService.dependencies(req.query as any); return res.send({ code: 200, data });
return res.send({ code: 200, data }); } catch (e) {
} catch (e) { logger.error('🔥 error: %o', e);
logger.error('🔥 error: %o', e); return next(e);
return next(e); }
} });
},
);
route.post( route.post(
'/dependencies', '/',
celebrate({ celebrate({
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
@@ -47,7 +45,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/dependencies', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
@@ -70,7 +68,7 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/dependencies', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -88,7 +86,7 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/dependencies/force', '/force',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -106,7 +104,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/dependencies/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -126,7 +124,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/dependencies/reinstall', '/reinstall',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
+21 -23
View File
@@ -6,24 +6,22 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/envs', route);
route.get(
'/envs', route.get('/', async (req: Request, res: Response, next: NextFunction) => {
async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger');
const logger: Logger = Container.get('logger'); try {
try { const envService = Container.get(EnvService);
const envService = Container.get(EnvService); const data = await envService.envs(req.query.searchValue as string);
const data = await envService.envs(req.query.searchValue as string); return res.send({ code: 200, data });
return res.send({ code: 200, data }); } catch (e) {
} catch (e) { logger.error('🔥 error: %o', e);
logger.error('🔥 error: %o', e); return next(e);
return next(e); }
} });
},
);
route.post( route.post(
'/envs', '/',
celebrate({ celebrate({
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
@@ -47,7 +45,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
@@ -70,7 +68,7 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/envs', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -88,7 +86,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/:id/move', '/:id/move',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -112,7 +110,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/disable', '/disable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -130,7 +128,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/enable', '/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -148,7 +146,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/name', '/name',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.string().required()),
@@ -169,7 +167,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/envs/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
+2
View File
@@ -7,6 +7,7 @@ import cron from './cron';
import script from './script'; import script from './script';
import open from './open'; import open from './open';
import dependence from './dependence'; import dependence from './dependence';
import system from './system';
export default () => { export default () => {
const app = Router(); const app = Router();
@@ -18,6 +19,7 @@ export default () => {
script(app); script(app);
open(app); open(app);
dependence(app); dependence(app);
system(app);
return app; return app;
}; };
+33 -35
View File
@@ -7,44 +7,42 @@ import { getFileContentByName } from '../config/util';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/logs', route);
route.get(
'/logs', route.get('/', async (req: Request, res: Response, next: NextFunction) => {
async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger');
const logger: Logger = Container.get('logger'); try {
try { const fileList = fs.readdirSync(config.logPath, 'utf-8');
const fileList = fs.readdirSync(config.logPath, 'utf-8'); const dirs = [];
const dirs = []; for (let i = 0; i < fileList.length; i++) {
for (let i = 0; i < fileList.length; i++) { const stat = fs.lstatSync(config.logPath + fileList[i]);
const stat = fs.lstatSync(config.logPath + fileList[i]); if (stat.isDirectory()) {
if (stat.isDirectory()) { const fileListTmp = fs.readdirSync(
const fileListTmp = fs.readdirSync( `${config.logPath}/${fileList[i]}`,
`${config.logPath}/${fileList[i]}`, 'utf-8',
'utf-8', );
); dirs.push({
dirs.push({ name: fileList[i],
name: fileList[i], isDir: true,
isDir: true, files: fileListTmp.reverse(),
files: fileListTmp.reverse(), });
}); } else {
} else { dirs.push({
dirs.push({ name: fileList[i],
name: fileList[i], isDir: false,
isDir: false, files: [],
files: [], });
});
}
} }
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
} }
}, res.send({ code: 200, dirs });
); } catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get( route.get(
'/logs/:dir/:file', '/:dir/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -61,7 +59,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/logs/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
+77 -17
View File
@@ -9,14 +9,15 @@ import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import path from 'path'; import path, { join } from 'path';
import ScriptService from '../services/script';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/scripts', route);
route.get( route.get(
'/scripts/files', '/files',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -43,7 +44,7 @@ export default (app: Router) => {
children.push({ children.push({
title: childFile, title: childFile,
value: childFile, value: childFile,
key: childFile, key: `${fileOrDir}/${childFile}`,
mtime: statObj.mtimeMs, mtime: statObj.mtimeMs,
parent: fileOrDir, parent: fileOrDir,
}); });
@@ -79,14 +80,16 @@ export default (app: Router) => {
); );
route.get( route.get(
'/scripts/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const path = req.query.path ? `${req.query.path}/` : ''; const filePath = join(
const content = getFileContentByName( config.scriptPath,
`${config.scriptPath}${path}${req.params.file}`, req.query.path as string,
req.params.file,
); );
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -96,11 +99,11 @@ export default (app: Router) => {
); );
route.post( route.post(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().allow(''), content: Joi.string().allow(''),
originFilename: Joi.string().allow(''), originFilename: Joi.string().allow(''),
}), }),
@@ -120,6 +123,9 @@ export default (app: Router) => {
if (!path.endsWith('/')) { if (!path.endsWith('/')) {
path += '/'; path += '/';
} }
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
}
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
code: 430, code: 430,
@@ -153,11 +159,11 @@ export default (app: Router) => {
); );
route.put( route.put(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().required(), content: Joi.string().required(),
}), }),
}), }),
@@ -169,7 +175,7 @@ export default (app: Router) => {
content: string; content: string;
path: string; path: string;
}; };
const filePath = `${config.scriptPath}${path}/${filename}`; const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -180,19 +186,21 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string;
}; };
const filePath = `${config.scriptPath}${filename}`; const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -203,7 +211,7 @@ export default (app: Router) => {
); );
route.post( route.post(
'/scripts/download', '/download',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
@@ -232,4 +240,56 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+114
View File
@@ -0,0 +1,114 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import { getFileContentByName } from '../config/util';
import UserService from '../services/user';
const route = Router();
export default (app: Router) => {
app.use('/system', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
version,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+29 -130
View File
@@ -9,7 +9,8 @@ import { getFileContentByName } from '../config/util';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/user', route);
route.post( route.post(
'/login', '/login',
celebrate({ celebrate({
@@ -47,7 +48,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
@@ -67,29 +68,26 @@ export default (app: Router) => {
}, },
); );
route.get( route.get('/', async (req: Request, res: Response, next: NextFunction) => {
'/user', const logger: Logger = Container.get('logger');
async (req: Request, res: Response, next: NextFunction) => { try {
const logger: Logger = Container.get('logger'); const userService = Container.get(UserService);
try { const authInfo = await userService.getUserInfo();
const userService = Container.get(UserService); res.send({
const authInfo = await userService.getUserInfo(); code: 200,
res.send({ data: {
code: 200, username: authInfo.username,
data: { twoFactorActivated: authInfo.twoFactorActivated,
username: authInfo.username, },
twoFactorActivated: authInfo.twoFactorActivated, });
}, } catch (e) {
}); logger.error('🔥 error: %o', e);
} catch (e) { return next(e);
logger.error('🔥 error: %o', e); }
return next(e); });
}
},
);
route.get( route.get(
'/user/two-factor/init', '/two-factor/init',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -104,7 +102,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/two-factor/active', '/two-factor/active',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
code: Joi.string().required(), code: Joi.string().required(),
@@ -124,7 +122,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/two-factor/deactive', '/two-factor/deactive',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -139,7 +137,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/two-factor/login', '/two-factor/login',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
code: Joi.string().required(), code: Joi.string().required(),
@@ -161,7 +159,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/user/login-log', '/login-log',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -176,7 +174,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/user/notification', '/notification',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -191,7 +189,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/notification', '/notification',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -205,40 +203,8 @@ export default (app: Router) => {
}, },
); );
route.get(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
// 初始化api
route.put( route.put(
'/init/user', '/init',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
@@ -259,7 +225,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/init/notification', '/notification/init',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
@@ -272,71 +238,4 @@ export default (app: Router) => {
} }
}, },
); );
route.get(
'/system/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+2
View File
@@ -9,6 +9,8 @@ import Logger from './loaders/logger';
async function startServer() { async function startServer() {
const app = express(); const app = express();
await require('./loaders/initFile').default();
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
+4 -10
View File
@@ -5,7 +5,7 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const lastVersionFile = const lastVersionFile =
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts'; 'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
const envFound = dotenv.config(); const envFound = dotenv.config();
const rootPath = process.cwd(); const rootPath = process.cwd();
@@ -32,16 +32,10 @@ const authDbFile = path.join(rootPath, 'db/auth.db');
const dependenceDbFile = path.join(rootPath, 'db/dependence.db'); const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
const versionFile = path.join(rootPath, 'src/version.ts'); const versionFile = path.join(rootPath, 'src/version.ts');
const configFound = dotenv.config({ path: confFile });
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
} }
if (configFound.error) {
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
}
export default { export default {
port: parseInt(process.env.PORT as string, 10), port: parseInt(process.env.PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),
@@ -83,12 +77,12 @@ export default {
writePathList: [configPath, scriptPath], writePathList: [configPath, scriptPath],
bakPath, bakPath,
apiWhiteList: [ apiWhiteList: [
'/api/login', '/api/user/login',
'/open/auth/token', '/open/auth/token',
'/api/user/two-factor/login', '/api/user/two-factor/login',
'/api/system', '/api/system',
'/api/init/user', '/api/user/init',
'/api/init/notification', '/api/user/notification/init',
], ],
versionFile, versionFile,
lastVersionFile, lastVersionFile,
+37
View File
@@ -231,3 +231,40 @@ export async function fileExist(file: any) {
} }
}); });
} }
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
) {
if (!fnList.length) return;
const replyList: any[] = []; // 收集任务执行结果
const startTime = new Date().getTime(); // 记录任务执行开始时间
// 任务执行程序
const schedule = async (index: number) => {
return new Promise(async (resolve) => {
const fn = fnList[index];
if (!fn) return resolve(null);
// 执行当前异步任务
const reply = await fn();
replyList[index] = reply;
// 执行完当前任务后,继续执行任务池的剩余任务
await schedule(index + max);
resolve(null);
});
};
// 任务池执行程序
const scheduleList = new Array(max)
.fill(0)
.map((_, index) => schedule(index));
// 使用 Promise.all 批量执行
const r = await Promise.all(scheduleList);
const cost = (new Date().getTime() - startTime) / 1000;
return replyList;
}
+8
View File
@@ -1,4 +1,5 @@
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan', 'serverChan' = 'serverChan',
'bark' = 'bark', 'bark' = 'bark',
@@ -15,6 +16,12 @@ abstract class NotificationBaseInfo {
public type!: NotificationMode; public type!: NotificationMode;
} }
export class GotifyNotification extends NotificationBaseInfo {
public gotifyUrl = '';
public gotifyToken = '';
public gotifyPriority = 0;
}
export class GoCqHttpBotNotification extends NotificationBaseInfo { export class GoCqHttpBotNotification extends NotificationBaseInfo {
public goCqHttpBotUrl = ''; public goCqHttpBotUrl = '';
public goCqHttpBotToken = ''; public goCqHttpBotToken = '';
@@ -70,6 +77,7 @@ export class EmailNotification extends NotificationBaseInfo {
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification,
ServerChanNotification, ServerChanNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
+2 -1
View File
@@ -13,4 +13,5 @@ export class SockMessage {
export type SockMessageType = export type SockMessageType =
| 'ping' | 'ping'
| 'installDependence' | 'installDependence'
| 'updateSystemVersion'; | 'updateSystemVersion'
| 'manuallyRunScript';
+4
View File
@@ -3,6 +3,7 @@ import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger'; import Logger from './logger';
import initData from './initData'; import initData from './initData';
import { Application } from 'express'; import { Application } from 'express';
import linkDeps from './deps';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
await dependencyInjectorLoader({ await dependencyInjectorLoader({
@@ -15,4 +16,7 @@ export default async ({ expressApp }: { expressApp: Application }) => {
await initData(); await initData();
Logger.info('✌️ init data loaded'); Logger.info('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps');
}; };
+31
View File
@@ -0,0 +1,31 @@
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import config from '../config/index';
function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
}
});
}
export default async (src: string = 'deps') => {
linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
watcher
.on('add', (path) => linkToNodeModule(src))
.on('change', (path) => linkToNodeModule(src));
};
+1 -1
View File
@@ -98,7 +98,7 @@ export default ({ app }: { app: Application }) => {
}); });
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) { if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next(); return next();
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
+2 -2
View File
@@ -22,14 +22,14 @@ export default async () => {
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => { dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec(async (err, docs) => {
const groups = _.groupBy(docs, 'type'); const groups = _.groupBy(docs, 'type');
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
const depIds = group.map((x) => x._id); const depIds = group.map((x) => x._id);
for (const dep of depIds) { for (const dep of depIds) {
dependenceService.reInstall([dep]); await dependenceService.reInstall([dep]);
} }
} }
} }
+46
View File
@@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
const rootPath = process.cwd();
const confFile = path.join(rootPath, 'config/config.sh');
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh');
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const logPath = path.join(rootPath, 'log/');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
const confFileExist = await fileExist(confFile);
const scriptDirExist = await fileExist(scriptPath);
const logDirExist = await fileExist(logPath);
const configDirExist = await fileExist(configPath);
if (!configDirExist) {
fs.mkdirSync(configPath);
}
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
};
+1 -1
View File
@@ -5,7 +5,7 @@ import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => { export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({ Sentry.init({
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906', 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 }),
+5 -2
View File
@@ -44,8 +44,11 @@ const run = async () => {
}; };
app app
.listen(config.cronPort, () => { .listen(config.cronPort, async () => {
run(); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(` Logger.info(`
################################################ ################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️ 🛡️ Schedule listening on port: ${config.cronPort} 🛡️
+33 -23
View File
@@ -6,7 +6,7 @@ import { Crontab, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process'; import { exec, execSync, spawn } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util'; import { getFileContentByName, concurrentRun } from '../config/util';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
@@ -93,17 +93,21 @@ export default class CronService {
last_running_time: number; last_running_time: number;
last_execution_time: number; last_execution_time: number;
}) { }) {
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb.update( this.cronDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ {
$set: { $set: options,
status,
pid,
log_path,
last_running_time,
last_execution_time,
},
}, },
{ multi: true, returnUpdatedDocs: true }, { multi: true, returnUpdatedDocs: true },
(err) => { (err) => {
@@ -155,7 +159,9 @@ export default class CronService {
public async crontabs(searchText?: string): Promise<Crontab[]> { public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {}; let query = {};
if (searchText) { if (searchText) {
const reg = new RegExp(searchText, 'i'); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
query = { query = {
$or: [ $or: [
{ {
@@ -194,10 +200,10 @@ export default class CronService {
{ $set: { status: CrontabStatus.queued } }, { $set: { status: CrontabStatus.queued } },
{ multi: true }, { multi: true },
); );
for (let i = 0; i < ids.length; i++) { concurrentRun(
const id = ids[i]; ids.map((id) => () => this.runSingle(id)),
this.queue.add(() => this.runSingle(id)); 10,
} );
} }
public async stop(ids: string[]) { public async stop(ids: string[]) {
@@ -235,7 +241,7 @@ export default class CronService {
}); });
} }
private async killTask(name: string) { public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec); const execAsync = promisify(exec);
try { try {
@@ -245,16 +251,20 @@ export default class CronService {
} else { } else {
return; return;
} }
const pids = pid.match(/\(\d+/g); let pids = pid.match(/\(\d+/g);
const killLogs = []; const killLogs = [];
for (const id of pids) { if (pids && pids.length > 0) {
const c = `kill -9 ${id.slice(1)}`; // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
const { stdout, stderr } = await execAsync(c); pids = pids.slice(0, 3);
if (stderr) { for (const id of pids) {
killLogs.push(stderr); const c = `kill -9 ${id.slice(1)}`;
} const { stdout, stderr } = await execAsync(c);
if (stdout) { if (stderr) {
killLogs.push(stdout); killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} }
} }
return killLogs.length > 0 ? JSON.stringify(killLogs) : ''; return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
+95 -89
View File
@@ -131,7 +131,7 @@ export default class DependenceService {
{ $set: { status: DependenceStatus.installing, log: [] } }, { $set: { status: DependenceStatus.installing, log: [] } },
{ multi: true, returnUpdatedDocs: true }, { multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => { async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs); await this.installOrUninstallDependencies(docs);
resolve(docs); resolve(docs);
}, },
); );
@@ -178,103 +178,109 @@ export default class DependenceService {
dependencies: Dependence[], dependencies: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
) { ) {
if (dependencies.length === 0) { return new Promise((resolve) => {
return; if (dependencies.length === 0) {
} resolve(null);
const depNames = dependencies.map((x) => x.name).join(' '); return;
const depRunCommand = ( }
isInstall const depNames = dependencies.map((x) => x.name).join(' ');
? InstallDependenceCommandTypes const depRunCommand = (
: unInstallDependenceCommandTypes isInstall
)[dependencies[0].type as any]; ? InstallDependenceCommandTypes
const actionText = isInstall ? '安装' : '删除'; : unInstallDependenceCommandTypes
const depIds = dependencies.map((x) => x._id) as string[]; )[dependencies[0].type as any];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); const actionText = isInstall ? '安装' : '删除';
const startTime = Date.now(); const depIds = dependencies.map((x) => x._id) as string[];
this.sockService.sendMessage({ const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
type: 'installDependence', const startTime = Date.now();
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}`,
references: depIds,
});
this.updateLog(
depIds,
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: data.toString(), message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
references: depIds, startTime,
}); ).toLocaleString()}`,
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'installDependence',
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds, references: depIds,
}); });
this.updateLog( this.updateLog(
depIds, depIds,
`依赖${actionText}${resultText}结束时间 ${new Date( `开始${actionText}依赖 ${depNames}开始时间 ${new Date(
endTime, startTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`, ).toLocaleString()}\n`,
); );
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
let status = null; cp.stderr.on('data', (data) => {
if (isSucceed) { this.sockService.sendMessage({
status = isInstall type: 'installDependence',
? DependenceStatus.installed message: data.toString(),
: DependenceStatus.removed; references: depIds,
} else { });
status = isInstall this.updateLog(depIds, data.toString());
? DependenceStatus.installFailed });
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录 cp.on('error', (err) => {
if (isSucceed && !isInstall) { this.sockService.sendMessage({
setTimeout(() => { type: 'installDependence',
this.removeDb(depIds); message: JSON.stringify(err),
}, 5000); references: depIds,
} });
this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
this.updateLog(
depIds,
`依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录
if (isSucceed && !isInstall) {
setTimeout(() => {
this.removeDb(depIds);
}, 5000);
}
resolve(null);
});
}); });
} }
} }
+11 -3
View File
@@ -113,7 +113,9 @@ export default class EnvService {
): Promise<Env[]> { ): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
condition = { condition = {
$or: [ $or: [
{ {
@@ -219,11 +221,17 @@ export default class EnvService {
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
env_string += `export ${key}="${_(group) let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled) .filter((x) => x.status !== EnvStatus.disabled)
.map('value') .map('value')
.join('&') .join('&')
.replace(/ /g, '')}"\n`; .replace(/ /g, '');
if (/"/.test(value)) {
value = `'${value}'`;
} else {
value = `"${value}"`;
}
env_string += `export ${key}=${value}\n`;
} }
} }
} }
+20
View File
@@ -13,6 +13,7 @@ export default class NotificationService {
private userService!: UserService; private userService!: UserService;
private modeMap = new Map([ private modeMap = new Map([
['gotify', this.gotify],
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan], ['serverChan', this.serverChan],
['bark', this.bark], ['bark', this.bark],
@@ -67,6 +68,25 @@ export default class NotificationService {
return true; return true;
} }
private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
this.content,
)}&priority=${gotifyPriority}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.json();
return typeof res.id === 'number';
}
private async goCqHttpBot() { private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got const res: any = await got
+3 -1
View File
@@ -97,7 +97,9 @@ export default class OpenService {
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
condition = { condition = {
$or: [ $or: [
{ {
+61
View File
@@ -0,0 +1,61 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { spawn } from 'child_process';
import SockService from './sock';
import CronService from './cron';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
private cronService: CronService,
) {}
public async runScript(path: string) {
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: JSON.stringify(err),
});
});
cp.on('close', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `执行结束`,
});
});
return { code: 200 };
}
public async stopScript(path: string) {
const err = await this.cronService.killTask(`task -l ${path} now`);
const str = err ? `\n${err}` : '';
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
return { code: 200 };
}
}
+184
View File
@@ -0,0 +1,184 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
import { dbs } from '../loaders/db';
@Service()
export default class SystemService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
private authDb = dbs.authDb;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async getLogRemoveFrequency() {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.removeLogFrequency })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({});
} else {
resolve(docs[0].info);
}
});
});
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
return new Promise((resolve) => {
this.authDb.update(
{ type: payload.type },
{ ...payload },
{ upsert: true, returnUpdatedDocs: true },
(err, num, doc: any) => {
if (err) {
resolve({} as NotificationInfo);
} else {
resolve({ ...doc.info, _id: doc._id });
}
},
);
});
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
_id: result._id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
}
+16 -7
View File
@@ -403,13 +403,22 @@ export default class UserService {
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1]; const currentVersion = currentVersionFile.match(versionRegx)![1];
const lastVersionFileContent = await ( let lastVersion = '';
await got.get(config.lastVersionFile) let lastLog = '';
).body; try {
const lastVersion = lastVersionFileContent.match(versionRegx)![1]; const result = await Promise.race([
const lastLog = lastVersionFileContent.match(logRegx) got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
? lastVersionFileContent.match(logRegx)![1] got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
: ''; timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return { return {
code: 200, code: 200,
+13
View File
@@ -0,0 +1,13 @@
import { CommandModule } from 'yargs';
export const updateCommand: CommandModule = {
command: 'update',
describe: 'Update and restart qinglong',
builder: (yargs) => {
return yargs.option('repositority', {
type: 'string',
alias: 'r',
describe: `Specify the release warehouse address of the package`,
});
},
handler: async (argv) => {},
};
+13
View File
@@ -0,0 +1,13 @@
import * as yargs from 'yargs';
import { green, red } from 'chalk';
import { updateCommand } from './commands/update';
yargs
.usage('Usage: ql [command] <options>')
.command(updateCommand)
.fail((err) => {
console.error(`${red(err)}`);
})
.alias('h', 'help')
.showHelp()
.recommendCommands().argv;
+2 -4
View File
@@ -8,9 +8,7 @@ services:
- ./data/db:/ql/db - ./data/db:/ql/db
- ./data/scripts:/ql/scripts - ./data/scripts:/ql/scripts
- ./data/repo:/ql/repo - ./data/repo:/ql/repo
- ./data/raw:/ql/raw
ports: ports:
- "0.0.0.0:5700:5700" - "0.0.0.0:5700:5700"
environment: restart: unless-stopped
- ENABLE_HANGUP=true
- ENABLE_WEB_PANEL=true
restart: always
+1 -3
View File
@@ -1,4 +1,4 @@
FROM node:lts-alpine FROM node:lts-alpine3.12
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
@@ -8,8 +8,6 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
QL_DIR=/ql \ QL_DIR=/ql \
QL_MAINTAINER=${QL_MAINTAINER} \
QL_URL=${QL_URL} \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
RUN set -x \ RUN set -x \
+1
View File
@@ -6,6 +6,7 @@ link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/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
pm2 l &>/dev/null pm2 l &>/dev/null
+3 -1
View File
@@ -29,6 +29,7 @@
"@sentry/tracing": "^6.14.0", "@sentry/tracing": "^6.14.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"celebrate": "^13.0.3", "celebrate": "^13.0.3",
"chokidar": "^3.5.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^3.5.0", "cron-parser": "^3.5.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
@@ -50,7 +51,8 @@
"sockjs": "^0.3.21", "sockjs": "^0.3.21",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3",
"yargs": "^17.2.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

+8
View File
@@ -112,4 +112,12 @@ export GOBOT_URL=""
export GOBOT_TOKEN="" export GOBOT_TOKEN=""
export GOBOT_QQ="" export GOBOT_QQ=""
## 10. gotify
## gotify_url 填写gotify地址,如https://push.example.de:8080
## gotify_token 填写gotify的消息应用token
## gotify_priority 填写推送消息优先级,默认为0
export GOTIFY_URL="";
export GOTIFY_TOKEN="";
export GOTIFY_PRIORITY=0;
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+57 -4
View File
@@ -13,6 +13,13 @@
const querystring = require('querystring'); const querystring = require('querystring');
const $ = new Env(); const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒) const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域=========================================== // =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg //gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥 //gobot_token 填写在go-cqhttp文件设置的访问密钥
@@ -84,6 +91,16 @@ let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = ''; let PUSH_PLUS_USER = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
if (process.env.GOTIFY_TOKEN) {
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
}
if (process.env.GOTIFY_PRIORITY) {
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
}
if (process.env.GOBOT_URL) { if (process.env.GOBOT_URL) {
GOBOT_URL = process.env.GOBOT_URL; GOBOT_URL = process.env.GOBOT_URL;
} }
@@ -200,9 +217,45 @@ async function sendNotify(
qywxamNotify(text, desp), //企业微信应用消息推送 qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp gobotNotify(text, desp),//go-cqhttp
gotifyNotify(text, desp),//gotify
]); ]);
} }
function gotifyNotify(text, desp) {
return new Promise((resolve) => {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('gotify发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.id) {
console.log('gotify发送通知消息成功🎉\n');
} else {
console.log(`${data.message}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve();
}
});
} else {
resolve();
}
});
}
function gobotNotify(text, desp, time = 2100) { function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
@@ -673,8 +726,8 @@ function qywxamNotify(text, desp) {
if (err) { if (err) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息失败!!\n', '企业微信应用消息发送通知消息失败!!\n',
); );
console.log(err); console.log(err);
} else { } else {
@@ -682,8 +735,8 @@ function qywxamNotify(text, desp) {
if (data.errcode === 0) { if (data.errcode === 0) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息成功🎉。\n', '企业微信应用消息发送通知消息成功🎉。\n',
); );
} else { } else {
console.log(`${data.errmsg}\n`); console.log(`${data.errmsg}\n`);
+26
View File
@@ -51,6 +51,10 @@ push_config = {
# /send_group_msg 时填入 group_id=QQ群 # /send_group_msg 时填入 group_id=QQ群
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token 'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
'GOTIFY_TOKEN': '', # gotify的消息应用token
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY 'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
@@ -194,6 +198,25 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!") print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
"""
使用 gotify 推送消息。
"""
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url,data=data).json()
if response.get("id"):
print("gotify 推送成功!")
else:
print("gotify 推送失败!")
def iGot(title: str, content: str) -> None: def iGot(title: str, content: str) -> None:
""" """
使用 iGot 推送消息。 使用 iGot 推送消息。
@@ -262,6 +285,7 @@ def pushplus_bot(title: str, content: str) -> None:
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json() response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200: if response["code"] == 200:
@@ -473,6 +497,8 @@ if push_config.get("FSKEY"):
notify_function.append(feishu_bot) notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"): if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp) notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"): if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot) notify_function.append(iGot)
if push_config.get("PUSH_KEY"): if push_config.get("PUSH_KEY"):
+1 -1
View File
@@ -31,7 +31,7 @@ cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
cd $dir_root cd $dir_root
cat requirements.txt | while read LREAD cat requirements.txt | while read LREAD
do do
if test ! -z "$(pip3 show "${LREAD%%=*}" 1>/dev/null)"; then if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD} pip3 --default-timeout=100 install ${LREAD}
fi fi
done done
+4 -10
View File
@@ -39,17 +39,11 @@ copy_dep() {
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
if test -z "$(pm2 info panel 1>/dev/null)"; then pm2 delete panel --source-map-support --time &>/dev/null
pm2 reload panel --source-map-support --time &>/dev/null pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
fi
if test -z "$(pm2 info schedule 1>/dev/null)"; then pm2 delete schedule --source-map-support --time &>/dev/null
pm2 reload schedule --source-map-support --time &>/dev/null pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
fi
} }
pm2_log() { pm2_log() {
+17
View File
@@ -10,6 +10,7 @@ dir_repo=$dir_root/repo
dir_raw=$dir_root/raw dir_raw=$dir_root/raw
dir_log=$dir_root/log dir_log=$dir_root/log
dir_db=$dir_root/db dir_db=$dir_root/db
dir_dep=$dir_root/deps
dir_list_tmp=$dir_log/.tmp dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code dir_code=$dir_log/code
dir_update_log=$dir_log/update dir_update_log=$dir_log/update
@@ -34,6 +35,8 @@ file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log 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_js=$dir_dep/sendNotify.js
## 清单文件 ## 清单文件
list_crontab_user=$dir_config/crontab.list list_crontab_user=$dir_config/crontab.list
@@ -154,6 +157,7 @@ fix_config() {
make_dir $dir_repo make_dir $dir_repo
make_dir $dir_raw make_dir $dir_raw
make_dir $dir_update_log make_dir $dir_update_log
make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
@@ -208,6 +212,19 @@ fix_config() {
cat /dev/null > /etc/nginx/conf.d/default.conf cat /dev/null > /etc/nginx/conf.d/default.conf
echo echo
fi fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
} }
npm_install_sub() { npm_install_sub() {
+23 -10
View File
@@ -106,7 +106,7 @@ run_normal() {
cd $dir_scripts cd $dir_scripts
local relative_path="${first_param%/*}" local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]]; then if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
first_param=${first_param/$relative_path\//} first_param=${first_param/$relative_path\//}
fi fi
@@ -174,7 +174,7 @@ run_concurrent() {
cd $dir_scripts cd $dir_scripts
local relative_path="${first_param%/*}" local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]]; then if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
first_param=${first_param/$relative_path\//} first_param=${first_param/$relative_path\//}
fi fi
@@ -248,7 +248,7 @@ run_designated() {
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]]; 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
@@ -264,13 +264,18 @@ run_designated() {
## 运行其他命令 ## 运行其他命令
run_else() { run_else() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local file_param="$1"
local log_dir_tmp="${1##*/}" define_program "$file_param"
local log_dir="$dir_log/${log_dir_tmp%%.*}" log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path" cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir" make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date "+%s" -d "$begin_time")
@@ -278,13 +283,21 @@ run_else() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}') local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
eval timeout -k 10s $command_timeout_time "$@" $cmd cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval . $file_task_after "$@" $cmd shift
eval timeout -k 10s $command_timeout_time $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time") local end_timestamp=$(date "+%s" -d "$end_time")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
+13 -19
View File
@@ -318,17 +318,11 @@ patch_version() {
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
if [[ $(pm2 info panel 2>/dev/null) ]]; then pm2 delete panel --source-map-support --time &>/dev/null
pm2 reload panel --source-map-support --time &>/dev/null pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
fi
if [[ $(pm2 info schedule 2>/dev/null) ]]; then pm2 delete schedule --source-map-support --time &>/dev/null
pm2 reload schedule --source-map-support --time &>/dev/null pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
fi
} }
## 对比脚本 ## 对比脚本
@@ -391,16 +385,11 @@ gen_list_repo() {
if [[ $blackword ]]; then if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword) files=$(echo "$files" | egrep -v $blackword)
fi fi
if [[ $dependence ]]; then
cd ${dir_scripts}
depInScripts=$(eval $cmd | sed 's/^..//' | egrep -v $uniq_path | egrep $dependence)
for dep in ${depInScripts}; do
file_path=$(dirname $dep)
[[ ! $file_path =~ "/" ]] && file_path=""
make_dir "${dir_scripts}/${uniq_path}/${file_path#*/}"
cp -f $dep "${dir_scripts}/${uniq_path}/${file_path#*/}"
done
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path} 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
@@ -409,6 +398,11 @@ gen_list_repo() {
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}" cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done done
fi fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
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}"
-1
View File
@@ -79,6 +79,5 @@ export default {
fixSiderbar: true, fixSiderbar: true,
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
logo: '/images/qinglong.png',
siderWidth: 180, siderWidth: 180,
} as any; } as any;
+21 -2
View File
@@ -152,8 +152,8 @@
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header { .ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
margin-bottom: 0 !important; margin-bottom: 0 !important;
min-height: calc(100vh - 66px); min-height: calc(100vh - 72px);
min-height: calc(100vh - var(--vh-offset, 0px) - 66px); min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
} }
.Resizer { .Resizer {
@@ -235,6 +235,7 @@
box-shadow: none; box-shadow: none;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color; -webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
} }
::placeholder { ::placeholder {
@@ -292,3 +293,21 @@
color: #1890ff; color: #1890ff;
} }
} }
.ant-pro-sider-logo {
padding: 16px 8px !important;
h1 {
margin-left: 5px !important;
}
img {
width: 32px !important;
border-radius: 52% !important;
}
}
.ant-pro-global-header-logo {
a img {
// 移动端logo被拉伸
width: auto !important;
}
}
+19 -11
View File
@@ -20,14 +20,14 @@ import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd'; import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing'; import { Integrations } from '@sentry/tracing';
Sentry.init({ Sentry.init({
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851', dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
integrations: [new Integrations.BrowserTracing()], integrations: [new Integrations.BrowserTracing()],
release: version, release: version,
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
@@ -44,7 +44,7 @@ export default function (props: any) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const logout = () => { const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => { request.post(`${config.apiPrefix}user/logout`).then(() => {
localStorage.removeItem(config.authKey); localStorage.removeItem(config.authKey);
history.push('/login'); history.push('/login');
}); });
@@ -232,6 +232,12 @@ export default function (props: any) {
selectedKeys={[props.location.pathname]} selectedKeys={[props.location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={
<Image
preview={false}
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/>
}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -275,14 +281,16 @@ export default function (props: any) {
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => ( rightContentRender={() =>
<Dropdown overlay={menu} trigger={['click']}> ctx.isPhone && (
<span className="side-menu-user-wrapper"> <Dropdown overlay={menu} trigger={['click']}>
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <span className="side-menu-user-wrapper">
<span style={{ marginLeft: 5 }}>admin</span> <Avatar shape="square" size="small" icon={<UserOutlined />} />
</span> <span style={{ marginLeft: 5 }}>admin</span>
</Dropdown> </span>
)} </Dropdown>
)
}
collapsedButtonRender={(collapsed) => ( collapsedButtonRender={(collapsed) => (
<span <span
className="side-menu-container" className="side-menu-container"
+27 -3
View File
@@ -35,6 +35,7 @@ import CronLogModal from './logModal';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less'; import './index.less';
const { Text } = Typography; const { Text } = Typography;
@@ -76,7 +77,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
width: 150, width: 150,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<span> <a
onClick={() => {
goToScriptManager(record);
}}
>
{record.name || record._id}{' '} {record.name || record._id}{' '}
{record.isPinned ? ( {record.isPinned ? (
<span> <span>
@@ -85,7 +90,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
) : ( ) : (
'' ''
)} )}
</span> </a>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
@@ -329,6 +334,24 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/scripts')) {
cmd[1] = cmd[1].replace('/ql/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') {
location.href = cmd[2];
}
};
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
request request
@@ -780,7 +803,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addCron()}> <Button key="2" type="primary" onClick={() => addCron()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -847,6 +870,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
defaultPageSize: 20, defaultPageSize: 20,
showTotal: (total: number, range: number[]) => showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`, `${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
}} }}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
+1 -1
View File
@@ -27,7 +27,7 @@ const CronModal = ({
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '添加Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else { } else {
message.error(data); message.error(data);
} }
+1 -1
View File
@@ -398,7 +398,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addDependence()}> <Button key="2" type="primary" onClick={() => addDependence()}>
</Button>, </Button>,
]} ]}
header={{ header={{
+1
View File
@@ -13,6 +13,7 @@
} }
.diff-switch-file { .diff-switch-file {
min-width: 768px;
.ant-form-item { .ant-form-item {
margin-bottom: 8px; margin-bottom: 8px;
} }
+18 -10
View File
@@ -118,13 +118,17 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
align: 'center' as const, align: 'center' as const,
width: '44%', width: '35%',
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tooltip placement="topLeft" title={text}> <Tooltip
placement="topLeft"
title={text}
trigger={['hover', 'click']}
>
<span>{text}</span> <span>{text}</span>
</Tooltip> </Tooltip>
); );
@@ -141,19 +145,23 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
align: 'center' as const, align: 'center' as const,
width: 164, width: 165,
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
const date = new Date(text) const date = new Date(text)
.toLocaleString(language, { .toLocaleString(language, {
hour12: false, hour12: false,
}) })
.replace(' 24:', ' 00:') .replace(' 24:', ' 00:');
return ( return (
<Tooltip placement="topLeft" title={date}> <Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span> <span>{date}</span>
</Tooltip> </Tooltip>
); );
@@ -477,7 +485,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addEnv()}> <Button key="2" type="primary" onClick={() => addEnv()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -528,7 +536,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
scroll={{ x: 768, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record: any, index: number) => { onRow={(record: any, index: number) => {
+2 -2
View File
@@ -40,7 +40,7 @@ const EnvModal = ({
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(env ? '更新变量成功' : '添加变量成功'); message.success(env ? '更新变量成功' : '新建变量成功');
} else { } else {
message.error(data); message.error(data);
} }
@@ -77,7 +77,7 @@ const EnvModal = ({
rules={[ rules={[
{ required: true, message: '请输入环境变量名称', whitespace: true }, { required: true, message: '请输入环境变量名称', whitespace: true },
{ {
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/, pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
message: '只能输入字母数字下划线,且不能以数字开头', message: '只能输入字母数字下划线,且不能以数字开头',
}, },
]} ]}
+12 -3
View File
@@ -35,7 +35,7 @@ const Initialization = () => {
const submitAccountSetting = (values: any) => { const submitAccountSetting = (values: any) => {
setLoading(true); setLoading(true);
request request
.put(`${config.apiPrefix}init/user`, { .put(`${config.apiPrefix}user/init`, {
data: { data: {
username: values.username, username: values.username,
password: values.password, password: values.password,
@@ -52,8 +52,9 @@ const Initialization = () => {
}; };
const submitNotification = (values: any) => { const submitNotification = (values: any) => {
setLoading(true);
request request
.put(`${config.apiPrefix}init/notification`, { .put(`${config.apiPrefix}user/notification/init`, {
data: { data: {
...values, ...values,
}, },
@@ -73,6 +74,10 @@ const Initialization = () => {
setFields(_fields || []); setFields(_fields || []);
}; };
useEffect(() => {
localStorage.removeItem(config.authKey);
}, []);
const steps = [ const steps = [
{ {
title: '欢迎使用', title: '欢迎使用',
@@ -225,7 +230,11 @@ const Initialization = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.top}> <div className={styles.top}>
<div className={styles.header}> <div className={styles.header}>
<img alt="logo" className={styles.logo} src="/images/qinglong.png" /> <img
alt="logo"
className={styles.logo}
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/>
<span className={styles.title}></span> <span className={styles.title}></span>
</div> </div>
</div> </div>
+7 -4
View File
@@ -16,7 +16,6 @@ function getFilterData(keyword: string, data: any) {
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
expandedKeys.push(...item.children.map((x: any) => x.key));
} else { } else {
const children: any[] = []; const children: any[] = [];
(item.children || []).forEach((subItem: any) => { (item.children || []).forEach((subItem: any) => {
@@ -29,7 +28,7 @@ function getFilterData(keyword: string, data: any) {
...item, ...item,
children, children,
}); });
expandedKeys.push(...children.map((x) => x.key)); expandedKeys.push(item.key);
} }
} }
}); });
@@ -47,6 +46,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getLogs = () => { const getLogs = () => {
setLoading(true); setLoading(true);
@@ -100,8 +100,12 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data); const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree); setFilterData(tree);
setExpandedKeys(expandedKeys);
}, },
[data, setFilterData], [data, setFilterData],
); );
@@ -127,7 +131,6 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
treeData={data} treeData={data}
placeholder="请选择日志文件" placeholder="请选择日志文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
] ]
+1
View File
@@ -32,6 +32,7 @@
.logo { .logo {
width: 48px; width: 48px;
height: 48px;
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
+20 -5
View File
@@ -31,7 +31,7 @@ const Login = () => {
setTwoFactor(false); setTwoFactor(false);
setWaitTime(null); setWaitTime(null);
request request
.post(`${config.apiPrefix}login`, { .post(`${config.apiPrefix}user/login`, {
data: { data: {
username: values.username, username: values.username,
password: values.password, password: values.password,
@@ -113,6 +113,14 @@ const Login = () => {
} }
}; };
const codeInputChange = (e: React.ChangeEvent) => {
const { value } = e.target as any;
const regx = /^[0-9]{6}$/;
if (regx.test(value)) {
completeTowFactor({ code: value });
}
};
useEffect(() => { useEffect(() => {
const isAuth = localStorage.getItem(config.authKey); const isAuth = localStorage.getItem(config.authKey);
if (isAuth) { if (isAuth) {
@@ -124,7 +132,11 @@ const Login = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.top}> <div className={styles.top}>
<div className={styles.header}> <div className={styles.header}>
<img alt="logo" className={styles.logo} src="/images/qinglong.png" /> <img
alt="logo"
className={styles.logo}
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/>
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
</span> </span>
@@ -140,12 +152,15 @@ const Login = () => {
{ {
pattern: /^[0-9]{6}$/, pattern: /^[0-9]{6}$/,
message: '验证码为6位数字', message: '验证码为6位数字',
validateTrigger: 'onBlur',
}, },
]} ]}
hasFeedback
> >
<Input placeholder="6位数字" autoFocus autoComplete="off" /> <Input
placeholder="6位数字"
onChange={codeInputChange}
autoFocus
autoComplete="off"
/>
</FormItem> </FormItem>
<Button <Button
type="primary" type="primary"
+100 -24
View File
@@ -24,72 +24,131 @@ const prefixMap: any = {
const EditModal = ({ const EditModal = ({
treeData, treeData,
currentFile, currentNode,
content, content,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
treeData?: any; treeData?: any;
currentFile?: string;
content?: string; content?: string;
visible: boolean; visible: boolean;
socketMessage: any;
currentNode: any;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript'); const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>(''); const [cNode, setCNode] = useState<any>();
const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [log, setLog] = useState<string>(''); const [log, setLog] = useState<string>('');
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === selectedKey || !value) {
return;
}
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setFileName(value); setCNode(node);
setLanguage(newMode); setLanguage(newMode);
getDetail(node); getDetail(node);
setSelectedKey(node.key);
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => { request
setValue(data.data); .get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
}); .then((data) => {
setValue(data.data);
});
}; };
const run = () => {}; const run = () => {
setLog('');
request
.put(`${config.apiPrefix}scripts/run`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(true);
});
};
const stop = () => {
request
.put(`${config.apiPrefix}scripts/stop`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(false);
});
};
useEffect(() => { useEffect(() => {
if (currentFile) { if (!socketMessage) {
setFileName(currentFile); return;
setValue(content as string);
} }
}, [currentFile, content]);
let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (_message === '执行结束') {
_message = '';
setTimeout(() => {
setIsRunning(false);
}, 300);
}
if (log) {
_message = `\n${_message}`;
}
setLog(`${log}${_message}`);
}, [socketMessage]);
useEffect(() => {
if (currentNode) {
setCNode(currentNode);
setValue(content as string);
setSelectedKey(currentNode.key);
}
}, [content, currentNode]);
return ( return (
<Drawer <Drawer
className="edit-modal" className="edit-modal"
closable={false}
title={ title={
<> <>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect <TreeSelect
style={{ marginRight: 8, width: 120 }} style={{ marginRight: 8, width: 150 }}
value={currentFile} value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData} treeData={treeData}
placeholder="请选择脚本文件" placeholder="请选择脚本文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/> />
<Select <Select
value={language} value={language}
style={{ width: 120, marginRight: 8 }} style={{ width: 110, marginRight: 8 }}
onChange={(e) => { onChange={(e) => {
setLanguage(e); setLanguage(e);
}} }}
@@ -99,8 +158,12 @@ const EditModal = ({
<Option value="shell">shell</Option> <Option value="shell">shell</Option>
<Option value="python">python</Option> <Option value="python">python</Option>
</Select> </Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}> <Button
type="primary"
style={{ marginRight: 8 }}
onClick={isRunning ? stop : run}
>
{isRunning ? '停止' : '运行'}
</Button> </Button>
<Button <Button
type="primary" type="primary"
@@ -129,6 +192,16 @@ const EditModal = ({
> >
</Button> </Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
stop();
handleCancel();
}}
>
退
</Button>
</> </>
} }
width={'100%'} width={'100%'}
@@ -136,7 +209,12 @@ const EditModal = ({
onClose={cancel} onClose={cancel}
visible={visible} visible={visible}
> >
<SplitPane split="vertical" minSize={200} defaultSize="50%"> <SplitPane
split="vertical"
minSize={200}
defaultSize="50%"
style={{ height: 'calc(100vh - 55px)' }}
>
<Editor <Editor
language={language} language={language}
value={value} value={value}
@@ -151,9 +229,7 @@ const EditModal = ({
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
<div> <pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
<pre>{log}</pre>
</div>
</SplitPane> </SplitPane>
<SaveModal <SaveModal
visible={saveModalVisible} visible={saveModalVisible}
@@ -164,7 +240,7 @@ const EditModal = ({
content: content:
editorRef.current && editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'), editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName, filename: cNode?.value,
}} }}
/> />
<SettingModal <SettingModal
+32 -4
View File
@@ -1,28 +1,42 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd'; import { Modal, message, Input, Form, Select } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
const { Option } = Select;
const EditScriptNameModal = ({ const EditScriptNameModal = ({
handleCancel, handleCancel,
treeData,
visible, visible,
}: { }: {
visible: boolean; visible: boolean;
handleCancel: (file?: { filename: string }) => void; treeData: any[];
handleCancel: (file?: {
filename: string;
path: string;
key: string;
}) => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
values.path = values.path || '';
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: { filename: values.filename, content: '' }, data: { filename: values.filename, path: values.path, content: '' },
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel({ filename: values.filename }); handleCancel({
filename: values.filename,
path: values.path,
key: `${values.path}-${values.filename}`,
});
} else { } else {
message.error(data); message.error(data);
} }
@@ -33,6 +47,8 @@ const EditScriptNameModal = ({
useEffect(() => { useEffect(() => {
form.resetFields(); form.resetFields();
const originDirs = treeData.filter((x) => x.disabled);
setDirs([{ key: '' }, ...originDirs]);
}, [visible]); }, [visible]);
return ( return (
@@ -56,10 +72,22 @@ const EditScriptNameModal = ({
<Form form={form} layout="vertical" name="edit_name_modal"> <Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item <Form.Item
name="filename" name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]} rules={[{ required: true, message: '请输入文件名' }]}
> >
<Input placeholder="请输入文件名" /> <Input placeholder="请输入文件名" />
</Form.Item> </Form.Item>
<Form.Item
label="父目录"
name="path"
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
>
<Select placeholder="请选择父目录">
{dirs.map((x) => (
<Option value={x.key}>{x.key || '根'}</Option>
))}
</Select>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
+118 -23
View File
@@ -31,20 +31,37 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import EditScriptNameModal from './editNameModal'; import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce';
import { history } from 'umi';
const { Text } = Typography; const { Text } = Typography;
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) { if (keyword) {
const tree: any = []; const tree: any = [];
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
} }
}); });
return { tree }; return { tree, expandedKeys };
} }
return { tree: data }; return { tree: data, expandedKeys };
} }
const LangMap: any = { const LangMap: any = {
@@ -54,7 +71,7 @@ const LangMap: any = {
'.ts': 'typescript', '.ts': 'typescript',
}; };
const Script = ({ headerStyle, isPhone, theme }: any) => { const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const [title, setTitle] = useState('请选择脚本文件'); const [title, setTitle] = useState('请选择脚本文件');
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<any>();
@@ -70,6 +87,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false); const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [currentNode, setCurrentNode] = useState<any>(); const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => { const getScripts = () => {
setLoading(true); setLoading(true);
@@ -78,6 +96,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
.then((data) => { .then((data) => {
setData(data.data); setData(data.data);
setFilterData(data.data); setFilterData(data.data);
initGetScript();
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -90,6 +109,22 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
}); });
}; };
const initGetScript = () => {
const { p, s } = history.location.query as any;
if (s) {
const obj = {
node: {
title: s,
value: s,
key: p ? `${p}/${s}` : s,
parent: p,
},
};
setExpandedKeys([p]);
onTreeSelect([`${p}/${s}`], obj);
}
};
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
@@ -97,12 +132,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
setValue('加载中...'); setValue('加载中...');
const newMode = value ? LangMap[value.slice(-3)] : ''; const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(value); setSelect(node.key);
setTitle(node.parent || node.value); setTitle(node.parent || node.value);
setCurrentNode(node); setCurrentNode(node);
getDetail(node); getDetail(node);
}; };
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback( const onTreeSelect = useCallback(
(keys: Key[], e: any) => { (keys: Key[], e: any) => {
const content = editorRef.current const content = editorRef.current
@@ -131,13 +170,24 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
setSearchValue(keyword); debounceSearch(keyword);
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
setFilterData(tree);
}, },
[data, setFilterData], [data, setFilterData],
); );
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300),
[data, setFilterData],
);
const editFile = () => { const editFile = () => {
setTimeout(() => { setTimeout(() => {
setIsEditing(true); setIsEditing(true);
@@ -147,7 +197,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const cancelEdit = () => { const cancelEdit = () => {
setIsEditing(false); setIsEditing(false);
setValue('加载中...'); setValue('加载中...');
getDetail({ value: select }); getDetail(currentNode);
}; };
const saveFile = () => { const saveFile = () => {
@@ -157,7 +207,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<> <>
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{select} {currentNode.value}
</Text>{' '} </Text>{' '}
</> </>
@@ -170,7 +220,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request request
.put(`${config.apiPrefix}scripts`, { .put(`${config.apiPrefix}scripts`, {
data: { data: {
filename: select, filename: currentNode.value,
path: currentNode.parent || '', path: currentNode.parent || '',
content, content,
}, },
@@ -210,15 +260,30 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request request
.delete(`${config.apiPrefix}scripts`, { .delete(`${config.apiPrefix}scripts`, {
data: { data: {
filename: select, filename: currentNode.value,
path: currentNode.parent || '',
}, },
}) })
.then((_data: any) => { .then((_data: any) => {
if (_data.code === 200) { if (_data.code === 200) {
message.success(`删除成功`); message.success(`删除成功`);
let newData = [...data]; let newData = [...data];
const index = newData.findIndex((x) => x.value === select); if (currentNode.parent) {
newData.splice(index, 1); const parentNodeIndex = newData.findIndex(
(x) => x.key === currentNode.parent,
);
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
newData.splice(index, 1);
}
setData(newData); setData(newData);
} else { } else {
message.error(_data); message.error(_data);
@@ -236,12 +301,27 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
}; };
const addFileModalClose = ( const addFileModalClose = (
{ filename }: { filename: string } = { filename: '' }, { filename, path, key }: { filename: string; path: string; key: string } = {
filename: '',
path: '',
key: '',
},
) => { ) => {
if (filename) { if (filename) {
const newData = [...data]; const newData = [...data];
const _file = { title: filename, key: filename, value: filename }; const _file = { title: filename, key, value: filename, parent: path };
newData.unshift(_file); if (path) {
const parentNodeIndex = newData.findIndex((x) => x.key === path);
const parentNode = newData[parentNodeIndex];
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.unshift(_file);
} else {
parentNode.children = [_file];
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
newData.unshift(_file);
}
setData(newData); setData(newData);
onSelect(_file.value, _file); onSelect(_file.value, _file);
setIsEditing(true); setIsEditing(true);
@@ -253,7 +333,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request request
.post(`${config.apiPrefix}scripts/download`, { .post(`${config.apiPrefix}scripts/download`, {
data: { data: {
filename: select, filename: currentNode.value,
}, },
}) })
.then((_data: any) => { .then((_data: any) => {
@@ -261,7 +341,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = select; a.download = currentNode.value;
document.documentElement.appendChild(a); document.documentElement.appendChild(a);
a.click(); a.click();
document.documentElement.removeChild(a); document.documentElement.removeChild(a);
@@ -297,12 +377,22 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
) : ( ) : (
<Menu> <Menu>
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}> <Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
</Menu.Item> </Menu.Item>
<Menu.Item key="edit" icon={<EditOutlined />} onClick={editFile}> <Menu.Item
key="edit"
icon={<EditOutlined />}
onClick={editFile}
disabled={!select}
>
</Menu.Item> </Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />} onClick={deleteFile}> <Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={deleteFile}
disabled={!select}
>
</Menu.Item> </Menu.Item>
</Menu> </Menu>
@@ -323,7 +413,6 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
treeData={data} treeData={data}
placeholder="请选择脚本文件" placeholder="请选择脚本文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}> <Dropdown overlay={menu} trigger={['click']}>
@@ -349,6 +438,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
</Tooltip>, </Tooltip>,
<Tooltip title="编辑"> <Tooltip title="编辑">
<Button <Button
disabled={!select}
type="primary" type="primary"
onClick={editFile} onClick={editFile}
icon={<EditOutlined />} icon={<EditOutlined />}
@@ -357,6 +447,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<Tooltip title="删除"> <Tooltip title="删除">
<Button <Button
type="primary" type="primary"
disabled={!select}
onClick={deleteFile} onClick={deleteFile}
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
/> />
@@ -390,6 +481,8 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
showIcon={true} showIcon={true}
height={height} height={height}
selectedKeys={[select]} selectedKeys={[select]}
expandedKeys={expandedKeys}
onExpand={onExpand}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
></Tree> ></Tree>
@@ -432,14 +525,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<EditModal <EditModal
visible={isLogModalVisible} visible={isLogModalVisible}
treeData={data} treeData={data}
currentFile={select} currentNode={currentNode}
content={value} content={value}
socketMessage={socketMessage}
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
/> />
<EditScriptNameModal <EditScriptNameModal
visible={isAddFileModalVisible} visible={isAddFileModalVisible}
treeData={data}
handleCancel={addFileModalClose} handleCancel={addFileModalClose}
/> />
</div> </div>
+1 -1
View File
@@ -26,7 +26,7 @@ const AppModal = ({
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(app ? '更新应用成功' : '添加应用成功'); message.success(app ? '更新应用成功' : '新建应用成功');
} else { } else {
message.error(data); message.error(data);
} }
+27 -1
View File
@@ -24,7 +24,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
message.success('已经是最新版了!'); showForceUpdateModal();
} }
} else { } else {
message.error(data); message.error(data);
@@ -39,6 +39,32 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
}; };
const showForceUpdateModal = () => {
Modal.confirm({
width: 500,
title: '更新',
content: (
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version}
</div>
</>
),
okText: '确认',
cancelText: '强制更新',
onCancel() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
.then((_data: any) => {})
.catch((error: any) => {
console.log(error);
});
},
});
};
const showConfirmUpdateModal = (data: any) => { const showConfirmUpdateModal = (data: any) => {
const { lastVersion, lastLog } = data; const { lastVersion, lastLog } = data;
Modal.confirm({ Modal.confirm({
+1 -1
View File
@@ -319,7 +319,7 @@ const Setting = ({
tabActiveKey === 'app' tabActiveKey === 'app'
? [ ? [
<Button key="2" type="primary" onClick={() => addApp()}> <Button key="2" type="primary" onClick={() => addApp()}>
</Button>, </Button>,
] ]
: [] : []
+18
View File
@@ -55,6 +55,14 @@ export default {
name: '任务日志', name: '任务日志',
value: 'logs', value: 'logs',
}, },
{
name: '依赖管理',
value: 'dependencies',
},
{
name: '系统信息',
value: 'system',
},
], ],
scopesMap: { scopesMap: {
crons: '定时任务', crons: '定时任务',
@@ -64,6 +72,7 @@ export default {
logs: '任务日志', logs: '任务日志',
}, },
notificationModes: [ notificationModes: [
{ value: 'gotify', label: 'Gotify' },
{ value: 'goCqHttpBot', label: 'GoCqHttpBot' }, { value: 'goCqHttpBot', label: 'GoCqHttpBot' },
{ value: 'serverChan', label: 'Server酱' }, { value: 'serverChan', label: 'Server酱' },
{ value: 'bark', label: 'Bark' }, { value: 'bark', label: 'Bark' },
@@ -77,6 +86,15 @@ export default {
{ value: 'closed', label: '已关闭' }, { value: 'closed', label: '已关闭' },
], ],
notificationModeMap: { notificationModeMap: {
gotify: [
{
label: 'gotifyUrl',
tip: 'gotify的url地址,例如 https://push.example.de:8080',
required: true,
},
{ label: 'gotifyToken', tip: 'gotify的消息应用token码', required: true },
{ label: 'gotifyPriority', tip: '推送消息的优先级' },
],
goCqHttpBot: [ goCqHttpBot: [
{ {
label: 'goCqHttpBotUrl', label: 'goCqHttpBotUrl',
+3 -3
View File
@@ -34,12 +34,12 @@ const errorHandler = function (error: any) {
const _request = extend({ timeout: 60000, params: { t: time }, errorHandler }); const _request = extend({ timeout: 60000, params: { t: time }, errorHandler });
const apiWhiteList = [ const apiWhiteList = [
'/api/login', '/api/user/login',
'/open/auth/token', '/open/auth/token',
'/api/user/two-factor/login', '/api/user/two-factor/login',
'/api/system', '/api/system',
'/api/init/user', '/api/user/init',
'/api/init/notification', '/api/user/notification/init',
]; ];
_request.interceptors.request.use((url, options) => { _request.interceptors.request.use((url, options) => {
+8 -8
View File
@@ -1,9 +1,9 @@
export const version = '2.10.6'; export const version = '2.10.13';
export const changeLogLink = 'https://t.me/jiao_long/225'; export const changeLogLink = 'https://t.me/jiao_long/233';
export const changeLog = `2.10.6 版本说明 export const changeLog = `2.10.13 版本说明
1. 增加各版本自动打包workflow,感谢 https://github.com/fzls PR 1. task 命令执行脚本支持任意参数,比如 task xxx.js a b c d
2. 环境变量添加更新时间,感谢 https://github.com/miniers PR 2. 应用权限增加依赖管理、系统信息
3. 修复脚本管理结构 3. 调试脚本增加停止操作,优化交互
4. 修复定时匹配规则 4. 定时任务任务名修改展示效果,感谢https://github.com/tinet-jutt
5. 修复检查更新 5. 其他bug修复
`; `;
+1 -7
View File
@@ -27,13 +27,7 @@
"allowJs": true, "allowJs": true,
"noEmit": false "noEmit": false
}, },
"include": [ "include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"],
"src/**/*",
"config/**/*",
".umirc.ts",
"typings.d.ts",
"back/**/*"
],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"lib", "lib",
+34 -2
View File
@@ -2889,7 +2889,7 @@ chokidar@3.5.1:
optionalDependencies: optionalDependencies:
fsevents "~2.3.1" fsevents "~2.3.1"
chokidar@^3.2.2: chokidar@^3.2.2, chokidar@^3.5.2:
version "3.5.2" version "3.5.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
@@ -2995,6 +2995,15 @@ cliui@^6.0.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clone-response@^1.0.2: clone-response@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@@ -4446,7 +4455,7 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-caller-file@^2.0.1: get-caller-file@^2.0.1, get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@@ -10819,6 +10828,11 @@ y18n@^4.0.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@^2.1.2: yallist@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
@@ -10842,6 +10856,11 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^20.2.2:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs@^15.4.1: yargs@^15.4.1:
version "15.4.1" version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
@@ -10859,6 +10878,19 @@ yargs@^15.4.1:
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^18.1.2" yargs-parser "^18.1.2"
yargs@^17.2.1:
version "17.2.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yn@3.1.1: yn@3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"