Compare commits

...

41 Commits

Author SHA1 Message Date
whyour e70cd6579f 更新版本 v2.15.6 2023-02-01 22:46:23 +08:00
whyour 99c047e9c7 修复定时任务表格滚动 2023-02-01 22:45:47 +08:00
whyour 978ae326f8 更新 sqlite3 依赖 2023-02-01 15:30:53 +08:00
whyour b01f27f2d3 修改更新列表后刷新逻辑 2023-01-31 23:52:02 +08:00
whyour 4d34e0dd96 修复环境变量滚动卡顿 2023-01-31 23:46:42 +08:00
whyour 9b0065e533 修复定时任务过多搜索卡顿 2023-01-31 23:30:47 +08:00
whyour 34a51feee9 修改定时任务列表title 2023-01-31 17:42:30 +08:00
whyour 679091de59 修改定时任务输入提示 2023-01-31 17:41:35 +08:00
whyour c6220cb920 修改定时任务查询日志逻辑 2023-01-29 18:07:57 +08:00
whyour 633b30269c 修复定时任务脚本跳转 2023-01-29 10:57:08 +08:00
whyour 8ad26267d8 修改 task before/after 逻辑 2023-01-20 23:38:32 +08:00
OceanJiang 7cfb125682 实现 Node.js 部分 SMTP 推送 (#1776) 2023-01-14 19:53:36 +08:00
Wankko Ree a2dc5de5ce 添加基于 SMTP 邮件 的消息通知服务 (#1763) 2023-01-14 19:48:41 +08:00
whyour 4f41ff29ec 修改资源通知逻辑 2023-01-14 19:46:33 +08:00
whyour 13d95b8c19 修复日志滚动逻辑 2023-01-11 23:46:47 +08:00
whyour 335c13ab44 修改日志滚动逻辑 2023-01-11 23:44:04 +08:00
whyour 015cdc10a7 publicPath使用相对目录 2023-01-11 22:52:51 +08:00
whyour a5075ab28f 修改新建任务命令提示 2023-01-11 22:14:32 +08:00
whyour 2e3c3274c7 修复antd兼容性,日志详情自动滚动 2023-01-11 22:03:44 +08:00
whyour 443c581955 修改定时任务 shouldCellUpdate 2023-01-08 22:30:29 +08:00
whyour 5e41faf590 修改 monaco-editor 版本 2023-01-08 22:16:22 +08:00
whyour 0579fb83d3 修改环境变量值省略展示 2023-01-08 21:56:29 +08:00
whyour 2864845d5b 优化定时任务表格渲染 2023-01-08 18:15:18 +08:00
whyour 659dddace7 梅子留酸软齿牙,芭蕉分绿与窗纱。 2023-01-08 16:49:21 +08:00
whyour b096f30558 更新 dockerfile 2023-01-08 16:39:01 +08:00
whyour dcfb72143a 修复定时任务每页数据量大时搜索卡顿 2023-01-07 18:19:03 +08:00
whyour c63bcd7706 修改docker编译 2023-01-07 18:16:39 +08:00
whyour bac306f9b3 更新版本 v2.15.5 2023-01-04 21:09:52 +08:00
whyour a6fd96e97b 修复定时规则修改 2023-01-04 21:09:27 +08:00
whyour 7edd91f923 修改服务异常判断逻辑 2023-01-04 19:27:15 +08:00
whyour 66df445bd5 修改添加任务命令提示 2023-01-04 17:28:26 +08:00
whyour 5c03034bb4 修复定时任务不以task开头时,任务无效 2023-01-04 17:25:30 +08:00
whyour 0eab181d46 修改update逻辑 2022-12-29 12:15:25 +08:00
whyour f6f95308c7 monaco editor改为本地加载 2022-12-28 22:12:37 +08:00
whyour e93cad91b4 更新版本 v2.15.4 2022-12-28 11:15:20 +08:00
whyour 08ee61b179 修改 pub 脚本 2022-12-28 11:14:57 +08:00
whyour 0ab756665e 修改版本文件 2022-12-28 11:06:47 +08:00
whyour 3570cddce0 关于增加更新日志查看 2022-12-27 17:10:21 +08:00
whyour a2d9f1a5db 脚本管理支持重命名文件/文件夹 2022-12-27 16:02:15 +08:00
whyour 0c6a214e55 修复启动public服务命令 2022-12-27 12:24:24 +08:00
whyour 095fcdbe69 修复任务脚本快捷跳转 2022-12-17 12:46:26 +08:00
53 changed files with 4462 additions and 2572 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ SECRET='whyour'
QINIU_AK=''
QINIU_SK=''
QINIU_SCOPE=''
QINIU_SCOPE=''
TEMP=''
+5 -3
View File
@@ -5,7 +5,6 @@ on:
branches:
- 'master'
- 'develop'
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule:
@@ -13,7 +12,6 @@ on:
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch:
jobs:
@@ -155,12 +153,16 @@ jobs:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
context: docker/
context: .
file: ./docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+2 -1
View File
@@ -6,10 +6,11 @@ export default defineConfig({
antd: {},
outputPath: 'static/dist',
fastRefresh: true,
favicons: ['/images/favicon.svg'],
favicons: ['./images/favicon.svg'],
mfsu: {
strategy: 'eager',
},
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: {
'/api/public': {
target: 'http://127.0.0.1:5400/',
+27
View File
@@ -274,4 +274,31 @@ export default (app: Router) => {
}
},
);
route.put(
'/rename',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
newFilename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type, newFilename } = req.body as {
filename: string;
path: string;
type: string;
newFilename: string;
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
};
+6 -5
View File
@@ -7,7 +7,7 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
import { promiseExec } from '../config/util';
import { parseVersion, promiseExec } from '../config/util';
import dayjs from 'dayjs';
const route = Router();
@@ -21,10 +21,9 @@ export default (app: Router) => {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
const { version, changeLog, changeLogLink } = await parseVersion(
config.versionFile,
);
const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
@@ -56,6 +55,8 @@ export default (app: Router) => {
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
changeLog,
changeLogLink,
},
});
} catch (e) {
+6
View File
@@ -1 +1,7 @@
export const LOG_END_SYMBOL = '\n          ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+2 -2
View File
@@ -14,7 +14,7 @@ if (!process.env.QL_DIR) {
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
}
const lastVersionFile = `https://qn.whyour.cn/version.ts`;
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -40,7 +40,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'src/version.ts');
const versionFile = path.join(rootPath, 'version.yaml');
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
+15
View File
@@ -6,6 +6,7 @@ import { exec } from 'child_process';
import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
@@ -482,3 +483,17 @@ export async function getPid(name: string) {
let pid = (await execAsync(taskCommand)).stdout;
return Number(pid);
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
+4 -6
View File
@@ -4,13 +4,11 @@ import * as Tracing from '@sentry/tracing';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
import { parseVersion } from '../config/util';
export default ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/;
export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
@@ -18,7 +16,7 @@ export default ({ expressApp }: { expressApp: Application }) => {
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 0.1,
release: currentVersion,
release: version,
});
expressApp.use(Sentry.Handlers.requestHandler());
+6 -9
View File
@@ -6,15 +6,12 @@ import config from './config';
const app = express();
app.get('/api/public/panel/log', (req, res) => {
exec(
'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr });
}
return res.send({ code: 200, data: stdout });
},
);
exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr });
}
return res.send({ code: 200, data: stdout });
});
});
app
+6 -2
View File
@@ -4,6 +4,7 @@ import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
import { QL_PREFIX, TASK_PREFIX } from './config/const';
const app = express();
@@ -23,8 +24,11 @@ const run = async () => {
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
if (
!command.startsWith(TASK_PREFIX) &&
!command.startsWith(QL_PREFIX)
) {
command = `${TASK_PREFIX}${command}`;
}
exec(`ID=${task.id} ${command}`);
});
+25 -84
View File
@@ -14,10 +14,11 @@ import {
import { promises, existsSync } from 'fs';
import { Op, where, col as colFn } from 'sequelize';
import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) {
const { schedule } = cron;
@@ -31,7 +32,7 @@ export default class CronService {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc));
await this.set_crontab();
return doc;
}
@@ -42,7 +43,7 @@ export default class CronService {
public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false;
const newDoc = await this.updateDb(payload);
await this.set_crontab(this.isSixCron(newDoc));
await this.set_crontab();
return newDoc;
}
@@ -81,7 +82,7 @@ export default class CronService {
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true);
await this.set_crontab();
}
public async pin(ids: number[]) {
@@ -361,8 +362,8 @@ export default class CronService {
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
@@ -408,12 +409,12 @@ export default class CronService {
public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true);
await this.set_crontab();
}
public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true);
await this.set_crontab();
}
public async log(id: number) {
@@ -426,104 +427,46 @@ export default class CronService {
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else {
return '';
return '任务未运行或运行失败,请尝试手动运行';
}
}
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
if (!doc || !doc.log_path) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: logPath,
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
} else {
return [];
}
}
private getKey(command: string): string {
const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
const tmpStr = command.substring(0, start - 1);
let index = 0;
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
index = tmpStr.lastIndexOf('/');
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
index = tmpStr.lastIndexOf(':');
}
if (index) {
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} else {
return command.substring(start, end);
}
}
private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string;
}
private async set_crontab(needReloadSchedule: boolean = false) {
private async set_crontab() {
const tabs = await this.crontabs();
var crontab_string = '';
tabs.data.forEach((tab) => {
@@ -546,9 +489,7 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
exec(`pm2 reload schedule`);
await CrontabModel.update({ saved: true }, { where: {} });
}
+3 -3
View File
@@ -6,7 +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';
import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
@Service()
@@ -42,7 +42,7 @@ export default class ScriptService {
public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`;
const command = `${TASK_COMMAND} -l ${relativePath} now`;
const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
@@ -56,7 +56,7 @@ export default class ScriptService {
let str = '';
if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`task -l ${relativePath} now`);
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
}
try {
await killTask(pid);
+15 -15
View File
@@ -9,6 +9,7 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
import { parseContentVersion, parseVersion } from '../config/util';
@Service()
export default class SystemService {
@@ -78,14 +79,9 @@ export default class SystemService {
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionContent = await parseVersion(config.versionFile);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
let lastVersionContent;
try {
const result = await got.get(
`${config.lastVersionFile}?t=${Date.now()}`,
@@ -93,19 +89,23 @@ export default class SystemService {
timeout: 30000,
},
);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
lastVersionContent = await parseContentVersion(result.body);
} catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
hasNewVersion: this.checkHasNewVersion(
currentVersionContent.version,
lastVersionContent.version,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
},
};
} catch (error: any) {
+20 -8
View File
@@ -1,3 +1,12 @@
FROM python:3.10-alpine as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm \
&& npm i -g pnpm \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour"
@@ -13,8 +22,6 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR}
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
@@ -42,19 +49,24 @@ RUN set -x \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \
&& pnpm add -g pm2 ts-node typescript tslib \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm
ARG SOURCE_COMMIT
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+14 -7
View File
@@ -4,7 +4,7 @@
"start": "concurrently -n w: npm:start:*",
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node back/public.ts",
"start:public": "ts-node --transpile-only ./back/public.ts",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js",
@@ -42,7 +42,8 @@
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup"
"rollup",
"styled-components"
],
"allowedVersions": {
"react": "18",
@@ -69,6 +70,7 @@
"got": "^11.8.2",
"hpagent": "^0.1.2",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"multer": "^1.4.4",
@@ -80,7 +82,7 @@
"sequelize": "^6.25.5",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
"sqlite3": "npm:@louislam/sqlite3@15.1.2",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2",
@@ -89,14 +91,15 @@
},
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "4.2.1",
"@ant-design/pro-layout": "6.38.22",
"@monaco-editor/react": "4.4.6",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7",
@@ -106,20 +109,22 @@
"@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4",
"@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/max": "^4.0.21",
"@umijs/max": "^4.0.42",
"@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6",
"antd": "^4.23.0",
"antd": "^4.24.7",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15",
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
@@ -128,6 +133,7 @@
"rc-tween-one": "^3.0.6",
"react": "18.2.0",
"react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
@@ -139,6 +145,7 @@
"typescript": "4.8.4",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"virtuallist-antd": "^0.7.6",
"webpack": "^5.70.0",
"yorkie": "^2.0.0"
}
+3478 -1944
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -160,4 +160,17 @@ export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+75
View File
@@ -125,6 +125,18 @@ let AIBOTK_NAME = '';
//FSKEY 飞书机器人的 FSKEY
let FSKEY = '';
// =======================================SMTP 邮件设置区域=======================================
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
let SMTP_SERVER = '';
let SMTP_SSL = 'false';
let SMTP_EMAIL = '';
let SMTP_PASSWORD = '';
let SMTP_NAME = '';
//==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
@@ -250,6 +262,22 @@ if (process.env.AIBOTK_NAME) {
if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
if (process.env.SMTP_SERVER) {
SMTP_SERVER = process.env.SMTP_SERVER;
}
if (process.env.SMTP_SSL) {
SMTP_SSL = process.env.SMTP_SSL;
}
if (process.env.SMTP_EMAIL) {
SMTP_EMAIL = process.env.SMTP_EMAIL;
}
if (process.env.SMTP_PASSWORD) {
SMTP_PASSWORD = process.env.SMTP_PASSWORD;
}
if (process.env.SMTP_NAME) {
SMTP_NAME = process.env.SMTP_NAME;
}
//==========================云端环境变量的判断与接收=========================
/**
@@ -287,6 +315,7 @@ async function sendNotify(
PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件
]);
}
@@ -1022,6 +1051,52 @@ function fsBotNotify(text, desp) {
});
}
async function smtpNotify(text, desp) {
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
{
auth: {
user: SMTP_EMAIL,
pass: SMTP_PASSWORD,
},
},
);
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({
from: addr,
to: addr,
subject: text,
text: desp,
});
if (!!info.messageId) {
console.log('SMTP发送通知消息成功🎉\n');
return true;
}
console.log('SMTP发送通知消息失败!!\n');
} catch (e) {
console.log('SMTP发送通知消息出现错误!!\n');
console.log(e);
}
}
function smtpNotify(text, desp) {
return new Promise((resolve) => {
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
} else {
resolve();
}
});
}
module.exports = {
sendNotify,
BARK_PUSH,
+47 -11
View File
@@ -9,6 +9,10 @@ import re
import threading
import time
import urllib.parse
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import requests
@@ -62,10 +66,10 @@ push_config = {
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -86,6 +90,12 @@ push_config = {
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
}
notify_function = []
# fmt: on
@@ -282,16 +292,16 @@ def pushdeer(title: str, content: str) -> None:
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push'
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!")
else:
print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None:
"""
通过Chat 推送消息
@@ -303,14 +313,14 @@ def chat(title: str, content: str) -> None:
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)
print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
@@ -562,6 +572,30 @@ def aibotk(title: str, content: str) -> None:
print(f'智能微秘书 推送失败!{response["error"]}')
def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息。
"""
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['Subject'] = Header(title, 'utf-8')
try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get("SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f'SMTP 邮件 推送失败!{e}')
def one() -> str:
"""
获取一条一言。
@@ -603,7 +637,9 @@ if push_config.get("QYWX_KEY"):
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)
notify_function.append(aibotk)
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
notify_function.append(smtp)
def send(title: str, content: str) -> None:
+3 -2
View File
@@ -5,12 +5,13 @@
"dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1",
"http-server": "^0.12.3",
"nodemailer": "^6.8.0",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.2",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
}
}
}
+2 -2
View File
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts';
const key = 'version.yaml';
const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts';
const localFile = 'version.yaml';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra(
+16 -13
View File
@@ -35,7 +35,7 @@ random_delay() {
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行任务,如需立即终止,请按 CTRL+C...\n"
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行\n"
sleep $delay_second
fi
}
@@ -81,18 +81,19 @@ run_nohup() {
}
check_server() {
local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | 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)
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=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
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 ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
fi
}
@@ -100,6 +101,8 @@ handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
@@ -110,7 +113,6 @@ handle_task_before() {
eval echo $cmd
fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $file_task_before "$@"
}
@@ -122,9 +124,10 @@ handle_task_after() {
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     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
}
## 正常运行单个脚本,$1:传入参数
+1 -1
View File
@@ -13,7 +13,7 @@ git push
echo -e "更新cdn文件"
ts-node sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string"
echo -e "当前版本$version"
+3 -3
View File
@@ -79,9 +79,9 @@ import_config() {
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
cpu_warn=${CpuWarn:-80}
mem_warn=${MemoryWarn:-80}
disk_warn=${DiskWarn:-90}
cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn}
disk_warn=${DiskWarn}
}
set_proxy() {
+3 -31
View File
@@ -26,33 +26,6 @@ diff_cron() {
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|")
## 删除旧的发送记录文件
[[ -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}')
## 如果是今天,并且版本号不一致,则发送通知
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
@@ -251,7 +224,7 @@ update_qinglong() {
if [ "$githubStatus" == "" ]; then
mirror="gitee"
fi
echo -e "\n使用 ${mirror} 源更新...\n"
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a)
@@ -265,8 +238,8 @@ update_qinglong() {
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n"
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
@@ -291,8 +264,7 @@ update_qinglong_static() {
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"
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
+39
View File
@@ -0,0 +1,39 @@
import React, { useRef, useState, useEffect } from 'react';
import { Tooltip, Typography } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const { Link } = Typography;
const Copy = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const copyIdRef = useRef<number>();
const copyText = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
setCopied(true);
cleanCopyId();
copyIdRef.current = window.setTimeout(() => {
setCopied(false);
}, 3000);
};
const cleanCopyId = () => {
window.clearTimeout(copyIdRef.current!);
};
return (
<Link onClick={copyText} style={{ marginLeft: 1 }}>
<CopyToClipboard text={text}>
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Tooltip>
</CopyToClipboard>
</Link>
);
};
export default Copy;
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_z0d9rbri1ci.js'],
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
});
export default IconFont;
+2 -1
View File
@@ -10,6 +10,7 @@ import {
ContainerOutlined,
} from '@ant-design/icons';
import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
export default {
route: {
@@ -93,4 +94,4 @@ export default {
contentWidth: 'Fixed',
splitMenus: false,
siderWidth: 180,
} as any;
} as BasicLayoutProps;
+1
View File
@@ -347,4 +347,5 @@ select:-webkit-autofill:focus {
pre {
word-break: break-all !important;
white-space: break-spaces !important;
padding: 0 !important;
}
+23 -20
View File
@@ -13,7 +13,6 @@ import config from '@/utils/config';
import { request } from '@/utils/http';
import './index.less';
import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks';
import {
message,
@@ -26,6 +25,7 @@ import {
Popover,
Descriptions,
Tooltip,
MenuProps,
} from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
@@ -52,6 +52,8 @@ interface TSystemInfo {
lastCommitId: string;
lastCommitTime: number;
version: string;
changeLog: string;
changeLogLink: string;
}
export default function () {
@@ -89,6 +91,7 @@ export default function () {
if (!data.isInitialized) {
history.push('/initialization');
} else {
init(data.version);
getUser();
}
}
@@ -143,7 +146,6 @@ export default function () {
useEffect(() => {
vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return;
@@ -221,9 +223,6 @@ export default function () {
}
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
} - 控制面板`;
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab');
}
@@ -250,13 +249,17 @@ export default function () {
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = (
<Menu
className="side-menu-user-drop-menu"
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]}
onClick={logout}
/>
);
const menu: MenuProps = {
items: [
{
label: '退出登录',
className: 'side-menu-user-drop-menu',
onClick: logout,
key: 'logout',
icon: <LogoutOutlined />,
},
],
};
return loading ? (
<PageLoading />
) : (
@@ -265,11 +268,12 @@ export default function () {
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
// @ts-ignore
title={
<>
<span style={{ fontSize: 16 }}></span>
<a
href={changeLogLink}
href={systemInfo?.changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
@@ -289,7 +293,7 @@ export default function () {
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{version}
v{systemInfo?.version}
</span>
</Badge>
</Tooltip>
@@ -307,16 +311,15 @@ export default function () {
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
pageTitleRender={(props, pageName, info) => {
if (info && typeof info.pageName === 'string') {
return `${info.pageName} - 控制面板`;
}
return '控制面板';
const title =
(config.documentTitleMap as any)[location.pathname] || '未找到';
return `${title} - 控制面板`;
}}
onCollapse={setCollapsed}
collapsed={collapsed}
rightContentRender={() =>
ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
<Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper">
<Avatar
shape="square"
@@ -338,7 +341,7 @@ export default function () {
}}
>
{!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
<Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper">
<Avatar
shape="square"
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Button, Result, Typography } from 'antd';
const { Link } = Typography;
const NotFound: React.FC = () => (
<Result
status="404"
title="404"
extra={
<Button type="primary">
<Link href="/"></Link>
</Button>
}
/>
);
export default NotFound;
+5 -16
View File
@@ -29,6 +29,7 @@ import config from '@/utils/config';
import CronLogModal from './logModal';
import Editor from '@monaco-editor/react';
import IconFont from '@/components/iconfont';
import { getCommandScript } from '@/utils';
const { Text } = Typography;
@@ -147,22 +148,10 @@ const CronDetailModal = ({
};
const getScript = () => {
const cmd = cron.command.split(' ') as string[];
if (cmd[0] === 'task') {
const result = getCommandScript(cron.command);
if (Array.isArray(result)) {
setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
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 = '';
}
const [s, p] = result;
setScriptInfo({ parent: p, filename: s });
request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
@@ -171,7 +160,7 @@ const CronDetailModal = ({
setValue(data);
}
});
} else {
} else if (result) {
setValidTabs([validTabs[0]]);
}
};
+87 -97
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
Button,
message,
@@ -14,6 +14,7 @@ import {
Popover,
Tabs,
TablePaginationConfig,
MenuProps,
} from 'antd';
import {
ClockCircleOutlined,
@@ -50,6 +51,9 @@ import ViewManageModal from './viewManageModal';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { VList } from 'virtuallist-antd';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -81,9 +85,23 @@ enum OperationPath {
'unpin',
}
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}
const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
const columns: ColumnProps<ICrontab>[] = [
{
title: '名称',
dataIndex: 'name',
@@ -137,16 +155,16 @@ const Crontab = () => {
</>
),
sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
compare: (a, b) => a?.name?.localeCompare(b?.name),
},
},
{
title: '命令',
title: '命令/脚本',
dataIndex: 'command',
key: 'command',
width: 300,
align: 'center' as const,
render: (text: string, record: any) => {
render: (text, record) => {
return (
<Paragraph
style={{
@@ -177,7 +195,7 @@ const Crontab = () => {
width: 110,
align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
compare: (a, b) => a.schedule.localeCompare(b.schedule),
},
},
{
@@ -187,11 +205,11 @@ const Crontab = () => {
key: 'last_execution_time',
width: 150,
sorter: {
compare: (a: any, b: any) => {
return a.last_execution_time - b.last_execution_time;
compare: (a, b) => {
return (a.last_execution_time || 0) - (b.last_execution_time || 0);
},
},
render: (text: string, record: any) => {
render: (text, record) => {
const language = navigator.language || navigator.languages[0];
return (
<span
@@ -221,7 +239,7 @@ const Crontab = () => {
return a.last_running_time - b.last_running_time;
},
},
render: (text: string, record: any) => {
render: (text, record) => {
return record.last_running_time
? diffTime(record.last_running_time)
: '-';
@@ -236,7 +254,7 @@ const Crontab = () => {
return a.nextRunTime - b.nextRunTime;
},
},
render: (text: string, record: any) => {
render: (text, record) => {
const language = navigator.language || navigator.languages[0];
return record.nextRunTime
.toLocaleString(language, {
@@ -270,14 +288,14 @@ const Crontab = () => {
value: 3,
},
],
onFilter: (value: number, record: any) => {
onFilter: (value, record) => {
if (record.isDisabled && record.status !== 0) {
return value === 2;
} else {
return record.status === value;
}
},
render: (text: string, record: any) => (
render: (text, record) => (
<>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<>
@@ -314,7 +332,7 @@ const Crontab = () => {
key: 'action',
align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => {
render: (text, record, index) => {
const isPc = !isPhone;
return (
<Space size="middle">
@@ -390,24 +408,12 @@ const Crontab = () => {
const tableScrollHeight = useTableScrollHeight(tableRef);
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
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 = '';
}
const result = getCommandScript(record.command);
if (Array.isArray(result)) {
const [s, p] = result;
history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') {
location.href = cmd[2];
} else if (result) {
location.href = result;
}
};
@@ -682,15 +688,13 @@ const Crontab = () => {
arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']}
overlay={
<Menu
items={getMenuItems(record)}
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
action(key, record, index);
}}
/>
}
menu={{
items: getMenuItems(record),
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
action(key, record, index);
},
}}
>
<a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined />
@@ -717,34 +721,15 @@ const Crontab = () => {
}
};
const handleCancel = (cron?: any) => {
const handleCancel = () => {
setIsModalVisible(false);
if (cron) {
handleCrons(cron);
}
getCrons();
};
const onSearch = (value: string) => {
setSearchText(value.trim());
};
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
.next()
.toDate();
if (index === -1) {
result.unshift(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => {
request
.get(`${config.apiPrefix}crons/${cron.id}`)
@@ -887,41 +872,39 @@ const Crontab = () => {
}
};
const menu = (
<Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
}}
items={[
...[...enabledCronViews].slice(4).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 menu: MenuProps = {
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
},
items: [
...[...enabledCronViews].slice(4).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' as 'group',
},
{
label: '新建视图',
key: 'new',
icon: <PlusOutlined />,
},
{
label: '视图管理',
key: 'manage',
icon: <SettingOutlined />,
},
],
};
const getCronViews = () => {
setLoading(true);
@@ -945,6 +928,12 @@ const Crontab = () => {
setViewConf(view ? view : null);
};
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
});
}, [tableScrollHeight]);
return (
<PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
@@ -975,7 +964,7 @@ const Crontab = () => {
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={
<Dropdown
overlay={menu}
menu={menu}
trigger={['click']}
overlayStyle={{ minWidth: 200 }}
>
@@ -1079,6 +1068,7 @@ const Crontab = () => {
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
components={vComponents}
/>
</div>
<CronLogModal
+10 -6
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
@@ -34,7 +34,6 @@ const CronLogModal = ({
const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>('');
const getCronLog = (isFirst?: boolean) => {
if (isFirst) {
@@ -49,10 +48,14 @@ const CronLogModal = ({
) {
const log = data as string;
setValue(log || '暂无日志');
setExecuting(
log && !logEnded(log) && !log.includes('重启面板'),
);
if (log && !logEnded(log) && !log.includes('重启面板')) {
const hasNext = log && !logEnded(log) && !log.includes('重启面板') && !log.includes('任务未运行或运行失败,请尝试手动运行');
setExecuting(hasNext);
setTimeout(() => {
document
.querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' });
}, 1000);
if (hasNext) {
setTimeout(() => {
getCronLog();
}, 2000);
@@ -155,6 +158,7 @@ const CronLogModal = ({
{value}
</pre>
)}
<div id="log-flag"></div>
</Modal>
);
};
+2 -2
View File
@@ -74,13 +74,13 @@ const CronModal = ({
</Form.Item>
<Form.Item
name="command"
label="命令"
label="命令/脚本"
rules={[{ required: true, whitespace: true }]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入要执行命令"
placeholder="支持输入脚本路径/任意系统可执行命令/task 脚本路径"
/>
</Form.Item>
<Form.Item
+6
View File
@@ -5,3 +5,9 @@ tr.drop-over-downward td {
tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
.text-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
+88 -78
View File
@@ -1,4 +1,10 @@
import React, { useCallback, useRef, useState, useEffect } from 'react';
import React, {
useCallback,
useRef,
useState,
useEffect,
useMemo,
} from 'react';
import {
Button,
message,
@@ -32,8 +38,10 @@ import { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from 'virtuallist-antd';
const { Text, Paragraph } = Typography;
const { Text } = Typography;
const { Search } = Input;
enum Status {
@@ -58,50 +66,6 @@ enum OperationPath {
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 Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [
@@ -128,17 +92,12 @@ const Env = () => {
width: '35%',
render: (text: string, record: any) => {
return (
<Paragraph
style={{
wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
copyable
>
{text}
</Paragraph>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
<Copy text={text} />
</div>
);
},
},
@@ -147,6 +106,13 @@ const Env = () => {
dataIndex: 'remarks',
key: 'remarks',
align: 'center' as const,
render: (text: string, record: any) => {
return (
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
);
},
},
{
title: '更新时间',
@@ -256,7 +222,7 @@ const Env = () => {
const [searchText, setSearchText] = useState('');
const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getEnvs = () => {
setLoading(true);
@@ -286,7 +252,8 @@ const Env = () => {
onOk() {
request
.put(
`${config.apiPrefix}envs/${record.status === Status. ? 'enable' : 'disable'
`${config.apiPrefix}envs/${
record.status === Status. ? 'enable' : 'disable'
}`,
{
data: [record.id],
@@ -356,7 +323,7 @@ const Env = () => {
const handleCancel = (env?: any[]) => {
setIsModalVisible(false);
env && handleEnv(env);
getEnvs();
};
const handleEditNameCancel = (env?: any[]) => {
@@ -364,28 +331,71 @@ const Env = () => {
getEnvs();
};
const handleEnv = (env: any) => {
const result = [...value];
const index = value.findIndex((x) => x.id === env.id);
if (index === -1) {
env = Array.isArray(env) ? env : [env];
result.push(...env);
} else {
result.splice(index, 1, {
...env,
});
}
setValue(result);
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
resetTopWhenDataChange: false,
});
}, [tableScrollHeight]);
const DragableBodyRow = (props: any) => {
const { index, moveRow, className, style, ...restProps } = props;
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(),
}),
});
useEffect(() => {
drop(drag(ref));
}, [drag, drop]);
const components = useMemo(() => vComponents.body.row, []);
const tempProps = useMemo(() => {
return {
ref: ref,
className: `${className}${isOver ? dropClassName : ''}`,
style: { cursor: 'move', ...style },
...restProps,
};
}, [className, dropClassName, restProps, style, isOver]);
return <> {components(tempProps, ref)} </>;
};
const components = {
body: {
row: DragableBodyRow,
},
};
const components = useMemo(() => {
return {
...vComponents,
body: {
...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
(dragIndex: number, hoverIndex: number) => {
if (dragIndex === hoverIndex) {
return;
}
+26 -5
View File
@@ -9,20 +9,37 @@ import './index.less';
import { SharedContext } from '@/layouts';
const Error = () => {
const { user, theme } = useOutletContext<SharedContext>();
const { user, theme, reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志');
const getLog = () => {
setLoading(true);
const getTimes = () => {
return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
};
let times = getTimes();
const getLog = (needLoading: boolean = true) => {
needLoading && setLoading(true);
request
.get(`${config.apiPrefix}public/panel/log`)
.then(({ code, data }) => {
if (code === 200) {
setData(data);
if (!data) {
times = getTimes();
if (times > 5) {
return;
}
localStorage.setItem('error_retry_times', `${times + 1}`);
setTimeout(() => {
reloadUser();
getLog(false);
}, 3000);
}
}
})
.finally(() => setLoading(false));
.finally(() => needLoading && setLoading(false));
};
useEffect(() => {
@@ -39,7 +56,7 @@ const Error = () => {
<div className="error-wrapper">
{loading ? (
<PageLoading />
) : (
) : data ? (
<Terminal
name="服务错误"
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
@@ -55,6 +72,10 @@ const Error = () => {
},
]}
/>
) : times > 5 ? (
<> ql -l check </>
) : (
<PageLoading tip="启动中,请稍后..." />
)}
</div>
);
+1 -1
View File
@@ -101,7 +101,7 @@ const EditModal = ({
};
const stop = () => {
if (!cNode || !cNode.title) {
if (!cNode || !cNode.title || !currentPid) {
return;
}
request
+71 -36
View File
@@ -11,6 +11,7 @@ import {
Dropdown,
Menu,
Empty,
MenuProps,
} from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -39,6 +40,8 @@ import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq';
import IconFont from '@/components/iconfont';
import RenameModal from './renameModal';
const { Text } = Typography;
@@ -64,20 +67,23 @@ const Script = () => {
const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [isRenameFileModalVisible, setIsRenameFileModalVisible] =
useState(false);
const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => {
setLoading(true);
const getScripts = (needLoading: boolean = true) => {
needLoading && setLoading(true);
request
.get(`${config.apiPrefix}scripts`)
.then(({ code, data }) => {
if (code === 200) {
setData(data);
initState();
initGetScript();
}
})
.finally(() => setLoading(false));
.finally(() => needLoading && setLoading(false));
};
const getDetail = (node: any) => {
@@ -287,6 +293,15 @@ const Script = () => {
});
};
const renameFile = () => {
setIsRenameFileModalVisible(true);
};
const handleRenameFileCancel = () => {
setIsRenameFileModalVisible(false);
getScripts(false);
};
const addFile = () => {
setIsAddFileModalVisible(true);
};
@@ -381,45 +396,52 @@ const Script = () => {
case 'delete':
deleteFile();
break;
case 'rename':
renameFile();
break;
default:
break;
}
};
const menu = isEditing ? (
<Menu
items={[
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
]}
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
action(key);
}}
/>
) : (
<Menu
items={[
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
{
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
const menu: MenuProps = isEditing
? {
items: [
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
action(key);
},
{
label: '删除',
key: 'delete',
icon: <DeleteOutlined />,
disabled: !select,
}
: {
items: [
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
{
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
},
{
label: '重命名',
key: 'rename',
icon: <IconFont type="ql-icon-rename" />,
disabled: !select,
},
{
label: '删除',
key: 'delete',
icon: <DeleteOutlined />,
disabled: !select,
},
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
},
]}
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}}
/>
);
};
return (
<PageContainer
@@ -442,7 +464,7 @@ const Script = () => {
allowClear
onSelect={onSelect}
/>,
<Dropdown overlay={menu} trigger={['click']}>
<Dropdown menu={menu} trigger={['click']}>
<Button type="primary" icon={<EllipsisOutlined />} />
</Dropdown>,
]
@@ -471,6 +493,14 @@ const Script = () => {
icon={<EditOutlined />}
/>
</Tooltip>,
<Tooltip title="重命名">
<Button
disabled={!select}
type="primary"
onClick={renameFile}
icon={<IconFont type="ql-icon-rename" />}
/>
</Tooltip>,
<Tooltip title="删除">
<Button
type="primary"
@@ -585,6 +615,11 @@ const Script = () => {
treeData={data}
handleCancel={addFileModalClose}
/>
<RenameModal
visible={isRenameFileModalVisible}
handleCancel={handleRenameFileCancel}
currentNode={currentNode}
/>
</div>
</PageContainer>
);
+79
View File
@@ -0,0 +1,79 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const RenameModal = ({
currentNode,
handleCancel,
visible,
}: {
currentNode?: any;
visible: boolean;
handleCancel: () => void;
}) => {
console.log(currentNode);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
try {
const { code, data } = await request.put(
`${config.apiPrefix}scripts/rename`,
{
data: {
filename: currentNode.title,
path: currentNode.parent || '',
newFilename: values.name,
},
},
);
if (code === 200) {
message.success('更新名称成功');
handleCancel();
}
setLoading(false);
} catch (error) {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
}, [currentNode, visible]);
return (
<Modal
title="重命名"
open={visible}
forceRender
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="edit_name_modal">
<Form.Item
name="name"
rules={[{ required: true, message: '请输入新名称' }]}
>
<Input placeholder="请输入新名称" />
</Form.Item>
</Form>
</Modal>
);
};
export default RenameModal;
+8
View File
@@ -38,6 +38,14 @@ const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
<Descriptions.Item label="更新ID" span={3}>
{systemInfo.lastCommitId}
</Descriptions.Item>
<Descriptions.Item label="更新日志" span={3}>
<Link
href={`https://qn.whyour.cn/version.yaml?t=${Date.now()}`}
target="_blank"
>
</Link>
</Descriptions.Item>
</Descriptions>
<div>
<Link
+5 -7
View File
@@ -2,11 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { version } from '../../version';
const { Countdown } = Statistic;
const CheckUpdate = ({ socketMessage }: any) => {
const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState('');
const modalRef = useRef<any>();
@@ -23,7 +22,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) {
showConfirmUpdateModal(data);
} else {
showForceUpdateModal();
showForceUpdateModal(data);
}
}
})
@@ -36,7 +35,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
});
};
const showForceUpdateModal = () => {
const showForceUpdateModal = (data: any) => {
Modal.confirm({
width: 500,
title: '更新',
@@ -44,7 +43,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version}
{data.lastVersion}
</div>
</>
),
@@ -70,14 +69,13 @@ const CheckUpdate = ({ socketMessage }: any) => {
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{lastVersion}使{version}
{lastVersion} 使 {systemInfo.version}
</div>
</>
),
content: (
<pre
style={{
paddingTop: 15,
fontSize: 12,
fontWeight: 400,
}}
+6 -102
View File
@@ -17,7 +17,6 @@ import {
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import * as DarkReader from '@umijs/ssr-darkreader';
import AppModal from './appModal';
import {
EditOutlined,
@@ -27,18 +26,13 @@ import {
import SecuritySettings from './security';
import LoginLog from './loginLog';
import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate';
import Other from './other';
import About from './about';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import './index.less';
const { Text } = Typography;
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Setting = () => {
const {
@@ -121,37 +115,12 @@ const Setting = () => {
];
const [loading, setLoading] = useState(true);
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [dataSource, setDataSource] = useState<any[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedApp, setEditedApp] = useState<any>();
const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [notificationInfo, setNotificationInfo] = useState<any>();
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getApps = () => {
setLoading(true);
@@ -276,8 +245,6 @@ const Setting = () => {
getLoginLog();
} else if (activeKey === 'notification') {
getNotification();
} else if (activeKey === 'other') {
getLogRemoveFrequency();
}
};
@@ -294,37 +261,6 @@ const Setting = () => {
});
};
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
return (
<PageContainer
className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
@@ -382,43 +318,11 @@ const Setting = () => {
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>
<Other
reloadTheme={reloadTheme}
socketMessage={socketMessage}
systemInfo={systemInfo}
/>
),
},
{
+120
View File
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { Button, InputNumber, Form, Radio, message, Input } from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config';
import { request } from '@/utils/http';
import CheckUpdate from './checkUpdate';
import { SharedContext } from '@/layouts';
import './index.less';
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Other = ({
systemInfo,
socketMessage,
reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number | null>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
useEffect(() => {
getLogRemoveFrequency();
}, []);
return (
<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 systemInfo={systemInfo} socketMessage={socketMessage} />
</Form.Item>
</Form>
);
};
export default Other;
+21 -26
View File
@@ -243,11 +243,10 @@ const Subscription = () => {
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>();
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef)
const tableScrollHeight = useTableScrollHeight(tableRef);
const runSubscription = (record: any, index: number) => {
Modal.confirm({
@@ -428,28 +427,26 @@ const Subscription = () => {
arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']}
overlay={
<Menu
items={[
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
{
label: record.is_disabled === 1 ? '启用' : '禁用',
key: 'enableOrDisable',
icon:
record.is_disabled === 1 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
),
},
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
]}
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
action(key, record, index);
}}
/>
}
menu={{
items: [
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
{
label: record.is_disabled === 1 ? '启用' : '禁用',
key: 'enableOrDisable',
icon:
record.is_disabled === 1 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
),
},
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
action(key, record, index);
},
}}
>
<a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined />
@@ -553,8 +550,6 @@ const Subscription = () => {
enterButton
allowClear
loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addSubscription()}>
+2 -1
View File
@@ -296,7 +296,7 @@ export default {
documentTitleMap: {
'/login': '登录',
'/initialization': '初始化',
'/cron': '定时任务',
'/crontab': '定时任务',
'/env': '环境变量',
'/subscription': '订阅管理',
'/config': '配置文件',
@@ -305,6 +305,7 @@ export default {
'/log': '日志管理',
'/setting': '系统设置',
'/error': '错误日志',
'/dependence': '依赖管理',
},
dependenceTypes: ['nodejs', 'python3', 'linux'],
};
-1
View File
@@ -56,7 +56,6 @@ _request.interceptors.request.use((url, options) => {
_request.interceptors.response.use(async (response) => {
const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error');
} else if (responseStatus === 401) {
if (history.location.pathname !== '/login') {
+31 -4
View File
@@ -152,9 +152,9 @@ export default function browserType() {
shell === 'none'
? {}
: {
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
);
console.log(
@@ -198,7 +198,7 @@ export function getTableScroll({
if (tHeader) {
mainTop = tHeader.getBoundingClientRect().top;
}
//窗体高度-表格内容顶部的高度-表格内容底部的高度
let height = document.body.clientHeight - mainTop - extraHeight;
return height;
@@ -276,3 +276,30 @@ export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x));
}
export function getCommandScript(
command: string,
): [string, string] | string | undefined {
const cmd = command.split(' ') as string[];
if (cmd[1] === 'repo' || cmd[1] === 'raw') {
return cmd[2];
}
let scriptsPart = cmd.find((x) =>
['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)),
);
if (!scriptsPart) return;
if (scriptsPart.startsWith('/ql/data/scripts')) {
scriptsPart = scriptsPart.replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = scriptsPart.lastIndexOf('/');
if (index >= 0) {
s = scriptsPart.slice(index + 1);
p = scriptsPart.slice(0, index);
} else {
s = scriptsPart;
p = '';
}
return [s, p];
}
+3 -5
View File
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { loader } from '@monaco-editor/react';
import { version } from '../version';
import * as monaco from 'monaco-editor';
export function init() {
export function init(version: string) {
// sentry监控 init
Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
@@ -27,9 +27,7 @@ export function init() {
// monaco 编辑器配置cdn和locale
loader.config({
paths: {
vs: 'https://cdn.staticfile.org/monaco-editor/0.33.0/min/vs',
},
monaco,
'vs/nls': {
availableLanguages: {
'*': 'zh-cn',
-10
View File
@@ -1,10 +0,0 @@
export const version = '2.15.3';
export const changeLogLink = 'https://t.me/jiao_long/354';
export const changeLog = `2.15.3 版本说明
1. 任务视图增加标签筛选
2. 修改默认镜像python版本为3.10
3. 修复notify.js中智能微秘书,感谢 https://github.com/CoolClash
4. 修复超时任务日志打印
5. 修复调试脚本页保存脚本父目录
6. 其他优化
`;
+8
View File
@@ -0,0 +1,8 @@
version: 2.15.6
changeLogLink: https://t.me/jiao_long/358
changeLog: |
1. 修复定时任务数据量大时卡顿,丝滑滚动
2. 修复查看运行中任务日志时自动向下滚动
3. 支持SMTP邮件通知,感谢 https://github.com/WankkoReehttps://github.com/catlair
4. 修改资源通知逻辑
5. 其他bug修复