diff --git a/back/api/system.ts b/back/api/system.ts index a324a3e1714..1ef84b588b0 100644 --- a/back/api/system.ts +++ b/back/api/system.ts @@ -70,12 +70,12 @@ export default (app: Router) => { }); route.get( - '/log/remove', + '/config', async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { const systemService = Container.get(SystemService); - const data = await systemService.getLogRemoveFrequency(); + const data = await systemService.getSystemConfig(); res.send({ code: 200, data }); } catch (e) { return next(e); @@ -84,18 +84,19 @@ export default (app: Router) => { ); route.put( - '/log/remove', + '/config', celebrate({ body: Joi.object({ - frequency: Joi.number().required(), + logRemoveFrequency: Joi.number().optional().allow(null), + cronConcurrency: Joi.number().optional().allow(null), }), }), async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { const systemService = Container.get(SystemService); - const result = await systemService.updateLogRemoveFrequency( - req.body.frequency, + const result = await systemService.updateSystemConfig( + req.body, ); res.send(result); } catch (e) { diff --git a/back/data/auth.ts b/back/data/auth.ts index e3000f434cc..d6789e83130 100644 --- a/back/data/auth.ts +++ b/back/data/auth.ts @@ -1,10 +1,11 @@ import { sequelize } from '.'; import { DataTypes, Model, ModelDefined } from 'sequelize'; +import { NotificationInfo } from './notify'; export class AuthInfo { ip?: string; type: AuthDataType; - info?: any; + info?: AuthModelInfo; id?: number; constructor(options: AuthInfo) { @@ -25,9 +26,25 @@ export enum AuthDataType { 'authToken' = 'authToken', 'notification' = 'notification', 'removeLogFrequency' = 'removeLogFrequency', + 'systemConfig' = 'systemConfig', } -interface AuthInstance extends Model, AuthInfo {} +export interface SystemConfigInfo { + logRemoveFrequency?: number; + cronConcurrency?: number; +} + +export interface LoginLogInfo { + timestamp?: number; + address?: string; + ip?: string; + platform?: string; + status?: LoginStatus, +} + +export interface AuthModelInfo extends SystemConfigInfo, NotificationInfo, LoginLogInfo { } + +interface AuthInstance extends Model, AuthInfo { } export const AuthModel = sequelize.define('Auth', { ip: DataTypes.STRING, type: DataTypes.STRING, diff --git a/back/loaders/initFile.ts b/back/loaders/initFile.ts index 6937c202016..e076f4253ce 100644 --- a/back/loaders/initFile.ts +++ b/back/loaders/initFile.ts @@ -18,10 +18,13 @@ const confFile = path.join(configPath, 'config.sh'); const authConfigFile = path.join(configPath, 'auth.json'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleAuthFile = path.join(samplePath, 'auth.sample.json'); +const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh'); const sampleNotifyJsFile = path.join(samplePath, 'notify.js'); const sampleNotifyPyFile = path.join(samplePath, 'notify.py'); const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js'); const scriptNotifyPyFile = path.join(scriptPath, 'notify.py'); +const TaskBeforeFile = path.join(configPath, 'task_before.sh'); +const TaskAfterFile = path.join(configPath, 'task_after.sh'); const homedir = os.homedir(); const sshPath = path.resolve(homedir, '.ssh'); const sshdPath = path.join(dataPath, 'ssh.d'); @@ -39,6 +42,8 @@ export default async () => { const tmpDirExist = await fileExist(tmpPath); const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile); const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile); + const TaskBeforeFileExist = await fileExist(TaskBeforeFile); + const TaskAfterFileExist = await fileExist(TaskAfterFile); if (!configDirExist) { fs.mkdirSync(configPath); @@ -89,6 +94,14 @@ export default async () => { fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile)); } + if (!TaskBeforeFileExist) { + fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile)); + } + + if (!TaskAfterFileExist) { + fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile)); + } + dotenv.config({ path: confFile }); Logger.info('✌️ Init file down'); diff --git a/back/loaders/initTask.ts b/back/loaders/initTask.ts index b219dbf9d33..06441242315 100644 --- a/back/loaders/initTask.ts +++ b/back/loaders/initTask.ts @@ -29,7 +29,7 @@ export default async () => { }); // 运行删除日志任务 - const data = await systemService.getLogRemoveFrequency(); + const data = await systemService.getSystemConfig(); if (data && data.info && data.info.frequency) { const rmlogCron = { id: data.id, diff --git a/back/services/cron.ts b/back/services/cron.ts index 1e3559f55c2..7799ad40046 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -16,7 +16,7 @@ import { Op, where, col as colFn, FindOptions } from 'sequelize'; import path from 'path'; import { TASK_PREFIX, QL_PREFIX } from '../config/const'; import cronClient from '../schedule/client'; -import { runWithCpuLimit } from '../shared/pLimit'; +import taskLimit from '../shared/pLimit'; import { spawn } from 'cross-spawn'; @Service() @@ -387,7 +387,7 @@ export default class CronService { } private async runSingle(cronId: number): Promise { - return runWithCpuLimit(() => { + return taskLimit.runWithCpuLimit(() => { return new Promise(async (resolve: any) => { const cron = await this.getDb({ id: cronId }); if (cron.status !== CrontabStatus.queued) { diff --git a/back/services/dependence.ts b/back/services/dependence.ts index 5323882f7f6..6b76f7b0f8a 100644 --- a/back/services/dependence.ts +++ b/back/services/dependence.ts @@ -14,7 +14,7 @@ import SockService from './sock'; import { FindOptions, Op } from 'sequelize'; import { concurrentRun } from '../config/util'; import dayjs from 'dayjs'; -import { runOneByOne, runWithCpuLimit } from '../shared/pLimit'; +import taskLimit from '../shared/pLimit'; @Service() export default class DependenceService { @@ -147,7 +147,7 @@ export default class DependenceService { isInstall: boolean = true, force: boolean = false, ) { - return runOneByOne(() => { + return taskLimit.runOneByOne(() => { return new Promise(async (resolve) => { const depIds = [dependency.id!]; const status = isInstall diff --git a/back/services/schedule.ts b/back/services/schedule.ts index 4eac4fc1a82..a0d157a9245 100644 --- a/back/services/schedule.ts +++ b/back/services/schedule.ts @@ -9,7 +9,7 @@ import { Task, } from 'toad-scheduler'; import dayjs from 'dayjs'; -import { runWithCpuLimit } from '../shared/pLimit'; +import taskLimit from '../shared/pLimit'; import { spawn } from 'cross-spawn'; interface ScheduleTaskType { @@ -49,7 +49,7 @@ export default class ScheduleService { callbacks: TaskCallbacks = {}, completionTime: 'start' | 'end' = 'end', ) { - return runWithCpuLimit(() => { + return taskLimit.runWithCpuLimit(() => { return new Promise(async (resolve, reject) => { try { const startTime = dayjs(); diff --git a/back/services/system.ts b/back/services/system.ts index 8bf00a5d036..e754a51ddeb 100644 --- a/back/services/system.ts +++ b/back/services/system.ts @@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi'; import winston from 'winston'; import config from '../config'; import * as fs from 'fs'; -import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; +import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo } from '../data/auth'; import { NotificationInfo } from '../data/notify'; import NotificationService from './notify'; import ScheduleService, { TaskCallbacks } from './schedule'; @@ -16,6 +16,7 @@ import { parseVersion, } from '../config/util'; import { TASK_COMMAND } from '../config/const'; +import taskLimit from '../shared/pLimit' @Service() export default class SystemService { @@ -28,8 +29,8 @@ export default class SystemService { private sockService: SockService, ) {} - public async getLogRemoveFrequency() { - const doc = await this.getDb({ type: AuthDataType.removeLogFrequency }); + public async getSystemConfig() { + const doc = await this.getDb({ type: AuthDataType.systemConfig }); return doc || {}; } @@ -62,25 +63,30 @@ export default class SystemService { } } - public async updateLogRemoveFrequency(frequency: number) { - const oDoc = await this.getLogRemoveFrequency(); + public async updateSystemConfig(info: AuthModelInfo) { + const oDoc = await this.getSystemConfig(); const result = await this.updateAuthDb({ ...oDoc, - type: AuthDataType.removeLogFrequency, - info: { frequency }, + type: AuthDataType.systemConfig, + info, }); - const cron = { - id: result.id, - name: '删除日志', - command: `ql rmlog ${frequency}`, - }; - await this.scheduleService.cancelIntervalTask(cron); - if (frequency > 0) { - this.scheduleService.createIntervalTask(cron, { - days: frequency, - }); + if (info.logRemoveFrequency) { + const cron = { + id: result.id, + name: '删除日志', + command: `ql rmlog ${info.logRemoveFrequency}`, + }; + await this.scheduleService.cancelIntervalTask(cron); + if (info.logRemoveFrequency > 0) { + this.scheduleService.createIntervalTask(cron, { + days: info.logRemoveFrequency, + }); + } + } + if (info.cronConcurrency) { + await taskLimit.setCustomLimit(info.cronConcurrency); } - return { code: 200, data: { ...cron } }; + return { code: 200, data: info }; } public async checkUpdate() { diff --git a/back/shared/pLimit.ts b/back/shared/pLimit.ts index a582c72a52f..a79eff60977 100644 --- a/back/shared/pLimit.ts +++ b/back/shared/pLimit.ts @@ -1,17 +1,40 @@ import pLimit from "p-limit"; import os from 'os'; +import { AuthDataType, AuthModel } from "../data/auth"; -const cpuLimit = pLimit(os.cpus().length); -const oneLimit = pLimit(1); +class TaskLimit { + private customLimit: number = 0; + private oneLimit = pLimit(1); + private get cpuLimit() { + return pLimit(this.customLimit || Math.max(os.cpus().length, 4)) + } -export function runWithCpuLimit(fn: () => Promise): Promise { - return cpuLimit(() => { - return fn(); - }); -} + constructor() { + this.setCustomLimit(); + } + + public async setCustomLimit(limit?: number) { + if (limit) { + this.customLimit = limit; + return; + } + const doc = await AuthModel.findOne({ where: { type: AuthDataType.systemConfig } }); + if (doc?.info?.cronConcurrency) { + this.customLimit = doc?.info?.cronConcurrency; + } + } -export function runOneByOne(fn: () => Promise): Promise { - return oneLimit(() => { - return fn(); - }); + public runWithCpuLimit(fn: () => Promise): Promise { + return this.cpuLimit(() => { + return fn(); + }); + } + + public runOneByOne(fn: () => Promise): Promise { + return this.oneLimit(() => { + return fn(); + }); + } } + +export default new TaskLimit(); diff --git a/back/shared/runCron.ts b/back/shared/runCron.ts index cd11bbff911..c9d2da9f7c0 100644 --- a/back/shared/runCron.ts +++ b/back/shared/runCron.ts @@ -1,9 +1,9 @@ import { spawn } from 'cross-spawn'; -import { runWithCpuLimit } from "./pLimit"; +import taskLimit from "./pLimit"; import Logger from '../loaders/logger'; export function runCron(cmd: string): Promise { - return runWithCpuLimit(() => { + return taskLimit.runWithCpuLimit(() => { return new Promise(async (resolve: any) => { Logger.silly('运行命令: ' + cmd); diff --git a/sample/config.sample.sh b/sample/config.sample.sh index c5e4ab176d1..85782464d4b 100644 --- a/sample/config.sample.sh +++ b/sample/config.sample.sh @@ -91,6 +91,10 @@ export TG_API_HOST="" export DD_BOT_TOKEN="" export DD_BOT_SECRET="" +## 企业微信反向代理地址 +## (环境变量名 QYWX_ORIGIN) +export QYWX_ORIGIN="" + ## 5. 企业微信机器人 ## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770 ## 下方填写密钥,企业微信推送 webhook 后面的 key diff --git a/src/pages/error/index.less b/src/pages/error/index.less index a1c5c974b7a..cd3cddc198f 100644 --- a/src/pages/error/index.less +++ b/src/pages/error/index.less @@ -1,6 +1,5 @@ .error-wrapper { display: flex; - align-items: center; justify-content: center; height: 100vh; diff --git a/src/pages/error/index.tsx b/src/pages/error/index.tsx index 6811ae4781a..c185e86b93f 100644 --- a/src/pages/error/index.tsx +++ b/src/pages/error/index.tsx @@ -81,7 +81,7 @@ const Error = () => { ) : ( - + )} ); diff --git a/src/pages/setting/other.tsx b/src/pages/setting/other.tsx index a216e24da85..62b36e3e253 100644 --- a/src/pages/setting/other.tsx +++ b/src/pages/setting/other.tsx @@ -19,7 +19,10 @@ const Other = ({ reloadTheme, }: Pick) => { const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto'; - const [logRemoveFrequency, setLogRemoveFrequency] = useState(); + const [systemConfig, setSystemConfig] = useState<{ + logRemoveFrequency?: number | null; + cronConcurrency?: number | null; + }>(); const [form] = Form.useForm(); const { @@ -45,13 +48,12 @@ const Other = ({ reloadTheme(); }; - const getLogRemoveFrequency = () => { + const getSystemConfig = () => { request - .get(`${config.apiPrefix}system/log/remove`) + .get(`${config.apiPrefix}system/config`) .then(({ code, data }) => { if (code === 200 && data.info) { - const { frequency } = data.info; - setLogRemoveFrequency(frequency); + setSystemConfig(data.info); } }) .catch((error: any) => { @@ -59,25 +61,23 @@ const Other = ({ }); }; - 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); - }); - }); + const updateSystemConfig = () => { + request + .put(`${config.apiPrefix}system/config`, { + data: { ...systemConfig }, + }) + .then(({ code, data }) => { + if (code === 200) { + message.success('更新成功'); + } + }) + .catch((error: any) => { + console.log(error); + }); }; useEffect(() => { - getLogRemoveFrequency(); + getSystemConfig(); }, []); return ( @@ -100,12 +100,29 @@ const Other = ({ setLogRemoveFrequency(value)} + value={systemConfig?.logRemoveFrequency} + onChange={(value) => { + setSystemConfig({ ...systemConfig, logRemoveFrequency: value }); + }} + /> + + + + + + { + setSystemConfig({ ...systemConfig, cronConcurrency: value }); + }} /> -