Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andwhyour 855f591992 Improve type safety for fileHandle and release variables
- Add explicit FileHandle type annotation for fileHandle variable
- Change fileHandle check from truthy to explicit undefined check
- Change release type from null to undefined for better type safety

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:49:27 +00:00
copilot-swe-agent[bot]andwhyour d30eb2008c Add error handling for file handle close operation
- Wrap fileHandle.close() in try-catch to prevent masking original errors
- Log close errors without throwing to preserve error context

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:46:51 +00:00
copilot-swe-agent[bot]andwhyour e1ce0f3fa9 Improve error handling based on code review feedback
- Use finally block to ensure file handle is closed even on error
- Extract error messages properly using instanceof Error check
- Improve error message formatting for better debugging

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:45:34 +00:00
copilot-swe-agent[bot]andwhyour 82514e65e1 Add server timeout configuration and improve error handling for script save operations
- Configure HTTP server timeouts (requestTimeout: 5min, headersTimeout: 2min, keepAliveTimeout: 65s)
- Add better error logging in PUT /scripts endpoint
- Improve writeFileWithLock error handling with descriptive messages and proper cleanup
- Ensure lock is always released even on error

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:43:11 +00:00
copilot-swe-agent[bot] 31261190f0 Initial plan 2025-12-27 08:35:20 +00:00
16 changed files with 353 additions and 825 deletions
+2
View File
@@ -206,6 +206,7 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body as { let { filename, content, path } = req.body as {
filename: string; filename: string;
@@ -223,6 +224,7 @@ export default (app: Router) => {
await writeFileWithLock(filePath, content); await writeFileWithLock(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
logger.error('🔥 error saving script: %o', e);
return next(e); return next(e);
} }
}, },
-13
View File
@@ -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) => {
-15
View File
@@ -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
View File
@@ -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 =
+2 -2
View File
@@ -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',
+11
View File
@@ -16,6 +16,17 @@ export class HttpServerService {
metricsService.record('http_service_start', 1, { metricsService.record('http_service_start', 1, {
port: port.toString(), port: port.toString(),
}); });
// Set server timeouts to prevent premature connection drops
if (this.server) {
// Timeout for receiving the entire request (including body) - 5 minutes
this.server.requestTimeout = 300000;
// Timeout for headers - 2 minutes
this.server.headersTimeout = 120000;
// Keep-alive timeout - 65 seconds (slightly more than typical load balancer timeout)
this.server.keepAliveTimeout = 65000;
}
resolve(this.server); resolve(this.server);
}); });
-37
View File
@@ -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}`;
+45 -7
View File
@@ -1,8 +1,9 @@
import { lock } from 'proper-lockfile'; import { lock } from 'proper-lockfile';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { writeFile, open, chmod } from 'fs/promises'; import { writeFile, open, chmod, FileHandle } from 'fs/promises';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import Logger from '../loaders/logger';
function getUniqueLockPath(filePath: string) { function getUniqueLockPath(filePath: string) {
const sanitizedPath = filePath const sanitizedPath = filePath
@@ -19,13 +20,32 @@ export async function writeFileWithLock(
if (typeof options === 'string') { if (typeof options === 'string') {
options = { encoding: options }; options = { encoding: options };
} }
if (!(await fileExist(filePath))) {
const fileHandle = await open(filePath, 'w');
fileHandle.close();
}
const lockfilePath = getUniqueLockPath(filePath);
const release = await lock(filePath, { // Ensure file exists before locking
if (!(await fileExist(filePath))) {
let fileHandle: FileHandle | undefined;
try {
fileHandle = await open(filePath, 'w');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create file ${filePath}: ${errorMessage}`);
} finally {
if (fileHandle !== undefined) {
try {
await fileHandle.close();
} catch (closeError) {
// Log close error but don't throw to avoid masking the original error
Logger.error(`Failed to close file handle for ${filePath}:`, closeError);
}
}
}
}
const lockfilePath = getUniqueLockPath(filePath);
let release: (() => Promise<void>) | undefined;
try {
release = await lock(filePath, {
retries: { retries: {
retries: 10, retries: 10,
factor: 2, factor: 2,
@@ -34,9 +54,27 @@ export async function writeFileWithLock(
}, },
lockfilePath, lockfilePath,
}); });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to acquire lock for ${filePath}: ${errorMessage}`);
}
try {
await writeFile(filePath, content, { encoding: 'utf8', ...options }); await writeFile(filePath, content, { encoding: 'utf8', ...options });
if (options?.mode) { if (options?.mode) {
await chmod(filePath, options.mode); await chmod(filePath, options.mode);
} }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to write to file ${filePath}: ${errorMessage}`);
} finally {
if (release) {
try {
await release(); await release();
} catch (error) {
// Log but don't throw on release failure
Logger.error(`Failed to release lock for ${filePath}:`, error);
}
}
}
} }
+3 -4
View File
@@ -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
View File
@@ -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"]
+2
View File
@@ -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
View File
@@ -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",
+259 -568
View File
File diff suppressed because it is too large Load Diff
-22
View File
@@ -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('依赖设置'),
-103
View File
@@ -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
View File
@@ -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. 修复定时任务无法停止