Compare commits

..
120 Commits
Author SHA1 Message Date
whyour 4af9b22442 更新版本 v2.14.9 2022-10-29 18:50:16 +08:00
whyour 8a392170cf webhook通知支持嵌套body 2022-10-29 18:43:16 +08:00
whyour 6385e053ae 增加飞书机器人通知 2022-10-29 16:41:37 +08:00
Leo_chenandGitHub 2789119882 添加智能微秘书通知 (#1680) 2022-10-29 15:45:16 +08:00
whyour 3e0553c3e8 修改登陆逻辑 2022-10-29 10:05:54 +08:00
whyour 5e16ace1e7 修改检查更新日志 2022-10-27 10:34:55 +08:00
whyour 0ca23cc4ca 修改环境变量默认排序 2022-10-27 00:02:50 +08:00
whyour 4949457001 修复ql命令日志打印 2022-10-26 23:50:07 +08:00
whyour 8ae13fe823 增加 -max 指定环境变量 2022-10-26 14:27:14 +08:00
whyour fdf672092a 修改获取version文件超时时间 2022-10-25 23:52:30 +08:00
whyour 48dd6ea826 修复表格滚动高度 2022-10-25 23:12:38 +08:00
whyour 68bb4f15e2 修复shell获取磁盘使用量 2022-10-24 22:45:06 +08:00
whyour 9c6d9ec22d 更新版本 v2.14.8 2022-10-22 18:36:38 +08:00
whyour ac075ace73 修复调试脚本语言判断 2022-10-22 00:31:34 +08:00
whyour eb58774ee9 优化task.sh 2022-10-22 00:25:54 +08:00
whyour 04613bf9a9 修复tsconfig 2022-10-20 15:30:22 +08:00
whyour 4194f1964d 修改日志结束符判定 2022-10-20 15:16:17 +08:00
whyour 42c64c82d7 修复 ql 命令日志目录 2022-10-16 21:59:19 +08:00
whyour d44a8966f2 修复环境变量名称验证 2022-10-16 09:22:37 +08:00
whyour ff44330aa1 修复 ql 命令日志目录 2022-10-15 18:40:15 +08:00
whyour ed259e8168 修复 task 命令执行时参数引号丢失 2022-10-15 18:29:34 +08:00
whyour 444836975d 修改 ql 命令打印日志逻辑 2022-10-14 23:02:20 +08:00
whyour 3756fa93e4 修复 ql 命令 2022-10-14 21:17:04 +08:00
whyour 064b52b306 修改 timezone action 2022-10-14 20:55:58 +08:00
whyour 648dc51474 更新ql指令 2022-10-14 15:17:37 +08:00
whyour 7c0bc32759 修改环境变量导入 2022-10-13 23:41:54 +08:00
whyour d4b6cc5e4d 修复调试脚本选择脚本 2022-10-13 23:39:28 +08:00
whyour 75fca715e3 增加环境变量导入 2022-10-11 22:16:36 +08:00
whyour 6e9a4a29ef 修复shell语法 2022-10-09 13:49:19 +08:00
whyour 78f643c50f 更新版本v2.14.7 2022-10-01 23:12:57 +08:00
whyour 2db4f1ba87 修改资源判定逻辑 2022-10-01 23:04:12 +08:00
whyour 5741dbb78c 修复获取task_before参数 2022-09-30 15:32:42 +08:00
whyour 88cf671465 更新版本v2.14.6 2022-09-29 23:06:31 +08:00
whyour 29f082dea2 增加config资源配置 2022-09-29 23:03:22 +08:00
whyour 9a55968ed6 修改资源异常推送 2022-09-29 13:29:22 +08:00
whyour 6ec6e25855 修改资源提示 2022-09-29 00:59:27 +08:00
whyour af48bc378b 修复获取cpu占用 2022-09-29 00:57:31 +08:00
whyour 3f5ae4bcb9 修复 echo 2022-09-29 00:44:07 +08:00
whyour c6624e9001 增加资源占用提示 2022-09-29 00:43:07 +08:00
whyour 5530ce76e3 添加系统资源消耗提示 2022-09-29 00:32:17 +08:00
whyour 654b51e476 修复脚本和日志搜索逻辑 2022-09-25 21:03:11 +08:00
whyour 6428ac3624 修复SplitPane类型错误 2022-09-25 14:02:27 +08:00
whyour c36450f436 修复ant-tree高度获取 2022-09-25 13:38:10 +08:00
whyour dcea231249 更新版本 v2.14.5 2022-09-24 21:00:47 +08:00
whyour f236119bd6 修改输入框自动填充样式 2022-09-24 14:40:13 +08:00
whyour cb870fad85 修改检查更新逻辑 2022-09-24 14:20:35 +08:00
whyour 44e2caedfc 修改更新日志清除评率 2022-09-24 13:59:56 +08:00
whyour c0b5192296 修改移动端脚本高亮 2022-09-24 11:27:16 +08:00
whyour ba43882c0b 修复脚本管理删除 2022-09-23 23:16:57 +08:00
whyour dfc706e16d 日志管理支持删除日志文件和目录 2022-09-23 20:09:10 +08:00
whyour 25b03d4345 脚本管理支持删除文件夹 2022-09-23 19:12:51 +08:00
whyour 4e8f36d9a4 修复登录错误提示 2022-09-23 17:44:48 +08:00
whyour 8f4d67ffa7 修改task命令ID获取 2022-09-23 14:37:56 +08:00
whyour ad47277149 修复登录验证 2022-09-23 12:15:15 +08:00
whyour 98c2a37ab6 修复初始化逻辑 2022-09-23 00:17:53 +08:00
whyour f2fea47336 重构前端错误提示 2022-09-22 23:59:23 +08:00
whyour e274d3e2f9 sentry增加版本号 2022-09-22 12:14:42 +08:00
whyour 36f4c3c02c 修改任务启动参数 2022-09-21 21:28:31 +08:00
whyour ae891f8e55 修复docker启动逻辑 2022-09-21 17:42:13 +08:00
whyour c874dc9705 更新readme 2022-09-21 16:03:53 +08:00
whyour 23bfbeb995 修改新建订阅提示 2022-09-21 14:54:45 +08:00
whyour a75df03243 更新版本 2.14.4 2022-09-20 19:59:51 +08:00
whyour e44a2b5995 修改tsconfig 2022-09-20 19:43:49 +08:00
whyour f23e7a9e49 修复自定义通知 2022-09-20 19:13:11 +08:00
whyour e08f0cd308 修复设置自定义通知 2022-09-20 18:00:36 +08:00
whyour 1ebcfd8001 修复自定义通知 2022-09-20 17:34:36 +08:00
whyour 5168b044a3 修复遍历key值 2022-09-20 01:23:33 +08:00
whyour 931638b687 增加自定义hook通知 2022-09-20 01:16:36 +08:00
whyour 4d9bbcfd3f 升级antd 2022-09-19 18:45:56 +08:00
whyour b0e3b26369 修改环境变量默认排序 2022-09-18 20:47:42 +08:00
whyour 1e55f4065d 升级umi4.0,修复新建脚本 2022-09-18 20:40:59 +08:00
whyour 518e8903a3 兼容Modal属性 2022-09-17 22:50:58 +08:00
whyour 6e56a55f6f 修改弹框表单逻辑 2022-09-16 00:28:04 +08:00
whyour 967071ad4e 脚本管理支持新增文件夹 2022-09-16 00:14:10 +08:00
whyour d23fcfaa5a 修复创建应用 2022-09-15 23:45:12 +08:00
whyour a2f2306430 增加批量重新安装依赖 2022-09-11 22:47:43 +08:00
whyour 144b543091 修改依赖管理默认排序 2022-09-11 22:26:33 +08:00
whyour e4ad8f512e 修改订阅默认排序 2022-09-11 22:10:17 +08:00
whyour 591a5006da 修复运行任务shell 2022-09-11 19:53:57 +08:00
whyour 281ed6cfba 修改任务执行日志目录规则 2022-09-11 19:43:37 +08:00
whyour 7354a61184 修改config配置说明 2022-09-10 14:23:16 +08:00
whyour 04037f29e5 更新版本 v2.14.3 2022-09-10 14:12:26 +08:00
whyour 6b124bad89 修复pnpm registry兼容 2022-09-09 00:21:10 +08:00
whyour 54d99a4954 更新版本 v2.14.2 2022-09-07 19:30:19 +08:00
whyour 9947594366 更新issue模板 2022-09-07 17:28:57 +08:00
whyour d7ff934336 增加github issue模版 2022-09-07 16:03:15 +08:00
whyour dc27856441 修复定时任务状态筛选 2022-09-07 11:30:11 +08:00
whyour bd28682769 排除认证失败错误上报 2022-09-06 23:57:21 +08:00
whyour 2060e62da4 更新sentry版本 2022-09-06 21:50:03 +08:00
whyour 70eac1e747 修改pnpm安装全局模块命令 2022-09-06 15:53:21 +08:00
whyour 6f5a5fe066 修改定时任务打开详情交互 2022-09-06 14:49:42 +08:00
景大侠andGitHub 8d24949202 修复#1614 正确解析多级目录的脚本路径 (#1616) 2022-09-06 13:25:12 +08:00
whyour f7b6237355 修复新建任务视图交互 2022-09-06 00:40:06 +08:00
景大侠andGitHub daf6f94c51 修复任务视图bug (#1612)
* 修复了视图无排序时无法再次修改的问题

* 修复在视图管理中编辑、新建视图点击确定后不能关闭页面的问题

* 修复#1611 避免查询条件被覆盖

* 修复视图筛选不能正确处理`不包含`
2022-09-06 00:25:05 +08:00
whyour 687d16e21b 增加npm镜像源配置 2022-09-06 00:21:53 +08:00
whyour 70571a9d34 更新版本 v2.14.1 2022-09-04 18:28:28 +08:00
whyour 4da8d8fe1e 修复node pushdeer通知 2022-09-04 18:25:40 +08:00
AppoipandGitHub 952cdc0e3f 添加群晖synology chat通知推送 (#1610) 2022-09-04 18:10:20 +08:00
whyour 2de189d189 修复视图切换页码初始值 2022-09-03 18:27:52 +08:00
whyour 3edc6e83d7 修复定时任务搜索 2022-09-03 18:23:35 +08:00
whyour 31a5386cfe 更新版本 v2.14.0 2022-09-03 15:26:04 +08:00
whyour 0198ebbbff 修复任务视图tab激活状态 2022-09-03 15:16:35 +08:00
whyour ce3df8704d 修复任务视图tab样式 2022-09-03 15:02:05 +08:00
whyour 1b4ca3a684 修复视图拖拽数据同步 2022-09-03 14:56:32 +08:00
whyour e31c2c0955 完善定时任务视图拖拽 2022-09-03 12:39:47 +08:00
whyour 4e389865b5 定时任务视图tab高亮 2022-09-03 11:58:21 +08:00
whyour e5d8adf955 修复定时任务视图排序 2022-09-03 02:15:32 +08:00
whyour 9a3181bc44 任务视图增加状态筛选 2022-09-03 01:56:47 +08:00
whyour 2f05c95422 完善定时任务视图 2022-09-02 17:52:06 +08:00
whyour bc5a3a2028 修复nginx配置文件 2022-09-02 11:28:08 +08:00
whyour 1fe91508f6 修复baseUrl 2022-09-02 10:54:44 +08:00
whyour b9253c8191 修复定时任务搜索api 2022-09-02 00:35:20 +08:00
whyour 7516acbc41 修复定时任务搜索 2022-08-31 00:02:35 +08:00
whyour 3b3d45da29 修复定时任务排序 2022-08-28 15:45:30 +08:00
whyour 918d68d140 修复布局样式 2022-08-28 00:44:17 +08:00
whyour a3f56be299 修改整体布局,减少无用间隔 2022-08-28 00:38:25 +08:00
whyour f8f63890e5 定时任务增加分组 2022-08-27 14:50:27 +08:00
whyour 9e997410ab 测试dockerfile 2022-08-26 18:42:25 +08:00
whyour 529880642f 修复typescript类型错误 2022-08-26 17:52:37 +08:00
景大侠andGitHub d2590edab3 支持非js任务随机延迟 (#1580)
* 修复非JS任务不能随机延迟的问题
2022-08-26 17:11:16 +08:00
99 changed files with 4793 additions and 2460 deletions
+42
View File
@@ -0,0 +1,42 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: input
id: version
attributes:
label: Qinglong version
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Chat
url: https://t.me/jiao_long
about: Ask questions and discuss with other Qinglong users in real time.
- name: Questions & Discussions
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
about: Use GitHub discussions for message-board style questions and discussions.
@@ -0,0 +1,40 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
+31
View File
@@ -0,0 +1,31 @@
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
+7
View File
@@ -0,0 +1,7 @@
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
+3 -5
View File
@@ -57,12 +57,10 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Set time zone
uses: szenius/set-timezone@v1.0
- name: Setup timezone
uses: zcong1993/setup-timezone@master
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
timezone: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v2
+11 -16
View File
@@ -1,20 +1,15 @@
import { defineConfig } from 'umi';
import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({
hash: true,
layout: false,
antd: {},
outputPath: 'static/dist',
nodeModulesTransform: {
type: 'none',
fastRefresh: true,
favicons: ['/images/favicon.svg'],
mfsu: {
strategy: 'eager',
},
fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/favicon.svg',
proxy: {
'/api/public': {
target: 'http://127.0.0.1:5400/',
@@ -26,7 +21,7 @@ export default defineConfig({
ws: true,
},
},
chainWebpack: (config) => {
chainWebpack: ((config: any) => {
config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({
algorithm: 'gzip',
@@ -35,13 +30,13 @@ export default defineConfig({
minRatio: 0.6,
}),
);
},
}) as any,
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM',
},
scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
headScripts: [
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
});
+8 -5
View File
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
@@ -57,11 +57,10 @@ https://podman.io/getting-started/installation
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
@@ -96,7 +95,7 @@ systemctl restart docker
```bash
docker run -dit \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
@@ -124,6 +123,10 @@ docker-compose up -d
docker-compose down
```
3. access
Open your browser and visit http://{ip}:5700
## Use
1. built-in commands
@@ -179,7 +182,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
+8 -5
View File
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
@@ -57,11 +57,10 @@ https://podman.io/getting-started/installation
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
@@ -97,7 +96,7 @@ systemctl restart docker
```bash
docker run -dit \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
@@ -125,6 +124,10 @@ docker-compose up -d
docker-compose down
```
3. 访问
打开你的浏览器,访问 http://{ip}:5700
## 使用
1. 内置命令
@@ -179,7 +182,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
+127 -13
View File
@@ -2,6 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
@@ -10,28 +11,141 @@ export default (app: Router) => {
app.use('/crons', route);
route.get(
'/',
celebrate({
query: Joi.object({
searchText: Joi.string().required().allow(''),
page: Joi.string().required(),
size: Joi.string().required(),
t: Joi.string().required(),
}),
}),
'/views',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any);
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/views',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/move',
celebrate({
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.move(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
+49 -1
View File
@@ -3,8 +3,21 @@ import { Container } from 'typedi';
import EnvService from '../services/env';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => {
app.use('/envs', route);
@@ -26,7 +39,9 @@ export default (app: Router) => {
body: Joi.array().items(
Joi.object({
value: Joi.string().required(),
name: Joi.string().required(),
name: Joi.string()
.required()
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
remarks: Joi.string().optional().allow(''),
}),
),
@@ -177,4 +192,37 @@ export default (app: Router) => {
}
},
);
route.post(
'/upload',
upload.single('env'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
if (data.every((x) => x.name && x.value)) {
const result = await envService.create(
data.map((x) => ({
name: x.name,
value: x.value,
remarks: x.remarks,
})),
);
return res.send({ code: 200, data: result });
} else {
return res.send({
code: 400,
message: '文件缺少name或者value字段,参考导出文件格式',
});
}
} catch (e) {
return next(e);
}
},
);
};
+32 -1
View File
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { getFileContentByName, readDirs } from '../config/util';
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router();
const blacklist = ['.tmp'];
@@ -45,4 +46,34 @@ export default (app: Router) => {
}
},
);
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
};
+27 -10
View File
@@ -4,6 +4,7 @@ import {
readDirs,
getLastModifyFilePath,
readDir,
emptyDir,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
@@ -77,12 +78,14 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path, content, originFilename } = req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
};
let { filename, path, content, originFilename, directory } =
req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
directory: string;
};
if (!path) {
path = config.scriptPath;
@@ -105,11 +108,19 @@ export default (app: Router) => {
return res.send({ code: 200 });
}
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
if (!originFilename) {
originFilename = filename;
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`;
const originFilePath = join(
path,
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
originFilePath,
@@ -159,17 +170,23 @@ export default (app: Router) => {
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
+1 -1
View File
@@ -17,7 +17,7 @@ async function startServer() {
const server = app
.listen(config.port, () => {
Logger.debug(`✌️ Back server launched on port ${config.port}`);
Logger.debug(`✌️ 后端服务启动成功!`);
})
.on('error', (err) => {
Logger.error(err);
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+1 -1
View File
@@ -11,7 +11,7 @@ if (!process.env.QL_DIR) {
if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../');
}
process.env.QL_DIR = qlHomePath;
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
}
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
+87 -1
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
import { exec } from 'child_process';
import FormData from 'form-data';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
@@ -301,7 +302,6 @@ export function readDirs(
title: file,
key,
type: 'directory',
disabled: true,
parent: relativePath,
children: readDirs(subPath, baseDir).sort(
(a: any, b: any) =>
@@ -312,6 +312,7 @@ export function readDirs(
return {
title: file,
type: 'file',
isLeaf: true,
key,
parent: relativePath,
};
@@ -344,6 +345,20 @@ export function readDir(
return result;
}
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
@@ -355,3 +370,74 @@ export function promiseExec(command: string): Promise<string> {
);
});
}
export function parseHeaders(headers: string) {
if (!headers) return {};
const parsed: any = {};
let key;
let val;
let i;
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
}
export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
) {
if (!body) return '';
const parsed: any = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
+47
View File
@@ -0,0 +1,47 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
type: 'or' | 'and';
value: string;
}
export class CrontabView {
name?: string;
id?: number;
position?: number;
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
constructor(options: CrontabView) {
this.name = options.name;
this.id = options.id;
this.position = options.position;
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
}
}
interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
'CrontabView',
{
name: {
unique: 'name',
type: DataTypes.STRING,
},
position: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
},
);
+37 -1
View File
@@ -1,16 +1,22 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer',
'bark' = 'bark',
'chat' = 'chat',
'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
'feishu' = 'feishu',
'webhook' = 'webhook',
}
abstract class NotificationBaseInfo {
@@ -37,6 +43,11 @@ export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = '';
}
export class ChatNotification extends NotificationBaseInfo {
public chatUrl = '';
public chatToken = '';
}
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png';
@@ -66,6 +77,12 @@ export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
}
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
}
export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = '';
}
@@ -81,16 +98,35 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = '';
}
export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = '';
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
}
export interface NotificationInfo
extends GoCqHttpBotNotification,
GotifyNotification,
ServerChanNotification,
PushDeerNotification,
ChatNotification,
BarkNotification,
TelegramBotNotification,
DingtalkBotNotification,
WeWorkBotNotification,
WeWorkAppNotification,
AibotkNotification,
IGotNotification,
PushPlusNotification,
EmailNotification {}
EmailNotification,
WebhookNotification,
LarkNotification {}
+2 -1
View File
@@ -6,9 +6,9 @@ import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth';
import { sequelize } from '../data';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config';
export default async () => {
@@ -19,6 +19,7 @@ export default async () => {
await AuthModel.sync();
await EnvModel.sync();
await SubscriptionModel.sync();
await CrontabViewModel.sync();
// try {
// const queryInterface = sequelize.getQueryInterface();
+19 -7
View File
@@ -3,7 +3,7 @@ import bodyParser from 'body-parser';
import cors from 'cors';
import routes from '../api';
import config from '../config';
import jwt from 'express-jwt';
import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs';
import { getPlatform, getToken } from '../config/util';
import Container from 'typedi';
@@ -75,22 +75,24 @@ export default ({ app }: { app: Application }) => {
if (
!headerToken &&
originPath &&
config.apiWhiteList.includes(originPath) &&
originPath !== '/api/crons/status'
config.apiWhiteList.includes(originPath)
) {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data) {
if (data && headerToken) {
const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) {
return next();
}
}
const err: any = new Error('UnauthorizedError');
err.status = 401;
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
const errorMessage = headerToken
? 'jwt malformed'
: 'No authorization token was found';
const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err);
});
@@ -168,7 +170,17 @@ export default ({ app }: { app: Application }) => {
},
);
app.use(Sentry.Handlers.errorHandler());
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use(
(
+3 -5
View File
@@ -13,18 +13,17 @@ export default async () => {
// 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}/static/build/token.js`;
const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: 'token',
id: NaN,
name: '生成token',
command: tokenCommand,
};
scheduleService.createIntervalTask(cron as any, {
scheduleService.createIntervalTask(cron, {
days: 28,
runImmediately: true,
});
// 运行删除日志任务
@@ -37,7 +36,6 @@ export default async () => {
};
scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
runImmediately: true,
});
}
+9 -2
View File
@@ -2,16 +2,23 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
export default ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 1.0,
tracesSampleRate: 0.1,
release: currentVersion,
});
expressApp.use(Sentry.Handlers.requestHandler());
+1 -1
View File
@@ -22,7 +22,7 @@ app
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
Logger.debug(`✌️ 公共服务启动成功!`);
})
.on('error', (err) => {
Logger.error(err);
+4 -7
View File
@@ -18,14 +18,15 @@ const run = async () => {
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled
!task.isDisabled &&
task.schedule
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(command);
exec(`ID=${task.id} ${command}`);
});
}
}
@@ -43,11 +44,7 @@ app
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
Logger.debug('定时任务服务启动成功!');
})
.on('error', (err) => {
Logger.error(err);
+155 -50
View File
@@ -11,6 +11,7 @@ import { promisify } from 'util';
import { Op } from 'sequelize';
import path from 'path';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class CronService {
@@ -113,17 +114,69 @@ export default class CronService {
}
}
public async crontabs(params?: {
searchText: string;
page: string;
size: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchText;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) {
query[Op.and] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let q: any = {};
let operate2 = null;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
operate2 = Op.or;
break;
case 'NotReg':
operate = Op.notLike;
operate2 = Op.and;
break;
case 'In':
q[Op.or] = [
{
[property]: value,
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
: {},
];
break;
case 'Nin':
q[Op.and] = [
{
[property]: {
[Op.notIn]: value,
},
},
property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } }
: {},
];
break;
default:
break;
}
if (operate && operate2) {
q[property] = {
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
};
}
query[Op.and].push(q);
}
}
}
let query = {};
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) {
if (!query[Op.and]) {
query[Op.and] = [];
}
let q: any = {};
const textArray = searchText.split(':');
switch (textArray[0]) {
case 'name':
@@ -131,13 +184,11 @@ export default class CronService {
case 'schedule':
case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
query = {
[column]: {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
],
},
q[column] = {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
],
};
break;
default:
@@ -147,33 +198,87 @@ export default class CronService {
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
],
};
query = {
[Op.or]: [
{
name: reg,
},
{
command: reg,
},
{
schedule: reg,
},
{
labels: reg,
},
],
};
q[Op.or] = [
{
name: reg,
},
{
command: reg,
},
{
schedule: reg,
},
{
labels: reg,
},
];
break;
}
query[Op.and].push(q);
}
}
private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) {
if (!query[Op.and]) {
query[Op.and] = [];
}
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
}
query[Op.and].push(q);
}
}
}
private formatViewSort(order: string[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]);
}
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
size: string;
sorter: string;
filters: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const viewQuery = JSON.parse(params?.queryString || '{}');
const filterQuery = JSON.parse(params?.filters || '{}');
const sorterQuery = JSON.parse(params?.sorter || '{}');
let query: any = {};
let order = [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
];
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatFilterQuery(query, filterQuery);
this.formatViewSort(order, viewQuery);
if (sorterQuery) {
const { field, type } = sorterQuery;
if (field && type) {
order.unshift([field, type]);
}
}
let condition: any = {
where: query,
order: [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
],
order: order,
};
if (page && size) {
condition.offset = (page - 1) * size;
@@ -181,7 +286,7 @@ export default class CronService {
}
try {
const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count();
const count = await CrontabModel.count({ where: query });
return { data: result, total: count };
} catch (error) {
throw error;
@@ -231,7 +336,7 @@ export default class CronService {
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`,
)}${diffTimeStr}${LOG_END_SYMBOL}`,
);
}
}
@@ -298,11 +403,17 @@ export default class CronService {
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
}
if (cmdStr.endsWith('.js')) {
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
const cp = spawn(cmdStr, { shell: '/bin/bash' });
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
@@ -310,7 +421,7 @@ export default class CronService {
);
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`);
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
}
});
cp.on('error', (err) => {
@@ -321,16 +432,10 @@ export default class CronService {
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
+92
View File
@@ -0,0 +1,92 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
}
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
return doc;
}
public async insert(payload: CrontabView): Promise<CrontabView> {
return await CrontabViewModel.create(payload, { returning: true });
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
return newDoc;
}
public async updateDb(payload: CrontabView): Promise<CrontabView> {
await CrontabViewModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[]) {
await CrontabViewModel.destroy({ where: { id: ids } });
}
public async list(): Promise<CrontabView[]> {
try {
const result = await CrontabViewModel.findAll({
where: {},
order: [['position', 'DESC']],
});
return result;
} catch (error) {
throw error;
}
}
public async getDb(query: any): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
public async disabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
public async move({
id,
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
id: number;
}): Promise<CrontabView> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position! * 2
: views[toIndex].position! / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
});
return newDoc;
}
}
+5 -2
View File
@@ -125,8 +125,11 @@ export default class DependenceService {
return docs;
}
private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } });
private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs;
}
+11 -8
View File
@@ -67,12 +67,12 @@ export default class EnvService {
const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward
? envs[0].position * 2
: envs[toIndex].position / 2;
? envs[0].position! * 2
: envs[toIndex].position! / 2;
} else {
targetPosition = isUpward
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
@@ -112,7 +112,11 @@ export default class EnvService {
};
}
try {
const result = await this.find(condition, [['position', 'DESC']]);
const result = await this.find(condition, [
['status', 'ASC'],
['position', 'DESC'],
['createdAt', 'ASC'],
]);
return result as any;
} catch (error) {
throw error;
@@ -154,7 +158,7 @@ export default class EnvService {
const envs = await this.envs(
'',
{ position: -1 },
{ name: { [Op.not]: null } },
{ name: { [Op.not]: null }, status: EnvStatus.normal },
);
const groups = _.groupBy(envs, 'name');
let env_string = '';
@@ -163,9 +167,8 @@ export default class EnvService {
const group = groups[key];
// 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled)
.map('value')
.join('&')
.replace(/(\\)[^n]/g, '\\\\')
+150 -27
View File
@@ -6,6 +6,7 @@ import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { parseBody, parseHeaders } from '../config/util';
@Service()
export default class NotificationService {
@@ -17,20 +18,27 @@ export default class NotificationService {
['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan],
['pushDeer', this.pushDeer],
['chat', this.chat],
['bark', this.bark],
['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
['webhook', this.webhook],
['lark', this.lark],
]);
private timeout = 10000;
private title = '';
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 30000,
retry: 1,
};
constructor(@Inject('logger') private logger: winston.Logger) {}
@@ -73,8 +81,7 @@ export default class NotificationService {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
@@ -92,8 +99,7 @@ export default class NotificationService {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
@@ -108,8 +114,7 @@ export default class NotificationService {
: `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
@@ -122,8 +127,7 @@ export default class NotificationService {
const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
@@ -135,6 +139,19 @@ export default class NotificationService {
);
}
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
const res: any = await got
.post(url, {
...this.gotOption,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
}
private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
@@ -147,8 +164,7 @@ export default class NotificationService {
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
@@ -186,8 +202,7 @@ export default class NotificationService {
}
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent,
@@ -209,8 +224,7 @@ export default class NotificationService {
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
msgtype: 'text',
text: {
@@ -227,8 +241,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
msgtype: 'text',
text: {
@@ -245,10 +258,9 @@ export default class NotificationService {
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const { access_token } = await got
const tokenRes: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
corpid,
corpsecret,
@@ -296,10 +308,9 @@ export default class NotificationService {
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
touser,
agentid,
@@ -313,13 +324,53 @@ export default class NotificationService {
return res.errcode === 0;
}
private async aibotk() {
const { aibotkKey, aibotkType, aibotkName } = this.params;
let url = '';
let json = {};
switch (aibotkType) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${aibotkKey}`,
roomName: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${aibotkKey}`,
name: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(url, {
...this.gotOption,
json: {
...json,
},
})
.json();
return res.code === 0;
}
private async iGot() {
const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
@@ -333,8 +384,7 @@ export default class NotificationService {
const url = `https://www.pushplus.plus/send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
@@ -347,6 +397,21 @@ export default class NotificationService {
return res.code === 200;
}
private async lark() {
const { larkKey } = this.params;
const res: any = await got
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
...this.gotOption,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
})
.json();
return res.StatusCode === 0;
}
private async email() {
const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({
@@ -368,4 +433,62 @@ export default class NotificationService {
return !!info.messageId;
}
private async webhook() {
const {
webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
const options = {
method: webhookMethod,
headers,
...this.gotOption,
allowGetBody: true,
...bodyParam,
};
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
}
private formatBody(contentType: string, body: any): object {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
private formatNotifyContent(url: string, body: string) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
.replaceAll('$title', this.title)
.replaceAll('$content', this.content),
};
}
}
+1 -2
View File
@@ -79,13 +79,12 @@ export default class ScheduleService {
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await callbacks.onEnd?.(
cp,
endTime,
+2 -1
View File
@@ -6,6 +6,7 @@ import SockService from './sock';
import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class ScriptService {
@@ -55,7 +56,7 @@ export default class ScriptService {
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
.replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`,
});
return { code: 200 };
+7 -3
View File
@@ -29,6 +29,7 @@ import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
import SshKeyService from './sshKey';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class SubscriptionService {
@@ -62,7 +63,10 @@ export default class SubscriptionService {
try {
const result = await SubscriptionModel.findAll({
where: query,
order: [['createdAt', 'DESC']],
order: [
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
});
return result as any;
} catch (error) {
@@ -229,7 +233,7 @@ export default class SubscriptionService {
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}`,
)} 耗时 ${diff}${LOG_END_SYMBOL}`,
);
await SubscriptionModel.update(
@@ -350,7 +354,7 @@ export default class SubscriptionService {
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')}${LOG_END_SYMBOL}`,
);
}
+2 -8
View File
@@ -70,9 +70,8 @@ export default class SystemService {
};
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, {
this.scheduleService.createIntervalTask(cron, {
days: frequency,
runImmediately: true,
});
}
return { code: 200, data: { ...cron } };
@@ -90,8 +89,7 @@ export default class SystemService {
let lastLog = '';
try {
const result = await got.get(config.lastVersionFile, {
timeout: 6000,
retry: 0,
timeout: 30000,
});
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
@@ -138,10 +136,6 @@ export default class SystemService {
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',
+33 -19
View File
@@ -65,32 +65,37 @@ export default class UserService {
return this.initAuthInfo();
}
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
const retriesTime = Math.pow(3, retries) * 1000;
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 1000,
);
return {
code: 410,
message: `失败次数过多,请${Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
)}秒后重试`,
data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
),
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
}
if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3;
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
@@ -155,7 +160,16 @@ export default class UserService {
status: LoginStatus.fail,
},
});
return { code: 400, message: config.authError };
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
}
} else {
return this.initAuthInfo();
+4 -4
View File
@@ -1,9 +1,9 @@
FROM node:alpine
FROM python:alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=master
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
@@ -29,10 +29,10 @@ RUN set -x \
perl \
openssl \
nginx \
python3 \
nodejs \
jq \
openssh \
py3-pip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+7 -2
View File
@@ -4,12 +4,17 @@ dir_shell=/ql/shell
. $dir_shell/share.sh
link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
patch_version &>/dev/null
echo
echo -e "======================2. 安装依赖========================\n"
@@ -37,13 +42,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 &
nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 &
nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
+12 -12
View File
@@ -1,8 +1,8 @@
upstream api {
upstream baseApi {
server 0.0.0.0:5600;
}
upstream public {
upstream publicApi {
server 0.0.0.0:5400;
}
@@ -16,42 +16,42 @@ server {
root /ql/static/dist;
ssl_session_timeout 5m;
location /api/public {
location QL_BASE_URL/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;
proxy_pass http://publicApi/api/public/;
}
location /api {
location QL_BASE_URL/api/ {
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://api;
proxy_pass http://baseApi/api/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location /open {
location QL_BASE_URL/open/ {
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://api;
proxy_pass http://baseApi/open/;
}
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
location / {
index index.html index.htm;
try_files $uri $uri/ /index.html;
location QL_BASE_URL/ {
index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html;
}
location ~ .*\.(html)$ {
+18 -18
View File
@@ -5,41 +5,41 @@ error_log /var/log/nginx/error.log warn;
include /etc/nginx/modules/*.conf;
events {
worker_connections 1024;
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
server_tokens off;
client_max_body_size 20m;
client_body_buffer_size 20m;
client_max_body_size 20m;
client_body_buffer_size 20m;
keepalive_timeout 65;
keepalive_timeout 65;
sendfile on;
sendfile on;
tcp_nodelay on;
tcp_nodelay on;
ssl_prefer_server_ciphers on;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:2m;
ssl_session_cache shared:SSL:2m;
gzip on;
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
+35 -18
View File
@@ -2,16 +2,16 @@
"private": true,
"scripts": {
"start": "concurrently -n w: npm:start:*",
"start:front": "umi dev",
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node back/public.ts",
"build:front": "umi build",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.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}'",
"postinstall": "umi generate tmp 2>/dev/null || true",
"postinstall": "max setup 2>/dev/null || true",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
@@ -28,16 +28,33 @@
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup"
],
"allowedVersions": {
"react": "17",
"react-dom": "17"
"react": "18",
"react-dom": "18",
"dva-core": "2"
}
}
},
"dependencies": {
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
"body-parser": "^1.19.2",
"celebrate": "^15.0.1",
"chokidar": "^3.5.3",
@@ -48,6 +65,7 @@
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"iconv-lite": "^0.6.3",
@@ -72,8 +90,8 @@
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1",
"@sentry/react": "^7.0.0",
"@monaco-editor/react": "4.2.1",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
@@ -86,18 +104,16 @@
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.15.0",
"@umijs/plugin-esbuild": "^1.4.1",
"@umijs/max": "^4.0.21",
"@umijs/ssr-darkreader": "^4.9.45",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6",
"antd": "^4.20.5",
"antd": "^4.23.0",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
@@ -107,18 +123,19 @@
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6",
"react": "17.x",
"react": "18.2.0",
"react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x",
"react-dom": "18.2.0",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"tslib": "^2.4.0",
"typescript": "^4.6.2",
"umi": "^3.5.21",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"webpack": "^5.70.0",
+32 -1
View File
@@ -19,6 +19,11 @@ RepoFileExtensions="js py"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h"
@@ -29,6 +34,14 @@ MaxConcurrentNum="5"
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300"
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true"
@@ -38,9 +51,12 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定npm源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
## 1. Server酱
## https://sct.ftqq.com
@@ -129,4 +145,19 @@ export GOTIFY_PRIORITY=0
## deer_key 填写PushDeer的key
export DEER_KEY=""
## 12. Chat
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
## chat_token 填写后面的token
export CHAT_URL=""
export CHAT_TOKEN=""
## 13. aibotk
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
## aibotk_key (必填)填写智能微秘书个人中心的apikey
export AIBOTK_KEY=""
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+218 -128
View File
@@ -39,6 +39,12 @@ let SCKEY = '';
//(环境变量名 DEER_KEY)
let PUSHDEER_KEY = '';
// =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN
//(环境变量名 CHAT_URL CHAT_TOKEN)
let CHAT_URL = '';
let CHAT_TOKEN = '';
// =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = '';
@@ -104,6 +110,20 @@ let PUSH_PLUS_USER = '';
let QQ_SKEY = '';
let QQ_MODE = '';
// =======================================智能微秘书设置区域=======================================
//官方文档:http://wechat.aibotk.com/docs/about
//AIBOTK_KEY 填写智能微秘书个人中心的apikey
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
let AIBOTK_KEY = '';
let AIBOTK_TYPE = '';
let AIBOTK_NAME = '';
// =======================================飞书机器人设置区域=======================================
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
//FSKEY 飞书机器人的 FSKEY
let FSKEY = '';
//==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
@@ -133,6 +153,14 @@ if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY;
}
if (process.env.CHAT_URL) {
CHAT_URL = process.env.CHAT_URL;
}
if (process.env.CHAT_TOKEN) {
CHAT_TOKEN = process.env.CHAT_TOKEN;
}
if (process.env.QQ_SKEY) {
QQ_SKEY = process.env.QQ_SKEY;
}
@@ -206,6 +234,20 @@ if (process.env.PUSH_PLUS_TOKEN) {
if (process.env.PUSH_PLUS_USER) {
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
}
if (process.env.AIBOTK_KEY) {
AIBOTK_KEY = process.env.AIBOTK_KEY;
}
if (process.env.AIBOTK_TYPE) {
AIBOTK_TYPE = process.env.AIBOTK_TYPE;
}
if (process.env.AIBOTK_NAME) {
AIBOTK_NAME = process.env.AIBOTK_NAME;
}
if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
//==========================云端环境变量的判断与接收=========================
/**
@@ -239,6 +281,10 @@ async function sendNotify(
iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
]);
}
@@ -279,7 +325,7 @@ function gotifyNotify(text, desp) {
});
}
function gobotNotify(text, desp, time = 2100) {
function gobotNotify(text, desp) {
return new Promise((resolve) => {
if (GOBOT_URL) {
const options = {
@@ -290,38 +336,34 @@ function gobotNotify(text, desp, time = 2100) {
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送go-cqhttp通知调用API失败!!\n');
console.log(err);
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送go-cqhttp通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else {
data = JSON.parse(data);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
);
}
console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}, time);
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function serverNotify(text, desp, time = 2100) {
function serverNotify(text, desp) {
return new Promise((resolve) => {
if (SCKEY) {
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
@@ -336,40 +378,36 @@ function serverNotify(text, desp, time = 2100) {
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else {
data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
);
}
console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}, time);
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function PushDeerNotify(text, desp, time = 2100) {
function PushDeerNotify(text, desp) {
return new Promise((resolve) => {
if (PUSHDEER_KEY) {
// PushDeer 建议对消息内容进行 urlencode
@@ -382,106 +420,62 @@ function PushDeerNotify(text, desp, time = 2100) {
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
$.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 {
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)}`,
);
}
console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}, time);
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function CoolPush(text, desp) {
function ChatNotify(text, desp) {
return new Promise((resolve) => {
if (QQ_SKEY) {
let options = {
url: `https://push.xuthus.cc/${QQ_MODE}/${QQ_SKEY}`,
if (CHAT_URL && CHAT_TOKEN) {
// 对消息内容进行 urlencode
desp = encodeURI(desp);
const options = {
url: `${CHAT_URL}${CHAT_TOKEN}`,
body: `payload={"text":"${text}\n${desp}"}`,
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
};
// 已知敏感词
text = text.replace(/京豆/g, '豆豆');
desp = desp.replace(/京豆/g, '');
desp = desp.replace(/🐶/g, '');
desp = desp.replace(/红包/g, 'H包');
switch (QQ_MODE) {
case 'email':
options.json = {
t: text,
c: desp,
};
break;
default:
options.body = `${text}\n\n${desp}`;
}
let pushMode = function (t) {
switch (t) {
case 'send':
return '个人';
case 'group':
return 'QQ群';
case 'wx':
return '微信';
case 'ww':
return '企业微信';
case 'email':
return '邮件';
default:
return '未知方式';
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log(`发送${pushMode(QQ_MODE)}通知调用API失败!!\n`);
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 200) {
console.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功🎉\n`);
} else if (data.code === 400) {
console.log(
`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${
data.msg
}\n`,
);
} else if (data.code === 503) {
console.log(`QQ酷推出错,${data.message}${data.data}\n`);
if (data.success) {
console.log('Chat发送通知消息成功🎉\n');
} else {
console.log(`酷推推送异常: ${JSON.stringify(data)}`);
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`);
}
}
} catch (e) {
$.logErr(e, resp);
$.logErr(e);
} finally {
resolve(data);
}
@@ -594,7 +588,7 @@ function ddBotNotify(text, desp) {
json: {
msgtype: 'text',
text: {
content: ` ${text}\n\n${desp}`,
content: `${text}\n\n${desp}`,
},
},
headers: {
@@ -661,7 +655,7 @@ function qywxBotNotify(text, desp) {
json: {
msgtype: 'text',
text: {
content: ` ${text}\n\n${desp}`,
content: `${text}\n\n${desp}`,
},
},
headers: {
@@ -928,6 +922,102 @@ function pushPlusNotify(text, desp) {
});
}
function aibotkNotify(text, desp) {
return new Promise((resolve) => {
if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {
let json = {};
let url = '';
switch (AIBOTK_TYPE) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${AIBOTK_KEY}`,
roomName: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${AIBOTK_KEY}`,
name: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
}
const options = {
url: url,
json,
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('智能微秘书发送通知消息失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 0) {
console.log('智能微秘书发送通知消息成功🎉。\n');
} else {
console.log(`${data.error}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}
});
}
function fsBotNotify(text, desp) {
return new Promise((resolve) => {
if (FSKEY) {
const options = {
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
json: { msg_type: 'text', content: { text: `${title}\n\n${content}` } },
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.StatusCode === 0) {
console.log('飞书发送通知消息成功🎉\n');
} else {
console.log(`${data.msg}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
module.exports = {
sendNotify,
BARK_PUSH,
+63
View File
@@ -62,6 +62,9 @@ push_config = {
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -78,6 +81,10 @@ push_config = {
'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
}
notify_function = []
# fmt: on
@@ -281,6 +288,25 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None:
"""
通过Chat 推送消息
"""
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
if response.status_code == 200:
print("Chat 推送成功!")
else:
print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
@@ -499,6 +525,39 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 推送失败!")
def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息。
"""
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room':
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
print(response)
if response["code"] == 0:
print("智能微秘书 推送成功!")
else:
print(f'智能微秘书 推送失败!{response["error"]}')
def one() -> str:
"""
获取一条一言。
@@ -527,6 +586,8 @@ if push_config.get("PUSH_KEY"):
notify_function.append(serverJ)
if push_config.get("DEER_KEY"):
notify_function.append(pushdeer)
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
@@ -537,6 +598,8 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk)
def send(title: str, content: str) -> None:
+160 -160
View File
@@ -1,187 +1,187 @@
#!/usr/bin/env bash
get_token() {
token=$(cat $file_auth_token | jq -r .value)
token=$(cat $file_auth_token | jq -r .value)
}
add_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-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 "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-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 "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
}
update_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?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 "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?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 "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
}
update_cron_command_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?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 "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?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 "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
}
del_cron_api() {
local ids=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-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 "[$ids]" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
local ids=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-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 "[$ids]" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
}
update_cron() {
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?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 "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path
fi
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?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 "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi
}
notify_api() {
local title=$1
local content=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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
local title=$1
local content=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/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
+4 -5
View File
@@ -35,11 +35,10 @@ fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
cat requirements.txt | while read LREAD
do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
cat requirements.txt | while read LREAD; do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
done
echo -e "\npython3依赖安装成功...\n"
+4
View File
@@ -31,6 +31,7 @@ copy_dep() {
echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n"
}
@@ -89,6 +90,9 @@ check_pm2() {
main() {
echo -e "=====> 开始检测"
npm i -g pnpm
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
pnpm add -g pm2
copy_dep
check_ql
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
check_server() {
cpu_use=$(top -b -n 1 | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(top -b -n 1 | grep -v -E 'grep|Mem|idle|Load' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
}
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
cat $single_log_path
[[ -f $single_log_path ]] && rm -f $single_log_path
done
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 运行其他命令
run_else() {
local file_param="$1"
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
local params=$(echo "$@" | sed 's/ /\" \"/g')
$timeoutCmd $which_program $file_param \"$params\"
}
## 命令检测
main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
handle_task_before "$@"
main "$@"
handle_task_after "$@"
+23 -23
View File
@@ -8,35 +8,35 @@ days=$1
## 删除运行js脚本的旧日志
remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi
done
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi
done
}
## 删除空文件夹
remove_empty_dir() {
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
}
## 运行
if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
fi
+314 -258
View File
@@ -50,378 +50,434 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系
link_name=(
task
ql
task
ql
)
original_name=(
task.sh
update.sh
task.sh
update.sh
)
init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
}
import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
cpu_warn=${CpuWarn:-80}
mem_warn=${MemoryWarn:-80}
disk_warn=${DiskWarn:-90}
}
set_proxy() {
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
}
unset_proxy() {
unset http_proxy
unset https_proxy
unset http_proxy
unset https_proxy
}
make_dir() {
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
}
detect_termux() {
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
}
detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
}
gen_random_num() {
local divi=$1
echo $((${RANDOM} % $divi))
local divi=$1
echo $((${RANDOM} % $divi))
}
link_shell_sub() {
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
}
link_shell() {
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
}
define_cmd() {
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_prefix="bash "
fi
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
cmd_prefix="bash "
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
}
fix_config() {
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
}
npm_install_sub() {
set_proxy
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production --registry=https://registry.npmmirror.com || npm install --production
else
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
fi
unset_proxy
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production
else
pnpm install --loglevel error --production
fi
}
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
}
npm_install_2() {
local dir_current=$(pwd)
local dir_work=$1
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
}
diff_and_copy() {
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
}
update_depend() {
local dir_current=$(pwd)
local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
cd $dir_current
cd $dir_current
}
git_clone_scripts() {
local url=$1
local dir=$2
local branch=$3
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
local url=$1
local dir=$2
local branch=$3
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
set_proxy
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
set_proxy
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
}
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
set_proxy
git fetch --all
exit_status=$?
git pull &>/dev/null
unset_proxy
reset_branch "$branch"
set_proxy
git fetch --all
exit_status=$?
git pull &>/dev/null
unset_proxy
reset_branch "$branch"
cd $dir_current
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
if [[ -d "$dir_work/.git" ]]; then
cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
if [[ -d "$dir_work/.git" ]]; then
cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
local part_cmd=""
reset_branch "$branch"
cd $dir_current
fi
local part_cmd=""
reset_branch "$branch"
cd $dir_current
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
local branch="$1"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() {
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
}
reload_pm2() {
pm2 l &>/dev/null
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
diff_time() {
local format="$1"
local begin_time="$2"
local end_time="$3"
local format="$1"
local begin_time="$2"
local end_time="$3"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
}
format_time() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
}
format_log_time() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
}
format_timestamp() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
}
patch_version() {
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false
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
}
init_env
@@ -430,4 +486,4 @@ detect_macos
define_cmd
fix_config
import_config $1 >$task_error_log_path 2>&1
import_config $1 2>$task_error_log_path
+61 -374
View File
@@ -7,386 +7,73 @@ dir_shell=$QL_DIR/shell
## 选择python3还是node
define_program() {
local file_param=$1
if [[ $file_param == *.js ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="bash"
elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only"
local file_param=$1
if [[ $file_param == *.js ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="bash"
elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only"
else
which_program=""
fi
}
handle_log_path() {
local file_param=$1
if [[ -z $file_param ]];then
file_param="task"
fi
local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
which_program=""
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
}
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local current_min=$(date "+%-M")
if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then
delay_second=$(($(gen_random_num $random_delay_max) + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
fi
format_params() {
time_format="%Y-%m-%d %H:%M:%S"
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time "
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
show_log="false"
while getopts ":l" opt; do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
## 使用说明
usage() {
define_cmd
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
define_program "$file_param"
if [[ $file_param == *.js ]]; then
if [[ $# -eq 1 ]]; then
random_delay
fi
fi
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval $timeoutCmd $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(expr $end_timestamp - $begin_timestamp)
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval cat $single_log_path $cmd
[[ -f $single_log_path ]] && rm -f $single_log_path
done
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval $timeoutCmd $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 运行其他命令
run_else() {
local file_param="$1"
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 命令检测
main() {
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time "
fi
time_format="%Y-%m-%d %H:%M:%S"
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
main "$@"
format_params
define_program "$@"
handle_log_path "$@"
eval . $dir_shell/otask.sh "$@" "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
exit 0
+411 -435
View File
@@ -8,544 +8,520 @@ send_mark=$dir_shell/send_mark
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() {
local list_scripts="$1"
local list_task="$2"
local list_add="$3"
local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop
fi
local list_scripts="$1"
local list_task="$2"
local list_add="$3"
local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop
fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add
fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add
fi
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
cp -f $list_task $list_drop
fi
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
cp -f $list_task $list_drop
fi
}
## 检测配置文件版本
detect_config_version() {
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
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"
echo -e $notify_content
notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
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"
echo -e $notify_content
notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
}
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() {
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
cat $list
echo
fi
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
cat $list
echo
fi
}
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务
## $1:失效任务清单文件路径
del_cron() {
local list_drop=$1
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
ids="$ids,\"$id\""
else
ids="\"$id\""
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
local list_drop=$1
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
ids="$ids,\"$id\""
else
ids="\"$id\""
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
fi
}
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
## $1:新任务清单文件路径
add_cron() {
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$(
perl -ne "{
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file |
perl -pe "{
perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g;
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result"
if [[ $detail ]]; then
detail="${detail}${result}\n"
else
detail="${result}\n"
fi
fi
done
notify_api "$path 新增任务" "$detail"
)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result"
if [[ $detail ]]; then
detail="${detail}${result}\n"
else
detail="${result}\n"
fi
fi
done
notify_api "$path 新增任务" "$detail"
}
## 更新仓库
update_repo() {
local url="$1"
local path="$2"
local blackword="$3"
local dependence="$4"
local branch="$5"
local extensions="$6"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local url="$1"
local path="$2"
local blackword="$3"
local dependence="$4"
local branch="$5"
local extensions="$6"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local repo_path="${dir_repo}/${uniq_path}"
local repo_path="${dir_repo}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}"
else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}"
else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
}
## 更新所有 raw 文件
update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1"
local raw_url="$url"
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
echo -e "--------------------------------------------------------------\n"
local url="$1"
local raw_url="$url"
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw
local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$(
perl -ne "{
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw
local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
}" $raw_file_name |
perl -pe "{
perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g;
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n"
notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n"
notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi
}
## 调用用户自定义的extra.sh
run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n"
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n"
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi
}
## 脚本用法
usage() {
echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
echo -e "ql命令使用方法:"
echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
}
## 更新qinglong
update_qinglong() {
patch_version
patch_version &>/dev/null
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)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
export isFirstStartServer=false
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
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)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else
echo -e "\n更新青龙源文件失败,请检查原因...\n"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi
else
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
fi
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
}
patch_version() {
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
update_qinglong_static() {
local no_restart="$1"
local primary_branch="$2"
local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi
if ! type ts-node &>/dev/null; then
pnpm i -g ts-node typescript tslib
fi
# 兼容pnpm@7
pnpm setup
source ~/.bashrc
git config --global pull.rebase false
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
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
fi
}
## 对比脚本
diff_scripts() {
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
local extensions="$6"
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
local extensions="$6"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path
fi
if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path
fi
if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then
add_cron $list_add $uniq_path
fi
fi
if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then
add_cron $list_add $uniq_path
fi
cd $dir_current
fi
cd $dir_current
}
## 生成脚本的路径清单文件
gen_list_repo() {
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
cd ${repo_path}
local cmd="find ."
local index=0
if [[ $6 ]]; then
file_extensions="$6"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path}
local cmd="find ."
local index=0
if [[ $6 ]]; then
file_extensions="$6"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi
done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current
done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current
}
get_uniq_path() {
local url="$1"
local branch="$2"
local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local url="$1"
local branch="$2"
local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
}
main() {
## for ql update
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1
local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in
update)
cmd=">> $log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
eval echo -e "## 开始执行... $begin_time\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
eval update_qinglong "$2" $cmd
;;
extra)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >>$log_path
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else
echo -e "命令输入错误...\n"
usage
fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
echo -e "命令输入错误...\n"
usage
fi
;;
rmlog)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >>$log_path
;;
bot)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >>$log_path
;;
check)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/check.sh >>$log_path
;;
resetlet)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
resettfa)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
*)
echo -e "命令输入错误...\n"
usage
;;
## for ql update
show_log="false"
while getopts ":l" opt; do
case $opt in
l)
show_log="true"
;;
esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time")
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path
cat $log_path
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1
local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_dir="${p1}"
make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="${log_dir}/${log_time}.log"
local file_path="$dir_log/$log_path"
cmd=">> $file_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
fi
case $p1 in
update)
eval update_qinglong "$2" $cmd
;;
extra)
eval run_extra_shell $cmd
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi
;;
rmlog)
eval . $dir_shell/rmlog.sh "$p2" $cmd
;;
bot)
eval . $dir_shell/bot.sh $cmd
;;
check)
eval . $dir_shell/check.sh $cmd
;;
resetlet)
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo "$auth_value" >$file_auth_user
echo -e "重置登录错误次数成功" $cmd
;;
resettfa)
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "禁用两步验证成功" $cmd
;;
*)
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
;;
esac
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n          " $cmd
if [[ -f $file_path ]]; then
cat $file_path
fi
}
main "$@"
+1
View File
@@ -50,6 +50,7 @@ const EditableTagGroup = ({
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem}
+48
View File
@@ -0,0 +1,48 @@
import { useMemo, useState } from 'react';
export default (
treeData: any[],
searchValue: string,
{
treeNodeFilterProp,
}: {
treeNodeFilterProp: string;
},
) => {
return useMemo(() => {
const keys: string[] = [];
if (!searchValue) {
return { treeData, keys };
}
const upperStr = searchValue.toUpperCase();
function filterOptionFunc(_: string, dataNode: any[]) {
const value = dataNode[treeNodeFilterProp as any];
return String(value).toUpperCase().includes(upperStr);
}
function dig(list: any[], keepAll: boolean = false): any[] {
return list
.map((dataNode) => {
const children = dataNode.children;
const match = keepAll || filterOptionFunc!(searchValue, dataNode);
const childList = dig(children || [], match);
if (match || childList.length) {
childList.length && keys.push(dataNode.key);
return {
...dataNode,
children: childList,
};
}
return null;
})
.filter((node) => node);
}
return { treeData: dig(treeData), keys };
}, [treeData, searchValue, treeNodeFilterProp]);
};
+6 -6
View File
@@ -68,18 +68,18 @@ export default {
icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index',
},
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/diff',
name: '对比工具',
icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index',
},
{
path: '/log',
name: '任务日志',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/setting',
name: '系统设置',
+43 -32
View File
@@ -56,7 +56,13 @@
overflow: auto;
height: 100%;
background-color: @component-background;
padding: 16px;
padding: 12px;
}
}
.ql-container-wrapper-has-tab {
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
padding-top: 0;
}
}
@@ -136,7 +142,7 @@
flex: 1;
.ant-pro-grid-content-children {
height: calc(100% - 36px);
height: 100%;
> div,
.log-container,
@@ -155,7 +161,7 @@
&.env-wrapper,
&.config-wrapper {
.CodeMirror {
width: calc(100vw - 80px);
width: calc(100vw - 24px);
}
}
}
@@ -237,43 +243,48 @@
font-size: 13px;
}
[data-dark='true'] {
/* Change autocomplete styles in WebKit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: 1px solid @border-color-base;
box-shadow: none;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
}
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
/* Change autocomplete styles in WebKit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: none;
box-shadow: none;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color;
}
.ant-pro-page-container-children-content {
margin: 18px 18px 18px;
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
.ant-pro-basicLayout-content {
margin: 18px;
margin: 12px;
.ant-pro-page-container {
margin: -18px -18px -18px;
margin: -12px;
}
.ant-pro-page-container-warp .ant-page-header {
border-bottom: 1px solid #eee;
}
.ant-pro-page-container-children-content {
margin: 0;
}
}
.ant-pro-global-header {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
}
.ant-menu-item.ant-pro-sider-collapsed-button {
+54 -40
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps';
import { Link, history } from 'umi';
import { Link, history, Outlet, useLocation } from '@umijs/max';
import {
LogoutOutlined,
MenuFoldOutlined,
@@ -20,8 +20,22 @@ import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export default function (props: any) {
export interface SharedContext {
headerStyle: React.CSSProperties;
isPhone: boolean;
theme: 'vs' | 'vs-dark';
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
}
export default function () {
const location = useLocation();
const ctx = useCtx();
const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({});
@@ -30,6 +44,7 @@ export default function (props: any) {
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const {
enable: enableDarkMode,
disable: disableDarkMode,
@@ -53,17 +68,15 @@ export default function (props: any) {
setSystemInfo(data);
if (!data.isInitialized) {
history.push('/initialization');
setLoading(false);
} else {
getUser();
}
} else {
message.error(data);
}
})
.catch((error) => {
console.log(error);
});
})
.finally(() => setInitLoading(false));
};
const getUser = (needLoading = true) => {
@@ -73,11 +86,9 @@ export default function (props: any) {
.then(({ code, data }) => {
if (code === 200 && data.username) {
setUser(data);
if (props.location.pathname === '/') {
if (location.pathname === '/') {
history.push('/crontab');
}
} else {
message.error(data);
}
needLoading && setLoading(false);
})
@@ -94,7 +105,7 @@ export default function (props: any) {
if (systemInfo && systemInfo.isInitialized && !user) {
getUser();
}
}, [props.location.pathname]);
}, [location.pathname]);
useEffect(() => {
if (!systemInfo) {
@@ -136,9 +147,11 @@ export default function (props: any) {
}, []);
useEffect(() => {
if (!user) return;
if (!user || !user.username) return;
ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
`${window.location.origin}/api/ws?token=${localStorage.getItem(
config.authKey,
)}`,
);
ws.current.onmessage = (e: any) => {
@@ -146,9 +159,9 @@ export default function (props: any) {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e);
console.log('WS connection succeeded !!!');
} else {
console.log('websocket连接失败', e);
console.log('WS connection Failed !!!', e);
}
}
setSocketMessage(data);
@@ -183,30 +196,31 @@ export default function (props: any) {
};
}, []);
if (
['/login', '/initialization', '/error'].includes(props.location.pathname)
) {
if (initLoading) {
return <PageLoading />;
}
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[props.location.pathname]
(config.documentTitleMap as any)[location.pathname]
} - `;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab');
}
if (systemInfo || props.location.pathname === '/error') {
return React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
...ctx,
theme,
user,
reloadUser,
reloadTheme,
ws: ws.current,
});
});
if (systemInfo || location.pathname === '/error') {
return (
<Outlet
context={{
...ctx,
theme,
user,
reloadUser,
reloadTheme,
ws: ws.current,
}}
/>
);
}
}
@@ -227,7 +241,7 @@ export default function (props: any) {
<PageLoading />
) : (
<ProLayout
selectedKeys={[props.location.pathname]}
selectedKeys={[location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
@@ -282,7 +296,7 @@ export default function (props: any) {
shape="square"
size="small"
icon={<UserOutlined />}
src={`/api/static/${user.avatar}`}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
@@ -304,7 +318,7 @@ export default function (props: any) {
shape="square"
size="small"
icon={<UserOutlined />}
src={`/api/static/${user.avatar}`}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
@@ -320,16 +334,16 @@ export default function (props: any) {
)}
{...defaultProps}
>
{React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
<Outlet
context={{
...ctx,
theme,
user,
reloadUser,
reloadTheme,
socketMessage,
});
})}
}}
/>
</ProLayout>
);
}
+17 -7
View File
@@ -11,8 +11,11 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const Config = ({ headerStyle, isPhone, theme }: any) => {
const Config = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState('');
const [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh');
@@ -22,8 +25,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
setValue(data.data);
request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
@@ -31,8 +36,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.then((data: any) => {
setData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
@@ -47,8 +54,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: select },
})
.then((data: any) => {
message.success(data.message);
.then(({ code, data }) => {
if (code === 200) {
message.success('保存成功');
}
setConfirmLoading(false);
});
};
@@ -71,6 +80,7 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
loading={loading}
extra={[
<TreeSelect
treeExpandAction="click"
className="config-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+31 -35
View File
@@ -103,12 +103,10 @@ const CronDetailModal = ({
fontSize: 12,
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
onMount={(editor, monaco) => {
console.log(monaco);
editorRef.current = editor;
}}
/>
@@ -124,9 +122,11 @@ const CronDetailModal = ({
.get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
)
.then((data) => {
setLog(data.data);
setIsLogModalVisible(true);
.then(({ code, data }) => {
if (code === 200) {
setLog(data);
setIsLogModalVisible(true);
}
});
};
@@ -138,9 +138,9 @@ const CronDetailModal = ({
setLoading(true);
request
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => {
if (data.code === 200) {
setLogs(data.data);
.then(({ code, data }) => {
if (code === 200) {
setLogs(data);
}
})
.finally(() => setLoading(false));
@@ -154,16 +154,22 @@ const CronDetailModal = ({
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
setScriptInfo({ parent: p, filename: s });
request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
} else {
setValidTabs([validTabs[0]]);
@@ -195,12 +201,10 @@ const CronDetailModal = ({
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setValue(content);
message.success(`保存成功`);
} else {
message.error(_data);
}
resolve(null);
})
@@ -228,14 +232,12 @@ const CronDetailModal = ({
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => {
getLogs();
}, 1000);
} else {
message.error(data);
}
});
},
@@ -260,11 +262,9 @@ const CronDetailModal = ({
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
} else {
message.error(data);
}
});
},
@@ -297,14 +297,12 @@ const CronDetailModal = ({
data: [currentCron.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({
...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
});
} else {
message.error(data);
}
});
},
@@ -337,14 +335,12 @@ const CronDetailModal = ({
data: [currentCron.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({
...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1,
});
} else {
message.error(data);
}
});
},
@@ -379,7 +375,7 @@ const CronDetailModal = ({
{currentCron.labels?.length > 0 &&
currentCron.labels[0] !== '' &&
currentCron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}>
<Tag key={label} color="blue" style={{ marginRight: 5 }}>
{label}
</Tag>
))}
@@ -442,7 +438,7 @@ const CronDetailModal = ({
</div>
}
centered
visible={visible}
open={visible}
forceRender
footer={false}
onCancel={() => handleCancel()}
+75 -6
View File
@@ -2,12 +2,6 @@
margin-bottom: 0 !important;
}
.cron {
&:hover {
cursor: pointer;
}
}
.crontab-detail {
.card-wrapper {
height: 80vh;
@@ -106,4 +100,79 @@
.ant-tabs-nav-wrap {
flex: unset !important;
}
.ant-tabs-nav-operations {
position: absolute;
visibility: hidden;
pointer-events: none;
}
.view-more {
margin-left: 32px;
padding: 8px 0;
cursor: pointer;
.ant-tabs-ink-bar {
width: 0;
}
&:hover,
&:focus-visible {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
&.active {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
}
&.more-active {
.ant-tabs-nav-list {
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: unset;
}
.ant-tabs-ink-bar {
width: 0 !important;
}
}
}
}
.view-create-modal-filters {
display: flex;
.ant-space-item:nth-child(3) {
flex: 1;
}
}
.view-create-modal-sorts {
display: flex;
.ant-space-item:nth-child(2) {
flex: 1;
}
}
tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
.view-manage-modal {
.ant-modal-body {
padding-top: 10px;
}
}
+279 -91
View File
@@ -1,4 +1,4 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Button,
message,
@@ -13,6 +13,7 @@ import {
Input,
Popover,
Tabs,
TablePaginationConfig,
} from 'antd';
import {
ClockCircleOutlined,
@@ -28,6 +29,11 @@ import {
PauseCircleOutlined,
FieldTimeOutlined,
PushpinOutlined,
DownOutlined,
SettingOutlined,
PlusOutlined,
UnorderedListOutlined,
CheckOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -38,8 +44,13 @@ import CronDetailModal from './detail';
import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import { history, useOutletContext } from '@umijs/max';
import './index.less';
import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -71,7 +82,8 @@ enum OperationPath {
'unpin',
}
const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
{
title: '名称',
@@ -83,10 +95,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
<>
<a
onClick={() => {
goToScriptManager(record);
setDetailCron(record);
setIsDetailModalVisible(true);
}}
>
{record.labels?.length > 0 && record.labels[0] !== '' ? (
{record.labels?.length > 0 && record.labels[0] !== '' && false ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
@@ -95,10 +108,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
{record.labels?.map((label: string) => (
<Tag
color="blue"
key={label}
style={{ cursor: 'point' }}
onClick={(e) => {
e.stopPropagation();
setSearchValue(`label:${label}`);
setSearchText(`label:${label}`);
}}
>
<a>{label}</a>
@@ -124,14 +139,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
),
sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
multiple: 2,
},
},
{
title: '命令',
dataIndex: 'command',
key: 'command',
width: 250,
width: 300,
align: 'center' as const,
render: (text: string, record: any) => {
return (
@@ -143,13 +157,18 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
{text}
<a
onClick={() => {
goToScriptManager(record);
}}
>
{text}
</a>
</Paragraph>
);
},
sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
},
},
{
@@ -160,12 +179,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
},
},
{
title: '最后运行时间',
align: 'center' as const,
dataIndex: 'last_execution_time',
key: 'last_execution_time',
width: 150,
sorter: {
compare: (a: any, b: any) => {
@@ -195,6 +215,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
title: '最后运行时长',
align: 'center' as const,
width: 120,
dataIndex: 'last_running_time',
key: 'last_running_time',
sorter: {
compare: (a: any, b: any) => {
return a.last_running_time - b.last_running_time;
@@ -347,14 +369,26 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [pageConf, setPageConf] = useState<{ page: number; size: number }>(
{} as any,
);
const [pageConf, setPageConf] = useState<{
page: number;
size: number;
sorter: any;
filters: any;
}>({} as any);
const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const [total, setTotal] = useState<number>();
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState(false);
const [isViewManageModalVisible, setIsViewManageModalVisible] =
useState(false);
const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
@@ -363,9 +397,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
@@ -376,24 +414,42 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCrons = () => {
setLoading(true);
const { page, size, sorter, filters } = pageConf;
let url = `${
config.apiPrefix
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters,
)}`;
if (sorter && sorter.field) {
url += `&sorter=${JSON.stringify({
field: sorter.field,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`;
}
if (viewConf) {
url += `&queryString=${JSON.stringify({
filters: viewConf.filters,
sorts: viewConf.sorts,
})}`;
}
request
.get(
`${config.apiPrefix}crons?searchText=${searchText}&page=${pageConf.page}&size=${pageConf.size}`,
)
.then((_data: any) => {
const { data, total } = _data.data;
setValue(
data.map((x) => {
return {
...x,
nextRunTime: cron_parser
.parseExpression(x.schedule)
.next()
.toDate(),
};
}),
);
setTotal(total);
.get(url)
.then(({ code, data: _data }) => {
if (code === 200) {
const { data, total } = _data;
setValue(
data.map((x) => {
return {
...x,
nextRunTime: cron_parser
.parseExpression(x.schedule)
.next()
.toDate(),
};
}),
);
setTotal(total);
}
})
.finally(() => setLoading(false));
};
@@ -423,8 +479,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -432,8 +488,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
result.splice(i, 1);
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -458,8 +512,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -469,8 +523,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -495,8 +547,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -507,8 +559,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -541,8 +591,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -553,8 +603,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -587,8 +635,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -599,8 +647,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -702,19 +748,21 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCronDetail = (cron: any) => {
request
.get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
data.data.nextRunTime = cron_parser
.parseExpression(data.data.schedule)
.next()
.toDate();
if (index !== -1) {
result.splice(index, 1, {
...cron,
...data.data,
});
setValue(result);
.then(({ code, data }) => {
if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
data.nextRunTime = cron_parser
.parseExpression(data.schedule)
.next()
.toDate();
if (index !== -1) {
result.splice(index, 1, {
...cron,
...data,
});
setValue(result);
}
}
})
.finally(() => setLoading(false));
@@ -742,13 +790,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getCrons();
} else {
message.error(data);
}
});
},
@@ -767,11 +813,9 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
getCrons();
} else {
message.error(data);
}
});
},
@@ -781,8 +825,18 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
});
};
const onPageChange = (page: number, pageSize: number | undefined) => {
setPageConf({ page, size: pageSize as number });
const onPageChange = (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setPageConf({
page: current as number,
size: pageSize as number,
sorter,
filters,
});
localStorage.setItem('pageSize', String(pageSize));
};
@@ -805,18 +859,31 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
if (pageConf.page && pageConf.size) {
getCrons();
}
}, [pageConf]);
}, [pageConf, viewConf]);
useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(2).find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view);
}
}, [viewConf, enabledCronViews]);
useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
getCronViews();
}, []);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll());
}
}, [tableRef.current]);
const panelContent = (
<>
{selectedRowIds.length > 0 && (
@@ -876,10 +943,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
</div>
)}
<Table
ref={tableRef}
columns={columns}
pagination={{
current: pageConf.page,
onChange: onPageChange,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
@@ -890,14 +957,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
(a, b) => a - b,
),
}}
onRow={(record) => {
return {
onClick: (event) => {
setDetailCron(record);
setIsDetailModalVisible(true);
},
};
}}
dataSource={value}
rowKey="id"
size="middle"
@@ -905,13 +964,86 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</>
);
const viewAction = (key: string) => {
switch (key) {
case 'new':
setIsCreateViewModalVisible(true);
break;
case 'manage':
setIsViewManageModalVisible(true);
break;
default:
tabClick(key);
break;
}
};
const menu = (
<Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
}}
items={[
...[...enabledCronViews].slice(2).map((x) => ({
label: (
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{x.name}</span>
{viewConf?.id === x.id && (
<CheckOutlined style={{ color: '#1890ff' }} />
)}
</Space>
),
key: x.id,
icon: <UnorderedListOutlined />,
})),
{
type: 'divider',
},
{
label: '新建视图',
key: 'new',
icon: <PlusOutlined />,
},
{
label: '视图管理',
key: 'manage',
icon: <SettingOutlined />,
},
]}
/>
);
const getCronViews = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/views`)
.then(({ code, data }) => {
if (code === 200) {
setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
}
})
.finally(() => {
setLoading(false);
});
};
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null);
};
return (
<PageContainer
className="ql-container-wrapper crontab-wrapper"
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
title="定时任务"
extra={[
<Search
@@ -936,11 +1068,44 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
defaultActiveKey="all"
size="small"
tabPosition="top"
className="crontab-view"
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={
<Dropdown
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 200 }}
>
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
<Space>
<DownOutlined />
</Space>
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</Dropdown>
}
onTabClick={tabClick}
items={[
{
key: 'all',
label: '全部任务',
children: panelContent,
},
...[...enabledCronViews].slice(0, 2).map((x) => ({
key: x.id,
label: x.name,
children: panelContent,
})),
]}
>
<Tabs.TabPane tab="全部任务" key="all">
{panelContent}
</Tabs.TabPane>
{[...enabledCronViews].slice(0, 2).map((x) => (
<Tabs.TabPane tab={x.name} key={x.id}>
{panelContent}
</Tabs.TabPane>
))}
</Tabs>
<CronLogModal
visible={isLogModalVisible}
@@ -974,6 +1139,29 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
theme={theme}
isPhone={isPhone}
/>
<ViewCreateModal
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
<ViewManageModal
cronViews={cronViews}
visible={isViewManageModalVisible}
handleCancel={() => {
setIsViewManageModalVisible(false);
}}
cronViewChange={(data) => {
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
</PageContainer>
);
};
+10 -6
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
enum CrontabStatus {
'running',
@@ -41,14 +42,17 @@ const CronLogModal = ({
}
request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => {
if (localStorage.getItem('logCron') === String(cron.id)) {
const log = data.data as string;
.then(({ code, data }) => {
if (
code === 200 &&
localStorage.getItem('logCron') === String(cron.id)
) {
const log = data as string;
setValue(log || '暂无日志');
setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'),
log && !logEnded(log) && !log.includes('重启面板'),
);
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
if (log && !logEnded(log) && !log.includes('重启面板')) {
setTimeout(() => {
getCronLog();
}, 2000);
@@ -119,7 +123,7 @@ const CronLogModal = ({
return (
<Modal
title={titleElement()}
visible={visible}
open={visible}
centered
className="log-modal"
bodyStyle={{
+20 -18
View File
@@ -28,13 +28,12 @@ const CronModal = ({
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else {
message.error(data);
handleCancel(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -47,7 +46,7 @@ const CronModal = ({
return (
<Modal
title={cron ? '编辑任务' : '新建任务'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
@@ -128,21 +127,24 @@ const CronLabelModal = ({
.then(async (values) => {
setLoading(true);
const payload = { ids, labels: values.labels };
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
);
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
try {
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
);
} else {
message.error(data);
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
handleCancel(true);
}
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
handleCancel(true);
})
.catch((info) => {
console.log('Validate Failed:', info);
@@ -166,7 +168,7 @@ const CronLabelModal = ({
return (
<Modal
title="批量修改标签"
visible={visible}
open={visible}
footer={buttons}
centered
maskClosable={false}
+262
View File
@@ -0,0 +1,262 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Statistic,
Button,
Space,
Select,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
const PROPERTIES = [
{ name: '命令', value: 'command' },
{ name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' },
];
const OPERATIONS = [
{ name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' },
{ name: '不属于', value: 'Nin' },
// { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' },
// { name: '不为空', value: 'NotNull' },
];
const SORTTYPES = [
{ name: '顺序', value: 'ASC' },
{ name: '倒序', value: 'DESC' },
];
const STATUS = [
{ name: '运行中', value: 0 },
{ name: '空闲中', value: 1 },
{ name: '已禁用', value: 2 },
];
const ViewCreateModal = ({
view,
handleCancel,
visible,
}: {
view?: any;
visible: boolean;
handleCancel: (param?: any) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [operationMap, setOperationMap] = useState<any>();
const handleOk = async (values: any) => {
setLoading(true);
const method = view ? 'put' : 'post';
try {
const { code, data } = await request[method](
`${config.apiPrefix}crons/views`,
{
data: view ? { ...values, id: view.id } : values,
},
);
if (code === 200) {
handleCancel(data);
}
setLoading(false);
} catch (error: any) {
setLoading(false);
}
};
useEffect(() => {
if (!view) {
form.resetFields();
}
form.setFieldsValue(
view || {
filters: [{ property: 'command', operation: 'Reg' }],
},
);
}, [view, visible]);
const operationElement = (
<Select
style={{ width: 100 }}
onChange={() => {
setOperationMap({});
}}
>
{OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const propertyElement = (props: any, style: React.CSSProperties = {}) => {
return (
<Select style={style}>
{props.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
};
const typeElement = (
<Select>
{SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const statusElement = (
<Select mode="multiple" allowClear placeholder="请选择状态">
{STATUS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
return (
<Modal
title={view ? '编辑视图' : '新建视图'}
open={visible}
forceRender
width={580}
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="env_modal">
<Form.Item
name="name"
label="视图名称"
rules={[{ required: true, message: '请输入视图名称' }]}
>
<Input placeholder="请输入视图名称" />
</Form.Item>
<Form.List name="filters">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
>
<Space className="view-create-modal-filters" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 120 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'operation']}
rules={[{ required: true }]}
>
{operationElement}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{['In', 'Nin'].includes(
form.getFieldValue(['filters', index, 'operation']),
) ? (
statusElement
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
</Form.Item>
))}
<Form.Item>
<a
onClick={() => add({ property: 'command', operation: 'Reg' })}
>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
<Form.List name="sorts">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '排序方式' : ''}
key={key}
style={{ marginBottom: 0 }}
>
<Space className="view-create-modal-sorts" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 240 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'type']}
rules={[{ required: true }]}
>
{typeElement}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
</Form.Item>
))}
<Form.Item>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
);
};
export default ViewCreateModal;
+266
View File
@@ -0,0 +1,266 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Space,
Table,
Tag,
Typography,
Button,
Switch,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import ViewCreateModal from './viewCreateModal';
const { Text } = Typography;
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const ViewManageModal = ({
cronViews,
handleCancel,
visible,
cronViewChange,
}: {
cronViews: any[];
visible: boolean;
handleCancel: () => void;
cronViewChange: (data?: any) => void;
}) => {
const columns: any = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: '显示',
key: 'isDisabled',
dataIndex: 'isDisabled',
align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => {
return (
<Switch
checked={!record.isDisabled}
onChange={(checked) => onShowChange(checked, record, index)}
/>
);
},
},
{
title: '操作',
key: 'action',
width: 140,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return (
<Space size="middle">
<a onClick={() => editView(record, index)}>
<EditOutlined />
</a>
<a onClick={() => deleteView(record, index)}>
<DeleteOutlined />
</a>
</Space>
);
},
},
];
const [list, setList] = useState<any[]>([]);
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState<boolean>(false);
const [editedView, setEditedView] = useState<any>(null);
const editView = (record: any, index: number) => {
setEditedView(record);
setIsCreateViewModalVisible(true);
};
const deleteView = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
cronViewChange();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const onShowChange = (checked: boolean, record: any, index: number) => {
console.log(checked);
request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id],
})
.then(({ code, data }) => {
if (code === 200) {
const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list);
cronViewChange();
}
});
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = list[dragIndex];
request
.put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
})
.then(({ code, data }) => {
if (code === 200) {
const newData = [...list];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setList(newData);
cronViewChange();
}
});
},
[list],
);
useEffect(() => {
setList(cronViews);
}, [cronViews]);
return (
<Modal
title="视图管理"
open={visible}
centered
width={620}
onCancel={() => handleCancel()}
className="view-manage-modal"
forceRender
footer={false}
maskClosable={false}
>
<Space
style={{
display: 'flex',
justifyContent: 'flex-end',
marginBottom: 10,
}}
>
<Button
key="2"
type="primary"
onClick={() => {
setEditedView(null);
setIsCreateViewModalVisible(true);
}}
>
</Button>
</Space>
<DndProvider backend={HTML5Backend}>
<Table
bordered
columns={columns}
pagination={false}
dataSource={list}
rowKey="id"
size="middle"
style={{ marginBottom: 20 }}
components={components}
onRow={(record: any, index: number) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<ViewCreateModal
view={editedView}
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
cronViewChange(data);
}}
/>
</Modal>
);
};
export default ViewManageModal;
+85 -47
View File
@@ -29,6 +29,8 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography;
const { Search } = Input;
@@ -48,7 +50,9 @@ enum StatusColor {
'error',
}
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const Dependence = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [
{
title: '序号',
@@ -164,6 +168,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs');
const tableRef = useRef<any>();
const getDependencies = () => {
setLoading(true);
@@ -171,8 +176,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
)
.then((data: any) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
})
.finally(() => setLoading(false));
};
@@ -208,18 +215,14 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
if (force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
setValue(result);
}
.then(({ code, data }) => {
if (code === 200 && force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -246,11 +249,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
handleDependence(data.data[0]);
} else {
message.error(data);
.then(({ code, data }) => {
if (code === 200) {
handleDependence(data[0]);
}
});
},
@@ -305,12 +306,32 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.delete(`${config.apiPrefix}dependencies${forceUrl}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]);
getDependencies();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handlereInstallDependencies = () => {
Modal.confirm({
title: '确认重新安装',
content: <></>,
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]);
getDependencies();
} else {
message.error(data);
}
});
},
@@ -323,15 +344,17 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const getDependenceDetail = (dependence: any) => {
request
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => {
const index = value.findIndex((x) => x.id === dependence.id);
const result = [...value];
if (index !== -1) {
result.splice(index, 1, {
...dependence,
...data.data,
});
setValue(result);
.then(({ code, data }) => {
if (code === 200) {
const index = value.findIndex((x) => x.id === dependence.id);
const result = [...value];
if (index !== -1) {
result.splice(index, 1, {
...dependence,
...data,
});
setValue(result);
}
}
})
.finally(() => setLoading(false));
@@ -346,10 +369,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
}, [searchText, type]);
useEffect(() => {
setTimeout(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
}
}, [tableRef.current]);
useEffect(() => {
if (logDependence) {
@@ -403,6 +426,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
@@ -425,6 +455,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
)}
<DndProvider backend={HTML5Backend}>
<Table
ref={tableRef}
columns={columns}
rowSelection={rowSelection}
pagination={false}
@@ -444,7 +475,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
return (
<PageContainer
className="ql-container-wrapper dependence-wrapper"
className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab"
title="依赖管理"
extra={[
<Search
@@ -467,17 +498,24 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
size="small"
tabPosition="top"
onChange={onTabChange}
>
<Tabs.TabPane tab="NodeJs" key="nodejs">
{panelContent()}
</Tabs.TabPane>
<Tabs.TabPane tab="Python3" key="python3">
{panelContent()}
</Tabs.TabPane>
<Tabs.TabPane tab="Linux" key="linux">
{panelContent()}
</Tabs.TabPane>
</Tabs>
items={[
{
key: 'nodejs',
label: 'NodeJs',
children: panelContent(),
},
{
key: 'python3',
label: 'Python3',
children: panelContent(),
},
{
key: 'linux',
label: 'Linux',
children: panelContent(),
},
]}
/>
<DependenceModal
visible={isModalVisible}
handleCancel={handleCancel}
+11 -6
View File
@@ -48,9 +48,12 @@ const DependenceLogModal = ({
setLoading(true);
request
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => {
if (localStorage.getItem('logDependence') === String(dependence.id)) {
const log = (data.data.log || []).join('') as string;
.then(({ code, data }) => {
if (
code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id)
) {
const log = (data.log || []).join('') as string;
setValue(log);
setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败'));
@@ -67,8 +70,10 @@ const DependenceLogModal = ({
.delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence.id],
})
.then((data: any) => {
cancel(true);
.then(({ code, data }) => {
if (code === 200) {
cancel(true);
}
})
.finally(() => {
setRemoveLoading(false);
@@ -112,7 +117,7 @@ const DependenceLogModal = ({
return (
<Modal
title={titleElement()}
visible={visible}
open={visible}
centered
className="log-modal"
bodyStyle={{
+7 -5
View File
@@ -52,11 +52,11 @@ const DependenceModal = ({
data: payload,
},
);
if (code !== 200) {
message.error(data);
if (code === 200) {
handleCancel(data);
}
setLoading(false);
handleCancel(data);
} catch (error) {
setLoading(false);
}
@@ -69,7 +69,7 @@ const DependenceModal = ({
return (
<Modal
title={dependence ? '编辑依赖' : '新建依赖'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
@@ -99,7 +99,9 @@ const DependenceModal = ({
>
<Select>
{config.dependenceTypes.map((x, i) => (
<Option value={i}>{x}</Option>
<Option key={i} value={i}>
{x}
</Option>
))}
</Select>
</Form.Item>
+4
View File
@@ -17,4 +17,8 @@
.ant-form-item {
margin-bottom: 8px;
}
+ section {
height: calc(100% - 40px) !important;
}
}
+32 -13
View File
@@ -6,10 +6,13 @@ import { request } from '@/utils/http';
import './index.less';
import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Option } = Select;
const Diff = ({ headerStyle, isPhone, theme }: any) => {
const Diff = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [origin, setOrigin] = useState('config.sample.sh');
const [current, setCurrent] = useState('config.sh');
const [originValue, setOriginValue] = useState('');
@@ -19,15 +22,23 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
const editorRef = useRef<any>(null);
const getConfig = () => {
request.get(`${config.apiPrefix}configs/${current}`).then((data) => {
setCurrentValue(data.data);
});
request
.get(`${config.apiPrefix}configs/${current}`)
.then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
}
});
};
const getSample = () => {
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => {
setOriginValue(data.data);
});
request
.get(`${config.apiPrefix}configs/${origin}`)
.then(({ code, data }) => {
if (code === 200) {
setOriginValue(data);
}
});
};
const updateConfig = () => {
@@ -39,8 +50,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: current },
})
.then((data: any) => {
message.success(data.message);
.then(({ code, data }) => {
if (code === 200) {
message.success('保存成功');
}
});
};
@@ -48,8 +61,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.then((data: any) => {
setFiles(data.data);
.then(({ code, data }) => {
if (code === 200) {
setFiles(data);
}
})
.finally(() => setLoading(false));
};
@@ -95,7 +110,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="源文件">
<Select value={origin} onChange={originFileChange}>
{files.map((x) => (
<Option value={x.value}>{x.title}</Option>
<Option key={x.value} value={x.value}>
{x.title}
</Option>
))}
</Select>
</Form.Item>
@@ -104,7 +121,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="当前文件">
<Select value={current} onChange={currentFileChange}>
{files.map((x) => (
<Option value={x.value}>{x.title}</Option>
<Option key={x.value} value={x.value}>
{x.title}
</Option>
))}
</Select>
</Form.Item>
+16 -13
View File
@@ -17,19 +17,22 @@ const EditNameModal = ({
const handleOk = async (values: any) => {
setLoading(true);
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: {
ids,
name: values.name,
},
});
if (code === 200) {
message.success('更新环境变量名称成功');
} else {
message.error(data);
try {
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: {
ids,
name: values.name,
},
});
if (code === 200) {
message.success('更新环境变量名称成功');
handleCancel();
}
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
handleCancel();
};
useEffect(() => {
@@ -39,7 +42,7 @@ const EditNameModal = ({
return (
<Modal
title="修改环境变量名称"
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+63 -26
View File
@@ -9,6 +9,8 @@ import {
Typography,
Tooltip,
Input,
UploadProps,
Upload,
} from 'antd';
import {
EditOutlined,
@@ -16,6 +18,7 @@ import {
SyncOutlined,
CheckCircleOutlined,
StopOutlined,
UploadOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -26,6 +29,8 @@ import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { exportJson, getTableScroll } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -96,7 +101,8 @@ const DragableBodyRow = ({
);
};
const Env = ({ headerStyle, isPhone, theme }: any) => {
const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
{
title: '序号',
@@ -247,13 +253,17 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const getEnvs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
})
.finally(() => setLoading(false));
};
@@ -281,8 +291,8 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success(
`${record.status === Status. ? '启用' : '禁用'}成功`,
);
@@ -294,8 +304,6 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
status: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
@@ -330,14 +338,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
message.error(data);
}
});
},
@@ -387,14 +393,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]);
} else {
message.error(data);
}
});
},
@@ -423,13 +427,11 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getEnvs();
} else {
message.error(data);
}
});
},
@@ -448,11 +450,9 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
getEnvs();
} else {
message.error(data);
}
});
},
@@ -477,15 +477,42 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
setSearchText(value.trim());
};
const uploadProps: UploadProps = {
accept: 'application/json',
beforeUpload: async (file) => {
const formData = new FormData();
formData.append('env', file);
setImportLoading(true);
try {
const { code, data } = await request.post(
`${config.apiPrefix}envs/upload`,
{
data: formData,
},
);
if (code === 200) {
message.success(`成功上传${data.length}个环境变量`);
getEnvs();
}
setImportLoading(false);
} catch (error: any) {
setImportLoading(false);
}
return false;
},
fileList: [],
};
useEffect(() => {
getEnvs();
}, [searchText]);
useEffect(() => {
setTimeout(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
}
}, [tableRef.current]);
return (
<PageContainer
@@ -499,6 +526,15 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
loading={loading}
onSearch={onSearch}
/>,
<Upload {...uploadProps}>
<Button
type="primary"
icon={<UploadOutlined />}
loading={importLoading}
>
</Button>
</Upload>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</Button>,
@@ -552,6 +588,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
)}
<DndProvider backend={HTML5Backend}>
<Table
ref={tableRef}
columns={columns}
rowSelection={rowSelection}
pagination={false}
+3 -4
View File
@@ -40,13 +40,12 @@ const EnvModal = ({
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功');
} else {
message.error(data);
handleCancel(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -59,7 +58,7 @@ const EnvModal = ({
return (
<Modal
title={env ? '编辑变量' : '新建变量'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+8 -4
View File
@@ -3,11 +3,13 @@ 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 { history, useOutletContext } from '@umijs/max';
import Ansi from 'ansi-to-react';
import './index.less';
import { SharedContext } from '@/layouts';
const Error = ({ user, theme }: any) => {
const Error = () => {
const { user, theme } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志');
@@ -15,8 +17,10 @@ const Error = ({ user, theme }: any) => {
setLoading(true);
request
.get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => {
setData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
+9 -10
View File
@@ -10,7 +10,7 @@ import {
Select,
} from 'antd';
import config from '@/utils/config';
import { history } from 'umi';
import { history } from '@umijs/max';
import styles from './index.less';
import { request } from '@/utils/http';
@@ -41,11 +41,9 @@ const Initialization = () => {
password: values.password,
},
})
.then((data) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
next();
} else {
message.error(data.message);
}
})
.finally(() => setLoading(false));
@@ -59,11 +57,9 @@ const Initialization = () => {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
next();
} else {
message.error(_data.message);
}
})
.finally(() => setLoading(false));
@@ -116,12 +112,15 @@ const Initialization = () => {
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option value={x.value}>{x.label}</Option>
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden;
position: relative;
background-color: @component-background;
height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px);
height: 100%;
display: flex;
flex-direction: column;
}
+155 -61
View File
@@ -1,5 +1,15 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input, Empty } from 'antd';
import {
TreeSelect,
Tree,
Input,
Empty,
Button,
message,
Modal,
Tooltip,
Typography,
} from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react';
@@ -7,53 +17,35 @@ import { request } from '@/utils/http';
import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import { DeleteOutlined } from '@ant-design/icons';
import { depthFirstSearch } from '@/utils';
import { debounce, uniq } from 'lodash';
import useFilterTreeData from '@/hooks/useFilterTreeData';
function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const { Text } = Typography;
const Log = ({ headerStyle, isPhone, theme }: any) => {
const [title, setTitle] = useState('请选择日志文件');
const Log = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<any>();
const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>();
const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [currentNode, setCurrentNode] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const getLogs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}logs`)
.then((data) => {
setData(data.data);
setFilterData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
@@ -61,18 +53,27 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const getLog = (node: any) => {
request
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) {
return;
}
if (node.type === 'directory') {
setValue('请选择日志文件');
return;
}
setValue('加载中...');
setSelect(value);
setTitle(node.key);
getLog(node);
};
@@ -83,41 +84,130 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback(
(e) => {
const keyword = e.target.value;
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree);
setExpandedKeys(expandedKeys);
debounceSearch(keyword);
},
[data, setFilterData],
[data],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
}, 300),
[data],
);
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
);
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const deleteFile = () => {
Modal.confirm({
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>
{currentNode.type === 'directory' ? '夹下所以日志' : ''}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}logs`, {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
type: currentNode.type,
},
})
.then(({ code }) => {
if (code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
newData = depthFirstSearch(
newData,
(c) => c.key === currentNode.key,
);
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
if (index !== -1) {
newData.splice(index, 1);
}
}
setData(newData);
initState();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const initState = () => {
setSelect('');
setCurrentNode(null);
setValue('请选择脚本文件');
};
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
useEffect(() => {
getLogs();
if (treeDom && treeDom.current) {
}, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight);
}
}, []);
}, [treeDom.current, data]);
return (
<PageContainer
className="ql-container-wrapper log-wrapper"
title={title}
title={select}
loading={loading}
extra={
isPhone && [
<TreeSelect
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择日志"
fieldNames={{ value: 'key', label: 'title' }}
showSearch
onSelect={onSelect}
/>,
]
isPhone
? [
<TreeSelect
treeExpandAction="click"
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择日志"
fieldNames={{ value: 'key' }}
treeNodeFilterProp="title"
showSearch
allowClear
onSelect={onSelect}
/>,
]
: [
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
]
}
header={{
style: headerStyle,
@@ -125,6 +215,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
>
<div className={`${styles['log-container']} log-container`}>
{!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}>
{data.length > 0 ? (
@@ -137,6 +228,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
expandAction="click"
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
@@ -144,6 +236,8 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
selectedKeys={[select]}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
expandedKeys={expandedKeys}
onExpand={onExpand}
></Tree>
</div>
</>
+20 -26
View File
@@ -9,16 +9,18 @@ import {
Statistic,
} from 'antd';
import config from '@/utils/config';
import { history, Link } from 'umi';
import { history, useOutletContext } from '@umijs/max';
import styles from './index.less';
import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons';
import { SharedContext } from '@/layouts';
const FormItem = Form.Item;
const { Countdown } = Statistic;
const Login = ({ reloadUser }: any) => {
const Login = () => {
const { reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false);
const [waitTime, setWaitTime] = useState<any>();
const { theme } = useTheme();
@@ -38,15 +40,7 @@ const Login = ({ reloadUser }: any) => {
},
})
.then((data) => {
if (data.code === 420) {
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
} else {
checkResponse(data);
}
checkResponse(data, values);
setLoading(false);
})
.catch(function (error) {
@@ -62,11 +56,7 @@ const Login = ({ reloadUser }: any) => {
data: { ...loginInfo, code: values.code },
})
.then((data: any) => {
if (data.code === 430) {
message.error(data.message);
} else {
checkResponse(data);
}
checkResponse(data);
setVerifying(false);
})
.catch((error: any) => {
@@ -75,8 +65,11 @@ const Login = ({ reloadUser }: any) => {
});
};
const checkResponse = (data: any) => {
if (data.code === 200) {
const checkResponse = (
{ code, data, message: _message }: any,
values?: any,
) => {
if (code === 200) {
const {
token,
lastip,
@@ -84,7 +77,7 @@ const Login = ({ reloadUser }: any) => {
lastlogon,
retries = 0,
platform,
} = data.data;
} = data;
localStorage.setItem(config.authKey, token);
notification.success({
message: '登录成功!',
@@ -103,13 +96,14 @@ const Login = ({ reloadUser }: any) => {
});
reloadUser(true);
history.push('/crontab');
} else if (data.code === 100) {
message.warn(data.message);
} else if (data.code === 410) {
message.error(data.message);
setWaitTime(data.data);
} else {
message.error(data.message);
} else if (code === 410) {
setWaitTime(data);
} else if (code === 420) {
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
}
};
+25 -8
View File
@@ -7,6 +7,7 @@ import Editor from '@monaco-editor/react';
import SaveModal from './saveModal';
import SettingModal from './setting';
import { useTheme } from '@/utils/hooks';
import { logEnded } from '@/utils';
const { Option } = Select;
const LangMap: any = {
@@ -57,6 +58,11 @@ const EditModal = ({
if (node.key === selectedKey || !value) {
return;
}
if (node.type === 'directory') {
return;
}
const newMode = LangMap[value.slice(-3)] || '';
setCNode(node);
setLanguage(newMode);
@@ -67,8 +73,10 @@ const EditModal = ({
const getDetail = (node: any) => {
request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
@@ -83,8 +91,10 @@ const EditModal = ({
content,
},
})
.then((data) => {
setIsRunning(true);
.then(({ code, data }) => {
if (code === 200) {
setIsRunning(true);
}
});
};
@@ -101,8 +111,10 @@ const EditModal = ({
content,
},
})
.then((data) => {
setIsRunning(false);
.then(({ code, data }) => {
if (code === 200) {
setIsRunning(false);
}
});
};
@@ -117,7 +129,7 @@ const EditModal = ({
return;
}
if (_message.includes('执行结束')) {
if (logEnded(_message)) {
setTimeout(() => {
setIsRunning(false);
}, 300);
@@ -130,10 +142,13 @@ const EditModal = ({
}, [socketMessage]);
useEffect(() => {
setLog('');
if (currentNode) {
setCNode(currentNode);
setValue(content as string);
setSelectedKey(currentNode.key);
const newMode = LangMap[currentNode.title.slice(-3)] || '';
setLanguage(newMode);
}
}, [content, currentNode]);
@@ -144,6 +159,7 @@ const EditModal = ({
title={
<>
<TreeSelect
treeExpandAction="click"
style={{ marginRight: 8, width: 150 }}
value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
@@ -214,8 +230,9 @@ const EditModal = ({
width={'100%'}
headerStyle={{ padding: '11px 24px' }}
onClose={cancel}
visible={visible}
open={visible}
>
{/* @ts-ignore */}
<SplitPane
split="vertical"
minSize={200}
+31 -14
View File
@@ -32,32 +32,31 @@ const EditScriptNameModal = ({
const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
const [file, setFile] = useState<File>();
const [type, setType] = useState<'blank' | 'upload'>('blank');
const [type, setType] = useState<'blank' | 'upload' | 'directory'>('blank');
const handleOk = async (values: any) => {
setLoading(true);
values.path = values.path || '';
const { path = '', filename: inputFilename, directory = '' } = values;
const formData = new FormData();
formData.append('file', file as any);
formData.append('filename', values.filename);
formData.append('path', values.path);
formData.append('filename', inputFilename);
formData.append('path', path);
formData.append('content', '');
formData.append('directory', directory);
request
.post(`${config.apiPrefix}scripts`, {
data: formData,
})
.then(({ code, data }) => {
if (code === 200) {
message.success('新建文件成功');
const key = values.path ? `${values.path}/` : '';
const filename = file ? file.name : values.filename;
message.success(directory ? '新建文件夹成功' : '新建文件成功');
const key = path ? `${path}/` : '';
const filename = file ? file.name : inputFilename;
handleCancel({
filename,
path: values.path,
path,
key: `${key}${filename}`,
});
} else {
message.error(data);
}
setLoading(false);
})
@@ -99,8 +98,8 @@ const EditScriptNameModal = ({
return (
<Modal
title="新建脚本"
visible={visible}
title="新建"
open={visible}
forceRender
centered
maskClosable={false}
@@ -126,18 +125,36 @@ const EditScriptNameModal = ({
>
<Radio.Group onChange={typeChange}>
<Radio value="blank"></Radio>
<Radio value="upload"></Radio>
<Radio value="upload"></Radio>
<Radio value="directory"></Radio>
</Radio.Group>
</Form.Item>
{type === 'blank' && (
<Form.Item
name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
rules={[
{ required: true, message: '请输入文件名' },
{
validator: (_, value) =>
value.includes('/')
? Promise.reject(new Error('文件名不能包含斜杠'))
: Promise.resolve(),
},
]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
)}
{type === 'directory' && (
<Form.Item
name="directory"
label="文件夹名"
rules={[{ required: true, message: '请输入文件夹名' }]}
>
<Input placeholder="请输入文件夹名" />
</Form.Item>
)}
<Form.Item label="父目录" name="path">
<TreeSelect
allowClear
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden;
position: relative;
background-color: @component-background;
height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px);
height: 100%;
display: flex;
flex-direction: column;
}
+97 -109
View File
@@ -33,38 +33,15 @@ import {
} from '@ant-design/icons';
import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce';
import { history } from 'umi';
import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import { uniq } from 'lodash';
const { Text } = Typography;
function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const LangMap: any = {
'.py': 'python',
'.js': 'javascript',
@@ -72,12 +49,12 @@ const LangMap: any = {
'.ts': 'typescript',
};
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const [title, setTitle] = useState('请选择脚本文件');
const Script = () => {
const { headerStyle, isPhone, theme, socketMessage } =
useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>();
const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState('');
const [height, setHeight] = useState<number>();
@@ -94,24 +71,31 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
setLoading(true);
request
.get(`${config.apiPrefix}scripts`)
.then((data) => {
setData(data.data);
setFilterData(data.data);
initGetScript();
.then(({ code, data }) => {
if (code === 200) {
setData(data);
initGetScript();
}
})
.finally(() => setLoading(false));
};
const getDetail = (node: any) => {
request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
.get(
`${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
node.parent || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
const initGetScript = () => {
const { p, s } = history.location.query as any;
const { p, s } = parse(history.location.search);
if (s) {
const vkey = `${p}/${s}`;
const obj = {
@@ -127,22 +111,24 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
};
const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) {
return;
}
setValue('加载中...');
if (node.type === 'directory') {
setValue('请选择脚本文件');
return;
}
const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(node.key);
setTitle(node.key);
setCurrentNode(node);
setValue('加载中...');
getDetail(node);
};
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback(
(keys: Key[], e: any) => {
const content = editorRef.current
@@ -173,22 +159,30 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const keyword = e.target.value;
debounceSearch(keyword);
},
[data, setFilterData],
[data],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300),
[data, setFilterData],
[data],
);
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
);
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const editFile = () => {
setTimeout(() => {
setIsEditing(true);
@@ -226,13 +220,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success(`保存成功`);
setValue(content);
setIsEditing(false);
} else {
message.error(_data);
}
resolve(null);
})
@@ -250,10 +242,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>{' '}
</Text>
{currentNode.type === 'directory' ? '夹及其子文件' : ''}
</>
),
@@ -263,24 +256,18 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
type: currentNode.type,
},
})
.then((_data: any) => {
if (_data.code === 200) {
.then(({ code }) => {
if (code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
const parentNodeIndex = newData.findIndex(
(x) => x.key === currentNode.parent,
newData = depthFirstSearch(
newData,
(c) => c.key === currentNode.key,
);
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
if (index !== -1 && parentNodeIndex !== -1) {
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
@@ -290,8 +277,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
}
}
setData(newData);
} else {
message.error(_data);
initState();
}
});
},
@@ -313,20 +299,17 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
},
) => {
if (filename) {
const newData = [...data];
let newData = [...data];
const _file = { title: filename, key, parent: path };
if (path) {
// TODO: 更新左侧树数据
const parentNodeIndex = newData.findIndex((x) => x.key === path);
if (parentNodeIndex !== -1) {
const parentNode = newData[parentNodeIndex];
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.unshift(_file);
} else {
parentNode.children = [_file];
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
newData = depthFirstSearch(newData, (c) => c.key === path, _file);
const keys = path.split('/');
const sKeys: string[] = [];
keys.reduce((p, c) => {
sKeys.push(p);
return `${p}/${c}`;
});
setExpandedKeys([...expandedKeys, ...sKeys, path]);
} else {
newData.unshift(_file);
}
@@ -344,34 +327,35 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
filename: currentNode.title,
},
})
.then((_data: any) => {
const blob = new Blob([_data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentNode.title;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
.then(({ code, data }) => {
if (code === 200) {
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentNode.title;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
}
});
};
useEffect(() => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree);
const initState = () => {
setSelect('');
setCurrentNode(null);
setTitle('请选择脚本文件');
setValue('请选择脚本文件');
}, [data]);
};
useEffect(() => {
getScripts();
if (treeDom && treeDom.current) {
}, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight);
}
}, []);
}, [treeDom.current, data]);
const action = (key: string | number) => {
switch (key) {
@@ -440,19 +424,22 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
return (
<PageContainer
className="ql-container-wrapper log-wrapper"
title={title}
title={select}
loading={loading}
extra={
isPhone
? [
<TreeSelect
treeExpandAction="click"
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择脚本"
fieldNames={{ value: 'key', label: 'title' }}
fieldNames={{ value: 'key' }}
treeNodeFilterProp="title"
showSearch
allowClear
onSelect={onSelect}
/>,
<Dropdown overlay={menu} trigger={['click']}>
@@ -508,6 +495,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
>
<div className={`${styles['log-container']} log-container`}>
{!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}>
{data.length > 0 ? (
@@ -520,6 +508,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
expandAction="click"
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
@@ -556,7 +545,6 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
readOnly: !isEditing,
fontSize: 12,
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onMount={(editor) => {
+1 -3
View File
@@ -26,8 +26,6 @@ const SaveModal = ({
if (code === 200) {
message.success('保存文件成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
});
@@ -41,7 +39,7 @@ const SaveModal = ({
return (
<Modal
title="保存文件"
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
+1 -3
View File
@@ -26,8 +26,6 @@ const SettingModal = ({
if (code === 200) {
message.success('保存文件成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
});
@@ -41,7 +39,7 @@ const SettingModal = ({
return (
<Modal
title="运行设置"
visible={visible}
open={visible}
forceRender
centered
onCancel={() => handleCancel()}
+30 -12
View File
@@ -22,16 +22,19 @@ const AppModal = ({
if (app) {
payload.id = app.id;
}
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload,
});
if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功');
} else {
message.error(data);
try {
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload,
});
if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功');
handleCancel(data);
}
setLoading(false);
} catch (error) {
setLoading(false);
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
@@ -41,7 +44,7 @@ const AppModal = ({
return (
<Modal
title={app ? '编辑应用' : '新建应用'}
visible={visible}
open={visible}
forceRender
centered
maskClosable={false}
@@ -64,7 +67,18 @@ const AppModal = ({
name="form_app_modal"
initialValues={app}
>
<Form.Item name="name" label="名称">
<Form.Item
name="name"
label="名称"
rules={[
{
validator: (_, value) =>
['system'].includes(value)
? Promise.reject(new Error('名称不能为保留关键字'))
: Promise.resolve(),
},
]}
>
<Input placeholder="请输入应用名称" />
</Form.Item>
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
@@ -75,7 +89,11 @@ const AppModal = ({
style={{ width: '100%' }}
>
{config.scopes.map((x) => {
return <Select.Option value={x.value}>{x.name}</Select.Option>;
return (
<Select.Option key={x.value} value={x.value}>
{x.name}
</Select.Option>
);
})}
</Select>
</Form.Item>
+15 -9
View File
@@ -17,17 +17,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
message.loading('检查更新中...', 0);
request
.put(`${config.apiPrefix}system/update-check`)
.then((_data: any) => {
.then(({ code, data }) => {
message.destroy();
const { code, data } = _data;
if (code === 200) {
if (data.hasNewVersion) {
showConfirmUpdateModal(data);
} else {
showForceUpdateModal();
}
} else {
message.error(data);
}
})
.catch((error: any) => {
@@ -105,15 +102,15 @@ const CheckUpdate = ({ socketMessage }: any) => {
};
const showUpdatingModal = () => {
setValue('');
modalRef.current = Modal.info({
width: 600,
maskClosable: false,
closable: false,
okButtonProps: { disabled: true },
title: '更新日志',
title: '更新中...',
centered: true,
content: (
<div style={{ height: '60vh', overflowY: 'auto' }}>
<pre
style={{
wordBreak: 'break-all',
@@ -124,7 +121,6 @@ const CheckUpdate = ({ socketMessage }: any) => {
>
{value}
</pre>
</div>
),
});
};
@@ -140,9 +136,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
}
const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes('失败,请检查');
modalRef.current.update({
maskClosable: updateFailed,
closable: updateFailed,
okButtonProps: { disabled: !updateFailed },
content: (
<div style={{ height: '60vh', overflowY: 'auto' }}>
<>
<pre
style={{
wordBreak: 'break-all',
@@ -154,9 +155,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
{newMessage}
</pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</div>
</>
),
});
if (updateFailed && !value.includes('失败,请检查')) {
message.error('更新失败,请检查网络及日志或稍后再试');
}
setValue(newMessage);
document.getElementById('log-identifier') &&
+110 -92
View File
@@ -29,6 +29,8 @@ import LoginLog from './loginLog';
import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate';
import About from './about';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography;
const optionsWithDisabled = [
@@ -37,14 +39,9 @@ const optionsWithDisabled = [
{ label: '跟随系统', value: 'auto' },
];
const Setting = ({
headerStyle,
isPhone,
user,
reloadUser,
reloadTheme,
socketMessage,
}: any) => {
const Setting = () => {
const { headerStyle, isPhone, user, reloadUser, reloadTheme, socketMessage } =
useOutletContext<SharedContext>();
const columns = [
{
title: '名称',
@@ -147,8 +144,10 @@ const Setting = ({
setLoading(true);
request
.get(`${config.apiPrefix}apps`)
.then((data: any) => {
setDataSource(data.data);
.then(({ code, data }) => {
if (code === 200) {
setDataSource(data);
}
})
.finally(() => setLoading(false));
};
@@ -178,14 +177,12 @@ const Setting = ({
onOk() {
request
.delete(`${config.apiPrefix}apps`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...dataSource];
result.splice(index, 1);
setDataSource(result);
} else {
message.error(data);
}
});
},
@@ -212,12 +209,10 @@ const Setting = ({
onOk() {
request
.put(`${config.apiPrefix}apps/${record.id}/reset-secret`)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('重置成功');
handleApp(data.data);
} else {
message.error(data);
handleApp(data);
}
});
},
@@ -250,8 +245,10 @@ const Setting = ({
const getLoginLog = () => {
request
.get(`${config.apiPrefix}user/login-log`)
.then((data: any) => {
setLoginLogData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setLoginLogData(data);
}
})
.catch((error: any) => {
console.log(error);
@@ -274,8 +271,10 @@ const Setting = ({
const getNotification = () => {
request
.get(`${config.apiPrefix}user/notification`)
.then((data: any) => {
setNotificationInfo(data.data);
.then(({ code, data }) => {
if (code === 200) {
setNotificationInfo(data);
}
})
.catch((error: any) => {
console.log(error);
@@ -285,9 +284,9 @@ const Setting = ({
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then((data: any) => {
if (data.data.info) {
const { frequency } = data.data.info;
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
@@ -302,8 +301,10 @@ const Setting = ({
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then((data: any) => {
message.success('更新成功');
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
@@ -313,7 +314,7 @@ const Setting = ({
return (
<PageContainer
className="ql-container-wrapper"
className="ql-container-wrapper ql-container-wrapper-has-tab"
title="系统设置"
header={{
style: headerStyle,
@@ -333,70 +334,87 @@ const Setting = ({
size="small"
tabPosition="top"
onChange={tabChange}
>
<Tabs.TabPane tab="安全设置" key="security">
<SecuritySettings user={user} userChange={reloadUser} />
</Tabs.TabPane>
<Tabs.TabPane tab="应用设置" key="app">
<Table
columns={columns}
pagination={false}
dataSource={dataSource}
rowKey="id"
size="middle"
scroll={{ x: 768 }}
loading={loading}
/>
</Tabs.TabPane>
<Tabs.TabPane tab="通知设置" key="notification">
<NotificationSetting data={notificationInfo} />
</Tabs.TabPane>
<Tabs.TabPane tab="登录日志" key="login">
<LoginLog data={loginLogData} />
</Tabs.TabPane>
<Tabs.TabPane tab="其他设置" key="other">
<Form layout="vertical" form={form}>
<Form.Item
label="主题设置"
name="theme"
initialValue={defaultTheme}
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
items={[
{
key: 'security',
label: '安全设置',
children: <SecuritySettings user={user} userChange={reloadUser} />,
},
{
key: 'app',
label: '应用设置',
children: (
<Table
columns={columns}
pagination={false}
dataSource={dataSource}
rowKey="id"
size="middle"
scroll={{ x: 768 }}
loading={loading}
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
</Tabs.TabPane>
<Tabs.TabPane tab="关于" key="about">
<About />
</Tabs.TabPane>
</Tabs>
),
},
{
key: 'notification',
label: '通知设置',
children: <NotificationSetting data={notificationInfo} />,
},
{
key: 'login',
label: '登录日志',
children: <LoginLog data={loginLogData} />,
},
{
key: 'other',
label: '其他设置',
children: (
<Form layout="vertical" form={form}>
<Form.Item
label="主题设置"
name="theme"
initialValue={defaultTheme}
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
),
},
{
key: 'about',
label: '关于',
children: <About />,
},
]}
></Tabs>
<AppModal
visible={isModalVisible}
handleCancel={handleCancel}
+23 -7
View File
@@ -12,20 +12,20 @@ const NotificationSetting = ({ data }: any) => {
const [form] = Form.useForm();
const handleOk = (values: any) => {
if (values.type == 'closed') {
const { type } = values;
if (type == 'closed') {
values.type = '';
}
request
.put(`${config.apiPrefix}user/notification`, {
data: {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success(values.type ? '通知发送成功' : '通知关闭成功');
} else {
message.error(_data.data);
}
})
.catch((error: any) => {
@@ -58,19 +58,35 @@ const NotificationSetting = ({ data }: any) => {
>
<Select onChange={notificationModeChange}>
{config.notificationModes.map((x) => (
<Option value={x.value}>{x.label}</Option>
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea autoSize={true} placeholder={`请输入${x.label}`} />
{x.items ? (
<Select placeholder={x.placeholder || `请选择${x.label}`}>
{x.items.map((y) => (
<Option key={y.value} value={y.value}>
{y.label || y.value}
</Option>
))}
</Select>
) : (
<Input.TextArea
autoSize={true}
placeholder={x.placeholder || `请输入${x.label}`}
/>
)}
</Form.Item>
))}
<Button type="primary" htmlType="submit">
+22 -16
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { history } from 'umi';
import { history } from '@umijs/max';
import QRCode from 'qrcode.react';
import { PageLoading } from '@ant-design/pro-layout';
import { UploadOutlined, UserOutlined } from '@ant-design/icons';
@@ -27,9 +27,11 @@ const SecuritySettings = ({ user, userChange }: any) => {
password: values.password,
},
})
.then((data: any) => {
localStorage.removeItem(config.authKey);
history.push('/login');
.then(({ code, data }) => {
if (code === 200) {
localStorage.removeItem(config.authKey);
history.push('/login');
}
})
.catch((error: any) => {
console.log(error);
@@ -48,8 +50,8 @@ const SecuritySettings = ({ user, userChange }: any) => {
const deactiveTowFactor = () => {
request
.put(`${config.apiPrefix}user/two-factor/deactive`)
.then((data: any) => {
if (data.data) {
.then(({ code, data }) => {
if (code === 200 && data) {
setTwoFactorActivated(false);
userChange();
}
@@ -63,14 +65,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}user/two-factor/active`, { data: { code } })
.then((data: any) => {
if (data.data) {
message.success('激活成功');
setTwoFactoring(false);
setTwoFactorActivated(true);
userChange();
} else {
message.success('验证失败');
.then(({ code, data }) => {
if (code === 200) {
if (data) {
message.success('激活成功');
setTwoFactoring(false);
setTwoFactorActivated(true);
userChange();
} else {
message.success('验证失败');
}
}
})
.catch((error: any) => {
@@ -82,8 +86,10 @@ const SecuritySettings = ({ user, userChange }: any) => {
const getTwoFactorInfo = () => {
request
.get(`${config.apiPrefix}user/two-factor/init`)
.then((data: any) => {
setTwoFactorInfo(data.data);
.then(({ code, data }) => {
if (code === 200) {
setTwoFactorInfo(data);
}
})
.catch((error: any) => {
console.log(error);
+28 -25
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Button,
message,
@@ -30,9 +30,10 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import { history, useOutletContext } from '@umijs/max';
import './index.less';
import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -57,7 +58,10 @@ export enum SubscriptionType {
'file' = '单文件',
}
const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
const Subscription = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [
{
title: '名称',
@@ -243,6 +247,7 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>();
const tableRef = useRef<any>();
const runSubscription = (record: any, index: number) => {
Modal.confirm({
@@ -259,8 +264,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() {
request
.put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -270,8 +275,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -296,8 +299,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() {
request
.put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -308,8 +311,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -323,9 +324,11 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
setLoading(true);
request
.get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)
.then((data: any) => {
setValue(data.data);
setCurrentPage(1);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
setCurrentPage(1);
}
})
.finally(() => setLoading(false));
};
@@ -355,8 +358,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() {
request
.delete(`${config.apiPrefix}subscriptions`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -364,8 +367,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
result.splice(i, 1);
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -398,8 +399,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newStatus = record.is_disabled === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -410,8 +411,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -541,11 +540,14 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
useEffect(() => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
}, []);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll());
}
}, [tableRef.current]);
return (
<PageContainer
className="ql-container-wrapper subscriptiontab-wrapper"
@@ -570,6 +572,7 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}}
>
<Table
ref={tableRef}
columns={columns}
pagination={{
current: currentPage,
+7 -5
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
const SubscriptionLogModal = ({
subscription,
@@ -36,14 +37,15 @@ const SubscriptionLogModal = ({
? logUrl
: `${config.apiPrefix}subscriptions/${subscription.id}/log`,
)
.then((data: any) => {
.then(({ code, data }) => {
if (
code === 200 &&
localStorage.getItem('logSubscription') === String(subscription.id)
) {
const log = data.data as string;
const log = data as string;
setValue(log || '暂无日志');
setExecuting(log && !log.includes('执行结束'));
if (log && !log.includes('执行结束')) {
setExecuting(log && !logEnded(log));
if (log && !logEnded(log)) {
setTimeout(() => {
getCronLog();
}, 2000);
@@ -93,7 +95,7 @@ const SubscriptionLogModal = ({
return (
<Modal
title={titleElement()}
visible={visible}
open={visible}
centered
className="log-modal"
bodyStyle={{
+16 -34
View File
@@ -39,11 +39,9 @@ const SubscriptionModal = ({
);
if (code === 200) {
message.success(subscription ? '更新订阅成功' : '新建订阅成功');
} else {
message.error(data);
handleCancel(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
@@ -190,8 +188,8 @@ const SubscriptionModal = ({
};
const onPaste = useCallback((e: any) => {
const text = e.clipboardData.getData('text');
if (!subscription && text.includes('ql ')) {
const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) {
const [
,
type,
@@ -225,6 +223,13 @@ const SubscriptionModal = ({
}
}, []);
const onNamePaste = useCallback((e) => {
const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) {
e.preventDefault();
}
}, []);
useEffect(() => {
if (visible) {
window.addEventListener('paste', onPaste);
@@ -243,36 +248,10 @@ const SubscriptionModal = ({
}
}, [subscription, visible]);
const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari =
navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
return (
<Modal
title={
subscription ? (
'编辑订阅'
) : (
<span>
<small
style={{
color: '#999',
fontWeight: 400,
fontSize: isFirefox ? 9 : 12,
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
repo/raw命令
</small>
</span>
)
}
visible={visible}
title={subscription ? '编辑订阅' : '新建订阅'}
open={visible}
forceRender
centered
maskClosable={false}
@@ -291,7 +270,10 @@ const SubscriptionModal = ({
>
<Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称">
<Input placeholder="请输入订阅名" />
<Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入"
onPaste={onNamePaste}
/>
</Form.Item>
<Form.Item
name="type"
+79 -4
View File
@@ -56,7 +56,7 @@ export default {
value: 'scripts',
},
{
name: '任务日志',
name: '日志管理',
value: 'logs',
},
{
@@ -74,7 +74,7 @@ export default {
subscriptions: '订阅管理',
configs: '配置文件',
scripts: '脚本管理',
logs: '任务日志',
logs: '日志管理',
dependencies: '依赖管理',
system: '系统信息',
},
@@ -88,9 +88,13 @@ export default {
{ value: 'dingtalkBot', label: '钉钉机器人' },
{ value: 'weWorkBot', label: '企业微信机器人' },
{ value: 'weWorkApp', label: '企业微信应用' },
{ value: 'aibotk', label: '智能微秘书' },
{ value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' },
{ value: 'chat', label: '群辉chat' },
{ value: 'email', label: '邮箱' },
{ value: 'lark', label: '飞书机器人' },
{ value: 'webhook', label: '自定义通知' },
{ value: 'closed', label: '已关闭' },
],
notificationModeMap: {
@@ -103,6 +107,14 @@ export default {
{ label: 'gotifyToken', tip: 'gotify的消息应用token码', required: true },
{ label: 'gotifyPriority', tip: '推送消息的优先级' },
],
chat: [
{
label: 'chatUrl',
tip: 'chat的url地址',
required: true,
},
{ label: 'chatToken', tip: 'chat的token码', required: true },
],
goCqHttpBot: [
{
label: 'goCqHttpBotUrl',
@@ -186,6 +198,28 @@ export default {
required: true,
},
],
aibotk: [
{
label: 'aibotkKey',
tip: '密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql',
required: true,
},
{
label: 'aibotkType',
tip: '发送的目标,群组或者好友',
required: true,
placeholder: '请输入要发送的目标',
items: [
{ value: 'room', label: '群聊' },
{ value: 'contact', label: '好友' },
],
},
{
label: 'aibotkName',
tip: '要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
required: true,
},
],
iGot: [
{
label: 'iGotPushKey',
@@ -196,7 +230,7 @@ export default {
pushPlus: [
{
label: 'pushPlusToken',
tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送',
tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
required: true,
},
{
@@ -204,6 +238,13 @@ export default {
tip: '一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
},
],
lark: [
{
label: 'larkKey',
tip: '飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973',
required: true,
},
],
email: [
{
label: 'emailService',
@@ -213,6 +254,40 @@ export default {
{ label: 'emailUser', tip: '邮箱地址', required: true },
{ label: 'emailPass', tip: '邮箱SMTP授权码', required: true },
],
webhook: [
{
label: 'webhookMethod',
tip: '请求方法',
required: true,
items: [{ value: 'GET' }, { value: 'POST' }, { value: 'PUT' }],
},
{
label: 'webhookContentType',
tip: '请求头Content-Type',
required: true,
items: [
{ value: 'application/json' },
{ value: 'multipart/form-data' },
{ value: 'application/x-www-form-urlencoded' },
],
},
{
label: 'webhookUrl',
tip: '请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置',
required: true,
placeholder: 'https://xxx.cn/api?content=$title\n',
},
{
label: 'webhookHeaders',
tip: '请求头格式Custom-Header1: Header1,多个换行分割',
placeholder: 'Custom-Header1: Header1\nCustom-Header2: Header2',
},
{
label: 'webhookBody',
tip: '请求体格式key1: value1,多个换行分割。url或者body中必须包含$title$content可选,对应api内容的位置',
placeholder: 'key1: $title\nkey2: $content',
},
],
},
documentTitleMap: {
'/login': '登录',
@@ -223,7 +298,7 @@ export default {
'/config': '配置文件',
'/script': '脚本管理',
'/diff': '对比工具',
'/log': '任务日志',
'/log': '日志管理',
'/setting': '系统设置',
'/error': '错误日志',
},
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+18 -2
View File
@@ -1,7 +1,7 @@
import { extend } from 'umi-request';
import { message } from 'antd';
import config from './config';
import { history } from 'umi';
import { history } from '@umijs/max';
message.config({
duration: 1.5,
@@ -54,7 +54,23 @@ _request.interceptors.request.use((url, options) => {
});
_request.interceptors.response.use(async (response) => {
const res = await response.clone();
const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error');
} else if (responseStatus === 401) {
if (history.location.pathname !== '/login') {
localStorage.removeItem(config.authKey);
history.push('/login');
}
} else {
const res = await response.clone().json();
if (res.code !== 200) {
const msg = res.message || res.data;
msg && message.error(msg);
}
return res;
}
return response;
});
+41 -2
View File
@@ -1,3 +1,5 @@
import { LOG_END_SYMBOL } from './const';
export default function browserType() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
const ua = navigator.userAgent.toLowerCase();
@@ -181,8 +183,8 @@ export function getTableScroll({
id,
}: { extraHeight?: number; id?: string } = {}) {
if (typeof extraHeight == 'undefined') {
// 47 + 40 + 10 + 24
extraHeight = 121;
// 47 + 40 + 12
extraHeight = 99;
}
let tHeader = null;
if (id) {
@@ -241,3 +243,40 @@ export function exportJson(name: string, data: string) {
createA.download = name;
automaticClick(createA);
}
export function depthFirstSearch<
T extends Record<string, any> & { children?: T[] },
>(children: T[], condition: (column: T) => boolean, item?: T) {
const c = [...children];
const keys = [];
(function find(cls: T[] | undefined) {
if (!cls) return;
for (let i = 0; i < cls?.length; i++) {
if (condition(cls[i])) {
if (!item) {
cls.splice(i, 1);
return;
}
if (cls[i].children) {
cls[i].children!.unshift(item);
} else {
cls[i].children = [item];
}
return;
}
if (cls[i].children) {
keys.push(cls[i].key);
find(cls[i].children);
}
}
})(c);
return c;
}
export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x));
}
+8 -6
View File
@@ -1,7 +1,9 @@
export const version = '2.13.9';
export const changeLogLink = 'https://t.me/jiao_long/324';
export const changeLog = `2.13.9 版本说明
1.
2. 使
3. favicon修改😀😀
export const version = '2.14.9';
export const changeLogLink = 'https://t.me/jiao_long/336';
export const changeLog = `2.14.9 版本说明
1. https://github.com/leochen-g
2. webhook通知body支持嵌套json
3. shell获取磁盘占用
4.
5. bug修复
`;
+4 -1
View File
@@ -1,12 +1,15 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["es2017", "esnext.asynciterable", "DOM"],
"lib": ["es2021", "esnext.asynciterable", "DOM"],
"typeRoots": [
"./node_modules/@types",
"./src/types",
"./node_modules/celebrate/lib/index.d.ts"
],
"paths": {
"@/*": ["./back/*"],
},
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,