mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-07 01:04:39 +08:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5faa6eaaa | |||
| eab476cfe8 | |||
| b773def012 | |||
| a9f8b23a7a | |||
| 61bb6535fb | |||
| eb0bdffe06 | |||
| e129485285 | |||
| aac7de172e | |||
| e19d2412fc | |||
| 7cba2cf76c | |||
| 55d7a49dbc | |||
| f695864fd8 | |||
| ae4883fbe7 | |||
| 836dfd2861 | |||
| 6e0523c6d7 | |||
| cd9ba084ae | |||
| c8a0c0de67 | |||
| 5dd8d9df91 | |||
| 2dbee94e85 | |||
| 9001797037 | |||
| 5e7104d5a3 | |||
| 7c33b22d63 | |||
| b7cb1e379c | |||
| 274a6ce52e | |||
| 8079e4e7b8 | |||
| 920efb8fab | |||
| 92aeb2b9da | |||
| ced76cf343 | |||
| 3750d6ecd5 | |||
| 7444f30384 | |||
| b0fd5ba47f | |||
| cc098bc2e5 |
@@ -105,7 +105,9 @@ jobs:
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
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
|
||||
context: docker/
|
||||
push: true
|
||||
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
# 安装说明
|
||||
|
||||
## 前置要求
|
||||
```bash
|
||||
1.已安装docker-ce
|
||||
2.选装docker-compose
|
||||
```
|
||||
## 安装方式1
|
||||
```bash
|
||||
1. 新建一个文件夹,用于存放相关数据
|
||||
2. 下载本仓库中的`docker-compose.yml`至本地,或是复制文件内容后在本地自行建立并粘贴内容
|
||||
3. 使用docker-compose启动
|
||||
4. 浏览器输入ip:5700即可进入面板
|
||||
|
||||
# 新建数据文件夹
|
||||
mkdir qinglong
|
||||
cd qinglong
|
||||
# 下载docker-compose.yml文件
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/develop/docker-compose.yml
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 安装方式2
|
||||
```bash
|
||||
# 复制下列命令在ssh执行(先安装docker)
|
||||
# 注:$PWD请修改为实际你想安装的路径
|
||||
docker run -dit \
|
||||
-v $PWD/ql/config:/ql/config \
|
||||
-v $PWD/ql/log:/ql/log \
|
||||
-v $PWD/ql/db:/ql/db \
|
||||
-v $PWD/ql/repo:/ql/repo \
|
||||
-v $PWD/ql/raw:/ql/raw \
|
||||
-v $PWD/ql/scripts:/ql/scripts \
|
||||
-v $PWD/ql/jbot:/ql/jbot \
|
||||
-p 5700:5700 \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
## 登录
|
||||
```bash
|
||||
|
||||
打开浏览器访问宿主机ip的5700端口即可
|
||||
例如http://192.168.100.123:5700即ip:5700
|
||||
|
||||
首次登录
|
||||
账号:admin 密码:admin
|
||||
会生成`auth.json`
|
||||
|
||||
在ssh输入
|
||||
1.docker exec -it qinglong bash
|
||||
2.cat /ql/config/auth.json
|
||||
|
||||
cat查看之后返回的结果类似如下字段
|
||||
|
||||
{"username":"admin","password":"Xb-ZYP526wmg4_h6q1WqIO"}
|
||||
# admin即为登录名;Xb-ZYP526wmg4_h6q1WqIO为登录密码
|
||||
```
|
||||
|
||||
输入此处记录的`username`及`password`,即可成功登录qinglong面板,登录后即可正常使用
|
||||
|
||||
## 拉取脚本
|
||||
```bash
|
||||
示例
|
||||
ql repo https://github.com/xxx.git #拉取仓库
|
||||
ql raw https://raw.githubusercontent.com/xxx #拉取单个脚本
|
||||
```
|
||||
## 备份
|
||||
|
||||
所有数据都将保存在`docker-compose.yml`所在的同级目录的`data`文件夹中,如需要备份,请直接备份`docker-compose.yml`及`data`文件夹即可
|
||||
|
||||
```bash
|
||||
root@debian:/opt/qinglong# ls -lah
|
||||
总用量 8.0K
|
||||
drwxr-xr-x 3 root root 4.0K 8月 30 01:29 .
|
||||
drwxr-xr-x 4 root root 4.0K 8月 30 00:51 ..
|
||||
drwxr-xr-x 8 root root 4.0K 8月 30 01:30 data
|
||||
-rw-r--r-- 1 root root 386 8月 30 01:29 docker-compose.yml
|
||||
```
|
||||
## 更新
|
||||
|
||||
在面板执行"更新面板"任务即可
|
||||
|
||||
或者
|
||||
```bash
|
||||
cd qinglong
|
||||
docker-compose down
|
||||
docker pull whyour/qinglong:latest
|
||||
docker-compose up -d
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="/public/images/qinglong.png">
|
||||
<img width="150" src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
A timed task management panel that supports typescript, javaScript, python3, and shell.(支持python3、javaScript、shell、typescript 的定时任务管理面板)
|
||||
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.)
|
||||
|
||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url]
|
||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
|
||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
|
||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||
@@ -24,31 +22,151 @@ A timed task management panel that supports typescript, javaScript, python3, and
|
||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<img width="49%" src="/public/images/login.png">
|
||||
<img width="49%" src="/public/images/home.png">
|
||||
</p>
|
||||
[](https://whyour.cn)
|
||||
|
||||
简体中文 | [English](./README-en.md)
|
||||
|
||||
## 功能
|
||||
|
||||
- 支持多种脚本语言(python3、javaScript、shell、typescript)
|
||||
- 支持在线管理脚本、环境变量、配置文件
|
||||
- 支持在线查看任务日志
|
||||
- 支持秒级任务设置
|
||||
- 支持系统级通知
|
||||
- 支持暗黑模式
|
||||
- 支持手机端操作
|
||||
|
||||
## 部署
|
||||
|
||||
### 本机部署
|
||||
|
||||
```bash
|
||||
# 待完善
|
||||
```
|
||||
|
||||
### 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/%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)
|
||||
|
||||
+38
-9
@@ -9,7 +9,8 @@ import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import path from 'path';
|
||||
import path, { join } from 'path';
|
||||
import ScriptService from '../services/script';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
@@ -43,7 +44,7 @@ export default (app: Router) => {
|
||||
children.push({
|
||||
title: childFile,
|
||||
value: childFile,
|
||||
key: `${fileOrDir}-${childFile}`,
|
||||
key: `${fileOrDir}/${childFile}`,
|
||||
mtime: statObj.mtimeMs,
|
||||
parent: fileOrDir,
|
||||
});
|
||||
@@ -83,10 +84,12 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const path = req.query.path ? `${req.query.path}/` : '';
|
||||
const content = getFileContentByName(
|
||||
`${config.scriptPath}${path}${req.params.file}`,
|
||||
const filePath = join(
|
||||
config.scriptPath,
|
||||
req.query.path as string,
|
||||
req.params.file,
|
||||
);
|
||||
const content = getFileContentByName(filePath);
|
||||
res.send({ code: 200, data: content });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -100,7 +103,7 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
content: Joi.string().allow(''),
|
||||
originFilename: Joi.string().allow(''),
|
||||
}),
|
||||
@@ -160,7 +163,7 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
content: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
@@ -172,7 +175,7 @@ export default (app: Router) => {
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${path}/${filename}`;
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
fs.writeFileSync(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
@@ -197,7 +200,7 @@ export default (app: Router) => {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = `${config.scriptPath}${path}/${filename}`;
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
fs.unlinkSync(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
@@ -237,4 +240,30 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/scripts/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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import Logger from './loaders/logger';
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
|
||||
await require('./loaders/initFile').default();
|
||||
|
||||
await require('./loaders/sentry').default({ expressApp: app });
|
||||
|
||||
await require('./loaders/db').default();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createRandomString } from './util';
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
const lastVersionFile =
|
||||
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
||||
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
||||
|
||||
const envFound = dotenv.config();
|
||||
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 versionFile = path.join(rootPath, 'src/version.ts');
|
||||
|
||||
const configFound = dotenv.config({ path: confFile });
|
||||
|
||||
if (envFound.error) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
}
|
||||
|
||||
if (configFound.error) {
|
||||
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
|
||||
}
|
||||
|
||||
export default {
|
||||
port: parseInt(process.env.PORT as string, 10),
|
||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NotificationMode {
|
||||
'gotify' = 'gotify',
|
||||
'goCqHttpBot' = 'goCqHttpBot',
|
||||
'serverChan' = 'serverChan',
|
||||
'bark' = 'bark',
|
||||
@@ -15,6 +16,12 @@ abstract class NotificationBaseInfo {
|
||||
public type!: NotificationMode;
|
||||
}
|
||||
|
||||
export class GotifyNotification extends NotificationBaseInfo {
|
||||
public gotifyUrl = '';
|
||||
public gotifyToken = '';
|
||||
public gotifyPriority = 0;
|
||||
}
|
||||
|
||||
export class GoCqHttpBotNotification extends NotificationBaseInfo {
|
||||
public goCqHttpBotUrl = '';
|
||||
public goCqHttpBotToken = '';
|
||||
@@ -70,6 +77,7 @@ export class EmailNotification extends NotificationBaseInfo {
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
|
||||
+2
-1
@@ -13,4 +13,5 @@ export class SockMessage {
|
||||
export type SockMessageType =
|
||||
| 'ping'
|
||||
| 'installDependence'
|
||||
| 'updateSystemVersion';
|
||||
| 'updateSystemVersion'
|
||||
| 'manuallyRunScript';
|
||||
|
||||
@@ -3,6 +3,7 @@ import dependencyInjectorLoader from './dependencyInjector';
|
||||
import Logger from './logger';
|
||||
import initData from './initData';
|
||||
import { Application } from 'express';
|
||||
import linkDeps from './deps';
|
||||
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
await dependencyInjectorLoader({
|
||||
@@ -15,4 +16,7 @@ export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
|
||||
await initData();
|
||||
Logger.info('✌️ init data loaded');
|
||||
|
||||
await linkDeps();
|
||||
Logger.info('✌️ link deps');
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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');
|
||||
for (const key in groups) {
|
||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||
const group = groups[key];
|
||||
const depIds = group.map((x) => x._id);
|
||||
for (const dep of depIds) {
|
||||
dependenceService.reInstall([dep]);
|
||||
await dependenceService.reInstall([dep]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
+27
-17
@@ -93,17 +93,21 @@ export default class CronService {
|
||||
last_running_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) => {
|
||||
this.cronDb.update(
|
||||
{ _id: { $in: ids } },
|
||||
{
|
||||
$set: {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
last_running_time,
|
||||
last_execution_time,
|
||||
},
|
||||
$set: options,
|
||||
},
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
(err) => {
|
||||
@@ -155,7 +159,9 @@ export default class CronService {
|
||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||
let query = {};
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText, 'i');
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
|
||||
|
||||
query = {
|
||||
$or: [
|
||||
{
|
||||
@@ -245,16 +251,20 @@ export default class CronService {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const pids = pid.match(/\(\d+/g);
|
||||
let pids = pid.match(/\(\d+/g);
|
||||
const killLogs = [];
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
if (pids && pids.length > 0) {
|
||||
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
||||
pids = pids.slice(0, 3);
|
||||
for (const id of pids) {
|
||||
const c = `kill -9 ${id.slice(1)}`;
|
||||
const { stdout, stderr } = await execAsync(c);
|
||||
if (stderr) {
|
||||
killLogs.push(stderr);
|
||||
}
|
||||
if (stdout) {
|
||||
killLogs.push(stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||
|
||||
+95
-89
@@ -131,7 +131,7 @@ export default class DependenceService {
|
||||
{ $set: { status: DependenceStatus.installing, log: [] } },
|
||||
{ multi: true, returnUpdatedDocs: true },
|
||||
async (err, num, docs: Dependence[]) => {
|
||||
this.installOrUninstallDependencies(docs);
|
||||
await this.installOrUninstallDependencies(docs);
|
||||
resolve(docs);
|
||||
},
|
||||
);
|
||||
@@ -178,103 +178,109 @@ export default class DependenceService {
|
||||
dependencies: Dependence[],
|
||||
isInstall: boolean = true,
|
||||
) {
|
||||
if (dependencies.length === 0) {
|
||||
return;
|
||||
}
|
||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependencies[0].type as any];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const depIds = dependencies.map((x) => x._id) as string[];
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
const startTime = Date.now();
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(
|
||||
depIds,
|
||||
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}\n`,
|
||||
);
|
||||
cp.stdout.on('data', (data) => {
|
||||
return new Promise((resolve) => {
|
||||
if (dependencies.length === 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||
const depRunCommand = (
|
||||
isInstall
|
||||
? InstallDependenceCommandTypes
|
||||
: unInstallDependenceCommandTypes
|
||||
)[dependencies[0].type as any];
|
||||
const actionText = isInstall ? '安装' : '删除';
|
||||
const depIds = dependencies.map((x) => x._id) as string[];
|
||||
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||
const startTime = Date.now();
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: JSON.stringify(err),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('close', (code) => {
|
||||
const endTime = Date.now();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).toLocaleString()}`,
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(
|
||||
depIds,
|
||||
`依赖${actionText}${resultText},结束时间 ${new Date(
|
||||
endTime,
|
||||
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||
startTime,
|
||||
).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;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed;
|
||||
} else {
|
||||
status = isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
}
|
||||
this.dependenceDb.update(
|
||||
{ _id: { $in: depIds } },
|
||||
{
|
||||
$set: { status },
|
||||
$unset: { pid: true },
|
||||
},
|
||||
{ multi: true },
|
||||
);
|
||||
cp.stderr.on('data', (data) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: data.toString(),
|
||||
references: depIds,
|
||||
});
|
||||
this.updateLog(depIds, data.toString());
|
||||
});
|
||||
|
||||
// 如果删除依赖成功,3秒后删除数据库记录
|
||||
if (isSucceed && !isInstall) {
|
||||
setTimeout(() => {
|
||||
this.removeDb(depIds);
|
||||
}, 5000);
|
||||
}
|
||||
cp.on('error', (err) => {
|
||||
this.sockService.sendMessage({
|
||||
type: 'installDependence',
|
||||
message: JSON.stringify(err),
|
||||
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
@@ -113,7 +113,9 @@ export default class EnvService {
|
||||
): Promise<Env[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
|
||||
|
||||
condition = {
|
||||
$or: [
|
||||
{
|
||||
@@ -219,11 +221,17 @@ export default class EnvService {
|
||||
|
||||
// 忽略不符合bash要求的环境变量名称
|
||||
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||
env_string += `export ${key}="${_(group)
|
||||
let value = _(group)
|
||||
.filter((x) => x.status !== EnvStatus.disabled)
|
||||
.map('value')
|
||||
.join('&')
|
||||
.replace(/ /g, '')}"\n`;
|
||||
.replace(/ /g, '');
|
||||
if (/"/.test(value)) {
|
||||
value = `'${value}'`;
|
||||
} else {
|
||||
value = `"${value}"`;
|
||||
}
|
||||
env_string += `export ${key}=${value}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export default class NotificationService {
|
||||
private userService!: UserService;
|
||||
|
||||
private modeMap = new Map([
|
||||
['gotify', this.gotify],
|
||||
['goCqHttpBot', this.goCqHttpBot],
|
||||
['serverChan', this.serverChan],
|
||||
['bark', this.bark],
|
||||
@@ -67,6 +68,25 @@ export default class NotificationService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async gotify() {
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||
const res: any = await got
|
||||
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
body: `title=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&message=${encodeURIComponent(
|
||||
this.content,
|
||||
)}&priority=${gotifyPriority}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return typeof res.id === 'number';
|
||||
}
|
||||
|
||||
private async goCqHttpBot() {
|
||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||
const res: any = await got
|
||||
|
||||
@@ -97,7 +97,9 @@ export default class OpenService {
|
||||
): Promise<App[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
const reg = new RegExp(searchText);
|
||||
const encodeText = encodeURIComponent(searchText);
|
||||
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
|
||||
|
||||
condition = {
|
||||
$or: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
|
||||
@Service()
|
||||
export default class ScriptService {
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
|
||||
public async runScript(path: string) {
|
||||
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
|
||||
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: `开始执行脚本`,
|
||||
});
|
||||
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),
|
||||
});
|
||||
});
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
}
|
||||
+16
-7
@@ -403,13 +403,22 @@ export default class UserService {
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
const lastVersionFileContent = await (
|
||||
await got.get(config.lastVersionFile)
|
||||
).body;
|
||||
const lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
const lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
yargs.help('h').alias('h', 'help').help().argv;
|
||||
+2
-4
@@ -8,9 +8,7 @@ services:
|
||||
- ./data/db:/ql/db
|
||||
- ./data/scripts:/ql/scripts
|
||||
- ./data/repo:/ql/repo
|
||||
- ./data/raw:/ql/raw
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
environment:
|
||||
- ENABLE_HANGUP=true
|
||||
- ENABLE_WEB_PANEL=true
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
|
||||
+1
-3
@@ -1,4 +1,4 @@
|
||||
FROM node:lts-alpine
|
||||
FROM node:lts-alpine3.12
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
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 \
|
||||
PS1="\u@\h:\w \$ " \
|
||||
QL_DIR=/ql \
|
||||
QL_MAINTAINER=${QL_MAINTAINER} \
|
||||
QL_URL=${QL_URL} \
|
||||
QL_BRANCH=${QL_BRANCH}
|
||||
WORKDIR ${QL_DIR}
|
||||
RUN set -x \
|
||||
|
||||
@@ -6,6 +6,7 @@ link_shell
|
||||
|
||||
echo -e "======================1. 检测配置文件========================\n"
|
||||
make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||
pm2 l &>/dev/null
|
||||
|
||||
+3
-1
@@ -29,6 +29,7 @@
|
||||
"@sentry/tracing": "^6.14.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"celebrate": "^13.0.3",
|
||||
"chokidar": "^3.5.2",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^3.5.0",
|
||||
"dotenv": "^8.2.0",
|
||||
@@ -50,7 +51,8 @@
|
||||
"sockjs": "^0.3.21",
|
||||
"typedi": "^0.8.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
"winston": "^3.3.3",
|
||||
"yargs": "^17.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 |
@@ -112,4 +112,12 @@ export GOBOT_URL=""
|
||||
export GOBOT_TOKEN=""
|
||||
export GOBOT_QQ=""
|
||||
|
||||
## 10. gotify
|
||||
## gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||
## gotify_token 填写gotify的消息应用token
|
||||
## gotify_priority 填写推送消息优先级,默认为0
|
||||
export GOTIFY_URL="";
|
||||
export GOTIFY_TOKEN="";
|
||||
export GOTIFY_PRIORITY=0;
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
|
||||
@@ -285,6 +285,7 @@ def pushplus_bot(title: str, content: str) -> None:
|
||||
else:
|
||||
|
||||
url_old = "http://pushplus.hxtrip.com/send"
|
||||
headers["Accept"] = "application/json"
|
||||
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||
|
||||
if response["code"] == 200:
|
||||
|
||||
@@ -35,6 +35,8 @@ file_notify_js=$dir_scripts/sendNotify.js
|
||||
task_error_log_path=$dir_log/task_error.log
|
||||
nginx_app_conf=$dir_root/docker/front.conf
|
||||
nginx_conf=$dir_root/docker/nginx.conf
|
||||
dep_notify_py=$dir_dep/notify.py
|
||||
dep_notify_js=$dir_dep/sendNotify.js
|
||||
|
||||
## 清单文件
|
||||
list_crontab_user=$dir_config/crontab.list
|
||||
@@ -210,6 +212,19 @@ fix_config() {
|
||||
cat /dev/null > /etc/nginx/conf.d/default.conf
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $dep_notify_js ]]; then
|
||||
echo -e "复制一份 $file_notify_js_sample 为 $dep_notify_js\n"
|
||||
cp -fv $file_notify_js_sample $dep_notify_js
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $dep_notify_py ]]; then
|
||||
echo -e "复制一份 $file_notify_py_sample 为 $dep_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $dep_notify_py
|
||||
echo
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
npm_install_sub() {
|
||||
|
||||
+1
-1
@@ -400,7 +400,7 @@ gen_list_repo() {
|
||||
fi
|
||||
|
||||
if [[ -d $dir_dep ]]; then
|
||||
cp $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
|
||||
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
|
||||
fi
|
||||
|
||||
for file in ${files}; do
|
||||
|
||||
@@ -79,6 +79,5 @@ export default {
|
||||
fixSiderbar: true,
|
||||
contentWidth: 'Fixed',
|
||||
splitMenus: false,
|
||||
logo: '/images/qinglong.png',
|
||||
siderWidth: 180,
|
||||
} as any;
|
||||
|
||||
+21
-2
@@ -152,8 +152,8 @@
|
||||
|
||||
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
|
||||
margin-bottom: 0 !important;
|
||||
min-height: calc(100vh - 66px);
|
||||
min-height: calc(100vh - var(--vh-offset, 0px) - 66px);
|
||||
min-height: calc(100vh - 72px);
|
||||
min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
|
||||
}
|
||||
|
||||
.Resizer {
|
||||
@@ -235,6 +235,7 @@
|
||||
box-shadow: none;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
-webkit-text-fill-color: @text-color;
|
||||
caret-color: #e8e6f3 !important;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
@@ -292,3 +293,21 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-9
@@ -20,7 +20,7 @@ import './index.less';
|
||||
import vhCheck from 'vh-check';
|
||||
import { version, changeLogLink, changeLog } from '../version';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd';
|
||||
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
|
||||
// @ts-ignore
|
||||
import SockJS from 'sockjs-client';
|
||||
import * as Sentry from '@sentry/react';
|
||||
@@ -232,6 +232,12 @@ export default function (props: any) {
|
||||
selectedKeys={[props.location.pathname]}
|
||||
loading={loading}
|
||||
ErrorBoundary={Sentry.ErrorBoundary}
|
||||
logo={
|
||||
<Image
|
||||
preview={false}
|
||||
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<>
|
||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||
@@ -275,14 +281,16 @@ export default function (props: any) {
|
||||
}}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
rightContentRender={() => (
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)}
|
||||
rightContentRender={() =>
|
||||
ctx.isPhone && (
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<span className="side-menu-user-wrapper">
|
||||
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||
<span style={{ marginLeft: 5 }}>admin</span>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
collapsedButtonRender={(collapsed) => (
|
||||
<span
|
||||
className="side-menu-container"
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.ant-table-pagination.ant-pagination {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
tr {
|
||||
td:nth-child(2) {
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import CronLogModal from './logModal';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { getTableScroll } from '@/utils/index';
|
||||
import { history } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -76,7 +77,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
width: 150,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => (
|
||||
<span>
|
||||
<span
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
}}
|
||||
>
|
||||
{record.name || record._id}{' '}
|
||||
{record.isPinned ? (
|
||||
<span>
|
||||
@@ -329,6 +334,20 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||
|
||||
const goToScriptManager = (record: any) => {
|
||||
const cmd = record.command.split(' ');
|
||||
if (cmd[0] === 'task') {
|
||||
let [p, s] = cmd[1].split('/');
|
||||
if (!s) {
|
||||
s = p;
|
||||
p = '';
|
||||
}
|
||||
history.push(`/script?p=${p}&s=${s}`);
|
||||
} else if (cmd[1] === 'repo') {
|
||||
location.href = cmd[2];
|
||||
}
|
||||
};
|
||||
|
||||
const getCrons = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
@@ -847,6 +866,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||
defaultPageSize: 20,
|
||||
showTotal: (total: number, range: number[]) =>
|
||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
|
||||
}}
|
||||
dataSource={value}
|
||||
rowKey="_id"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
|
||||
.diff-switch-file {
|
||||
min-width: 768px;
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -77,7 +77,7 @@ const EnvModal = ({
|
||||
rules={[
|
||||
{ required: true, message: '请输入环境变量名称', whitespace: true },
|
||||
{
|
||||
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/,
|
||||
pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
|
||||
message: '只能输入字母数字下划线,且不能以数字开头',
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -225,7 +225,11 @@ const Initialization = () => {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.top}>
|
||||
<div className={styles.header}>
|
||||
<img alt="logo" className={styles.logo} src="/images/qinglong.png" />
|
||||
<img
|
||||
alt="logo"
|
||||
className={styles.logo}
|
||||
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
|
||||
/>
|
||||
<span className={styles.title}>初始化配置</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,11 @@ const Login = () => {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.top}>
|
||||
<div className={styles.header}>
|
||||
<img alt="logo" className={styles.logo} src="/images/qinglong.png" />
|
||||
<img
|
||||
alt="logo"
|
||||
className={styles.logo}
|
||||
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
|
||||
/>
|
||||
<span className={styles.title}>
|
||||
{twoFactor ? '两步验证' : config.siteName}
|
||||
</span>
|
||||
|
||||
@@ -24,20 +24,23 @@ const prefixMap: any = {
|
||||
|
||||
const EditModal = ({
|
||||
treeData,
|
||||
currentFile,
|
||||
currentNode,
|
||||
content,
|
||||
handleCancel,
|
||||
visible,
|
||||
socketMessage,
|
||||
}: {
|
||||
treeData?: any;
|
||||
currentFile?: string;
|
||||
content?: string;
|
||||
visible: boolean;
|
||||
socketMessage: any;
|
||||
currentNode: any;
|
||||
handleCancel: () => void;
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [language, setLanguage] = useState<string>('javascript');
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
const [cNode, setCNode] = useState<any>();
|
||||
const [selectedKey, setSelectedKey] = useState<string>('');
|
||||
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
|
||||
const [settingModalVisible, setSettingModalVisible] =
|
||||
useState<boolean>(false);
|
||||
@@ -50,46 +53,79 @@ const EditModal = ({
|
||||
};
|
||||
|
||||
const onSelect = (value: any, node: any) => {
|
||||
if (node.key === selectedKey || !value) {
|
||||
return;
|
||||
}
|
||||
const newMode = LangMap[value.slice(-3)] || '';
|
||||
setFileName(value);
|
||||
setCNode(node);
|
||||
setLanguage(newMode);
|
||||
getDetail(node);
|
||||
setSelectedKey(node.key);
|
||||
};
|
||||
|
||||
const getDetail = (node: any) => {
|
||||
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => {
|
||||
setValue(data.data);
|
||||
});
|
||||
request
|
||||
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
|
||||
.then((data) => {
|
||||
setValue(data.data);
|
||||
});
|
||||
};
|
||||
|
||||
const run = () => {};
|
||||
const run = () => {
|
||||
setLog('');
|
||||
request
|
||||
.put(`${config.apiPrefix}scripts/run`, {
|
||||
data: {
|
||||
filename: cNode.value,
|
||||
path: cNode.parent || '',
|
||||
},
|
||||
})
|
||||
.then((data) => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFile) {
|
||||
setFileName(currentFile);
|
||||
setValue(content as string);
|
||||
if (!socketMessage) {
|
||||
return;
|
||||
}
|
||||
}, [currentFile, content]);
|
||||
|
||||
let { type, message: _message, references } = socketMessage;
|
||||
|
||||
if (type !== 'manuallyRunScript') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (log) {
|
||||
_message = `\n${_message}`;
|
||||
}
|
||||
setLog(`${log}${_message}`);
|
||||
}, [socketMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentNode) {
|
||||
setCNode(currentNode);
|
||||
setValue(content as string);
|
||||
setSelectedKey(currentNode.key);
|
||||
}
|
||||
}, [content, currentNode]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="edit-modal"
|
||||
closable={false}
|
||||
title={
|
||||
<>
|
||||
<span style={{ marginRight: 8 }}>{fileName}</span>
|
||||
<TreeSelect
|
||||
style={{ marginRight: 8, width: 120 }}
|
||||
value={currentFile}
|
||||
style={{ marginRight: 8, width: 150 }}
|
||||
value={selectedKey}
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeData={treeData}
|
||||
placeholder="请选择脚本文件"
|
||||
showSearch
|
||||
key="value"
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<Select
|
||||
value={language}
|
||||
style={{ width: 120, marginRight: 8 }}
|
||||
style={{ width: 110, marginRight: 8 }}
|
||||
onChange={(e) => {
|
||||
setLanguage(e);
|
||||
}}
|
||||
@@ -129,6 +165,15 @@ const EditModal = ({
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => {
|
||||
handleCancel();
|
||||
}}
|
||||
>
|
||||
退出
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={'100%'}
|
||||
@@ -164,7 +209,7 @@ const EditModal = ({
|
||||
content:
|
||||
editorRef.current &&
|
||||
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
||||
filename: fileName,
|
||||
filename: cNode?.value,
|
||||
}}
|
||||
/>
|
||||
<SettingModal
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import EditScriptNameModal from './editNameModal';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { history } from 'umi';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -69,7 +71,7 @@ const LangMap: any = {
|
||||
'.ts': 'typescript',
|
||||
};
|
||||
|
||||
const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
||||
const [title, setTitle] = useState('请选择脚本文件');
|
||||
const [value, setValue] = useState('请选择脚本文件');
|
||||
const [select, setSelect] = useState<any>();
|
||||
@@ -94,6 +96,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
setFilterData(data.data);
|
||||
initGetScript();
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
@@ -106,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) => {
|
||||
if (node.key === select || !value) {
|
||||
return;
|
||||
@@ -119,6 +138,10 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
getDetail(node);
|
||||
};
|
||||
|
||||
const onExpand = (expKeys: any) => {
|
||||
setExpandedKeys(expKeys);
|
||||
};
|
||||
|
||||
const onTreeSelect = useCallback(
|
||||
(keys: Key[], e: any) => {
|
||||
const content = editorRef.current
|
||||
@@ -147,6 +170,13 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
const onSearch = useCallback(
|
||||
(e) => {
|
||||
const keyword = e.target.value;
|
||||
debounceSearch(keyword);
|
||||
},
|
||||
[data, setFilterData],
|
||||
);
|
||||
|
||||
const debounceSearch = useCallback(
|
||||
debounce((keyword) => {
|
||||
setSearchValue(keyword);
|
||||
const { tree, expandedKeys } = getFilterData(
|
||||
keyword.toLocaleLowerCase(),
|
||||
@@ -154,7 +184,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
);
|
||||
setExpandedKeys(expandedKeys);
|
||||
setFilterData(tree);
|
||||
},
|
||||
}, 300),
|
||||
[data, setFilterData],
|
||||
);
|
||||
|
||||
@@ -451,6 +481,8 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
showIcon={true}
|
||||
height={height}
|
||||
selectedKeys={[select]}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={onExpand}
|
||||
showLine={{ showLeafIcon: true }}
|
||||
onSelect={onTreeSelect}
|
||||
></Tree>
|
||||
@@ -493,8 +525,9 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
|
||||
<EditModal
|
||||
visible={isLogModalVisible}
|
||||
treeData={data}
|
||||
currentFile={select}
|
||||
currentNode={currentNode}
|
||||
content={value}
|
||||
socketMessage={socketMessage}
|
||||
handleCancel={() => {
|
||||
setIsLogModalVisible(false);
|
||||
}}
|
||||
|
||||
@@ -24,7 +24,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
|
||||
if (data.hasNewVersion) {
|
||||
showConfirmUpdateModal(data);
|
||||
} else {
|
||||
message.success('已经是最新版了!');
|
||||
showForceUpdateModal();
|
||||
}
|
||||
} else {
|
||||
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 { lastVersion, lastLog } = data;
|
||||
Modal.confirm({
|
||||
|
||||
+7
-8
@@ -1,9 +1,8 @@
|
||||
export const version = '2.10.8';
|
||||
export const changeLogLink = 'https://t.me/jiao_long/227';
|
||||
export const changeLog = `2.10.8 版本说明
|
||||
1. 脚本管理新建文件增加选择父目录
|
||||
2. 修复脚本管理更新文件
|
||||
3. 增加gotify推送,感谢 https://github.com/kilo5hz PR
|
||||
4. 修复不能复制deps目录文件
|
||||
5. 修复可能的玩客云问题
|
||||
export const version = '2.10.11';
|
||||
export const changeLogLink = 'https://t.me/jiao_long/230';
|
||||
export const changeLog = `2.10.11 版本说明
|
||||
1. 修复环境变量中包含双引号变量异常,感谢 https://github.com/mengshouer
|
||||
2. 修复最后运行时长覆盖逻辑
|
||||
3. 修复内容区高度和logo样式
|
||||
4. 修复搜索urlencode的文本时,结果异常
|
||||
`;
|
||||
|
||||
@@ -2889,7 +2889,7 @@ chokidar@3.5.1:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.1"
|
||||
|
||||
chokidar@^3.2.2:
|
||||
chokidar@^3.2.2, chokidar@^3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
|
||||
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
|
||||
@@ -2995,6 +2995,15 @@ cliui@^6.0.0:
|
||||
strip-ansi "^6.0.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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
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"
|
||||
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:
|
||||
version "2.1.2"
|
||||
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"
|
||||
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:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||
@@ -10859,6 +10878,19 @@ yargs@^15.4.1:
|
||||
y18n "^4.0.0"
|
||||
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:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
|
||||
Reference in New Issue
Block a user