feat: add notification audit log feature

Agent-Logs-Url: https://github.com/whyour/qinglong/sessions/4c9f0ab1-8b0e-4b94-b295-39c90ed942c2

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-25 06:49:18 +00:00 committed by GitHub
parent bcb2471768
commit 79964f149c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 179 additions and 0 deletions

View File

@ -374,6 +374,19 @@ 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) => {

View File

@ -28,6 +28,12 @@ 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 {
@ -49,6 +55,14 @@ 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;
@ -81,6 +95,7 @@ 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

View File

@ -30,6 +30,8 @@ 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';
@ -389,11 +391,33 @@ export default class SystemService {
if (notificationInfo && typeString) { if (notificationInfo && typeString) {
notificationInfo.type = typeString; notificationInfo.type = typeString;
} }
let notifyType: string | undefined;
try {
const notifConfig = await this.getDb({ type: AuthDataType.notification });
notifyType = notifConfig.info?.type as string | undefined;
} catch (e) {}
if (notificationInfo?.type) {
notifyType = typeString || (notificationInfo.type as string);
}
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 {
@ -401,6 +425,18 @@ 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}`;

View File

@ -26,6 +26,7 @@ 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';
@ -125,6 +126,7 @@ 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);
@ -253,6 +255,8 @@ 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();
} }
@ -271,6 +275,19 @@ 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();
@ -344,6 +361,11 @@ 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('依赖设置'),

View File

@ -0,0 +1,93 @@
import intl from 'react-intl-universal';
import React from 'react';
import { Table, Tag } from 'antd';
import dayjs from 'dayjs';
enum NotifyStatus {
'成功',
'失败',
}
enum NotifyStatusColor {
'success',
'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: any) => {
return (
<Tag
color={NotifyStatusColor[record.status]}
style={{ marginRight: 0 }}
>
{intl.get(NotifyStatus[record.status])}
</Tag>
);
},
},
];
const NotifyLog = ({
data,
height,
}: {
data: Array<object>;
height: number;
}) => {
return (
<>
<Table
columns={columns}
pagination={false}
dataSource={data}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: height }}
/>
</>
);
};
export default NotifyLog;