Compare commits

...
17 Commits
30 changed files with 372 additions and 181 deletions
+3 -1
View File
@@ -105,7 +105,9 @@ jobs:
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
build-args: MAINTAINER=${{ github.repository_owner }}, QL_BRANCH=${{ github.ref_name }} build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
context: docker/ context: docker/
push: true push: true
+1 -1
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png">
</a> </a>
</p> </p>
+12 -10
View File
@@ -9,7 +9,7 @@ import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import path from 'path'; import path, { join } from 'path';
import ScriptService from '../services/script'; import ScriptService from '../services/script';
const route = Router(); const route = Router();
@@ -44,7 +44,7 @@ export default (app: Router) => {
children.push({ children.push({
title: childFile, title: childFile,
value: childFile, value: childFile,
key: `${fileOrDir}-${childFile}`, key: `${fileOrDir}/${childFile}`,
mtime: statObj.mtimeMs, mtime: statObj.mtimeMs,
parent: fileOrDir, parent: fileOrDir,
}); });
@@ -84,10 +84,12 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const path = req.query.path ? `${req.query.path}/` : ''; const filePath = join(
const content = getFileContentByName( config.scriptPath,
`${config.scriptPath}${path}${req.params.file}`, req.query.path as string,
req.params.file,
); );
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -101,7 +103,7 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().allow(''), content: Joi.string().allow(''),
originFilename: Joi.string().allow(''), originFilename: Joi.string().allow(''),
}), }),
@@ -161,7 +163,7 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().required(), content: Joi.string().required(),
}), }),
}), }),
@@ -173,7 +175,7 @@ export default (app: Router) => {
content: string; content: string;
path: string; path: string;
}; };
const filePath = `${config.scriptPath}${path}/${filename}`; const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -198,7 +200,7 @@ export default (app: Router) => {
filename: string; filename: string;
path: string; path: string;
}; };
const filePath = `${config.scriptPath}${path}/${filename}`; const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -254,7 +256,7 @@ export default (app: Router) => {
filename: string; filename: string;
path: string; path: string;
}; };
const filePath = `${path}/${filename}`; const filePath = join(path, filename);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath); const result = await scriptService.runScript(filePath);
res.send(result); res.send(result);
+1 -1
View File
@@ -5,7 +5,7 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const lastVersionFile = const lastVersionFile =
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts'; 'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
const envFound = dotenv.config(); const envFound = dotenv.config();
const rootPath = process.cwd(); const rootPath = process.cwd();
+4
View File
@@ -3,6 +3,7 @@ import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger'; import Logger from './logger';
import initData from './initData'; import initData from './initData';
import { Application } from 'express'; import { Application } from 'express';
import linkDeps from './deps';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
await dependencyInjectorLoader({ await dependencyInjectorLoader({
@@ -15,4 +16,7 @@ export default async ({ expressApp }: { expressApp: Application }) => {
await initData(); await initData();
Logger.info('✌️ init data loaded'); Logger.info('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps');
}; };
+31
View File
@@ -0,0 +1,31 @@
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import config from '../config/index';
function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
}
});
}
export default async (src: string = 'deps') => {
linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
watcher
.on('add', (path) => linkToNodeModule(src))
.on('change', (path) => linkToNodeModule(src));
};
+2 -2
View File
@@ -22,14 +22,14 @@ export default async () => {
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => { dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec(async (err, docs) => {
const groups = _.groupBy(docs, 'type'); const groups = _.groupBy(docs, 'type');
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
const depIds = group.map((x) => x._id); const depIds = group.map((x) => x._id);
for (const dep of depIds) { for (const dep of depIds) {
dependenceService.reInstall([dep]); await dependenceService.reInstall([dep]);
} }
} }
} }
+27 -17
View File
@@ -93,17 +93,21 @@ export default class CronService {
last_running_time: number; last_running_time: number;
last_execution_time: number; last_execution_time: number;
}) { }) {
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb.update( this.cronDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ {
$set: { $set: options,
status,
pid,
log_path,
last_running_time,
last_execution_time,
},
}, },
{ multi: true, returnUpdatedDocs: true }, { multi: true, returnUpdatedDocs: true },
(err) => { (err) => {
@@ -155,7 +159,9 @@ export default class CronService {
public async crontabs(searchText?: string): Promise<Crontab[]> { public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {}; let query = {};
if (searchText) { if (searchText) {
const reg = new RegExp(searchText, 'i'); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
query = { query = {
$or: [ $or: [
{ {
@@ -245,16 +251,20 @@ export default class CronService {
} else { } else {
return; return;
} }
const pids = pid.match(/\(\d+/g); let pids = pid.match(/\(\d+/g);
const killLogs = []; const killLogs = [];
for (const id of pids) { if (pids && pids.length > 0) {
const c = `kill -9 ${id.slice(1)}`; // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
const { stdout, stderr } = await execAsync(c); pids = pids.slice(0, 3);
if (stderr) { for (const id of pids) {
killLogs.push(stderr); const c = `kill -9 ${id.slice(1)}`;
} const { stdout, stderr } = await execAsync(c);
if (stdout) { if (stderr) {
killLogs.push(stdout); killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} }
} }
return killLogs.length > 0 ? JSON.stringify(killLogs) : ''; return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
+95 -89
View File
@@ -131,7 +131,7 @@ export default class DependenceService {
{ $set: { status: DependenceStatus.installing, log: [] } }, { $set: { status: DependenceStatus.installing, log: [] } },
{ multi: true, returnUpdatedDocs: true }, { multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => { async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs); await this.installOrUninstallDependencies(docs);
resolve(docs); resolve(docs);
}, },
); );
@@ -178,103 +178,109 @@ export default class DependenceService {
dependencies: Dependence[], dependencies: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
) { ) {
if (dependencies.length === 0) { return new Promise((resolve) => {
return; if (dependencies.length === 0) {
} resolve(null);
const depNames = dependencies.map((x) => x.name).join(' '); return;
const depRunCommand = ( }
isInstall const depNames = dependencies.map((x) => x.name).join(' ');
? InstallDependenceCommandTypes const depRunCommand = (
: unInstallDependenceCommandTypes isInstall
)[dependencies[0].type as any]; ? InstallDependenceCommandTypes
const actionText = isInstall ? '安装' : '删除'; : unInstallDependenceCommandTypes
const depIds = dependencies.map((x) => x._id) as string[]; )[dependencies[0].type as any];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); const actionText = isInstall ? '安装' : '删除';
const startTime = Date.now(); const depIds = dependencies.map((x) => x._id) as string[];
this.sockService.sendMessage({ const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
type: 'installDependence', const startTime = Date.now();
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}`,
references: depIds,
});
this.updateLog(
depIds,
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: data.toString(), message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
references: depIds, startTime,
}); ).toLocaleString()}`,
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'installDependence',
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds, references: depIds,
}); });
this.updateLog( this.updateLog(
depIds, depIds,
`依赖${actionText}${resultText}结束时间 ${new Date( `开始${actionText}依赖 ${depNames}开始时间 ${new Date(
endTime, startTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`, ).toLocaleString()}\n`,
); );
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
let status = null; cp.stderr.on('data', (data) => {
if (isSucceed) { this.sockService.sendMessage({
status = isInstall type: 'installDependence',
? DependenceStatus.installed message: data.toString(),
: DependenceStatus.removed; references: depIds,
} else { });
status = isInstall this.updateLog(depIds, data.toString());
? DependenceStatus.installFailed });
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录 cp.on('error', (err) => {
if (isSucceed && !isInstall) { this.sockService.sendMessage({
setTimeout(() => { type: 'installDependence',
this.removeDb(depIds); message: JSON.stringify(err),
}, 5000); references: depIds,
} });
this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
this.updateLog(
depIds,
`依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录
if (isSucceed && !isInstall) {
setTimeout(() => {
this.removeDb(depIds);
}, 5000);
}
resolve(null);
});
}); });
} }
} }
+11 -3
View File
@@ -113,7 +113,9 @@ export default class EnvService {
): Promise<Env[]> { ): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
condition = { condition = {
$or: [ $or: [
{ {
@@ -219,11 +221,17 @@ export default class EnvService {
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
env_string += `export ${key}="${_(group) let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled) .filter((x) => x.status !== EnvStatus.disabled)
.map('value') .map('value')
.join('&') .join('&')
.replace(/ /g, '')}"\n`; .replace(/ /g, '');
if (/"/.test(value)) {
value = `'${value}'`;
} else {
value = `"${value}"`;
}
env_string += `export ${key}=${value}\n`;
} }
} }
} }
+3 -1
View File
@@ -97,7 +97,9 @@ export default class OpenService {
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
condition = { condition = {
$or: [ $or: [
{ {
+16 -7
View File
@@ -403,13 +403,22 @@ export default class UserService {
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1]; const currentVersion = currentVersionFile.match(versionRegx)![1];
const lastVersionFileContent = await ( let lastVersion = '';
await got.get(config.lastVersionFile) let lastLog = '';
).body; try {
const lastVersion = lastVersionFileContent.match(versionRegx)![1]; const result = await Promise.race([
const lastLog = lastVersionFileContent.match(logRegx) got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
? lastVersionFileContent.match(logRegx)![1] got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
: ''; timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return { return {
code: 200, code: 200,
+3
View File
@@ -0,0 +1,3 @@
import * as yargs from 'yargs';
yargs.help('h').alias('h', 'help').help().argv;
+1
View File
@@ -6,6 +6,7 @@ link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
+3 -1
View File
@@ -29,6 +29,7 @@
"@sentry/tracing": "^6.14.0", "@sentry/tracing": "^6.14.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"celebrate": "^13.0.3", "celebrate": "^13.0.3",
"chokidar": "^3.5.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^3.5.0", "cron-parser": "^3.5.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
@@ -50,7 +51,8 @@
"sockjs": "^0.3.21", "sockjs": "^0.3.21",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3",
"yargs": "^17.2.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
+1
View File
@@ -285,6 +285,7 @@ def pushplus_bot(title: str, content: str) -> None:
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json() response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200: if response["code"] == 200:
+15
View File
@@ -35,6 +35,8 @@ file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py
dep_notify_js=$dir_dep/sendNotify.js
## 清单文件 ## 清单文件
list_crontab_user=$dir_config/crontab.list list_crontab_user=$dir_config/crontab.list
@@ -210,6 +212,19 @@ fix_config() {
cat /dev/null > /etc/nginx/conf.d/default.conf cat /dev/null > /etc/nginx/conf.d/default.conf
echo echo
fi fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
} }
npm_install_sub() { npm_install_sub() {
-1
View File
@@ -79,6 +79,5 @@ export default {
fixSiderbar: true, fixSiderbar: true,
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
logo: 'https://z3.ax1x.com/2021/11/18/I7MpAe.png',
siderWidth: 180, siderWidth: 180,
} as any; } as any;
+20 -2
View File
@@ -152,8 +152,8 @@
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header { .ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
margin-bottom: 0 !important; margin-bottom: 0 !important;
min-height: calc(100vh - 66px); min-height: calc(100vh - 72px);
min-height: calc(100vh - var(--vh-offset, 0px) - 66px); min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
} }
.Resizer { .Resizer {
@@ -293,3 +293,21 @@
color: #1890ff; color: #1890ff;
} }
} }
.ant-pro-sider-logo {
padding: 16px 8px !important;
h1 {
margin-left: 5px !important;
}
img {
width: 32px !important;
border-radius: 52% !important;
}
}
.ant-pro-global-header-logo {
a img {
// 移动端logo被拉伸
width: auto !important;
}
}
+17 -9
View File
@@ -20,7 +20,7 @@ import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd'; import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
@@ -232,6 +232,12 @@ export default function (props: any) {
selectedKeys={[props.location.pathname]} selectedKeys={[props.location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={
<Image
preview={false}
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/>
}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -275,14 +281,16 @@ export default function (props: any) {
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => ( rightContentRender={() =>
<Dropdown overlay={menu} trigger={['click']}> ctx.isPhone && (
<span className="side-menu-user-wrapper"> <Dropdown overlay={menu} trigger={['click']}>
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <span className="side-menu-user-wrapper">
<span style={{ marginLeft: 5 }}>admin</span> <Avatar shape="square" size="small" icon={<UserOutlined />} />
</span> <span style={{ marginLeft: 5 }}>admin</span>
</Dropdown> </span>
)} </Dropdown>
)
}
collapsedButtonRender={(collapsed) => ( collapsedButtonRender={(collapsed) => (
<span <span
className="side-menu-container" className="side-menu-container"
+1
View File
@@ -13,6 +13,7 @@
} }
.diff-switch-file { .diff-switch-file {
min-width: 768px;
.ant-form-item { .ant-form-item {
margin-bottom: 8px; margin-bottom: 8px;
} }
+1 -1
View File
@@ -77,7 +77,7 @@ const EnvModal = ({
rules={[ rules={[
{ required: true, message: '请输入环境变量名称', whitespace: true }, { required: true, message: '请输入环境变量名称', whitespace: true },
{ {
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/, pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
message: '只能输入字母数字下划线,且不能以数字开头', message: '只能输入字母数字下划线,且不能以数字开头',
}, },
]} ]}
+1 -1
View File
@@ -228,7 +228,7 @@ const Initialization = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="https://z3.ax1x.com/2021/11/18/I7MpAe.png" src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/> />
<span className={styles.title}></span> <span className={styles.title}></span>
</div> </div>
+1
View File
@@ -32,6 +32,7 @@
.logo { .logo {
width: 48px; width: 48px;
height: 48px;
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
+1 -1
View File
@@ -127,7 +127,7 @@ const Login = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="https://z3.ax1x.com/2021/11/18/I7MpAe.png" src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/> />
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
+30 -18
View File
@@ -24,22 +24,22 @@ const prefixMap: any = {
const EditModal = ({ const EditModal = ({
treeData, treeData,
currentFile, currentNode,
content, content,
handleCancel, handleCancel,
visible, visible,
socketMessage, socketMessage,
}: { }: {
treeData?: any; treeData?: any;
currentFile?: string;
content?: string; content?: string;
visible: boolean; visible: boolean;
socketMessage: any; socketMessage: any;
currentNode: any;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript'); const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>(''); const [cNode, setCNode] = useState<any>();
const [selectedKey, setSelectedKey] = useState<string>(''); const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
@@ -53,28 +53,31 @@ const EditModal = ({
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.value === fileName || !value) { if (node.key === selectedKey || !value) {
return; return;
} }
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setFileName(value); setCNode(node);
setLanguage(newMode); setLanguage(newMode);
getDetail(node); getDetail(node);
setSelectedKey(node.key); setSelectedKey(node.key);
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => { request
setValue(data.data); .get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
}); .then((data) => {
setValue(data.data);
});
}; };
const run = () => { const run = () => {
setLog('');
request request
.put(`${config.apiPrefix}scripts/run`, { .put(`${config.apiPrefix}scripts/run`, {
data: { data: {
filename: fileName, filename: cNode.value,
path: '', path: cNode.parent || '',
}, },
}) })
.then((data) => {}); .then((data) => {});
@@ -98,21 +101,21 @@ const EditModal = ({
}, [socketMessage]); }, [socketMessage]);
useEffect(() => { useEffect(() => {
if (currentFile) { if (currentNode) {
setFileName(currentFile); setCNode(currentNode);
setValue(content as string); setValue(content as string);
setSelectedKey(currentFile); setSelectedKey(currentNode.key);
} }
}, [currentFile, content]); }, [content, currentNode]);
return ( return (
<Drawer <Drawer
className="edit-modal" className="edit-modal"
closable={false}
title={ title={
<> <>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect <TreeSelect
style={{ marginRight: 8, width: 120 }} style={{ marginRight: 8, width: 150 }}
value={selectedKey} value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData} treeData={treeData}
@@ -122,7 +125,7 @@ const EditModal = ({
/> />
<Select <Select
value={language} value={language}
style={{ width: 120, marginRight: 8 }} style={{ width: 110, marginRight: 8 }}
onChange={(e) => { onChange={(e) => {
setLanguage(e); setLanguage(e);
}} }}
@@ -162,6 +165,15 @@ const EditModal = ({
> >
</Button> </Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
handleCancel();
}}
>
退
</Button>
</> </>
} }
width={'100%'} width={'100%'}
@@ -197,7 +209,7 @@ const EditModal = ({
content: content:
editorRef.current && editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'), editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName, filename: cNode?.value,
}} }}
/> />
<SettingModal <SettingModal
+3 -3
View File
@@ -116,12 +116,12 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
node: { node: {
title: s, title: s,
value: s, value: s,
key: p ? `${p}-${s}` : s, key: p ? `${p}/${s}` : s,
parent: p, parent: p,
}, },
}; };
setExpandedKeys([p]); setExpandedKeys([p]);
onTreeSelect([`${p}-${s}`], obj); onTreeSelect([`${p}/${s}`], obj);
} }
}; };
@@ -525,7 +525,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
<EditModal <EditModal
visible={isLogModalVisible} visible={isLogModalVisible}
treeData={data} treeData={data}
currentFile={select} currentNode={currentNode}
content={value} content={value}
socketMessage={socketMessage} socketMessage={socketMessage}
handleCancel={() => { handleCancel={() => {
+27 -1
View File
@@ -24,7 +24,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
message.success('已经是最新版了!'); showForceUpdateModal();
} }
} else { } else {
message.error(data); message.error(data);
@@ -39,6 +39,32 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
}; };
const showForceUpdateModal = () => {
Modal.confirm({
width: 500,
title: '更新',
content: (
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version}
</div>
</>
),
okText: '确认',
cancelText: '强制更新',
onCancel() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
.then((_data: any) => {})
.catch((error: any) => {
console.log(error);
});
},
});
};
const showConfirmUpdateModal = (data: any) => { const showConfirmUpdateModal = (data: any) => {
const { lastVersion, lastLog } = data; const { lastVersion, lastLog } = data;
Modal.confirm({ Modal.confirm({
+7 -9
View File
@@ -1,10 +1,8 @@
export const version = '2.10.9'; export const version = '2.10.11';
export const changeLogLink = 'https://t.me/jiao_long/228'; export const changeLogLink = 'https://t.me/jiao_long/230';
export const changeLog = `2.10.9 版本说明 export const changeLog = `2.10.11 版本说明
1. 任务管理支持任务名跳转脚本管理页,感谢 https://github.com/kilo5hz PR 1. 修复环境变量中包含双引号变量异常,感谢 https://github.com/mengshouer
2. 系统通知支持gotify,感谢 https://github.com/kilo5hz PR 2. 修复最后运行时长覆盖逻辑
3. 定时任务列表pageSize增加200/500/1000,感谢 https://github.com/fzls PR 3. 修复内容区高度和logo样式
4. 修复deps目录依赖文件拷贝 4. 修复搜索urlencode的文本时,结果异常
5. 修改alpine基础镜像版本,解决arm32位系统无法启动容器,感谢 https://github.com/lx200916
6. 修复调试功能
`; `;
+34 -2
View File
@@ -2889,7 +2889,7 @@ chokidar@3.5.1:
optionalDependencies: optionalDependencies:
fsevents "~2.3.1" fsevents "~2.3.1"
chokidar@^3.2.2: chokidar@^3.2.2, chokidar@^3.5.2:
version "3.5.2" version "3.5.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
@@ -2995,6 +2995,15 @@ cliui@^6.0.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clone-response@^1.0.2: clone-response@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@@ -4446,7 +4455,7 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-caller-file@^2.0.1: get-caller-file@^2.0.1, get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@@ -10819,6 +10828,11 @@ y18n@^4.0.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@^2.1.2: yallist@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
@@ -10842,6 +10856,11 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^20.2.2:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs@^15.4.1: yargs@^15.4.1:
version "15.4.1" version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
@@ -10859,6 +10878,19 @@ yargs@^15.4.1:
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^18.1.2" yargs-parser "^18.1.2"
yargs@^17.2.1:
version "17.2.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yn@3.1.1: yn@3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"