mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-06 16:54:33 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04037f29e5 | |||
| 6b124bad89 | |||
| 54d99a4954 | |||
| 9947594366 | |||
| d7ff934336 | |||
| dc27856441 | |||
| bd28682769 | |||
| 2060e62da4 | |||
| 70eac1e747 | |||
| 6f5a5fe066 | |||
| 8d24949202 | |||
| f7b6237355 | |||
| daf6f94c51 | |||
| 687d16e21b | |||
| 70571a9d34 | |||
| 4da8d8fe1e | |||
| 952cdc0e3f | |||
| 2de189d189 | |||
| 3edc6e83d7 |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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! 🎉🎉🎉
|
||||
+2
-2
@@ -28,7 +28,7 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
sorts: Joi.array().optional(),
|
||||
sorts: Joi.array().optional().allow(null),
|
||||
filters: Joi.array().optional(),
|
||||
}),
|
||||
}),
|
||||
@@ -49,7 +49,7 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
id: Joi.number().required(),
|
||||
sorts: Joi.array().optional(),
|
||||
sorts: Joi.array().optional().allow(null),
|
||||
filters: Joi.array().optional(),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum NotificationMode {
|
||||
'serverChan' = 'serverChan',
|
||||
'pushDeer' = 'pushDeer',
|
||||
'bark' = 'bark',
|
||||
'chat' = 'chat',
|
||||
'telegramBot' = 'telegramBot',
|
||||
'dingtalkBot' = 'dingtalkBot',
|
||||
'weWorkBot' = 'weWorkBot',
|
||||
@@ -37,6 +38,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';
|
||||
@@ -86,6 +92,7 @@ export interface NotificationInfo
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
ChatNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
|
||||
+19
-7
@@ -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(
|
||||
(
|
||||
|
||||
+63
-34
@@ -115,18 +115,25 @@ export default class CronService {
|
||||
|
||||
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':
|
||||
query[Op.or] = [
|
||||
q[Op.or] = [
|
||||
{
|
||||
[property]: value,
|
||||
},
|
||||
@@ -136,7 +143,7 @@ export default class CronService {
|
||||
];
|
||||
break;
|
||||
case 'Nin':
|
||||
query[Op.and] = [
|
||||
q[Op.and] = [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: value,
|
||||
@@ -150,20 +157,25 @@ export default class CronService {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (operate) {
|
||||
query[property] = {
|
||||
[Op.or]: [
|
||||
if (operate && operate2) {
|
||||
q[property] = {
|
||||
[operate2]: [
|
||||
{ [operate]: `%${value}%` },
|
||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
||||
],
|
||||
};
|
||||
}
|
||||
query[Op.and].push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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':
|
||||
@@ -171,13 +183,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:
|
||||
@@ -187,24 +197,39 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,16 +245,16 @@ export default class CronService {
|
||||
searchValue: string;
|
||||
page: string;
|
||||
size: string;
|
||||
sortField: string;
|
||||
sortType: 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 sortField = params?.sortField || '';
|
||||
const sortType = params?.sortType || '';
|
||||
const viewQuery = JSON.parse(params?.queryString || '{}');
|
||||
const filterQuery = JSON.parse(params?.filters || '{}');
|
||||
const sorterQuery = JSON.parse(params?.sorter || '{}');
|
||||
|
||||
let query: any = {};
|
||||
let order = [
|
||||
@@ -241,10 +266,14 @@ export default class CronService {
|
||||
|
||||
this.formatViewQuery(query, viewQuery);
|
||||
this.formatSearchText(query, searchText);
|
||||
this.formatFilterQuery(query, filterQuery);
|
||||
this.formatViewSort(order, viewQuery);
|
||||
|
||||
if (sortType && sortField) {
|
||||
order.unshift([sortField, sortType]);
|
||||
if (sorterQuery) {
|
||||
const { field, type } = sorterQuery;
|
||||
if (field && type) {
|
||||
order.unshift([field, type]);
|
||||
}
|
||||
}
|
||||
let condition: any = {
|
||||
where: query,
|
||||
|
||||
@@ -17,6 +17,7 @@ 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],
|
||||
@@ -135,6 +136,20 @@ export default class NotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
private async chat() {
|
||||
const { chatUrl, chatToken } = this.params;
|
||||
const url = `${chatUrl}${chatToken}`;
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
timeout: this.timeout,
|
||||
retry: 0,
|
||||
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')) {
|
||||
|
||||
@@ -11,6 +11,13 @@ 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
|
||||
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
if [[ $NpmMirror ]]; then
|
||||
npm config set registry $NpmMirror
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "======================2. 安装依赖========================\n"
|
||||
|
||||
+3
-3
@@ -36,8 +36,8 @@
|
||||
},
|
||||
"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",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/pro-layout": "^6.33.1",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@sentry/react": "^7.0.0",
|
||||
"@sentry/react": "^7.12.1",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
|
||||
+10
-1
@@ -46,9 +46,12 @@ AutoStartBot=""
|
||||
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
|
||||
BotRepoUrl=""
|
||||
|
||||
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
|
||||
## 安装python依赖时指定pip源,默认使用清华源
|
||||
PipMirror="https://pypi.doubanio.com/simple/"
|
||||
|
||||
## 安装node依赖时指定pip源,默认使用清华源
|
||||
NpmMirror="https://registry.npmmirror.com"
|
||||
|
||||
## 通知环境变量
|
||||
## 1. Server酱
|
||||
## https://sct.ftqq.com
|
||||
@@ -137,4 +140,10 @@ 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=""
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
|
||||
+60
-5
@@ -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 = '';
|
||||
@@ -133,6 +139,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;
|
||||
}
|
||||
@@ -239,6 +253,8 @@ 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
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -369,7 +385,7 @@ function serverNotify(text, desp, time = 2100) {
|
||||
});
|
||||
}
|
||||
|
||||
function PushDeerNotify(text, desp, time = 2100) {
|
||||
function PushDeerNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
if (PUSHDEER_KEY) {
|
||||
// PushDeer 建议对消息内容进行 urlencode
|
||||
@@ -382,8 +398,9 @@ function PushDeerNotify(text, desp, time = 2100) {
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
setTimeout(() => {
|
||||
$.post(options, (err, resp, data) => {
|
||||
$.post(
|
||||
options,
|
||||
(err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('发送通知调用API失败!!\n');
|
||||
@@ -407,8 +424,46 @@ function PushDeerNotify(text, desp, time = 2100) {
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}, time);
|
||||
},
|
||||
time,
|
||||
);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ChatNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
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/x-www-form-urlencoded',
|
||||
},
|
||||
};
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('发送通知调用API失败!!\n');
|
||||
console.log(err);
|
||||
} else {
|
||||
data = JSON.parse(data);
|
||||
if (data.success) {
|
||||
console.log('Chat发送通知消息成功🎉\n');
|
||||
} else {
|
||||
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
+26
-2
@@ -61,7 +61,10 @@ push_config = {
|
||||
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||
|
||||
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
|
||||
|
||||
|
||||
'CHAT_URL': '', # synology chat url
|
||||
'CHAT_TOKEN': '', # synology chat token
|
||||
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
|
||||
@@ -279,8 +282,27 @@ def pushdeer(title: str, content: str) -> None:
|
||||
print("PushDeer 推送成功!")
|
||||
else:
|
||||
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+ 推送消息。
|
||||
@@ -527,6 +549,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"):
|
||||
|
||||
@@ -90,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
|
||||
|
||||
+3
-5
@@ -245,15 +245,13 @@ fix_config() {
|
||||
}
|
||||
|
||||
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
|
||||
npm install --production --no-bin-links
|
||||
elif ! type pnpm &>/dev/null; then
|
||||
npm install --production --registry=https://registry.npmmirror.com || npm install --production
|
||||
npm install --production
|
||||
else
|
||||
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
|
||||
pnpm install --loglevel error --production
|
||||
fi
|
||||
unset_proxy
|
||||
}
|
||||
|
||||
npm_install_1() {
|
||||
|
||||
+13
-5
@@ -295,6 +295,11 @@ update_qinglong() {
|
||||
}
|
||||
|
||||
patch_version() {
|
||||
# 兼容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
|
||||
@@ -303,13 +308,9 @@ patch_version() {
|
||||
fi
|
||||
|
||||
if ! type ts-node &>/dev/null; then
|
||||
pnpm i -g ts-node typescript tslib
|
||||
pnpm add -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
|
||||
@@ -338,6 +339,13 @@ patch_version() {
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ $PipMirror ]]; then
|
||||
pip3 config set global.index-url $PipMirror
|
||||
fi
|
||||
if [[ $NpmMirror ]]; then
|
||||
npm config set registry $NpmMirror
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
## 对比脚本
|
||||
|
||||
@@ -153,9 +153,13 @@ 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 });
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.cron {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.crontab-detail {
|
||||
.card-wrapper {
|
||||
height: 80vh;
|
||||
@@ -161,6 +155,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.view-create-modal-sorts {
|
||||
display: flex;
|
||||
|
||||
.ant-space-item:nth-child(2) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
tr.drop-over-downward td {
|
||||
border-bottom: 2px dashed #1890ff;
|
||||
}
|
||||
|
||||
+36
-29
@@ -93,10 +93,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'}
|
||||
@@ -109,6 +110,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchValue(`label:${label}`);
|
||||
setSearchText(`label:${label}`);
|
||||
}}
|
||||
>
|
||||
<a>{label}</a>
|
||||
@@ -140,7 +142,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
title: '命令',
|
||||
dataIndex: 'command',
|
||||
key: 'command',
|
||||
width: 250,
|
||||
width: 300,
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
@@ -152,7 +154,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
>
|
||||
{text}
|
||||
<a
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
@@ -173,6 +181,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
{
|
||||
title: '最后运行时间',
|
||||
align: 'center' as const,
|
||||
dataIndex: 'last_execution_time',
|
||||
key: 'last_execution_time',
|
||||
width: 150,
|
||||
sorter: {
|
||||
compare: (a: any, b: any) => {
|
||||
@@ -189,10 +199,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
>
|
||||
{record.last_execution_time
|
||||
? new Date(record.last_execution_time * 1000)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
: '-'}
|
||||
</span>
|
||||
);
|
||||
@@ -202,6 +212,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;
|
||||
@@ -358,6 +370,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
page: number;
|
||||
size: number;
|
||||
sorter: any;
|
||||
filters: any;
|
||||
}>({} as any);
|
||||
const [viewConf, setViewConf] = useState<any>();
|
||||
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||
@@ -380,9 +393,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}`);
|
||||
@@ -393,12 +410,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
|
||||
const getCrons = () => {
|
||||
setLoading(true);
|
||||
const { page, size, sorter } = pageConf;
|
||||
let url = `${config.apiPrefix}crons?searchValue=${searchText}&page=${page}&size=${size}`;
|
||||
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 += `&sortField=${sorter.field}&sortType=${
|
||||
sorter.order === 'ascend' ? 'ASC' : 'DESC'
|
||||
}`;
|
||||
url += `&sorter=${JSON.stringify({ field: sorter.field, type: sorter.order === 'ascend' ? 'ASC' : 'DESC' })}`;
|
||||
}
|
||||
if (viewConf) {
|
||||
url += `&queryString=${JSON.stringify({
|
||||
@@ -562,8 +577,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
`${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: [record.id],
|
||||
@@ -608,8 +622,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
`${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
}`,
|
||||
{
|
||||
data: [record.id],
|
||||
@@ -815,7 +828,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
sorter: SorterResult<any> | SorterResult<any>[],
|
||||
) => {
|
||||
const { current, pageSize } = pagination;
|
||||
setPageConf({ page: current as number, size: pageSize as number, sorter });
|
||||
setPageConf({ page: current as number, size: pageSize as number, sorter, filters });
|
||||
localStorage.setItem('pageSize', String(pageSize));
|
||||
};
|
||||
|
||||
@@ -852,6 +865,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
page: 1,
|
||||
size: parseInt(localStorage.getItem('pageSize') || '20'),
|
||||
sorter: {},
|
||||
filters: {}
|
||||
});
|
||||
setTimeout(() => {
|
||||
setTableScrollHeight(getTableScroll());
|
||||
@@ -931,14 +945,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"
|
||||
@@ -1017,6 +1023,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
|
||||
|
||||
const tabClick = (key: string) => {
|
||||
const view = enabledCronViews.find((x) => x.id == key);
|
||||
setPageConf({ ...pageConf, page: 1 });
|
||||
setViewConf(view ? view : null);
|
||||
};
|
||||
|
||||
|
||||
@@ -101,9 +101,9 @@ const ViewCreateModal = ({
|
||||
</Select>
|
||||
);
|
||||
|
||||
const propertyElement = (props: any) => {
|
||||
const propertyElement = (props: any, style: React.CSSProperties = {}) => {
|
||||
return (
|
||||
<Select style={{ width: 120 }}>
|
||||
<Select style={style}>
|
||||
{props.map((x) => (
|
||||
<Select.Option key={x.name} value={x.value}>
|
||||
{x.name}
|
||||
@@ -114,7 +114,7 @@ const ViewCreateModal = ({
|
||||
};
|
||||
|
||||
const typeElement = (
|
||||
<Select style={{ width: 120 }}>
|
||||
<Select>
|
||||
{SORTTYPES.map((x) => (
|
||||
<Select.Option key={x.name} value={x.value}>
|
||||
{x.name}
|
||||
@@ -170,6 +170,7 @@ const ViewCreateModal = ({
|
||||
label={index === 0 ? '筛选条件' : ''}
|
||||
key={key}
|
||||
style={{ marginBottom: 0 }}
|
||||
required
|
||||
>
|
||||
<Space className="view-create-modal-filters" align="baseline">
|
||||
<Form.Item
|
||||
@@ -177,7 +178,7 @@ const ViewCreateModal = ({
|
||||
name={[name, 'property']}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
{propertyElement(PROPERTIES)}
|
||||
{propertyElement(PROPERTIES, { width: 120 })}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
@@ -225,13 +226,13 @@ const ViewCreateModal = ({
|
||||
key={key}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Space className="view-create-modal-filters" align="baseline">
|
||||
<Space className="view-create-modal-sorts" align="baseline">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'property']}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
{propertyElement(PROPERTIES)}
|
||||
{propertyElement(PROPERTIES, { width: 240 })}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
@@ -240,9 +241,7 @@ const ViewCreateModal = ({
|
||||
>
|
||||
{typeElement}
|
||||
</Form.Item>
|
||||
{index !== 0 && (
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
)}
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Space>
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
@@ -231,7 +231,10 @@ const ViewManageModal = ({
|
||||
<Button
|
||||
key="2"
|
||||
type="primary"
|
||||
onClick={() => setIsCreateViewModalVisible(true)}
|
||||
onClick={() => {
|
||||
setEditedView(null);
|
||||
setIsCreateViewModalVisible(true);
|
||||
}}
|
||||
>
|
||||
新建视图
|
||||
</Button>
|
||||
@@ -258,8 +261,8 @@ const ViewManageModal = ({
|
||||
view={editedView}
|
||||
visible={isCreateViewModalVisible}
|
||||
handleCancel={(data) => {
|
||||
cronViewChange(data);
|
||||
setIsCreateViewModalVisible(false);
|
||||
cronViewChange(data);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -90,6 +90,7 @@ export default {
|
||||
{ value: 'weWorkApp', label: '企业微信应用' },
|
||||
{ value: 'iGot', label: 'IGot' },
|
||||
{ value: 'pushPlus', label: 'PushPlus' },
|
||||
{ value: 'chat', label: '群辉chat' },
|
||||
{ value: 'email', label: '邮箱' },
|
||||
{ value: 'closed', label: '已关闭' },
|
||||
],
|
||||
@@ -103,6 +104,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',
|
||||
|
||||
+5
-7
@@ -1,8 +1,6 @@
|
||||
export const version = '2.14.0';
|
||||
export const changeLogLink = 'https://t.me/jiao_long/325';
|
||||
export const changeLog = `2.14.0 版本说明
|
||||
1. 定时任务自定义视图功能来啦,支持自定义筛选条件、排序规则等
|
||||
2. 配置文件可设置需要延迟的脚本后缀,默认只有js脚本随机延迟执行,感谢 https://github.com/cddjr
|
||||
3. 修复定时任务搜索及搜索分页
|
||||
4. 其他bug修复
|
||||
export const version = '2.14.3';
|
||||
export const changeLogLink = 'https://t.me/jiao_long/330';
|
||||
export const changeLog = `2.14.8 版本说明
|
||||
1. 修复安装依赖可能出现的 ERR_PNPM_REGISTRIES_MISMATCH
|
||||
2. 今人不见古时月,今月曾经照古人 -> 中秋快乐
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user