Compare commits

...
73 Commits
Author SHA1 Message Date
whyour 133e9e0e1f 更新版本 v2.12.0 2022-03-24 08:47:35 +08:00
whyour 7cc6081d15 修复bot依赖安装目录 2022-03-19 16:44:00 +08:00
whyour 5a24f62a62 更新bot.sh 2022-03-17 23:45:37 +08:00
whyour be597355a7 修复任务详情展示脚本逻辑 2022-03-13 13:21:00 +08:00
whyour 51afcb6c5b 修复任务详情交互样式 2022-03-13 11:09:31 +08:00
whyour 92ebb70eaa 修复定时任务名称标签显示 2022-03-13 00:06:24 +08:00
whyour ab19f30342 修复系统更新时间弹窗 2022-03-13 00:03:58 +08:00
whyour fc9bc7bea2 修复任务列表名称标签提示 2022-03-12 23:57:38 +08:00
whyour 793da29f22 修复定时任务详情日志查看 2022-03-12 23:49:09 +08:00
whyour 5afe4da607 修复退出登录样式 2022-03-12 18:15:42 +08:00
whyour a7a56c03ce 修复退出登录和定时任务搜索 2022-03-12 16:14:18 +08:00
whyour 7639b588d8 修改退出登录样式 2022-03-10 23:58:57 +08:00
whyour 1d536cdd53 修改退出登录样式 2022-03-10 23:53:39 +08:00
whyour 9f08245b6d 修复编辑应用初始值 2022-03-10 22:37:44 +08:00
whyour ff9bf20e57 添加podman安装 2022-03-10 00:29:28 +08:00
whyour b201730100 修复任务详情样式 2022-03-09 23:40:59 +08:00
whyour 6ca098f9e2 修复任务详情手机样式 2022-03-09 22:32:50 +08:00
whyour e0eef05f5c 修复dockerfile 2022-03-09 00:24:36 +08:00
whyour 6398e5ec70 修复dockerfile 2022-03-09 00:17:33 +08:00
whyour 6f93219c44 任务详情脚本增加编辑功能 2022-03-08 00:11:37 +08:00
whyour 9b957ac019 修复任务详情样式 2022-03-07 23:56:15 +08:00
whyour e0d1b127aa 修改更新青龙日志 2022-03-07 23:47:20 +08:00
whyour 64072b3435 修改重置分支shell 2022-03-07 23:40:49 +08:00
whyour f81f7720f0 更新git pull参数 2022-03-07 23:27:16 +08:00
whyour 69fab610e6 修复编译静态资源目录 2022-03-07 23:21:18 +08:00
whyour d693e1f9ea 修改shell根目录获取,移除notify.sh 2022-03-07 23:16:36 +08:00
whyour 2a9ca67355 修复更新青龙命令 2022-03-07 22:33:15 +08:00
whyour 3fb01860d4 修复重置repo地址命令 2022-03-07 22:31:46 +08:00
whyour d92a46d09c 修复更新青龙命令 2022-03-07 22:27:04 +08:00
whyour ec605bfba4 修复shell更新命令 2022-03-06 11:27:48 +08:00
whyour d2f999a5e2 修改定时任务初始化查询条件 2022-03-06 00:07:59 +08:00
whyour 627f541f17 更新版本拷贝2.11.3以前版本数据到data目录 2022-03-06 00:07:08 +08:00
whyour c457dca555 自动更新定时任务2.11.3以前脚本路径 2022-03-06 00:01:03 +08:00
whyour ff3ec7395e 修改任务详情样式 2022-03-05 20:52:21 +08:00
whyour fdaba146ca 移除s390x支持,修改sqlite3包 2022-03-05 14:39:06 +08:00
whyour 31a6d30c23 修改sqlite3包 2022-03-05 13:42:45 +08:00
whyour fa3b185fd5 修改sqlite3包 2022-03-05 12:57:48 +08:00
whyour 101d4fb497 修改 sqlite3 依赖 2022-03-05 12:18:30 +08:00
whyour ccd308f5b0 修改update对应分支 2022-03-05 11:46:21 +08:00
whyour 999d9f8e01 任务详情添加日志列表,更新依赖 2022-03-05 01:24:52 +08:00
whyour 313db0d74e logo使用七牛云 2022-03-04 00:15:42 +08:00
whyour 634e9705a7 增加任务详情 2022-03-04 00:08:33 +08:00
NekoMioandGitHub c959cec738 增加PushDeer推送 #1161 (#1194)
* 增加PushDeer推送
2022-03-03 18:42:11 +08:00
whyour 95330e04a3 修复task修改环境变量时机 2022-03-02 21:37:45 +08:00
whyour a099d09af7 修改openapi获取token逻辑 2022-02-28 22:30:58 +08:00
whyour a0d0197ad7 修改error页面样式 2022-02-26 14:24:23 +08:00
whyour d49e711a34 修复error页面样式 2022-02-26 14:16:51 +08:00
whyour e3b8cd75b9 修改错误日志页面 2022-02-26 14:14:10 +08:00
whyour 7a240eaabb 修改默认跳转 2022-02-26 00:13:52 +08:00
whyour 01564e96d3 添加系统错误页面 2022-02-26 00:12:12 +08:00
whyour 03f4b5c219 修改public服务 2022-02-25 23:06:35 +08:00
whyour 99e1d44f7d 修复public服务 2022-02-25 22:53:24 +08:00
whyour 8c55659c18 增加2.10-2.11升级代码 2022-02-25 22:49:17 +08:00
whyour 3ea980b2f8 增加public服务,查询panel日志 2022-02-24 23:50:04 +08:00
whyour f5fc987b51 修改pane样式 2022-02-24 23:24:21 +08:00
whyour 9cfa4af2b2 更新git pull参数 2022-02-24 22:36:49 +08:00
whyour 3efdf1fa87 修复git reset 2022-02-24 22:22:33 +08:00
whyour 25fc832605 增加使用系统通知配置发送通知api 2022-02-24 22:10:03 +08:00
whyour a7e404c5d5 修改编辑器cdn 2022-02-20 21:36:01 +08:00
whyour 90b27f0564 修复sql查询 2022-02-20 10:15:17 +08:00
whyour 632440965d 修改杀死进程逻辑 2022-02-19 22:54:27 +08:00
whyour aedfac446b 版本文件改为七牛云存储 2022-02-19 22:48:06 +08:00
whyour 3372b0025a 修复定时删除日志 2022-02-19 17:36:49 +08:00
whyour 0112b92e77 调整数据目录 2022-02-19 13:08:14 +08:00
whyour 0caf53b147 修复删除环境变量无效 2022-02-17 11:14:43 +08:00
whyour 4d1354d5f4 更新版本 v2.11.3 2022-02-16 23:09:33 +08:00
whyour fc4562eca2 修改check命令,安装依赖使用代理
修改apk源地址
2022-02-16 23:06:12 +08:00
whyour 5b048a5510 修改logo地址 2022-02-16 22:56:54 +08:00
whyour d23049b2fe 移除esbuild 2022-02-11 13:52:12 +08:00
whyour caac5607b9 环境变量增加更新时间排序 2022-02-11 13:41:48 +08:00
Ghost LeeandGitHub fb2e69b12d 修复goCqHttpBotToken中存在&字符导致认证失败的问题 (#993)
修改goCqHttpBot鉴权方式,调整为使用Header模式
2022-02-08 18:45:24 +08:00
UkennandGitHub fc1838ff39 bark支持自定义推送图标 (#1146)
* bark添加自定义推送图标

* 脚本bark通知添加自定义推送图标
2022-02-08 18:21:37 +08:00
whyour 25689cf40f 修复定时任务排序 2022-02-01 22:33:18 +08:00
72 changed files with 1753 additions and 11982 deletions
+5 -3
View File
@@ -1,6 +1,4 @@
MONGODB_URI='mongodb://' PUBLIC_PORT=5400
YIYAN_MONGODB_URI=''
CRON_PORT=5500 CRON_PORT=5500
BACK_PORT=5600 BACK_PORT=5600
PORT=5700 PORT=5700
@@ -8,3 +6,7 @@ PORT=5700
LOG_LEVEL='debug' LOG_LEVEL='debug'
SECRET='whyour' SECRET='whyour'
QINIU_AK = ''
QINIU_SK = ''
QINIU_SCOPE = ''
-59
View File
@@ -1,59 +0,0 @@
name: Build QL BASE
on:
schedule:
- cron: '00 18 * * *' # GMT 14:00 => 北京时间 2:00
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: Set time zone
uses: szenius/set-timezone@v1.0
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
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: .
file: docker/base/Dockerfile
push: true
tags: whyour/ql:base
+4 -4
View File
@@ -36,9 +36,9 @@ jobs:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }} GITHUB_BRANCH: ${{ github.ref_name }}
run: | run: |
mkdir -p static mkdir -p tmp
cd ./static cd ./tmp
cp -rf ../dist ./ && cp -rf ../build ./ cp -rf ../static/* ./
git init -b ${GITHUB_BRANCH} && git add . git init -b ${GITHUB_BRANCH} && git add .
git config --local user.name 'github-actions[bot]' git config --local user.name 'github-actions[bot]'
git config --local user.email 'github-actions[bot]@users.noreply.github.com' git config --local user.email 'github-actions[bot]@users.noreply.github.com'
@@ -111,7 +111,7 @@ jobs:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
context: docker/ context: docker/
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
+3 -2
View File
@@ -4,12 +4,13 @@
/node_modules /node_modules
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock
/pnpm-lock.yaml /pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
/dist /static
/build /data
# misc # misc
.DS_Store .DS_Store
+7
View File
@@ -4,15 +4,22 @@ const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
layout: false, layout: false,
outputPath: 'static/dist',
nodeModulesTransform: { nodeModulesTransform: {
type: 'none', type: 'none',
}, },
fastRefresh: {}, fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: { dynamicImport: {
loading: '@/components/pageLoading', loading: '@/components/pageLoading',
}, },
favicon: '/images/g5.ico', favicon: '/images/g5.ico',
proxy: { proxy: {
'/api/public': {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
},
'/api': { '/api': {
target: 'http://127.0.0.1:5600/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
+23 -7
View File
@@ -44,6 +44,27 @@
# 待完善 # 待完善
``` ```
### podman 部署
1. podman 安装
```bash
https://podman.io/getting-started/installation
```
2. 启动容器
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### docker 部署 ### docker 部署
1. docker 安装 1. docker 安装
@@ -56,12 +77,7 @@ sudo curl -sSL get.docker.com | sh
```bash ```bash
docker run -dit \ docker run -dit \
-v $PWD/ql/config:/ql/config \ -v $PWD/ql:/ql/data \
-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 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
@@ -81,7 +97,7 @@ sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-c
```bash ```bash
mkdir qinglong mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动 # 启动
docker-compose up -d docker-compose up -d
+20
View File
@@ -333,4 +333,24 @@ export default (app: Router) => {
} }
}, },
); );
route.get(
'/:id/logs',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.logs(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+24 -3
View File
@@ -5,8 +5,8 @@ import * as fs from 'fs';
import config from '../config'; import config from '../config';
import SystemService from '../services/system'; import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { getFileContentByName } from '../config/util';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -17,7 +17,7 @@ export default (app: Router) => {
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile); const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/; const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
@@ -28,7 +28,7 @@ export default (app: Router) => {
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin' &&
envDbContent.length === 0 envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
@@ -111,4 +111,25 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/notify',
celebrate({
body: Joi.object({
title: Joi.string().required(),
content: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.notify(req.body);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+19 -24
View File
@@ -4,32 +4,30 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const lastVersionFile = const lastVersionFile = 'https://qn.whyour.cn/version.ts';
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
const envFound = dotenv.config(); const envFound = dotenv.config();
const rootPath = process.cwd(); const rootPath = process.cwd();
const envFile = path.join(rootPath, 'config/env.sh'); const dataPath = path.join(rootPath, 'data/');
const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
const crontabFile = path.join(rootPath, 'config/crontab.list');
const confBakDir = path.join(rootPath, 'config/bak/');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const extraFile = path.join(rootPath, 'config/extra.sh');
const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const bakPath = path.join(rootPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const logPath = path.join(rootPath, 'log/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh');
const crontabFile = path.join(configPath, 'crontab.list');
const authConfigFile = path.join(configPath, 'auth.json');
const extraFile = path.join(configPath, 'extra.sh');
const confBakDir = path.join(dataPath, 'config/bak/');
const sampleFile = path.join(samplePath, 'config.sample.sh');
const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const dbPath = path.join(rootPath, 'db/');
const cronDbFile = path.join(rootPath, 'db/crontab.db');
const envDbFile = path.join(rootPath, 'db/env.db');
const appDbFile = path.join(rootPath, 'db/app.db');
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 versionFile = path.join(rootPath, 'src/version.ts');
if (envFound.error) { if (envFound.error) {
@@ -39,6 +37,7 @@ if (envFound.error) {
export default { export default {
port: parseInt(process.env.BACK_PORT as string, 10), port: parseInt(process.env.BACK_PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
secret: process.env.SECRET || createRandomString(16, 32), secret: process.env.SECRET || createRandomString(16, 32),
logs: { logs: {
level: process.env.LOG_LEVEL || 'silly', level: process.env.LOG_LEVEL || 'silly',
@@ -59,11 +58,6 @@ export default {
confFile, confFile,
envFile, envFile,
dbPath, dbPath,
cronDbFile,
envDbFile,
appDbFile,
authDbFile,
dependenceDbFile,
configPath, configPath,
scriptPath, scriptPath,
samplePath, samplePath,
@@ -86,4 +80,5 @@ export default {
], ],
versionFile, versionFile,
lastVersionFile, lastVersionFile,
sqliteFile,
}; };
+1 -1
View File
@@ -107,7 +107,7 @@ export function createRandomString(min: number, max: number): string {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]); newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
} }
function getOne(arr) { function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)]; return arr[Math.floor(Math.random() * arr.length)];
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Crontab { export class Crontab {
name?: string; name?: string;
command: string; command: string;
schedule: string; schedule?: string;
timestamp?: string; timestamp?: string;
saved?: boolean; saved?: boolean;
id?: number; id?: number;
+7
View File
@@ -2,6 +2,7 @@ export enum NotificationMode {
'gotify' = 'gotify', 'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan', 'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer',
'bark' = 'bark', 'bark' = 'bark',
'telegramBot' = 'telegramBot', 'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
@@ -32,8 +33,13 @@ export class ServerChanNotification extends NotificationBaseInfo {
public serverChanKey = ''; public serverChanKey = '';
} }
export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = '';
}
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
} }
@@ -79,6 +85,7 @@ export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
+26 -17
View File
@@ -1,33 +1,42 @@
import DataStore from 'nedb';
import config from '../config';
import Logger from './logger'; import Logger from './logger';
import { fileExist } from '../config/util'; import path from 'path';
import DataStore from 'nedb';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron'; import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open'; import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth'; import { AuthModel } from '../data/auth';
import { sequelize } from '../data'; import { sequelize } from '../data';
import { fileExist } from '../config/util';
import config from '../config';
export default async () => { export default async () => {
try { try {
await CrontabModel.sync();
await DependenceModel.sync();
await AppModel.sync();
await AuthModel.sync();
await EnvModel.sync();
await sequelize.sync(); await sequelize.sync();
await new Promise((resolve) => setTimeout(() => resolve(null), 5000));
// try { // try {
// const queryInterface = sequelize.getQueryInterface(); // const queryInterface = sequelize.getQueryInterface();
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true }); // await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true }); // await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
// await queryInterface.addIndex('Apps', ['name'], { unique: true }); // await queryInterface.addIndex('Apps', ['name'], { unique: true });
// } catch (error) { // } catch (error) { }
// } // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
const crondbExist = await fileExist(config.cronDbFile); const envDbFile = path.join(config.rootPath, 'db/env.db');
const dependenceDbExist = await fileExist(config.dependenceDbFile); const appDbFile = path.join(config.rootPath, 'db/app.db');
const envDbExist = await fileExist(config.envDbFile); const authDbFile = path.join(config.rootPath, 'db/auth.db');
const appDbExist = await fileExist(config.appDbFile); const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
const authDbExist = await fileExist(config.authDbFile); const crondbExist = await fileExist(cronDbFile);
const dependenceDbExist = await fileExist(dependenceDbFile);
const envDbExist = await fileExist(envDbFile);
const appDbExist = await fileExist(appDbFile);
const authDbExist = await fileExist(authDbFile);
const cronCount = await CrontabModel.count(); const cronCount = await CrontabModel.count();
const dependenceCount = await DependenceModel.count(); const dependenceCount = await DependenceModel.count();
@@ -36,7 +45,7 @@ export default async () => {
const authCount = await AuthModel.count(); const authCount = await AuthModel.count();
if (crondbExist && cronCount === 0) { if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({ const cronDb = new DataStore({
filename: config.cronDbFile, filename: cronDbFile,
autoload: true, autoload: true,
}); });
cronDb.persistence.compactDatafile(); cronDb.persistence.compactDatafile();
@@ -47,7 +56,7 @@ export default async () => {
if (dependenceDbExist && dependenceCount === 0) { if (dependenceDbExist && dependenceCount === 0) {
const dependenceDb = new DataStore({ const dependenceDb = new DataStore({
filename: config.dependenceDbFile, filename: dependenceDbFile,
autoload: true, autoload: true,
}); });
dependenceDb.persistence.compactDatafile(); dependenceDb.persistence.compactDatafile();
@@ -58,7 +67,7 @@ export default async () => {
if (envDbExist && envCount === 0) { if (envDbExist && envCount === 0) {
const envDb = new DataStore({ const envDb = new DataStore({
filename: config.envDbFile, filename: envDbFile,
autoload: true, autoload: true,
}); });
envDb.persistence.compactDatafile(); envDb.persistence.compactDatafile();
@@ -69,7 +78,7 @@ export default async () => {
if (appDbExist && appCount === 0) { if (appDbExist && appCount === 0) {
const appDb = new DataStore({ const appDb = new DataStore({
filename: config.appDbFile, filename: appDbFile,
autoload: true, autoload: true,
}); });
appDb.persistence.compactDatafile(); appDb.persistence.compactDatafile();
@@ -80,7 +89,7 @@ export default async () => {
if (authDbExist && authCount === 0) { if (authDbExist && authCount === 0) {
const authDb = new DataStore({ const authDb = new DataStore({
filename: config.authDbFile, filename: authDbFile,
autoload: true, autoload: true,
}); });
authDb.persistence.compactDatafile(); authDb.persistence.compactDatafile();
+5 -4
View File
@@ -5,13 +5,14 @@ import routes from '../api';
import config from '../config'; import config from '../config';
import jwt from 'express-jwt'; import jwt from 'express-jwt';
import fs from 'fs'; import fs from 'fs';
import { getFileContentByName, getPlatform, getToken } from '../config/util'; import { getPlatform, getToken } from '../config/util';
import Container from 'typedi'; import Container from 'typedi';
import OpenService from '../services/open'; import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user'; import UserService from '../services/user';
import handler from 'serve-handler'; import handler from 'serve-handler';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
@@ -22,7 +23,7 @@ export default ({ app }: { app: Application }) => {
next(); next();
} else { } else {
return handler(req, res, { return handler(req, res, {
public: 'dist', public: 'static/dist',
rewrites: [{ source: '**', destination: '/index.html' }], rewrites: [{ source: '**', destination: '/index.html' }],
}); });
} }
@@ -104,14 +105,14 @@ export default ({ app }: { app: Application }) => {
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile); const envCount = await EnvModel.count();
let isInitialized = true; let isInitialized = true;
if ( if (
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin' &&
envDbContent.length === 0 envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
+82 -2
View File
@@ -7,11 +7,16 @@ import EnvService from '../services/env';
import _ from 'lodash'; import _ from 'lodash';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import config from '../config';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService);
const scheduleService = Container.get(ScheduleService);
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update( await CrontabModel.update(
@@ -27,7 +32,9 @@ export default async () => {
const group = groups[key]; const group = groups[key];
const depIds = group.map((x) => x.id); const depIds = group.map((x) => x.id);
for (const dep of depIds) { for (const dep of depIds) {
await dependenceService.reInstall([dep]); if (dep) {
await dependenceService.reInstall([dep]);
}
} }
} }
} }
@@ -38,7 +45,7 @@ export default async () => {
where: { where: {
isDisabled: { [Op.ne]: 1 }, isDisabled: { [Op.ne]: 1 },
command: { command: {
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%$ql raw%` }], [Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
}, },
}, },
}).then((docs) => { }).then((docs) => {
@@ -50,9 +57,82 @@ export default async () => {
} }
}); });
// 更新2.11.3以前的脚本路径
CrontabModel.findAll({
where: {
command: {
[Op.or]: [
{ [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
{ [Op.like]: `%\/${config.rootPath}\/config\/%` },
{ [Op.like]: `%\/${config.rootPath}\/log\/%` },
{ [Op.like]: `%\/${config.rootPath}\/db\/%` },
],
},
},
}).then(async (docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc) {
if (doc.command.includes(`${config.rootPath}/scripts/`)) {
await CrontabModel.update(
{ command: doc.command.replace(`${config.rootPath}/scripts/`, '') },
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/log/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/log/${doc.command.replace(
`${config.rootPath}/log/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/config/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/config/${doc.command.replace(
`${config.rootPath}/config/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/db/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/db/${doc.command.replace(
`${config.rootPath}/db/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
}
}
});
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await envService.set_envs(); await envService.set_envs();
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const cron = {
id: data.id,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
};
await scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
runImmediately: true,
});
}
}; };
function randomSchedule(from: number, to: number) { function randomSchedule(from: number, to: number) {
+9 -7
View File
@@ -5,13 +5,15 @@ import Logger from './logger';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
const rootPath = process.cwd(); const rootPath = process.cwd();
const confFile = path.join(rootPath, 'config/config.sh'); const dataPath = path.join(rootPath, 'data/');
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh'); const configPath = path.join(dataPath, 'config/');
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json'); const scriptPath = path.join(dataPath, 'scripts/');
const authConfigFile = path.join(rootPath, 'config/auth.json'); const logPath = path.join(dataPath, 'log/');
const configPath = path.join(rootPath, 'config/'); const samplePath = path.join(rootPath, 'sample/');
const scriptPath = path.join(rootPath, 'scripts/'); const confFile = path.join(configPath, 'config.sh');
const logPath = path.join(rootPath, 'log/'); const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
export default async () => { export default async () => {
const authFileExist = await fileExist(authConfigFile); const authFileExist = await fileExist(authConfigFile);
+34
View File
@@ -0,0 +1,34 @@
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import config from './config';
const app = express();
app.get('/api/public/panel/log', (req, res) => {
exec(
'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, data: err || stderr });
}
return res.send({ code: 200, data: stdout });
},
);
});
app
.listen(config.publicPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
Logger.info(`
################################################
🛡️ Public listening on port: ${config.publicPort} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+43 -7
View File
@@ -226,15 +226,18 @@ export default class CronService {
const killLogs = []; const killLogs = [];
if (pids && pids.length > 0) { if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个 // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) { for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`; const c = `kill -9 ${id.slice(1)}`;
const { stdout, stderr } = await execAsync(c); try {
if (stderr) { const { stdout, stderr } = await execAsync(c);
killLogs.push(stderr); if (stderr) {
} killLogs.push(stderr);
if (stdout) { }
killLogs.push(stdout); if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
} }
} }
} }
@@ -344,6 +347,39 @@ export default class CronService {
} }
} }
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files
.map((x) => ({
filename: x,
directory: logPath,
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
} else {
return [];
}
}
private getKey(command: string) { private getKey(command: string) {
const start = const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0; command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
+1 -1
View File
@@ -80,7 +80,7 @@ export default class DependenceService {
const encodeText = encodeURIComponent(searchValue); const encodeText = encodeURIComponent(searchValue);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchValue}&` }, { [Op.like]: `%${searchValue}%` },
{ [Op.like]: `%${encodeText}%` }, { [Op.like]: `%${encodeText}%` },
], ],
}; };
+1
View File
@@ -49,6 +49,7 @@ export default class EnvService {
public async remove(ids: string[]) { public async remove(ids: string[]) {
await EnvModel.destroy({ where: { id: ids } }); await EnvModel.destroy({ where: { id: ids } });
await this.set_envs();
} }
public async move( public async move(
+28 -10
View File
@@ -16,6 +16,7 @@ export default class NotificationService {
['gotify', this.gotify], ['gotify', this.gotify],
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan], ['serverChan', this.serverChan],
['pushDeer', this.pushDeer],
['bark', this.bark], ['bark', this.bark],
['telegramBot', this.telegramBot], ['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
@@ -90,14 +91,12 @@ export default class NotificationService {
private async goCqHttpBot() { private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got const res: any = await got
.post( .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`, timeout: this.timeout,
{ retry: 0,
timeout: this.timeout, json: { message: `${this.title}\n${this.content}` },
retry: 0, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
json: { message: `${this.title}\n${this.content}` }, })
},
)
.json(); .json();
return res.retcode === 0; return res.retcode === 0;
} }
@@ -118,8 +117,27 @@ export default class NotificationService {
return res.errno === 0 || res.data.errno === 0; return res.errno === 0 || res.data.errno === 0;
} }
private async pushDeer() {
const { pushDeerKey } = this.params;
// https://api2.pushdeer.com/message/push?pushkey=<key>&text=标题&desp=<markdown>&type=markdown
const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${
this.title
}&desp=${encodeURIComponent(this.content)}&type=markdown`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return (
res.content.result.length !== undefined && res.content.result.length > 0
);
}
private async bark() { private async bark() {
let { barkPush, barkSound, barkGroup } = this.params; let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) { if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
@@ -127,7 +145,7 @@ export default class NotificationService {
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
timeout: this.timeout, timeout: this.timeout,
+16 -2
View File
@@ -108,12 +108,26 @@ export default class OpenService {
client_id: string; client_id: string;
client_secret: string; client_secret: string;
}): Promise<any> { }): Promise<any> {
const token = uuidV4(); let token = uuidV4();
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天 const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
const doc = await AppModel.findOne({ where: { client_id, client_secret } }); const doc = await AppModel.findOne({ where: { client_id, client_secret } });
if (doc) { if (doc) {
const timestamp = Math.round(Date.now() / 1000);
const invalidTokens = (doc.tokens || []).filter(
(x) => x.expiration >= timestamp,
);
let tokens = invalidTokens;
if (invalidTokens.length > 5) {
tokens = [
...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration },
];
token = invalidTokens[4].value;
} else {
tokens = [...invalidTokens, { value: token, expiration }];
}
await AppModel.update( await AppModel.update(
{ tokens: [...(doc.tokens || []), { value: token, expiration }] }, { tokens },
{ where: { client_id, client_secret } }, { where: { client_id, client_secret } },
); );
return { return {
+75 -7
View File
@@ -3,24 +3,33 @@ import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { Crontab } from '../data/cron'; import { Crontab } from '../data/cron';
import { exec } from 'child_process'; import { exec } from 'child_process';
import {
ToadScheduler,
SimpleIntervalJob,
Task,
SimpleIntervalSchedule,
} from 'toad-scheduler';
@Service() @Service()
export default class ScheduleService { export default class ScheduleService {
private scheduleStacks = new Map<number, nodeSchedule.Job>(); private scheduleStacks = new Map<string, nodeSchedule.Job>();
private intervalSchedule = new ToadScheduler();
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async generateSchedule({ id = 0, command, name, schedule }: Crontab) { async createCronTask({ id = 0, command, name, schedule = '' }: Crontab) {
const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建定时任务],任务ID: %scron: %s,任务名: %s,执行命令: %s', '[创建cron任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
id, _id,
schedule, schedule,
name, name,
command, command,
); );
this.scheduleStacks.set( this.scheduleStacks.set(
id, _id,
nodeSchedule.scheduleJob(id + '', schedule, async () => { nodeSchedule.scheduleJob(id + '', schedule, async () => {
try { try {
exec(command, async (error, stdout, stderr) => { exec(command, async (error, stdout, stderr) => {
@@ -55,8 +64,67 @@ export default class ScheduleService {
); );
} }
async cancelSchedule({ id = 0, name }: Crontab) { async cancelCronTask({ id = 0, name }: Crontab) {
const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name); this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(id) && this.scheduleStacks.get(id)?.cancel(); this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
}
async createIntervalTask(
{ id = 0, command, name = '' }: Crontab,
schedule: SimpleIntervalSchedule,
) {
const _id = this.formatId(id);
this.logger.info(
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
_id,
name,
command,
);
const task = new Task(name, async () => {
try {
exec(command, async (error, stdout, stderr) => {
if (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
}
if (stderr) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
stderr,
);
}
});
} catch (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
} finally {
}
});
const job = new SimpleIntervalJob({ ...schedule }, task, _id);
this.intervalSchedule.addIntervalJob(job);
}
async cancelIntervalTask({ id = 0, name }: Crontab) {
const _id = this.formatId(id);
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
this.intervalSchedule.removeById(_id);
}
private formatId(id: number): string {
return String(id);
} }
} }
+19 -11
View File
@@ -24,7 +24,7 @@ export default class SystemService {
public async getLogRemoveFrequency() { public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency }); const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return (doc && doc.info) || {}; return doc || {};
} }
private async updateAuthDb(payload: AuthInfo): Promise<any> { private async updateAuthDb(payload: AuthInfo): Promise<any> {
@@ -65,11 +65,13 @@ export default class SystemService {
id: result.id, id: result.id,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${frequency}`, command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
}; };
await this.scheduleService.cancelSchedule(cron); await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) { if (frequency > 0) {
await this.scheduleService.generateSchedule(cron); await this.scheduleService.createIntervalTask(cron, {
days: frequency,
runImmediately: true,
});
} }
return { code: 200, data: { ...cron } }; return { code: 200, data: { ...cron } };
} }
@@ -85,13 +87,10 @@ export default class SystemService {
let lastVersion = ''; let lastVersion = '';
let lastLog = ''; let lastLog = '';
try { try {
const result = await Promise.race([ const result = await got.get(config.lastVersionFile, {
got.get(config.lastVersionFile, { timeout: 6000, retry: 0 }), timeout: 6000,
got.get(`https://ghproxy.com/${config.lastVersionFile}`, { retry: 0,
timeout: 6000, });
retry: 0,
}),
]);
const lastVersionFileContent = result.body; const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1]; lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx) lastLog = lastVersionFileContent.match(logRegx)
@@ -164,4 +163,13 @@ export default class SystemService {
return { code: 200 }; return { code: 200 };
} }
public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) {
return { code: 200, data: '通知发送成功' };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
} }
-114
View File
@@ -307,11 +307,6 @@ export default class UserService {
return (doc && doc.info) || {}; return (doc && doc.info) || {};
} }
public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return (doc && doc.info) || {};
}
private async updateAuthDb(payload: AuthInfo): Promise<any> { private async updateAuthDb(payload: AuthInfo): Promise<any> {
let doc = await AuthModel.findOne({ type: payload.type }); let doc = await AuthModel.findOne({ type: payload.type });
if (doc) { if (doc) {
@@ -348,113 +343,4 @@ export default class UserService {
return { code: 400, data: '通知发送失败,请检查参数' }; return { code: 400, data: '通知发送失败,请检查参数' };
} }
} }
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
} }
-14
View File
@@ -1,14 +0,0 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data/config:/ql/config
- ./data/log:/ql/log
- ./data/db:/ql/db
- ./data/scripts:/ql/scripts
- ./data/repo:/ql/repo
- ./data/raw:/ql/raw
ports:
- "0.0.0.0:5700:5700"
restart: unless-stopped
+34 -5
View File
@@ -1,4 +1,5 @@
FROM whyour/ql:base FROM node:alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
@@ -13,17 +14,45 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
python3 \
jq \
openssh \
py3-pip \
&& rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& cp -rf /node_modules ./ \
&& rm -rf /node_modules \
&& pnpm install --prod \ && pnpm install --prod \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.cache \ && rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& cp -rf /static/* ${QL_DIR} \ && mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
-49
View File
@@ -1,49 +0,0 @@
FROM node:alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ "
COPY package.json /
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
python3 \
jq \
openssh \
py3-pip \
python2 \
g++ \
make \
&& rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& cd / && pnpm install --prod \
&& apk --purge del python2 g++ make \
&& rm -rf /root/.npm \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.cache \
&& rm -f /package.json
CMD [ "node" ]
+9
View File
@@ -0,0 +1,9 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data:/ql/data
ports:
- "0.0.0.0:5700:5700"
restart: unless-stopped
+11 -6
View File
@@ -20,24 +20,29 @@ echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n" echo -e "nginx启动成功...\n"
echo -e "======================4. 启动控制面板========================\n" echo -e "======================4. 启动面板监控========================\n"
pm2 delete public &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time
echo -e "监控服务启动成功...\n"
echo -e "======================5. 启动控制面板========================\n"
pm2 delete panel &>/dev/null pm2 delete panel &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time pm2 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n" echo -e "控制面板启动成功...\n"
echo -e "======================5. 启动定时任务========================\n" echo -e "======================6. 启动定时任务========================\n"
pm2 delete schedule &>/dev/null pm2 delete schedule &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n" echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================6. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 & nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================7. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 & nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+12 -1
View File
@@ -2,6 +2,10 @@ upstream api {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
upstream public {
server 0.0.0.0:5400;
}
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default keep-alive; default keep-alive;
'websocket' upgrade; 'websocket' upgrade;
@@ -9,9 +13,16 @@ map $http_upgrade $connection_upgrade {
server { server {
listen 5700; listen 5700;
root /ql/dist; root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location /api/public {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://public;
}
location /api { location /api {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
+57 -52
View File
@@ -4,10 +4,12 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "umi dev", "start:front": "umi dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts",
"build:front": "umi build", "build:front": "umi build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node build/schedule.js", "schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"prepare": "umi generate tmp", "prepare": "umi generate tmp",
"test": "umi-test", "test": "umi-test",
@@ -26,17 +28,16 @@
}, },
"dependencies": { "dependencies": {
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^6.17.2", "@sentry/node": "^6.18.1",
"@sentry/tracing": "^6.17.2", "@sentry/tracing": "^6.18.1",
"body-parser": "^1.19.0", "body-parser": "^1.19.2",
"celebrate": "^13.0.3", "celebrate": "^15.0.1",
"chokidar": "^3.5.2", "chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^3.5.0", "cron-parser": "^4.2.1",
"dotenv": "^8.2.0", "dotenv": "^16.0.0",
"esbuild": "^0.14.14", "express": "^4.17.3",
"express": "^4.17.1", "express-jwt": "^6.1.1",
"express-jwt": "^6.0.0",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
@@ -44,52 +45,56 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-fetch": "^3.2.1",
"node-schedule": "^2.0.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.0", "nodemailer": "^6.7.2",
"p-queue": "6.6.2", "p-queue": "7.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^7.0.0-alpha.3", "sequelize": "^6.17.0",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.21", "sockjs": "^0.3.24",
"sqlite3": "^5.0.2", "sqlite3": "npm:@louislam/sqlite3@^6.0.1",
"typedi": "^0.8.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.3.3", "winston": "^3.6.0",
"yargs": "^17.2.1" "yargs": "^17.3.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.26.0", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "^4.3.1",
"@sentry/react": "^6.17.2", "@sentry/react": "^6.18.1",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.12",
"@types/express": "^4.17.8", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.1", "@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.170", "@types/lodash": "^4.14.179",
"@types/nedb": "^1.8.11", "@types/nedb": "^1.8.12",
"@types/node": "^14.11.2", "@types/node": "^17.0.21",
"@types/node-fetch": "^2.5.8", "@types/node-fetch": "^2.6.1",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.1", "@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.0", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.13",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.3", "@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.11.0", "@umijs/plugin-antd": "^0.15.0",
"@umijs/test": "^3.3.9", "@umijs/plugin-esbuild": "^1.4.1",
"antd": "^4.17.0-alpha.6", "@umijs/test": "^3.5.21",
"codemirror": "^5.62.2", "ansi-to-react": "^6.1.6",
"compression-webpack-plugin": "6.1.1", "antd": "^4.18.9",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"darkreader": "4.9.40", "darkreader": "4.9.44",
"lint-staged": "^10.0.7", "lint-staged": "^12.3.4",
"nodemon": "^2.0.4", "nodemon": "^2.0.15",
"prettier": "^2.2.0", "prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"react": "17.x", "react": "17.x",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
@@ -98,13 +103,13 @@
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x", "react-dom": "17.x",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.5.2", "sockjs-client": "^1.6.0",
"ts-node": "^9.0.0", "ts-node": "^10.6.0",
"typescript": "^4.1.2", "typescript": "^4.6.2",
"umi": "^3.5.0", "umi": "^3.5.21",
"umi-request": "^1.3.5", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.28.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
} }
+6
View File
@@ -50,6 +50,8 @@ export PUSH_KEY=""
## 2. BARK ## 2. BARK
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123 ## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH="" export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong" ## 下方填写推送消息分组,默认为"QingLong"
@@ -123,4 +125,8 @@ export GOTIFY_URL=""
export GOTIFY_TOKEN="" export GOTIFY_TOKEN=""
export GOTIFY_PRIORITY=0 export GOTIFY_PRIORITY=0
## 11. PushDeer
## deer_key 填写PushDeer的key
export DEER_KEY=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+76 -12
View File
@@ -25,18 +25,25 @@ let GOTIFY_PRIORITY = 0;
//gobot_token 填写在go-cqhttp文件设置的访问密钥 //gobot_token 填写在go-cqhttp文件设置的访问密钥
//gobot_qq 填写推送到个人QQ或者QQ群号 //gobot_qq 填写推送到个人QQ或者QQ群号
//go-cqhttp相关API https://docs.go-cqhttp.org/api //go-cqhttp相关API https://docs.go-cqhttp.org/api
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
let GOBOT_TOKEN = ''; //访问密钥 let GOBOT_TOKEN = ''; //访问密钥
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群 let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
// =======================================微信server酱通知设置区域=========================================== // =======================================微信server酱通知设置区域===========================================
//此处填你申请的SCKEY. //此处填你申请的SCKEY.
//(环境变量名 PUSH_KEY) //(环境变量名 PUSH_KEY)
let SCKEY = ''; let SCKEY = '';
// =======================================PushDeer通知设置区域===========================================
//此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY)
let PUSHDEER_KEY = '';
// =======================================Bark App通知设置区域=========================================== // =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX) //此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = ''; let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'https://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
@@ -115,6 +122,10 @@ if (process.env.PUSH_KEY) {
SCKEY = process.env.PUSH_KEY; SCKEY = process.env.PUSH_KEY;
} }
if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY;
}
if (process.env.QQ_SKEY) { if (process.env.QQ_SKEY) {
QQ_SKEY = process.env.QQ_SKEY; QQ_SKEY = process.env.QQ_SKEY;
} }
@@ -133,6 +144,9 @@ if (process.env.BARK_PUSH) {
} else { } else {
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`; BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
} }
if (process.env.BARK_ICON) {
BARK_ICON = process.env.BARK_ICON;
}
if (process.env.BARK_SOUND) { if (process.env.BARK_SOUND) {
BARK_SOUND = process.env.BARK_SOUND; BARK_SOUND = process.env.BARK_SOUND;
} }
@@ -216,8 +230,8 @@ async function sendNotify(
qywxBotNotify(text, desp), //企业微信机器人 qywxBotNotify(text, desp), //企业微信机器人
qywxamNotify(text, desp), //企业微信应用消息推送 qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp),//gotify gotifyNotify(text, desp), //gotify
]); ]);
} }
@@ -226,10 +240,12 @@ function gotifyNotify(text, desp) {
if (GOTIFY_URL && GOTIFY_TOKEN) { if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = { const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`, url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`, body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
desp,
)}&priority=${GOTIFY_PRIORITY}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
} },
}; };
$.post(options, (err, resp, data) => { $.post(options, (err, resp, data) => {
try { try {
@@ -261,7 +277,7 @@ function gobotNotify(text, desp, time = 2100) {
if (GOBOT_URL) { if (GOBOT_URL) {
const options = { const options = {
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`, url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
json: {message:`${text}\n${desp}`}, json: { message: `${text}\n${desp}` },
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@@ -346,6 +362,52 @@ function serverNotify(text, desp, time = 2100) {
}); });
} }
function PushDeerNotify(text, desp, time = 2100) {
return new Promise((resolve) => {
if (PUSHDEER_KEY) {
// PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp);
const options = {
url: `https://api2.pushdeer.com/message/push`,
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
// 通过反悔的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
console.log(
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}, time);
} else {
resolve();
}
});
}
function CoolPush(text, desp) { function CoolPush(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (QQ_SKEY) { if (QQ_SKEY) {
@@ -429,7 +491,9 @@ function BarkNotify(text, desp, params = {}) {
const options = { const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent( url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp, desp,
)}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`, )}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params,
)}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
@@ -726,8 +790,8 @@ function qywxamNotify(text, desp) {
if (err) { if (err) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息失败!!\n', '企业微信应用消息发送通知消息失败!!\n',
); );
console.log(err); console.log(err);
} else { } else {
@@ -735,8 +799,8 @@ function qywxamNotify(text, desp) {
if (data.errcode === 0) { if (data.errcode === 0) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息成功🎉。\n', '企业微信应用消息发送通知消息成功🎉。\n',
); );
} else { } else {
console.log(`${data.errmsg}\n`); console.log(`${data.errmsg}\n`);
+24
View File
@@ -35,6 +35,7 @@ push_config = {
'BARK_ARCHIVE': '', # bark 推送是否存档 'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组 'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音 'BARK_SOUND': '', # bark 推送声音
'BARK_ICON': '', # bark 推送图标
'CONSOLE': True, # 控制台输出 'CONSOLE': True, # 控制台输出
@@ -59,6 +60,8 @@ push_config = {
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -104,6 +107,7 @@ def bark(title: str, content: str) -> None:
"BARK_ARCHIVE": "isArchive", "BARK_ARCHIVE": "isArchive",
"BARK_GROUP": "group", "BARK_GROUP": "group",
"BARK_SOUND": "sound", "BARK_SOUND": "sound",
"BARK_ICON": "icon",
} }
params = "" params = ""
for pair in filter( for pair in filter(
@@ -259,6 +263,24 @@ def serverJ(title: str, content: str) -> None:
print(f'serverJ 推送失败!错误码:{response["message"]}') print(f'serverJ 推送失败!错误码:{response["message"]}')
def pushdeer(title: str, content: str) -> None:
"""
通过PushDeer 推送消息
"""
if not push_config.get("DEER_KEY"):
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return
print("PushDeer 服务启动")
data = {"text": title, "desp": urllib.parse.urlencode({"text": content})}
url = 'https://api2.pushdeer.com/message/push'
response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!")
else:
print("PushDeer 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -503,6 +525,8 @@ if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot) notify_function.append(iGot)
if push_config.get("PUSH_KEY"): if push_config.get("PUSH_KEY"):
notify_function.append(serverJ) notify_function.append(serverJ)
if push_config.get("DEER_KEY"):
notify_function.append(pushdeer)
if push_config.get("PUSH_PLUS_TOKEN"): if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot) notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"): if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
+36
View File
@@ -0,0 +1,36 @@
import * as qiniu from 'qiniu';
import dotenv from 'dotenv';
const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const options = {
scope: process.env.QINIU_SCOPE,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra('', '', 'text/plain');
const key = 'version.ts';
// 文件上传
formUploader.putFile(
uploadToken,
key,
localFile,
putExtra,
function (respErr, respBody, respInfo) {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
console.log(respBody);
} else {
console.log(respInfo.statusCode);
console.log(respBody);
}
},
);
+26
View File
@@ -176,4 +176,30 @@ update_cron() {
fi fi
} }
notify_api() {
local title=$1
local content=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
}
get_token get_token
+7 -7
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 ## 导入通用变量与函数
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
if [[ -z ${BotRepoUrl} ]]; then if [[ -z ${BotRepoUrl} ]]; then
@@ -22,9 +22,9 @@ if [[ ! -d ${repo_path}/.git ]]; then
git_clone_scripts ${url} ${repo_path} "main" git_clone_scripts ${url} ${repo_path} "main"
fi fi
cp -rf "$repo_path/jbot" $dir_root cp -rf "$repo_path/jbot" $dir_data
if [[ ! -f "$dir_root/config/bot.json" ]]; then if [[ ! -f "$dir_config/bot.json" ]]; then
cp -f "$repo_path/config/bot.json" "$dir_root/config" cp -f "$repo_path/config/bot.json" "$dir_config"
fi fi
echo -e "\nbot文件下载成功...\n" echo -e "\nbot文件下载成功...\n"
@@ -32,9 +32,9 @@ echo -e "3、安装python3依赖...\n"
if [[ $PipMirror ]]; then if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror pip3 config set global.index-url $PipMirror
fi fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_root" cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_root cd $dir_data
cat requirements.txt | while read LREAD cat requirements.txt | while read LREAD
do do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
@@ -46,7 +46,7 @@ echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n" echo -e "4、启动bot程序...\n"
make_dir $dir_log/bot make_dir $dir_log/bot
cd $dir_root cd $dir_data
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 & nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n" echo -e "bot启动成功...\n"
+6 -7
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
@@ -10,13 +10,11 @@ reset_env() {
echo -e "---> 配置文件检测完成\n" echo -e "---> 配置文件检测完成\n"
echo -e "---> 2. 开始安装青龙依赖\n" echo -e "---> 2. 开始安装青龙依赖\n"
rm -rf $dir_root/node_modules
npm_install_2 $dir_root npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n" echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n" echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
rm -rf $dir_scripts/node_modules
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n" echo -e "---> 脚本依赖安装完成\n"
} }
@@ -40,18 +38,18 @@ reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
} }
pm2_log() { pm2_log() {
echo -e "---> pm2日志" echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log" local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log" local panelError="/root/.pm2/logs/panel-error.log"
tail -n 20 "$panelOut" tail -n 100 "$panelOut"
tail -n 20 "$panelError" tail -n 100 "$panelError"
} }
check_nginx() { check_nginx() {
@@ -100,6 +98,7 @@ check_pm2() {
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
pnpm install -g pm2
copy_dep copy_dep
check_ql check_ql
check_nginx check_nginx
-5
View File
@@ -1,5 +0,0 @@
const notify = require('/ql/scripts/sendNotify.js');
const title = process.argv[2];
const content = process.argv[3];
notify.sendNotify(`${title}`, `${content}`);
-8
View File
@@ -1,8 +0,0 @@
#!/bin/bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts
. /ql/config/config.sh
title=$(echo $1|sed 's/-/_/g')
msg=$(echo -e $2)
node /ql/shell/notify.js "$title" "$msg"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 ## 导入通用变量与函数
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
days=$1 days=$1
+35 -20
View File
@@ -1,16 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 目录 ## 目录
dir_root=/ql dir_root=$QL_DIR
dir_data=$dir_root/data
dir_shell=$dir_root/shell dir_shell=$dir_root/shell
dir_sample=$dir_root/sample dir_sample=$dir_root/sample
dir_config=$dir_root/config dir_static=$dir_root/static
dir_scripts=$dir_root/scripts dir_config=$dir_data/config
dir_repo=$dir_root/repo dir_scripts=$dir_data/scripts
dir_raw=$dir_root/raw dir_repo=$dir_data/repo
dir_log=$dir_root/log dir_raw=$dir_data/raw
dir_db=$dir_root/db dir_log=$dir_data/log
dir_dep=$dir_root/deps dir_db=$dir_data/db
dir_dep=$dir_data/deps
dir_list_tmp=$dir_log/.tmp dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code dir_code=$dir_log/code
dir_update_log=$dir_log/update dir_update_log=$dir_log/update
@@ -163,6 +165,8 @@ define_cmd() {
} }
fix_config() { fix_config() {
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config make_dir $dir_config
make_dir $dir_log make_dir $dir_log
make_dir $dir_db make_dir $dir_db
@@ -223,7 +227,7 @@ fix_config() {
if [[ -s /etc/nginx/conf.d/default.conf ]]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n" echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null > /etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
echo echo
fi fi
@@ -238,10 +242,11 @@ fix_config() {
cp -fv $file_notify_py_sample $dep_notify_py cp -fv $file_notify_py_sample $dep_notify_py
echo echo
fi fi
} }
npm_install_sub() { npm_install_sub() {
set_proxy
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then elif ! type pnpm &>/dev/null; then
@@ -249,6 +254,7 @@ npm_install_sub() {
else else
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
fi fi
unset_proxy
} }
npm_install_1() { npm_install_1() {
@@ -301,7 +307,7 @@ git_clone_scripts() {
local branch=$3 local branch=$3
[[ $branch ]] && local part_cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始克隆仓库 $url$dir\n"
set_proxy set_proxy
git clone $part_cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
@@ -312,17 +318,16 @@ git_pull_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work="$1" local dir_work="$1"
local branch="$2" local branch="$2"
[[ $branch ]] && local part_cmd="origin/${branch}"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work\n"
set_proxy set_proxy
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git reset --hard $part_cmd git pull &>/dev/null
git pull
unset_proxy unset_proxy
reset_branch "$branch"
cd $dir_current cd $dir_current
} }
@@ -332,17 +337,27 @@ reset_romote_url() {
local url=$2 local url=$2
local branch="$3" local branch="$3"
[[ $branch ]] && local part_cmd="origin/${branch}"
if [[ -d "$dir_work/.git" ]]; then if [[ -d "$dir_work/.git" ]]; then
cd $dir_work cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null [[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url >/dev/null git remote set-url origin $url &>/dev/null
git reset --hard $part_cmd >/dev/null
local part_cmd=""
reset_branch "$branch"
cd $dir_current cd $dir_current
fi fi
} }
reset_branch() {
local branch="$1"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch"
git branch --set-upstream-to=$part_cmd $branch
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() { random_range() {
local beg=$1 local beg=$1
local end=$2 local end=$2
@@ -355,4 +370,4 @@ detect_macos
define_cmd define_cmd
fix_config fix_config
import_config $1 > $task_error_log_path 2>&1 import_config $1 >$task_error_log_path 2>&1
+3 -5
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 ## 导入通用变量与函数
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
@@ -143,6 +143,7 @@ run_concurrent() {
done done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$first_param" define_program "$first_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S") log_time=$(date "+%Y-%m-%d-%H-%M-%S")
@@ -166,8 +167,6 @@ run_concurrent() {
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
@@ -236,6 +235,7 @@ run_designated() {
done done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
@@ -244,8 +244,6 @@ run_designated() {
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
+55 -33
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
@@ -43,7 +43,7 @@ detect_config_version() {
local notify_title="配置文件更新通知" local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n" local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content echo -e $notify_content
notify "$notify_title" "$notify_content" notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark [[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi fi
else else
@@ -91,7 +91,7 @@ del_cron() {
done done
if [[ $ids ]]; then if [[ $ids ]]; then
result=$(del_cron_api "$ids") result=$(del_cron_api "$ids")
notify "$path 删除任务${result}" "$detail" notify_api "$path 删除任务${result}" "$detail"
fi fi
} }
@@ -131,7 +131,7 @@ add_cron() {
fi fi
fi fi
done done
notify "$path 新增任务" "$detail" notify_api "$path 新增任务" "$detail"
} }
## 更新仓库 ## 更新仓库
@@ -161,15 +161,7 @@ update_repo() {
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else else
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
rm -rf ${repo_path}
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
fi fi
} }
@@ -211,7 +203,7 @@ update_raw() {
if [[ -z $cron_id ]]; then if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n" echo -e "$result\n"
notify "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi fi
else else
@@ -252,12 +244,17 @@ update_qinglong() {
patch_version patch_version
local no_restart="$1" local no_restart="$1"
local all_branch=$(git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${current_branch} reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${current_branch} git_pull_scripts $dir_root ${primary_branch}
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$dir_root成功...\n" echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version detect_config_version
update_depend update_depend
@@ -265,23 +262,23 @@ update_qinglong() {
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else else
echo -e "\n更新$dir_root失败,请检查原因...\n" echo -e "\n更新青龙源文件失败,请检查原因...\n"
fi fi
local url="https://github.com/whyour/qinglong-static.git" local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${current_branch} reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${current_branch} git_pull_scripts ${ql_static_repo} ${primary_branch}
else else
git_clone_scripts ${url} ${ql_static_repo} git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$ql_static_repo成功...\n" echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1) local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n" echo -e "\n当前版本 $static_version...\n"
cd $dir_root
rm -rf $dir_root/build && rm -rf $dir_root/dist rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_root cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..." echo -e "重启面板中..."
@@ -289,16 +286,16 @@ update_qinglong() {
reload_pm2 reload_pm2
fi fi
else else
echo -e "\n更新$dir_root失败,请检查原因...\n" echo -e "\n更新青龙静态资源失败,请检查原因...\n"
fi fi
} }
patch_version() { patch_version() {
if [[ -f "/ql/db/cookie.db" ]]; then if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n" echo -e "检测到旧的db文件,拷贝为新db...\n"
mv /ql/db/cookie.db /ql/db/env.db mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf /ql/db/cookie.db rm -rf $dir_root/db/cookie.db
echo echo
fi fi
@@ -308,17 +305,42 @@ patch_version() {
git config --global pull.rebase false git config --global pull.rebase false
cp -f /ql/.env.example /ql/.env cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
} }
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
} }
## 对比脚本 ## 对比脚本
+77
View File
@@ -0,0 +1,77 @@
.react-terminal-wrapper {
width: 100%;
background: #252a33;
color: #eee;
font-size: 18px;
font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier,
monospace;
border-radius: 4px;
padding: 75px 45px 35px;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.react-terminal {
overflow: auto;
display: flex;
flex-direction: column;
}
.react-terminal-wrapper.react-terminal-light {
background: #ddd;
color: #1a1e24;
}
.react-terminal-wrapper:before {
content: '';
position: absolute;
top: 15px;
left: 15px;
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
background: #d9515d;
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
}
.react-terminal-wrapper:after {
content: attr(data-terminal-name);
position: absolute;
color: #a2a2a2;
top: 5px;
left: 0;
width: 100%;
text-align: center;
}
.react-terminal-wrapper.react-terminal-light:after {
color: #d76d77;
}
.react-terminal-line {
display: block;
line-height: 1.5;
}
.react-terminal-line:before {
content: '';
display: inline-block;
vertical-align: middle;
color: #a2a2a2;
}
.react-terminal-light .react-terminal-line:before {
color: #d76d77;
}
.react-terminal-input:before {
margin-right: 0.75em;
content: '$';
}
.react-terminal-input[data-terminal-prompt]:before {
content: attr(data-terminal-prompt);
}
+80
View File
@@ -0,0 +1,80 @@
import React, { useEffect, useRef } from 'react';
import './index.less';
export enum LineType {
Input,
Output,
}
export enum ColorMode {
Light,
Dark,
}
export interface Props {
name?: string;
prompt?: string;
colorMode?: ColorMode;
lineData: Array<{ type: LineType; value: string | React.ReactNode }>;
startingInputValue?: string;
}
const Terminal = ({
name,
prompt,
colorMode,
lineData,
startingInputValue = '',
}: Props) => {
const lastLineRef = useRef<null | HTMLElement>(null);
// An effect that handles scrolling into view the last line of terminal input or output
const performScrolldown = useRef(false);
useEffect(() => {
if (performScrolldown.current) {
// skip scrolldown when the component first loads
setTimeout(
() =>
lastLineRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
}),
500,
);
}
performScrolldown.current = true;
}, [lineData.length]);
const renderedLineData = lineData.map((ld, i) => {
const classes = ['react-terminal-line'];
if (ld.type === LineType.Input) {
classes.push('react-terminal-input');
}
// `lastLineRef` is used to ensure the terminal scrolls into view to the last line; make sure to add the ref to the last
if (lineData.length === i + 1) {
return (
<span className={classes.join(' ')} key={i} ref={lastLineRef}>
{ld.value}
</span>
);
} else {
return (
<span className={classes.join(' ')} key={i}>
{ld.value}
</span>
);
}
});
const classes = ['react-terminal-wrapper'];
if (colorMode === ColorMode.Light) {
classes.push('react-terminal-light');
}
return (
<div className={classes.join(' ')} data-terminal-name={name}>
<div className="react-terminal">{renderedLineData}</div>
</div>
);
};
export default Terminal;
+6
View File
@@ -25,6 +25,12 @@ export default {
hideInMenu: true, hideInMenu: true,
component: '@/pages/initialization/index', component: '@/pages/initialization/index',
}, },
{
name: '错误',
path: '/error',
hideInMenu: true,
component: '@/pages/error/index',
},
{ {
path: '/crontab', path: '/crontab',
name: '定时任务', name: '定时任务',
+23 -9
View File
@@ -26,6 +26,7 @@
.monaco-editor:not(.rename-box) { .monaco-editor:not(.rename-box) {
height: calc(100vh - 128px) !important; height: calc(100vh - 128px) !important;
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important; height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
.view-overlays .current-line { .view-overlays .current-line {
border-width: 0; border-width: 0;
} }
@@ -33,6 +34,7 @@
.rename-box { .rename-box {
height: 0; height: 0;
.rename-input { .rename-input {
height: 0; height: 0;
padding: 0 !important; padding: 0 !important;
@@ -43,6 +45,7 @@
.ant-pro-grid-content.wide { .ant-pro-grid-content.wide {
max-width: unset !important; max-width: unset !important;
overflow: auto; overflow: auto;
.ant-pro-page-container-children-content { .ant-pro-page-container-children-content {
overflow: auto; overflow: auto;
height: 100%; height: 100%;
@@ -57,6 +60,7 @@
.ant-tooltip { .ant-tooltip {
max-width: 500px !important; max-width: 500px !important;
.ant-tooltip-inner { .ant-tooltip-inner {
word-break: break-all !important; word-break: break-all !important;
} }
@@ -72,6 +76,7 @@
.log-select { .log-select {
width: 250px; width: 250px;
} }
.ant-page-header-heading-left { .ant-page-header-heading-left {
min-width: 100px; min-width: 100px;
} }
@@ -81,6 +86,7 @@
.config-select { .config-select {
width: 250px; width: 250px;
} }
.ant-page-header-heading-left { .ant-page-header-heading-left {
min-width: 100px; min-width: 100px;
} }
@@ -119,10 +125,13 @@
flex-direction: column; flex-direction: column;
height: calc(100vh - 48px); height: calc(100vh - 48px);
height: calc(100vh - var(--vh-offset, 0px) - 48px); height: calc(100vh - var(--vh-offset, 0px) - 48px);
.ant-pro-grid-content.wide { .ant-pro-grid-content.wide {
flex: 1; flex: 1;
.ant-pro-grid-content-children { .ant-pro-grid-content-children {
height: calc(100% - 36px); height: calc(100% - 36px);
> div, > div,
.log-container, .log-container,
.react-codemirror2, .react-codemirror2,
@@ -197,6 +206,7 @@
.Resizer.disabled { .Resizer.disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.Resizer.disabled:hover { .Resizer.disabled:hover {
border-color: transparent; border-color: transparent;
} }
@@ -209,6 +219,7 @@
.inline-countdown.ant-statistic { .inline-countdown.ant-statistic {
display: inline-block; display: inline-block;
.ant-statistic-content { .ant-statistic-content {
font-size: 14px; font-size: 14px;
padding: 0 3px; padding: 0 3px;
@@ -253,6 +264,7 @@
.ant-pro-basicLayout-content { .ant-pro-basicLayout-content {
margin: 18px; margin: 18px;
.ant-pro-page-container { .ant-pro-page-container {
margin: -18px -18px -18px; margin: -18px -18px -18px;
} }
@@ -280,25 +292,27 @@
.side-menu-user-drop-menu { .side-menu-user-drop-menu {
position: relative; position: relative;
margin: 0;
padding: 4px 0;
text-align: left; text-align: left;
background-clip: padding-box;
outline: none; outline: none;
background-color: #fff; padding: 4px 0;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 4px; border-radius: 4px;
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%), box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
0 12px 48px 16px rgb(0 0 0 / 3%); overflow: auto;
.ant-dropdown-menu-item:hover { background-color: #fff;
color: #1890ff; }
}
[data-dark='true'] .side-menu-user-drop-menu {
background-color: #373739;
} }
.ant-pro-sider-logo { .ant-pro-sider-logo {
padding: 16px 8px !important; padding: 16px 8px !important;
h1 { h1 {
margin-left: 5px !important; margin-left: 5px !important;
} }
img { img {
width: 32px !important; width: 32px !important;
border-radius: 52% !important; border-radius: 52% !important;
+11 -22
View File
@@ -24,14 +24,7 @@ import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing'; import { init } from '../utils/init';
Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
integrations: [new Integrations.BrowserTracing()],
release: version,
tracesSampleRate: 1.0,
});
export default function (props: any) { export default function (props: any) {
const ctx = useCtx(); const ctx = useCtx();
@@ -107,10 +100,6 @@ export default function (props: any) {
} }
}; };
useEffect(() => {
vhCheck();
}, []);
useEffect(() => { useEffect(() => {
if (systemInfo && systemInfo.isInitialized && !user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
@@ -128,6 +117,9 @@ export default function (props: any) {
}, [theme.theme]); }, [theme.theme]);
useEffect(() => { useEffect(() => {
vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
setFetchMethod(window.fetch); setFetchMethod(window.fetch);
if (_theme === 'dark') { if (_theme === 'dark') {
@@ -187,7 +179,9 @@ export default function (props: any) {
}; };
}, []); }, []);
if (['/login', '/initialization'].includes(props.location.pathname)) { if (
['/login', '/initialization', '/error'].includes(props.location.pathname)
) {
document.title = `${ document.title = `${
(config.documentTitleMap as any)[props.location.pathname] (config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`; } - 控制面板`;
@@ -198,7 +192,7 @@ export default function (props: any) {
history.push('/crontab'); history.push('/crontab');
} }
if (systemInfo) { if (systemInfo || props.location.pathname === '/error') {
return React.Children.map(props.children, (child) => { return React.Children.map(props.children, (child) => {
return React.cloneElement(child, { return React.cloneElement(child, {
...ctx, ...ctx,
@@ -232,12 +226,7 @@ export default function (props: any) {
selectedKeys={[props.location.pathname]} selectedKeys={[props.location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={ logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
<Image
preview={false}
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3"
/>
}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -283,7 +272,7 @@ export default function (props: any) {
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => rightContentRender={() =>
ctx.isPhone && ( ctx.isPhone && (
<Dropdown overlay={menu} trigger={['click']}> <Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span> <span style={{ marginLeft: 5 }}>admin</span>
@@ -300,7 +289,7 @@ export default function (props: any) {
}} }}
> >
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} trigger={['hover']}> <Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span> <span style={{ marginLeft: 5 }}>admin</span>
+340
View File
@@ -0,0 +1,340 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Button,
Card,
Tag,
List,
Divider,
Typography,
} from 'antd';
import {
ClockCircleOutlined,
CloseCircleOutlined,
FieldTimeOutlined,
Loading3QuartersOutlined,
FileOutlined,
} from '@ant-design/icons';
import { CrontabStatus } from './index';
import { diffTime } from '@/utils/date';
import { request } from '@/utils/http';
import config from '@/utils/config';
import CronLogModal from './logModal';
import Editor from '@monaco-editor/react';
const { Text } = Typography;
const tabList = [
{
key: 'log',
tab: '日志',
},
{
key: 'script',
tab: '脚本',
},
];
interface LogItem {
directory: string;
filename: string;
}
const language = navigator.language || navigator.languages[0];
const CronDetailModal = ({
cron = {},
handleCancel,
visible,
theme,
isPhone,
}: {
cron?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
theme: string;
isPhone: boolean;
}) => {
const [activeTabKey, setActiveTabKey] = useState('log');
const [loading, setLoading] = useState(true);
const [logs, setLogs] = useState<LogItem[]>([]);
const [log, setLog] = useState('');
const [value, setValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const editorRef = useRef<any>(null);
const [scriptInfo, setScriptInfo] = useState<any>({});
const [logUrl, setLogUrl] = useState('');
const [validTabs, setValidTabs] = useState(tabList);
const contentList: any = {
log: (
<List
dataSource={logs}
loading={loading}
renderItem={(item) => (
<List.Item className="log-item" onClick={() => onClickItem(item)}>
<FileOutlined style={{ marginRight: 10 }} />
{item.directory}/{item.filename}
</List.Item>
)}
/>
),
script: (
<Editor
language="shell"
theme={theme}
value={value}
options={{
fontSize: 12,
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
),
};
const onClickItem = (item: LogItem) => {
localStorage.setItem('logCron', cron.id);
setLogUrl(`${config.apiPrefix}logs/${item.directory}/${item.filename}`);
request
.get(`${config.apiPrefix}logs/${item.directory}/${item.filename}`)
.then((data) => {
setLog(data.data);
setIsLogModalVisible(true);
});
};
const onTabChange = (key: string) => {
setActiveTabKey(key);
};
const getLogs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => {
if (data.code === 200) {
setLogs(data.data);
}
})
.finally(() => setLoading(false));
};
const getScript = () => {
const cmd = cron.command.split(' ') as string[];
if (cmd[0] === 'task') {
setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
setScriptInfo({ parent: p, filename: s });
request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => {
setValue(data.data);
});
} else {
setValidTabs([validTabs[0]]);
}
};
const saveFile = () => {
Modal.confirm({
title: `确认保存`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{scriptInfo.filename}
</Text>{' '}
</>
),
onOk() {
const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value;
return new Promise((resolve, reject) => {
request
.put(`${config.apiPrefix}scripts`, {
data: {
filename: scriptInfo.filename,
path: scriptInfo.parent || '',
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
setValue(content);
message.success(`保存成功`);
} else {
message.error(_data);
}
resolve(null);
})
.catch((e) => reject(e));
});
},
onCancel() {
console.log('Cancel');
},
});
};
useEffect(() => {
if (cron && cron.id) {
getLogs();
getScript();
}
}, [cron]);
return (
<Modal
title={
<>
<span>{cron.name}</span>
{cron.labels?.length > 0 && cron.labels[0] !== '' && (
<Divider type="vertical"></Divider>
)}
{cron.labels?.length > 0 &&
cron.labels[0] !== '' &&
cron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}>
{label}
</Tag>
))}
</>
}
centered
visible={visible}
forceRender
footer={false}
onCancel={() => handleCancel()}
wrapClassName="crontab-detail"
width={!isPhone ? '80vw' : ''}
>
<div className="card-wrapper">
<Card>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">{cron.command}</div>
</div>
</Card>
<Card style={{ marginTop: 10 }}>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{(!cron.isDisabled || cron.status !== CrontabStatus.idle) && (
<>
{cron.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
</Tag>
)}
{cron.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
</Tag>
)}
{cron.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
</Tag>
)}
</>
)}
{cron.isDisabled === 1 && cron.status === CrontabStatus.idle && (
<Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
)}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">{cron.schedule}</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{cron.last_execution_time
? new Date(cron.last_execution_time * 1000)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
: '-'}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{cron.last_running_time ? diffTime(cron.last_running_time) : '-'}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{cron.nextRunTime &&
cron.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</div>
</div>
</Card>
<Card
style={{ marginTop: 10 }}
tabList={validTabs}
activeTabKey={activeTabKey}
onTabChange={(key) => {
onTabChange(key);
}}
tabBarExtraContent={
activeTabKey === 'script' && (
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={saveFile}
>
</Button>
)
}
>
{contentList[activeTabKey]}
</Card>
</div>
<CronLogModal
visible={isLogModalVisible}
handleCancel={() => {
setIsLogModalVisible(false);
}}
cron={cron}
data={log}
logUrl={logUrl}
/>
</Modal>
);
};
export default CronDetailModal;
+84
View File
@@ -1,3 +1,87 @@
.ant-table-pagination.ant-pagination { .ant-table-pagination.ant-pagination {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.cron {
&:hover {
cursor: pointer;
}
}
.crontab-detail {
.card-wrapper {
height: 80vh;
height: calc(80vh - var(--vh-offset, 0px));
.ant-card:last-child {
.ant-card-body {
height: calc(80vh - 238px);
height: calc(80vh - var(--vh-offset, 0px) - 238px);
overflow-y: auto;
}
}
}
.ant-modal-body {
background: #eee;
padding: 12px;
}
.ant-card-body {
padding: 18px;
}
.ant-card-head {
padding: 0 18px;
}
.ant-card:first-child {
max-height: 66px;
overflow: auto;
.ant-card-body {
min-width: 600px;
}
.cron-detail-info-item {
display: flex;
.cron-detail-info-title {
width: 50px;
}
.cron-detail-info-value {
flex: 1;
margin-top: 0;
}
}
}
.ant-card:nth-child(2) {
overflow-x: auto;
.ant-card-body {
display: flex;
justify-content: space-between;
min-width: 600px;
}
}
.cron-detail-info-item {
flex: auto;
.cron-detail-info-title {
color: #888;
}
.cron-detail-info-value {
margin-top: 12px;
}
}
}
.log-item {
cursor: pointer;
&:hover {
background: #fafafa;
}
}
+86 -54
View File
@@ -33,16 +33,17 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import CronModal, { CronLabelModal } from './modal'; import CronModal, { CronLabelModal } from './modal';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import CronDetailModal from './detail';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
import { history } from 'umi'; import { history } from 'umi';
import './index.less'; import './index.less';
const { Text } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
enum CrontabStatus { export enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
@@ -69,7 +70,7 @@ enum OperationPath {
'unpin', 'unpin',
} }
const Crontab = ({ headerStyle, isPhone }: any) => { const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const columns: any = [ const columns: any = [
{ {
title: '任务名', title: '任务名',
@@ -84,7 +85,32 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
goToScriptManager(record); goToScriptManager(record);
}} }}
> >
{record.name || record._id}{' '} {record.labels?.length > 0 && record.labels[0] !== '' ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
content={
<div>
{record.labels?.map((label: string) => (
<Tag
color="blue"
style={{ cursor: 'point' }}
onClick={(e) => {
e.stopPropagation();
setSearchValue(`label:${label}`);
}}
>
<a>{label}</a>
</Tag>
))}
</div>
}
>
{record.name || '-'}
</Popover>
) : (
record.name || '-'
)}
{record.isPinned ? ( {record.isPinned ? (
<span> <span>
<PushpinOutlined /> <PushpinOutlined />
@@ -93,32 +119,6 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
'' ''
)} )}
</a> </a>
<span>
{record.labels?.length > 0 && record.labels[0] !== '' ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
content={
<div>
{record.labels?.map((label: string, i: number) => (
<Tag
color="blue"
onClick={() => {
onSearch(`label:${label}`);
}}
>
{label}
</Tag>
))}
</div>
}
>
<Tag color="blue">{record.labels[0]}</Tag>
</Popover>
) : (
''
)}
</span>
</> </>
), ),
sorter: { sorter: {
@@ -134,16 +134,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<span <Paragraph
style={{ style={{
textAlign: 'left',
width: '100%',
display: 'inline-block',
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }}
> >
{text} {text}
</span> </Paragraph>
); );
}, },
sorter: { sorter: {
@@ -316,7 +316,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
{record.status === CrontabStatus.idle && ( {record.status === CrontabStatus.idle && (
<Tooltip title={isPc ? '运行' : ''}> <Tooltip title={isPc ? '运行' : ''}>
<a <a
onClick={() => { onClick={(e) => {
e.stopPropagation();
runCron(record, index); runCron(record, index);
}} }}
> >
@@ -327,7 +328,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
{record.status !== CrontabStatus.idle && ( {record.status !== CrontabStatus.idle && (
<Tooltip title={isPc ? '停止' : ''}> <Tooltip title={isPc ? '停止' : ''}>
<a <a
onClick={() => { onClick={(e) => {
e.stopPropagation();
stopCron(record, index); stopCron(record, index);
}} }}
> >
@@ -337,7 +339,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
)} )}
<Tooltip title={isPc ? '日志' : ''}> <Tooltip title={isPc ? '日志' : ''}>
<a <a
onClick={() => { onClick={(e) => {
e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() }); setLogCron({ ...record, timestamp: Date.now() });
}} }}
> >
@@ -354,7 +357,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [value, setValue] = useState<any[]>([]); const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [isLabelModalVisible, setisLabelModalVisible] = useState(false); const [isLabelModalVisible, setIsLabelModalVisible] = useState(false);
const [editedCron, setEditedCron] = useState(); const [editedCron, setEditedCron] = useState();
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
@@ -363,12 +366,15 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') { if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/scripts')) { if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/scripts/', ''); cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
} }
let [p, s] = cmd[1].split('/'); let [p, s] = cmd[1].split('/');
@@ -391,16 +397,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
data.data data.data
.sort((a: any, b: any) => { .sort((a: any, b: any) => {
const sortA = const sortA =
a.isDisabled && a.status !== 0 a.isPinned && a.status !== 0
? 4
: a.isPinned && a.status !== 0
? 5 ? 5
: a.isDisabled && a.status !== 0
? 4
: a.status; : a.status;
const sortB = const sortB =
b.isDisabled && b.status !== 0 b.isPinned && b.status !== 0
? 4
: b.isPinned && b.status !== 0
? 5 ? 5
: b.isDisabled && b.status !== 0
? 4
: b.status; : b.status;
return CrontabSort[sortA] - CrontabSort[sortB]; return CrontabSort[sortA] - CrontabSort[sortB];
}) })
@@ -626,10 +632,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
index: number; index: number;
}> = ({ record, index }) => ( }> = ({ record, index }) => (
<Dropdown <Dropdown
arrow arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ overlay={
<Menu onClick={({ key }) => action(key, record, index)}> <Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
action(key, record, index);
}}
>
<Menu.Item key="edit" icon={<EditOutlined />}> <Menu.Item key="edit" icon={<EditOutlined />}>
</Menu.Item> </Menu.Item>
@@ -659,7 +671,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
</Menu> </Menu>
} }
> >
<a> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
</a> </a>
</Dropdown> </Dropdown>
@@ -804,7 +816,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}; };
const getRowClassName = (record: any, index: number) => { const getRowClassName = (record: any, index: number) => {
return record.isPinned ? 'pinned-cron' : ''; return record.isPinned ? 'pinned-cron cron' : 'cron';
}; };
useEffect(() => { useEffect(() => {
@@ -834,7 +846,10 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
placeholder="请输入名称或者关键词" placeholder="请输入名称或者关键词"
style={{ width: 'auto' }} style={{ width: 'auto' }}
enterButton enterButton
allowClear
loading={loading} loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addCron()}> <Button key="2" type="primary" onClick={() => addCron()}>
@@ -890,7 +905,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
</Button> </Button>
<Button <Button
type="primary" type="primary"
onClick={() => setisLabelModalVisible(true)} onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }} style={{ marginLeft: 8, marginRight: 8 }}
> >
@@ -912,7 +927,15 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
defaultPageSize: 20, defaultPageSize: 20,
showTotal: (total: number, range: number[]) => showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`, `${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000], pageSizeOptions: [20, 100, 500, 1000] as any,
}}
onRow={(record) => {
return {
onClick: (event) => {
setDetailCron(record);
setIsDetailModalVisible(true);
},
};
}} }}
dataSource={value} dataSource={value}
rowKey="id" rowKey="id"
@@ -938,13 +961,22 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
<CronLabelModal <CronLabelModal
visible={isLabelModalVisible} visible={isLabelModalVisible}
handleCancel={(needUpdate?: boolean) => { handleCancel={(needUpdate?: boolean) => {
setisLabelModalVisible(false); setIsLabelModalVisible(false);
if (needUpdate) { if (needUpdate) {
getCrons(); getCrons();
} }
}} }}
ids={selectedRowIds} ids={selectedRowIds}
/> />
<CronDetailModal
visible={isDetailModalVisible}
handleCancel={() => {
setIsDetailModalVisible(false);
}}
cron={detailCron}
theme={theme}
isPhone={isPhone}
/>
</PageContainer> </PageContainer>
); );
}; };
+13 -3
View File
@@ -20,10 +20,14 @@ const CronLogModal = ({
cron, cron,
handleCancel, handleCancel,
visible, visible,
data,
logUrl,
}: { }: {
cron?: any; cron?: any;
visible: boolean; visible: boolean;
handleCancel: () => void; handleCancel: () => void;
data?: string;
logUrl?: string;
}) => { }) => {
const [value, setValue] = useState<string>('启动中...'); const [value, setValue] = useState<string>('启动中...');
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
@@ -36,7 +40,7 @@ const CronLogModal = ({
setLoading(true); setLoading(true);
} }
request request
.get(`${config.apiPrefix}crons/${cron.id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => { .then((data: any) => {
if (localStorage.getItem('logCron') === String(cron.id)) { if (localStorage.getItem('logCron') === String(cron.id)) {
const log = data.data as string; const log = data.data as string;
@@ -97,10 +101,16 @@ const CronLogModal = ({
}; };
useEffect(() => { useEffect(() => {
if (cron) { if (cron && cron.id && visible) {
getCronLog(true); getCronLog(true);
} }
}, [cron]); }, [cron, visible]);
useEffect(() => {
if (data) {
setValue(data);
}
}, [data]);
useEffect(() => { useEffect(() => {
setIsPhone(document.body.clientWidth < 768); setIsPhone(document.body.clientWidth < 768);
+17 -10
View File
@@ -27,7 +27,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
const { Text } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status { enum Status {
@@ -119,18 +119,18 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
key: 'value', key: 'value',
align: 'center' as const, align: 'center' as const,
width: '35%', width: '35%',
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tooltip <Paragraph
placement="topLeft" style={{
title={text} wordBreak: 'break-all',
trigger={['hover', 'click']} marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
> >
<span>{text}</span> {text}
</Tooltip> </Paragraph>
); );
}, },
}, },
@@ -149,6 +149,13 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
sorter: {
compare: (a: any, b: any) => {
const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();
const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();
return updatedAtA - updatedAtB;
},
},
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
const time = record.updatedAt || record.timestamp; const time = record.updatedAt || record.timestamp;
+12
View File
@@ -0,0 +1,12 @@
.error-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
.react-terminal-wrapper {
max-width: 90%;
height: calc(100vh - 80px);
overflow-y: auto;
}
}
+59
View File
@@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react';
import config from '@/utils/config';
import { request } from '@/utils/http';
import Terminal, { ColorMode, LineType } from '../../components/terminal';
import { PageLoading } from '@ant-design/pro-layout';
import { history } from 'umi';
import Ansi from 'ansi-to-react';
import './index.less';
const Error = ({ user, theme }: any) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志');
const getLog = () => {
setLoading(true);
request
.get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => {
setData(data.data);
})
.finally(() => setLoading(false));
};
useEffect(() => {
if (user) {
history.push('/crontab');
}
}, [user]);
useEffect(() => {
getLog();
}, []);
return (
<div className="error-wrapper">
{loading ? (
<PageLoading />
) : (
<Terminal
name="服务错误"
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
lineData={[
{ type: LineType.Input, value: 'pm2 logs panel' },
{
type: LineType.Output,
value: (
<pre>
<Ansi>{data}</Ansi>
</pre>
),
},
]}
/>
)}
</div>
);
};
export default Error;
+1 -1
View File
@@ -233,7 +233,7 @@ const Initialization = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3" src="https://qn.whyour.cn/logo.png"
/> />
<span className={styles.title}></span> <span className={styles.title}></span>
</div> </div>
+7 -1
View File
@@ -13,7 +13,6 @@
&-scroller { &-scroller {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-right: 1px dashed #f0f0f0;
padding-top: 6px; padding-top: 6px;
} }
} }
@@ -22,3 +21,10 @@
display: flex; display: flex;
position: relative; position: relative;
} }
:global {
.Pane.vertical.Pane1 {
padding-right: 5px;
border-right: 1px dashed #f0f0f0;
}
}
+1 -1
View File
@@ -135,7 +135,7 @@ const Login = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3" src="https://qn.whyour.cn/logo.png"
/> />
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
+7 -1
View File
@@ -13,7 +13,6 @@
&-scroller { &-scroller {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-right: 1px dashed #f0f0f0;
padding-top: 6px; padding-top: 6px;
} }
} }
@@ -22,3 +21,10 @@
display: flex; display: flex;
position: relative; position: relative;
} }
:global {
.Pane.vertical.Pane1 {
padding-right: 5px;
border-right: 1px dashed #f0f0f0;
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ const About = () => {
<img <img
alt="logo" alt="logo"
style={{ width: 140, marginRight: 20 }} style={{ width: 140, marginRight: 20 }}
src="https://lf9-survey.bytetos.com/obj/web.business.image/202201205d0d7b5e576ee603497ab6f3" src="https://qn.whyour.cn/logo.png"
/> />
<div className={styles.right}> <div className={styles.right}>
<span className={styles.title}></span> <span className={styles.title}></span>
+1 -1
View File
@@ -177,7 +177,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
</span> </span>
), ),
duration: 10, duration: 30,
}); });
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
+4 -2
View File
@@ -1,8 +1,10 @@
.container { .container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; flex-wrap: wrap;
padding: 50px 130px; max-width: 800px;
margin: 20px auto;
.right { .right {
display: flex; display: flex;
justify-content: center; justify-content: center;
+20 -11
View File
@@ -12,6 +12,7 @@ import {
Modal, Modal,
message, message,
Typography, Typography,
Input,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -119,7 +120,7 @@ const Setting = ({
const [theme, setTheme] = useState(defaultDarken); const [theme, setTheme] = useState(defaultDarken);
const [dataSource, setDataSource] = useState<any[]>([]); const [dataSource, setDataSource] = useState<any[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [editedApp, setEditedApp] = useState(); const [editedApp, setEditedApp] = useState<any>();
const [tabActiveKey, setTabActiveKey] = useState('security'); const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]); const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [notificationInfo, setNotificationInfo] = useState<any>(); const [notificationInfo, setNotificationInfo] = useState<any>();
@@ -142,6 +143,7 @@ const Setting = ({
}; };
const addApp = () => { const addApp = () => {
setEditedApp(null);
setIsModalVisible(true); setIsModalVisible(true);
}; };
@@ -273,8 +275,10 @@ const Setting = ({
request request
.get(`${config.apiPrefix}system/log/remove`) .get(`${config.apiPrefix}system/log/remove`)
.then((data: any) => { .then((data: any) => {
setLogRemoveFrequency(data.data.frequency); if (data.data.info) {
form.setFieldsValue({ frequency: data.data.frequency }); const { frequency } = data.data.info;
setLogRemoveFrequency(frequency);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -367,14 +371,19 @@ const Setting = ({
name="frequency" name="frequency"
tooltip="每x天自动删除x天以前的日志" tooltip="每x天自动删除x天以前的日志"
> >
<InputNumber <Input.Group compact>
addonBefore="每" <InputNumber
addonAfter="" addonBefore=""
style={{ width: 150 }} addonAfter="天"
min={0} style={{ width: 150 }}
onBlur={updateRemoveLogFrequency} min={0}
onChange={(value) => setLogRemoveFrequency(value)} value={logRemoveFrequency}
/> onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item> </Form.Item>
<Form.Item label="检查更新" name="update"> <Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} /> <CheckUpdate socketMessage={socketMessage} />
+7
View File
@@ -77,6 +77,7 @@ export default {
{ value: 'gotify', label: 'Gotify' }, { value: 'gotify', label: 'Gotify' },
{ value: 'goCqHttpBot', label: 'GoCqHttpBot' }, { value: 'goCqHttpBot', label: 'GoCqHttpBot' },
{ value: 'serverChan', label: 'Server酱' }, { value: 'serverChan', label: 'Server酱' },
{ value: 'PushDeer', label: 'PushDeer' },
{ value: 'bark', label: 'Bark' }, { value: 'bark', label: 'Bark' },
{ value: 'telegramBot', label: 'Telegram机器人' }, { value: 'telegramBot', label: 'Telegram机器人' },
{ value: 'dingtalkBot', label: '钉钉机器人' }, { value: 'dingtalkBot', label: '钉钉机器人' },
@@ -113,12 +114,17 @@ export default {
serverChan: [ serverChan: [
{ label: 'serverChanKey', tip: 'Server酱SENDKEY', required: true }, { label: 'serverChanKey', tip: 'Server酱SENDKEY', required: true },
], ],
PushDeer: [{ label: 'PushDeerKey', tip: 'PushDeer的Key', required: true }],
bark: [ bark: [
{ {
label: 'barkPush', label: 'barkPush',
tip: 'Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX', tip: 'Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
required: true, required: true,
}, },
{
label: 'barkIcon',
tip: 'BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)',
},
{ label: 'barkSound', tip: 'BARK推送铃声,铃声列表去APP查看复制填写' }, { label: 'barkSound', tip: 'BARK推送铃声,铃声列表去APP查看复制填写' },
{ label: 'barkGroup', tip: 'BARK推送消息的分组, 默认为qinglong' }, { label: 'barkGroup', tip: 'BARK推送消息的分组, 默认为qinglong' },
], ],
@@ -207,6 +213,7 @@ export default {
'/diff': '对比工具', '/diff': '对比工具',
'/log': '任务日志', '/log': '任务日志',
'/setting': '系统设置', '/setting': '系统设置',
'/error': '错误日志',
}, },
dependenceTypes: ['nodejs', 'python3', 'linux'], dependenceTypes: ['nodejs', 'python3', 'linux'],
}; };
+2 -2
View File
@@ -14,8 +14,8 @@ const errorHandler = function (error: any) {
? error.data.message || error.data ? error.data.message || error.data
: error.response.statusText; : error.response.statusText;
const responseStatus = error.response.status; const responseStatus = error.response.status;
if (responseStatus === 502) { if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请手动执行ql check检查服务状态'); history.push('/error');
} else if (responseStatus === 401) { } else if (responseStatus === 401) {
if (history.location.pathname !== '/login') { if (history.location.pathname !== '/login') {
message.error('登录已过期,请重新登录'); message.error('登录已过期,请重新登录');
+26
View File
@@ -0,0 +1,26 @@
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { loader } from '@monaco-editor/react';
import { version } from '../version';
export function init() {
// sentry监控 init
Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
integrations: [new Integrations.BrowserTracing()],
release: version,
tracesSampleRate: 1.0,
});
// monaco 编辑器配置cdn和locale
loader.config({
paths: {
vs: 'https://cdn.staticfile.org/monaco-editor/0.32.1/min/vs',
},
'vs/nls': {
availableLanguages: {
'*': 'zh-cn',
},
},
});
}
+14 -11
View File
@@ -1,12 +1,15 @@
export const version = '2.11.2'; export const version = '2.12.0';
export const changeLogLink = 'https://t.me/jiao_long/261'; export const changeLogLink = 'https://t.me/jiao_long/288';
export const changeLog = `2.11.2 版本说明 export const changeLog = `2.12.0 版本说明
1. 修复更新通知 1. 全新定时任务详情,支持日志查看、脚本编辑
2. 修复定时任务排序 2. openapi增加发送通知接口,可用于脚本直接调用
3. 移除cdn文件 3. 增加pushDeer推送,感谢 https://github.com/NekoMio PR
4. 修复日志加载状态 4. 增加public服务,当服务异常时,查询服务状态及日志,供页面使用。
5. python任务测试实时输出 5. 增加ql check可视化错误提示
6. 修改定时任务、环境变量唯一值 6. 修改openapi获取token逻辑,最多存储5个可用的token。
6. 修复shell克隆仓库状态获取 7. 调整数据目录,log、db、scripts、config等目录迁移到 /ql/data 目录,docker映射只需映射data目录
7. 重构基础镜像,缩减镜像大小、打包时间 8. 版本文件存储到七牛云,方便检查更新
9. 修复编辑应用初始值
10. 修复退出登录和定时任务搜索
11. 其他bug修复
`; `;
+1 -1
View File
@@ -15,7 +15,7 @@
"module": "commonjs", "module": "commonjs",
"pretty": true, "pretty": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./build", "outDir": "./static/build",
"allowJs": true, "allowJs": true,
"noEmit": false, "noEmit": false,
"esModuleInterop": true "esModuleInterop": true
+2 -1
View File
@@ -32,7 +32,8 @@
"node_modules", "node_modules",
"lib", "lib",
"es", "es",
"dist", "static",
"data",
"typings", "typings",
"**/__test__", "**/__test__",
"test", "test",
-11339
View File
File diff suppressed because it is too large Load Diff