mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 11:22:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bbff927b1 | ||
|
|
58eb9feec0 | ||
|
|
802ca93a3d |
@@ -374,19 +374,6 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/notify-log',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const data = await systemService.getNotifyLog();
|
|
||||||
res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/log',
|
'/log',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -28,12 +28,6 @@ export enum AuthDataType {
|
|||||||
'removeLogFrequency' = 'removeLogFrequency',
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
'systemConfig' = 'systemConfig',
|
'systemConfig' = 'systemConfig',
|
||||||
'authConfig' = 'authConfig',
|
'authConfig' = 'authConfig',
|
||||||
'notifyLog' = 'notifyLog',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum NotifyStatus {
|
|
||||||
'success',
|
|
||||||
'fail',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemConfigInfo {
|
export interface SystemConfigInfo {
|
||||||
@@ -55,14 +49,6 @@ export interface LoginLogInfo {
|
|||||||
status?: LoginStatus;
|
status?: LoginStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotifyLogInfo {
|
|
||||||
timestamp?: number;
|
|
||||||
title?: string;
|
|
||||||
content?: string;
|
|
||||||
status?: NotifyStatus;
|
|
||||||
notifyType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenInfo {
|
export interface TokenInfo {
|
||||||
value: string;
|
value: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -95,7 +81,6 @@ export interface AuthInfo {
|
|||||||
export type SystemModelInfo = SystemConfigInfo &
|
export type SystemModelInfo = SystemConfigInfo &
|
||||||
Partial<NotificationInfo> &
|
Partial<NotificationInfo> &
|
||||||
LoginLogInfo &
|
LoginLogInfo &
|
||||||
Partial<NotifyLogInfo> &
|
|
||||||
Partial<AuthInfo>;
|
Partial<AuthInfo>;
|
||||||
|
|
||||||
export interface SystemInstance
|
export interface SystemInstance
|
||||||
|
|||||||
+5
-34
@@ -13,29 +13,9 @@ import { isValidToken } from '../shared/auth';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
// Security: Enable strict routing to prevent case-insensitive path bypass
|
|
||||||
app.set('case sensitive routing', true);
|
|
||||||
app.set('strict routing', true);
|
|
||||||
app.set('trust proxy', 'loopback');
|
app.set('trust proxy', 'loopback');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// Security: Path normalization middleware to prevent case variation attacks
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const originalPath = req.path;
|
|
||||||
const normalizedPath = originalPath.toLowerCase();
|
|
||||||
|
|
||||||
// Block requests with case variations on protected paths
|
|
||||||
if (originalPath !== normalizedPath &&
|
|
||||||
(normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) {
|
|
||||||
return res.status(400).json({
|
|
||||||
code: 400,
|
|
||||||
message: 'Invalid path format'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rewrite URLs to strip baseUrl prefix if configured
|
// Rewrite URLs to strip baseUrl prefix if configured
|
||||||
// This allows the rest of the app to work without baseUrl awareness
|
// This allows the rest of the app to work without baseUrl awareness
|
||||||
if (config.baseUrl) {
|
if (config.baseUrl) {
|
||||||
@@ -56,7 +36,7 @@ export default ({ app }: { app: Application }) => {
|
|||||||
secret: config.jwt.secret,
|
secret: config.jwt.secret,
|
||||||
algorithms: ['HS384'],
|
algorithms: ['HS384'],
|
||||||
}).unless({
|
}).unless({
|
||||||
path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i],
|
path: [...config.apiWhiteList, /^\/(?!api\/).*/],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -71,20 +51,19 @@ export default ({ app }: { app: Application }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(async (req: Request, res, next) => {
|
app.use(async (req: Request, res, next) => {
|
||||||
const pathLower = req.path.toLowerCase();
|
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {
|
||||||
if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) {
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerToken = getToken(req);
|
const headerToken = getToken(req);
|
||||||
if (pathLower.startsWith('/open/')) {
|
if (req.path.startsWith('/open/')) {
|
||||||
const apps = await shareStore.getApps();
|
const apps = await shareStore.getApps();
|
||||||
const doc = apps?.filter((x) =>
|
const doc = apps?.filter((x) =>
|
||||||
x.tokens?.find((y) => y.value === headerToken),
|
x.tokens?.find((y) => y.value === headerToken),
|
||||||
)?.[0];
|
)?.[0];
|
||||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||||
const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/);
|
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||||
const key = keyMatch && keyMatch[1];
|
const key = keyMatch && keyMatch[1];
|
||||||
if (
|
if (
|
||||||
doc.scopes.includes(key as any) &&
|
doc.scopes.includes(key as any) &&
|
||||||
@@ -119,15 +98,7 @@ export default ({ app }: { app: Application }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(async (req, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
const pathLower = req.path.toLowerCase();
|
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||||
if (
|
|
||||||
![
|
|
||||||
'/api/user/init',
|
|
||||||
'/api/user/notification/init',
|
|
||||||
'/open/user/init',
|
|
||||||
'/open/user/notification/init',
|
|
||||||
].includes(req.path)
|
|
||||||
) {
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const authInfo =
|
const authInfo =
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { AuthDataType, SystemModel } from '../data/system';
|
|||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { writeFile, readFile } from 'fs/promises';
|
import { writeFile, readFile } from 'fs/promises';
|
||||||
import { createRandomString, fileExist, isDemoEnv, safeJSONParse } from '../config/util';
|
import { createRandomString, fileExist, safeJSONParse } from '../config/util';
|
||||||
import OpenService from '../services/open';
|
import OpenService from '../services/open';
|
||||||
import { shareStore } from '../shared/store';
|
import { shareStore } from '../shared/store';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
@@ -50,7 +50,7 @@ export default async () => {
|
|||||||
const [authConfig] = await SystemModel.findOrCreate({
|
const [authConfig] = await SystemModel.findOrCreate({
|
||||||
where: { type: AuthDataType.authConfig },
|
where: { type: AuthDataType.authConfig },
|
||||||
});
|
});
|
||||||
if (!authConfig?.info || isDemoEnv()) {
|
if (!authConfig?.info) {
|
||||||
let authInfo = {
|
let authInfo = {
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: 'admin',
|
password: 'admin',
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ import {
|
|||||||
SystemInstance,
|
SystemInstance,
|
||||||
SystemModel,
|
SystemModel,
|
||||||
SystemModelInfo,
|
SystemModelInfo,
|
||||||
NotifyStatus,
|
|
||||||
NotifyLogInfo,
|
|
||||||
} from '../data/system';
|
} from '../data/system';
|
||||||
import taskLimit from '../shared/pLimit';
|
import taskLimit from '../shared/pLimit';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
@@ -391,34 +389,11 @@ export default class SystemService {
|
|||||||
if (notificationInfo && typeString) {
|
if (notificationInfo && typeString) {
|
||||||
notificationInfo.type = typeString;
|
notificationInfo.type = typeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
let notifyType: string | undefined;
|
|
||||||
if (notificationInfo?.type) {
|
|
||||||
notifyType = typeString || (notificationInfo.type as string);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const notifConfig = await this.getDb({ type: AuthDataType.notification });
|
|
||||||
notifyType = notifConfig.info?.type as string | undefined;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSuccess = await this.notificationService.notify(
|
const isSuccess = await this.notificationService.notify(
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
notificationInfo,
|
notificationInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
await SystemModel.create({
|
|
||||||
type: AuthDataType.notifyLog,
|
|
||||||
info: {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
status: isSuccess ? NotifyStatus.success : NotifyStatus.fail,
|
|
||||||
notifyType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
return { code: 200, message: '通知发送成功' };
|
return { code: 200, message: '通知发送成功' };
|
||||||
} else {
|
} else {
|
||||||
@@ -426,18 +401,6 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotifyLog(): Promise<Array<NotifyLogInfo>> {
|
|
||||||
const docs = await SystemModel.findAll({
|
|
||||||
where: { type: AuthDataType.notifyLog },
|
|
||||||
order: [['id', 'DESC']],
|
|
||||||
});
|
|
||||||
if (docs.length > 200) {
|
|
||||||
const ids = docs.slice(200).map((x) => x.id!);
|
|
||||||
await SystemModel.destroy({ where: { id: ids } });
|
|
||||||
}
|
|
||||||
return docs.slice(0, 200).map((x) => ({ ...x.info, id: x.id }));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {
|
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {
|
||||||
if (!command.startsWith(TASK_COMMAND)) {
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
command = `${TASK_COMMAND} ${command}`;
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
|
|||||||
+28
-2
@@ -14,6 +14,7 @@ import {
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { credentials } from '@grpc/grpc-js';
|
import { credentials } from '@grpc/grpc-js';
|
||||||
import { ApiClient } from '../protos/api';
|
import { ApiClient } from '../protos/api';
|
||||||
|
import { CrontabModel } from '../data/cron';
|
||||||
|
|
||||||
class TaskLimit {
|
class TaskLimit {
|
||||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||||
@@ -131,13 +132,38 @@ class TaskLimit {
|
|||||||
let runs = this.queuedCrons.get(cron.id);
|
let runs = this.queuedCrons.get(cron.id);
|
||||||
const result = runs?.length ? [...runs, fn] : [fn];
|
const result = runs?.length ? [...runs, fn] : [fn];
|
||||||
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
||||||
if (result?.length > 5) {
|
|
||||||
|
// Check instance mode from database to determine queue limit
|
||||||
|
let maxQueueSize = 10; // Default for multi-instance mode (increased from 5)
|
||||||
|
let isSingleInstanceMode = false;
|
||||||
|
try {
|
||||||
|
const cronRecord = await CrontabModel.findOne({
|
||||||
|
where: { id: Number(cron.id) },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default to single instance mode (0) for backward compatibility
|
||||||
|
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
||||||
|
isSingleInstanceMode = cronRecord?.allow_multiple_instances !== 1;
|
||||||
|
|
||||||
|
if (isSingleInstanceMode) {
|
||||||
|
// For single instance mode, allow up to 2 queued tasks
|
||||||
|
// This allows the new task to be queued while the old one is being killed
|
||||||
|
maxQueueSize = 2;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][检查实例模式失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.length > maxQueueSize) {
|
||||||
if (repeatTimes < 3) {
|
if (repeatTimes < 3) {
|
||||||
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
||||||
|
const modeStr = isSingleInstanceMode ? '单实例' : '多实例';
|
||||||
this.client.systemNotify(
|
this.client.systemNotify(
|
||||||
{
|
{
|
||||||
title: '任务重复运行',
|
title: '任务重复运行',
|
||||||
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
content: `任务:${cron.name}(${modeStr}模式),命令:${cron.command},定时:${cron.schedule},处于运行中的超过 ${maxQueueSize} 个,请检查定时设置`,
|
||||||
},
|
},
|
||||||
(err, res) => {
|
(err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
+27
-3
@@ -15,11 +15,12 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Default to single instance mode (0) for backward compatibility
|
// Default to single instance mode (0) for backward compatibility
|
||||||
const allowSingleInstances =
|
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
||||||
existingCron?.allow_multiple_instances === 0;
|
const isSingleInstanceMode =
|
||||||
|
existingCron?.allow_multiple_instances !== 1;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
allowSingleInstances &&
|
isSingleInstanceMode &&
|
||||||
existingCron &&
|
existingCron &&
|
||||||
existingCron.pid &&
|
existingCron.pid &&
|
||||||
(existingCron.status === CrontabStatus.running ||
|
(existingCron.status === CrontabStatus.running ||
|
||||||
@@ -49,6 +50,18 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
);
|
);
|
||||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
// Update status to running after spawning the process
|
||||||
|
try {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.running, pid: cp.pid },
|
||||||
|
{ where: { id: Number(cron.id) } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
@@ -66,6 +79,17 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
taskLimit.removeQueuedCron(cron.id);
|
taskLimit.removeQueuedCron(cron.id);
|
||||||
|
// Update status to idle after task completes
|
||||||
|
try {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: Number(cron.id) } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -69,10 +69,9 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
|||||||
|
|
||||||
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||||
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||||
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \
|
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||||
HOME=/root
|
|
||||||
|
|
||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
|
||||||
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
||||||
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||||
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||||
@@ -84,6 +83,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
|||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
CMD curl -sf --noproxy '*' http://127.0.0.1:${QlPort:-5700}/api/health || exit 1
|
CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
+3
-4
@@ -69,10 +69,9 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
|||||||
|
|
||||||
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||||
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||||
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \
|
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||||
HOME=/root
|
|
||||||
|
|
||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
|
||||||
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
||||||
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||||
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||||
@@ -84,6 +83,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
|||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
|
||||||
CMD curl -sf --noproxy '*' http://127.0.0.1:${QlPort:-5700}/api/health || exit 1
|
CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
export PATH="$HOME/bin:$PATH"
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=/ql/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -77,9 +77,9 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "2.1.1",
|
"multer": "1.4.5-lts.1",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemailer": "^8.0.1",
|
"nodemailer": "^6.9.16",
|
||||||
"p-queue-cjs": "7.3.4",
|
"p-queue-cjs": "7.3.4",
|
||||||
"@bufbuild/protobuf": "^2.10.0",
|
"@bufbuild/protobuf": "^2.10.0",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
|
|||||||
Generated
+259
-568
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,6 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import SecuritySettings from './security';
|
import SecuritySettings from './security';
|
||||||
import LoginLog from './loginLog';
|
import LoginLog from './loginLog';
|
||||||
import NotifyLog from './notifyLog';
|
|
||||||
import NotificationSetting from './notification';
|
import NotificationSetting from './notification';
|
||||||
import Other from './other';
|
import Other from './other';
|
||||||
import About from './about';
|
import About from './about';
|
||||||
@@ -126,7 +125,6 @@ const Setting = () => {
|
|||||||
const [editedApp, setEditedApp] = useState<any>();
|
const [editedApp, setEditedApp] = useState<any>();
|
||||||
const [tabActiveKey, setTabActiveKey] = useState('security');
|
const [tabActiveKey, setTabActiveKey] = useState('security');
|
||||||
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
||||||
const [notifyLogData, setNotifyLogData] = useState<any[]>([]);
|
|
||||||
const [notificationInfo, setNotificationInfo] = useState<any>();
|
const [notificationInfo, setNotificationInfo] = useState<any>();
|
||||||
const containergRef = useRef<HTMLDivElement>(null);
|
const containergRef = useRef<HTMLDivElement>(null);
|
||||||
const [height, setHeight] = useState<number>(0);
|
const [height, setHeight] = useState<number>(0);
|
||||||
@@ -255,8 +253,6 @@ const Setting = () => {
|
|||||||
getApps();
|
getApps();
|
||||||
} else if (activeKey === 'login') {
|
} else if (activeKey === 'login') {
|
||||||
getLoginLog();
|
getLoginLog();
|
||||||
} else if (activeKey === 'notifylog') {
|
|
||||||
getNotifyLog();
|
|
||||||
} else if (activeKey === 'notification') {
|
} else if (activeKey === 'notification') {
|
||||||
getNotification();
|
getNotification();
|
||||||
}
|
}
|
||||||
@@ -275,19 +271,6 @@ const Setting = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNotifyLog = () => {
|
|
||||||
request
|
|
||||||
.get(`${config.apiPrefix}system/notify-log`)
|
|
||||||
.then(({ code, data }) => {
|
|
||||||
if (code === 200) {
|
|
||||||
setNotifyLogData(data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDemoEnv) {
|
if (isDemoEnv) {
|
||||||
getApps();
|
getApps();
|
||||||
@@ -361,11 +344,6 @@ const Setting = () => {
|
|||||||
label: intl.get('登录日志'),
|
label: intl.get('登录日志'),
|
||||||
children: <LoginLog height={height} data={loginLogData} />,
|
children: <LoginLog height={height} data={loginLogData} />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'notifylog',
|
|
||||||
label: intl.get('通知日志'),
|
|
||||||
children: <NotifyLog height={height} data={notifyLogData} />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'dependence',
|
key: 'dependence',
|
||||||
label: intl.get('依赖设置'),
|
label: intl.get('依赖设置'),
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import intl from 'react-intl-universal';
|
|
||||||
import React from 'react';
|
|
||||||
import { Table, Tag } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
interface NotifyLogItem {
|
|
||||||
id?: number;
|
|
||||||
timestamp?: number;
|
|
||||||
title?: string;
|
|
||||||
content?: string;
|
|
||||||
status?: number;
|
|
||||||
notifyType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NotifyStatusLabel: Record<number, string> = {
|
|
||||||
0: '成功',
|
|
||||||
1: '失败',
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotifyStatusColor: Record<number, string> = {
|
|
||||||
0: 'success',
|
|
||||||
1: 'error',
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: intl.get('序号'),
|
|
||||||
width: 50,
|
|
||||||
render: (text: string, record: any, index: number) => {
|
|
||||||
return index + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('发送时间'),
|
|
||||||
dataIndex: 'timestamp',
|
|
||||||
key: 'timestamp',
|
|
||||||
width: 160,
|
|
||||||
render: (text: string, record: any) => {
|
|
||||||
return dayjs(record.timestamp).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('标题'),
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('内容'),
|
|
||||||
dataIndex: 'content',
|
|
||||||
key: 'content',
|
|
||||||
render: (text: string) => {
|
|
||||||
if (!text) return '';
|
|
||||||
return text.length > 100 ? text.slice(0, 100) + '...' : text;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('推送渠道'),
|
|
||||||
dataIndex: 'notifyType',
|
|
||||||
key: 'notifyType',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('发送状态'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
width: 90,
|
|
||||||
render: (text: string, record: NotifyLogItem) => {
|
|
||||||
const statusKey = record.status ?? 1;
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
color={NotifyStatusColor[statusKey]}
|
|
||||||
style={{ marginRight: 0 }}
|
|
||||||
>
|
|
||||||
{intl.get(NotifyStatusLabel[statusKey])}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const NotifyLog = ({
|
|
||||||
data,
|
|
||||||
height,
|
|
||||||
}: {
|
|
||||||
data: Array<NotifyLogItem>;
|
|
||||||
height: number;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
pagination={false}
|
|
||||||
dataSource={data}
|
|
||||||
rowKey="id"
|
|
||||||
size="middle"
|
|
||||||
scroll={{ x: 1000, y: height }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotifyLog;
|
|
||||||
+10
-5
@@ -1,6 +1,11 @@
|
|||||||
version: 2.20.2
|
version: 2.20.1
|
||||||
changeLogLink: https://t.me/jiao_long/434
|
changeLogLink: https://t.me/jiao_long/433
|
||||||
publishTime: 2026-03-01 1800
|
publishTime: 2025-12-26 22:00
|
||||||
changeLog: |
|
changeLog: |
|
||||||
1. 修复 path 安全漏洞(重要)
|
1. 修复获取依赖管理列表
|
||||||
|
2. notify.js 修复 TG_PROXY_AUTH 参数拼接
|
||||||
|
3. QLAPI.notify larkSecret 参数
|
||||||
|
4. 修复 cron parser 定时规则校验
|
||||||
|
5. 修复设置 baseUrl 后无法访问
|
||||||
|
6. 修复环境变量排序
|
||||||
|
7. 修复定时任务无法停止
|
||||||
Reference in New Issue
Block a user