Compare commits

...
28 Commits
Author SHA1 Message Date
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
37 changed files with 690 additions and 228 deletions
-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">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="/public/images/qinglong.png">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
@@ -8,12 +8,10 @@
<div align="center">
A timed task management panel that supports typescript, javaScript, python3, and shell.(支持python3、javaScript、shell、typescript 的定时任务管理面板
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url]
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
[donate-url]: https://qinglong.whyour.cn/nice.png
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
@@ -24,31 +22,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://z3.ax1x.com/2021/11/18/I7KrTg.jpg)](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)
+35 -3
View File
@@ -10,6 +10,7 @@ import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import path 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: childFile,
key: `${fileOrDir}-${childFile}`,
mtime: statObj.mtimeMs,
parent: fileOrDir,
});
@@ -120,6 +121,9 @@ export default (app: Router) => {
if (!path.endsWith('/')) {
path += '/';
}
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
code: 430,
@@ -184,15 +188,17 @@ export default (app: Router) => {
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename } = req.body as {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = `${config.scriptPath}${filename}`;
const filePath = `${config.scriptPath}${path}/${filename}`;
fs.unlinkSync(filePath);
res.send({ code: 200 });
} catch (e) {
@@ -232,4 +238,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 = `${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);
}
},
);
};
+2
View File
@@ -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();
-6
View File
@@ -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),
+8
View File
@@ -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
View File
@@ -13,4 +13,5 @@ export class SockMessage {
export type SockMessageType =
| 'ping'
| 'installDependence'
| 'updateSystemVersion';
| 'updateSystemVersion'
| 'manuallyRunScript';
+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');
};
+5 -2
View File
@@ -44,8 +44,11 @@ const run = async () => {
};
app
.listen(config.cronPort, () => {
run();
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
+20
View File
@@ -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
+43
View File
@@ -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 };
}
}
+2 -4
View File
@@ -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
View File
@@ -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 \
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_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 变量名= 声明即可
+53
View File
@@ -13,6 +13,13 @@
const querystring = require('querystring');
const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥
@@ -84,6 +91,16 @@ let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = '';
//==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
if (process.env.GOTIFY_TOKEN) {
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
}
if (process.env.GOTIFY_PRIORITY) {
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
}
if (process.env.GOBOT_URL) {
GOBOT_URL = process.env.GOBOT_URL;
}
@@ -200,9 +217,45 @@ async function sendNotify(
qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp
gotifyNotify(text, desp),//gotify
]);
}
function gotifyNotify(text, desp) {
return new Promise((resolve) => {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('gotify发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.id) {
console.log('gotify发送通知消息成功🎉\n');
} else {
console.log(`${data.message}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve();
}
});
} else {
resolve();
}
});
}
function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => {
if (GOBOT_URL) {
+25
View File
@@ -51,6 +51,10 @@ push_config = {
# /send_group_msg 时填入 group_id=QQ群
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
'GOTIFY_TOKEN': '', # gotify的消息应用token
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
@@ -194,6 +198,25 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
"""
使用 gotify 推送消息。
"""
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url,data=data).json()
if response.get("id"):
print("gotify 推送成功!")
else:
print("gotify 推送失败!")
def iGot(title: str, content: str) -> None:
"""
使用 iGot 推送消息。
@@ -473,6 +496,8 @@ if push_config.get("FSKEY"):
notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot)
if push_config.get("PUSH_KEY"):
+1 -1
View File
@@ -31,7 +31,7 @@ cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
cd $dir_root
cat requirements.txt | while read LREAD
do
if test ! -z "$(pip3 show "${LREAD%%=*}" 1>/dev/null)"; then
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
done
+2 -8
View File
@@ -39,17 +39,11 @@ copy_dep() {
reload_pm2() {
pm2 l &>/dev/null
if test -z "$(pm2 info panel 1>/dev/null)"; then
pm2 reload panel --source-map-support --time &>/dev/null
else
pm2 delete panel --source-map-support --time &>/dev/null
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 reload schedule --source-map-support --time &>/dev/null
else
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
fi
}
pm2_log() {
+2
View File
@@ -10,6 +10,7 @@ dir_repo=$dir_root/repo
dir_raw=$dir_root/raw
dir_log=$dir_root/log
dir_db=$dir_root/db
dir_dep=$dir_root/deps
dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code
dir_update_log=$dir_log/update
@@ -154,6 +155,7 @@ fix_config() {
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
+3 -3
View File
@@ -106,7 +106,7 @@ run_normal() {
cd $dir_scripts
local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]]; then
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path}
first_param=${first_param/$relative_path\//}
fi
@@ -174,7 +174,7 @@ run_concurrent() {
cd $dir_scripts
local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]]; then
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path}
first_param=${first_param/$relative_path\//}
fi
@@ -248,7 +248,7 @@ run_designated() {
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]]; then
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
+11 -17
View File
@@ -318,17 +318,11 @@ patch_version() {
reload_pm2() {
pm2 l &>/dev/null
if [[ $(pm2 info panel 2>/dev/null) ]]; then
pm2 reload panel --source-map-support --time &>/dev/null
else
pm2 delete panel --source-map-support --time &>/dev/null
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 reload schedule --source-map-support --time &>/dev/null
else
pm2 delete schedule --source-map-support --time &>/dev/null
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
files=$(echo "$files" | egrep -v $blackword)
fi
if [[ $dependence ]]; then
cd ${dir_scripts}
depInScripts=$(eval $cmd | sed 's/^..//' | egrep -v $uniq_path | egrep $dependence)
for dep in ${depInScripts}; do
file_path=$(dirname $dep)
[[ ! $file_path =~ "/" ]] && file_path=""
make_dir "${dir_scripts}/${uniq_path}/${file_path#*/}"
cp -f $dep "${dir_scripts}/${uniq_path}/${file_path#*/}"
done
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
@@ -409,6 +398,11 @@ gen_list_repo() {
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done
fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
+1 -1
View File
@@ -79,6 +79,6 @@ export default {
fixSiderbar: true,
contentWidth: 'Fixed',
splitMenus: false,
logo: '/images/qinglong.png',
logo: 'https://z3.ax1x.com/2021/11/18/I7MpAe.png',
siderWidth: 180,
} as any;
+1
View File
@@ -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 {
+8
View File
@@ -1,3 +1,11 @@
.ant-table-pagination.ant-pagination {
margin-bottom: 0 !important;
}
tr {
td:nth-child(2) {
span {
cursor: pointer;
}
}
}
+21 -1
View File
@@ -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"
+14 -6
View File
@@ -118,13 +118,17 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataIndex: 'value',
key: 'value',
align: 'center' as const,
width: '44%',
width: '35%',
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => {
return (
<Tooltip placement="topLeft" title={text}>
<Tooltip
placement="topLeft"
title={text}
trigger={['hover', 'click']}
>
<span>{text}</span>
</Tooltip>
);
@@ -141,7 +145,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
width: 164,
width: 165,
ellipsis: {
showTitle: false,
},
@@ -151,9 +155,13 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
.replace(' 24:', ' 00:');
return (
<Tooltip placement="topLeft" title={date}>
<Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span>
</Tooltip>
);
@@ -528,7 +536,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataSource={value}
rowKey="_id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
loading={loading}
onRow={(record: any, index: number) => {
+5 -1
View File
@@ -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://z3.ax1x.com/2021/11/18/I7MpAe.png"
/>
<span className={styles.title}></span>
</div>
</div>
+7 -4
View File
@@ -16,7 +16,6 @@ function getFilterData(keyword: string, data: any) {
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
expandedKeys.push(...item.children.map((x: any) => x.key));
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
@@ -29,7 +28,7 @@ function getFilterData(keyword: string, data: any) {
...item,
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 [height, setHeight] = useState<number>();
const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getLogs = () => {
setLoading(true);
@@ -100,8 +100,12 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback(
(e) => {
const keyword = e.target.value;
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree);
setExpandedKeys(expandedKeys);
},
[data, setFilterData],
);
@@ -127,7 +131,6 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
treeData={data}
placeholder="请选择日志文件"
showSearch
key="value"
onSelect={onSelect}
/>,
]
+5 -1
View File
@@ -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://z3.ax1x.com/2021/11/18/I7MpAe.png"
/>
<span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName}
</span>
+36 -3
View File
@@ -28,16 +28,19 @@ const EditModal = ({
content,
handleCancel,
visible,
socketMessage,
}: {
treeData?: any;
currentFile?: string;
content?: string;
visible: boolean;
socketMessage: any;
handleCancel: () => void;
}) => {
const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>('');
const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false);
@@ -50,10 +53,14 @@ const EditModal = ({
};
const onSelect = (value: any, node: any) => {
if (node.value === fileName || !value) {
return;
}
const newMode = LangMap[value.slice(-3)] || '';
setFileName(value);
setLanguage(newMode);
getDetail(node);
setSelectedKey(node.key);
};
const getDetail = (node: any) => {
@@ -62,12 +69,39 @@ const EditModal = ({
});
};
const run = () => {};
const run = () => {
request
.put(`${config.apiPrefix}scripts/run`, {
data: {
filename: fileName,
path: '',
},
})
.then((data) => {});
};
useEffect(() => {
if (!socketMessage) {
return;
}
let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (log) {
_message = `\n${_message}`;
}
setLog(`${log}${_message}`);
}, [socketMessage]);
useEffect(() => {
if (currentFile) {
setFileName(currentFile);
setValue(content as string);
setSelectedKey(currentFile);
}
}, [currentFile, content]);
@@ -79,12 +113,11 @@ const EditModal = ({
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect
style={{ marginRight: 8, width: 120 }}
value={currentFile}
value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData}
placeholder="请选择脚本文件"
showSearch
key="value"
onSelect={onSelect}
/>
<Select
+32 -4
View File
@@ -1,28 +1,42 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { Modal, message, Input, Form, Select } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const { Option } = Select;
const EditScriptNameModal = ({
handleCancel,
treeData,
visible,
}: {
visible: boolean;
handleCancel: (file?: { filename: string }) => void;
treeData: any[];
handleCancel: (file?: {
filename: string;
path: string;
key: string;
}) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
const handleOk = async (values: any) => {
setLoading(true);
values.path = values.path || '';
request
.post(`${config.apiPrefix}scripts`, {
data: { filename: values.filename, content: '' },
data: { filename: values.filename, path: values.path, content: '' },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('保存文件成功');
handleCancel({ filename: values.filename });
handleCancel({
filename: values.filename,
path: values.path,
key: `${values.path}-${values.filename}`,
});
} else {
message.error(data);
}
@@ -33,6 +47,8 @@ const EditScriptNameModal = ({
useEffect(() => {
form.resetFields();
const originDirs = treeData.filter((x) => x.disabled);
setDirs([{ key: '' }, ...originDirs]);
}, [visible]);
return (
@@ -56,10 +72,22 @@ const EditScriptNameModal = ({
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
<Form.Item
label="父目录"
name="path"
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
>
<Select placeholder="请选择父目录">
{dirs.map((x) => (
<Option value={x.key}>{x.key || '根'}</Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
+114 -19
View File
@@ -31,20 +31,37 @@ import {
UserOutlined,
} from '@ant-design/icons';
import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce';
import { history } from 'umi';
const { Text } = Typography;
function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
return { tree };
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
return { tree: data };
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const LangMap: any = {
@@ -54,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>();
@@ -70,6 +87,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => {
setLoading(true);
@@ -78,6 +96,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
.then((data) => {
setData(data.data);
setFilterData(data.data);
initGetScript();
})
.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) => {
if (node.key === select || !value) {
return;
@@ -97,12 +132,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
setValue('加载中...');
const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(value);
setSelect(node.key);
setTitle(node.parent || node.value);
setCurrentNode(node);
getDetail(node);
};
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback(
(keys: Key[], e: any) => {
const content = editorRef.current
@@ -131,13 +170,24 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback(
(e) => {
const keyword = e.target.value;
setSearchValue(keyword);
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
setFilterData(tree);
debounceSearch(keyword);
},
[data, setFilterData],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300),
[data, setFilterData],
);
const editFile = () => {
setTimeout(() => {
setIsEditing(true);
@@ -147,7 +197,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const cancelEdit = () => {
setIsEditing(false);
setValue('加载中...');
getDetail({ value: select });
getDetail(currentNode);
};
const saveFile = () => {
@@ -157,7 +207,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
{currentNode.value}
</Text>{' '}
</>
@@ -170,7 +220,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request
.put(`${config.apiPrefix}scripts`, {
data: {
filename: select,
filename: currentNode.value,
path: currentNode.parent || '',
content,
},
@@ -210,15 +260,30 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request
.delete(`${config.apiPrefix}scripts`, {
data: {
filename: select,
filename: currentNode.value,
path: currentNode.parent || '',
},
})
.then((_data: any) => {
if (_data.code === 200) {
message.success(`删除成功`);
let newData = [...data];
const index = newData.findIndex((x) => x.value === select);
if (currentNode.parent) {
const parentNodeIndex = newData.findIndex(
(x) => x.key === currentNode.parent,
);
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
newData.splice(index, 1);
}
setData(newData);
} else {
message.error(_data);
@@ -236,12 +301,27 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
};
const addFileModalClose = (
{ filename }: { filename: string } = { filename: '' },
{ filename, path, key }: { filename: string; path: string; key: string } = {
filename: '',
path: '',
key: '',
},
) => {
if (filename) {
const newData = [...data];
const _file = { title: filename, key: filename, value: filename };
const _file = { title: filename, key, value: filename, parent: path };
if (path) {
const parentNodeIndex = newData.findIndex((x) => x.key === path);
const parentNode = newData[parentNodeIndex];
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.unshift(_file);
} else {
parentNode.children = [_file];
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
newData.unshift(_file);
}
setData(newData);
onSelect(_file.value, _file);
setIsEditing(true);
@@ -253,7 +333,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request
.post(`${config.apiPrefix}scripts/download`, {
data: {
filename: select,
filename: currentNode.value,
},
})
.then((_data: any) => {
@@ -261,7 +341,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = select;
a.download = currentNode.value;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
@@ -299,10 +379,20 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
</Menu.Item>
<Menu.Item key="edit" icon={<EditOutlined />} onClick={editFile}>
<Menu.Item
key="edit"
icon={<EditOutlined />}
onClick={editFile}
disabled={!select}
>
</Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />} onClick={deleteFile}>
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={deleteFile}
disabled={!select}
>
</Menu.Item>
</Menu>
@@ -323,7 +413,6 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
treeData={data}
placeholder="请选择脚本文件"
showSearch
key="value"
onSelect={onSelect}
/>,
<Dropdown overlay={menu} trigger={['click']}>
@@ -349,6 +438,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
</Tooltip>,
<Tooltip title="编辑">
<Button
disabled={!select}
type="primary"
onClick={editFile}
icon={<EditOutlined />}
@@ -357,6 +447,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
@@ -390,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>
@@ -434,12 +527,14 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
treeData={data}
currentFile={select}
content={value}
socketMessage={socketMessage}
handleCancel={() => {
setIsLogModalVisible(false);
}}
/>
<EditScriptNameModal
visible={isAddFileModalVisible}
treeData={data}
handleCancel={addFileModalClose}
/>
</div>
+10
View File
@@ -64,6 +64,7 @@ export default {
logs: '任务日志',
},
notificationModes: [
{ value: 'gotify', label: 'Gotify' },
{ value: 'goCqHttpBot', label: 'GoCqHttpBot' },
{ value: 'serverChan', label: 'Server酱' },
{ value: 'bark', label: 'Bark' },
@@ -77,6 +78,15 @@ export default {
{ value: 'closed', label: '已关闭' },
],
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: [
{
label: 'goCqHttpBotUrl',
+9 -8
View File
@@ -1,9 +1,10 @@
export const version = '2.10.6';
export const changeLogLink = 'https://t.me/jiao_long/225';
export const changeLog = `2.10.6 版本说明
1. 增加各版本自动打包workflow,感谢 https://github.com/fzls PR
2. 环境变量添加更新时间,感谢 https://github.com/miniers PR
3. 修复脚本管理结构
4. 修复定时匹配规则
5. 修复检查更新
export const version = '2.10.9';
export const changeLogLink = 'https://t.me/jiao_long/228';
export const changeLog = `2.10.9 版本说明
1. 任务管理支持任务名跳转脚本管理页,感谢 https://github.com/kilo5hz PR
2. 系统通知支持gotify,感谢 https://github.com/kilo5hz PR
3. 定时任务列表pageSize增加200/500/1000,感谢 https://github.com/fzls PR
4. 修复deps目录依赖文件拷贝
5. 修改alpine基础镜像版本,解决arm32位系统无法启动容器,感谢 https://github.com/lx200916
6. 修复调试功能
`;