mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-14 21:24:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25a8fa692a | ||
|
|
df677b8ad0 | ||
|
|
3464c4da61 | ||
|
|
1315578878 | ||
|
|
a1ae08da58 | ||
|
|
66700ebe1a |
@@ -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(
|
||||
'/log',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -9,6 +9,8 @@ dotenv.config({
|
||||
interface Config {
|
||||
port: number;
|
||||
grpcPort: number;
|
||||
bindHost: string;
|
||||
bindHostGrpc: string;
|
||||
nodeEnv: string;
|
||||
isDevelopment: boolean;
|
||||
isProduction: boolean;
|
||||
@@ -31,6 +33,8 @@ interface Config {
|
||||
const config: Config = {
|
||||
port: parseInt(process.env.BACK_PORT || '5700', 10),
|
||||
grpcPort: parseInt(process.env.GRPC_PORT || '5500', 10),
|
||||
bindHost: process.env.BIND_HOST || '::',
|
||||
bindHostGrpc: process.env.BIND_HOST_GRPC || '::',
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
isDevelopment: process.env.NODE_ENV === 'development',
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
|
||||
+9
-1
@@ -20,6 +20,7 @@ export enum NotificationMode {
|
||||
'chronocat' = 'Chronocat',
|
||||
'ntfy' = 'ntfy',
|
||||
'wxPusherBot' = 'wxPusherBot',
|
||||
'openiLink' = 'openiLink',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -161,6 +162,12 @@ export class WxPusherBotNotification extends NotificationBaseInfo {
|
||||
public wxPusherBotUids = '';
|
||||
}
|
||||
|
||||
export class OpeniLinkNotification extends NotificationBaseInfo {
|
||||
public openiLinkAppToken = '';
|
||||
public openiLinkHubUrl = '';
|
||||
public openiLinkContextToken = '';
|
||||
}
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
GotifyNotification,
|
||||
@@ -182,4 +189,5 @@ export interface NotificationInfo
|
||||
ChronocatNotification,
|
||||
LarkNotification,
|
||||
NtfyNotification,
|
||||
WxPusherBotNotification {}
|
||||
WxPusherBotNotification,
|
||||
OpeniLinkNotification {}
|
||||
|
||||
@@ -28,12 +28,6 @@ export enum AuthDataType {
|
||||
'removeLogFrequency' = 'removeLogFrequency',
|
||||
'systemConfig' = 'systemConfig',
|
||||
'authConfig' = 'authConfig',
|
||||
'notifyLog' = 'notifyLog',
|
||||
}
|
||||
|
||||
export enum NotifyStatus {
|
||||
'success',
|
||||
'fail',
|
||||
}
|
||||
|
||||
export interface SystemConfigInfo {
|
||||
@@ -55,14 +49,6 @@ export interface LoginLogInfo {
|
||||
status?: LoginStatus;
|
||||
}
|
||||
|
||||
export interface NotifyLogInfo {
|
||||
timestamp?: number;
|
||||
title?: string;
|
||||
content?: string;
|
||||
status?: NotifyStatus;
|
||||
notifyType?: string;
|
||||
}
|
||||
|
||||
export interface TokenInfo {
|
||||
value: string;
|
||||
timestamp: number;
|
||||
@@ -95,7 +81,6 @@ export interface AuthInfo {
|
||||
export type SystemModelInfo = SystemConfigInfo &
|
||||
Partial<NotificationInfo> &
|
||||
LoginLogInfo &
|
||||
Partial<NotifyLogInfo> &
|
||||
Partial<AuthInfo>;
|
||||
|
||||
export interface SystemInstance
|
||||
|
||||
@@ -20,6 +20,7 @@ const uploadPath = path.join(dataPath, 'upload/');
|
||||
const bakPath = path.join(dataPath, 'bak/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const tmpPath = path.join(logPath, '.tmp/');
|
||||
const rootTmpPath = path.join(rootPath, '.tmp/');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
||||
@@ -44,6 +45,7 @@ const directories = [
|
||||
preloadPath,
|
||||
logPath,
|
||||
tmpPath,
|
||||
rootTmpPath,
|
||||
uploadPath,
|
||||
sshPath,
|
||||
bakPath,
|
||||
|
||||
@@ -10,7 +10,7 @@ import config from '../config';
|
||||
|
||||
class Client {
|
||||
private client = new CronClient(
|
||||
`0.0.0.0:${config.grpcPort}`,
|
||||
`localhost:${config.grpcPort}`,
|
||||
credentials.createInsecure(),
|
||||
{ 'grpc.enable_http_proxy': 0 },
|
||||
);
|
||||
|
||||
@@ -27,10 +27,10 @@ export default class EnvService {
|
||||
envs.length > 0 &&
|
||||
typeof envs[envs.length - 1].position === 'number'
|
||||
) {
|
||||
position = envs[envs.length - 1].position!;
|
||||
position = this.getPrecisionPosition(envs[envs.length - 1].position!);
|
||||
}
|
||||
const tabs = payloads.map((x) => {
|
||||
position = position - stepPosition;
|
||||
position = this.getPrecisionPosition(position - stepPosition);
|
||||
const tab = new Env({ ...x, position });
|
||||
return tab;
|
||||
});
|
||||
@@ -116,7 +116,7 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
private getPrecisionPosition(position: number): number {
|
||||
return parseFloat(position.toPrecision(16));
|
||||
return Math.trunc(parseFloat(position.toPrecision(16)));
|
||||
}
|
||||
|
||||
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
||||
|
||||
+30
-9
@@ -16,6 +16,13 @@ import { Service } from 'typedi';
|
||||
export class GrpcServerService {
|
||||
private server: Server = new Server({ 'grpc.enable_http_proxy': 0 });
|
||||
|
||||
private formatGrpcAddress(host: string, port: number): string {
|
||||
if (host === '::') {
|
||||
return `[::]:${port}`;
|
||||
}
|
||||
return `${host}:${port}`;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
this.server.addService(HealthService, { check });
|
||||
@@ -23,18 +30,32 @@ export class GrpcServerService {
|
||||
this.server.addService(ApiService, Api);
|
||||
|
||||
const grpcPort = config.grpcPort;
|
||||
const hostsToTry = [
|
||||
config.bindHostGrpc,
|
||||
...(config.bindHostGrpc !== '0.0.0.0' ? ['0.0.0.0'] : [])
|
||||
];
|
||||
const bindAsync = promisify(this.server.bindAsync).bind(this.server);
|
||||
await bindAsync(
|
||||
`0.0.0.0:${grpcPort}`,
|
||||
ServerCredentials.createInsecure(),
|
||||
);
|
||||
Logger.debug(`✌️ gRPC service started successfully`);
|
||||
|
||||
metricsService.record('grpc_service_start', 1, {
|
||||
port: grpcPort.toString(),
|
||||
});
|
||||
let lastError: Error | null = null;
|
||||
|
||||
return grpcPort;
|
||||
for (const host of hostsToTry) {
|
||||
try {
|
||||
const address = this.formatGrpcAddress(host, grpcPort);
|
||||
await bindAsync(address, ServerCredentials.createInsecure());
|
||||
Logger.debug(`✌️ gRPC service started successfully on ${address}`);
|
||||
metricsService.record('grpc_service_start', 1, {
|
||||
port: grpcPort.toString(),
|
||||
host
|
||||
});
|
||||
return grpcPort;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
Logger.warn(`Failed to bind gRPC on ${host}:${grpcPort}, trying next...`, err);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.error('Failed to start gRPC service on all hosts');
|
||||
throw lastError || new Error('Failed to start gRPC service');
|
||||
} catch (err) {
|
||||
Logger.error('Failed to start gRPC service:', err);
|
||||
throw err;
|
||||
|
||||
+36
-16
@@ -3,31 +3,51 @@ import Logger from '../loaders/logger';
|
||||
import { metricsService } from './metrics';
|
||||
import { Service } from 'typedi';
|
||||
import { Server } from 'http';
|
||||
import config from '../config';
|
||||
|
||||
@Service()
|
||||
export class HttpServerService {
|
||||
private server?: Server = undefined;
|
||||
|
||||
async initialize(expressApp: express.Application, port: number) {
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = expressApp.listen(port, '0.0.0.0', () => {
|
||||
Logger.debug(`✌️ HTTP service started successfully`);
|
||||
metricsService.record('http_service_start', 1, {
|
||||
port: port.toString(),
|
||||
});
|
||||
resolve(this.server);
|
||||
});
|
||||
const hostsToTry = [
|
||||
config.bindHost,
|
||||
...(config.bindHost !== '0.0.0.0' ? ['0.0.0.0'] : [])
|
||||
];
|
||||
|
||||
this.server?.on('error', (err: Error) => {
|
||||
Logger.error('Failed to start HTTP service:', err);
|
||||
reject(err);
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const host of hostsToTry) {
|
||||
try {
|
||||
const server = await this.tryListen(expressApp, port, host);
|
||||
Logger.debug(`✌️ HTTP service started successfully on ${host}:${port}`);
|
||||
metricsService.record('http_service_start', 1, {
|
||||
port: port.toString(),
|
||||
host
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
Logger.error('Failed to start HTTP service:', err);
|
||||
throw err;
|
||||
this.server = server;
|
||||
return server;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
Logger.warn(`Failed to bind HTTP on ${host}:${port}, trying next...`, err);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.error('Failed to start HTTP service on all hosts');
|
||||
throw lastError || new Error('Failed to start HTTP service');
|
||||
}
|
||||
|
||||
private async tryListen(expressApp: express.Application, port: number, host: string): Promise<Server> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = expressApp.listen(port, host, () => {
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.on('error', (err: Error) => {
|
||||
server.close();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
|
||||
+42
-1
@@ -34,6 +34,7 @@ export default class NotificationService {
|
||||
['chronocat', this.chronocat],
|
||||
['ntfy', this.ntfy],
|
||||
['wxPusherBot', this.wxPusherBot],
|
||||
['openiLink', this.openiLink],
|
||||
]);
|
||||
|
||||
private title = '';
|
||||
@@ -90,6 +91,14 @@ export default class NotificationService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private parseMailRecipients(value?: string) {
|
||||
const recipients = (value || '')
|
||||
.split(/[;;]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
return recipients.length > 0 ? recipients : undefined;
|
||||
}
|
||||
|
||||
private async gotify() {
|
||||
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
||||
try {
|
||||
@@ -591,6 +600,7 @@ export default class NotificationService {
|
||||
|
||||
private async email() {
|
||||
const { emailPass, emailService, emailUser, emailTo } = this.params;
|
||||
const recipients = this.parseMailRecipients(emailTo) || emailUser;
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
@@ -603,7 +613,7 @@ export default class NotificationService {
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: `"青龙快讯" <${emailUser}>`,
|
||||
to: emailTo ? emailTo.split(';') : emailUser,
|
||||
to: recipients,
|
||||
subject: `${this.title}`,
|
||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||
});
|
||||
@@ -858,4 +868,35 @@ export default class NotificationService {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private async openiLink() {
|
||||
const { openiLinkAppToken, openiLinkHubUrl, openiLinkContextToken } =
|
||||
this.params;
|
||||
const baseUrl = openiLinkHubUrl?.replace(/\/$/, '') || 'https://hub.openilink.com';
|
||||
const url = `${baseUrl}/bot/v1/message/send`;
|
||||
const body: Record<string, string> = {
|
||||
type: 'text',
|
||||
content: `${this.title}\n\n${this.content}`,
|
||||
};
|
||||
if (openiLinkContextToken) {
|
||||
body.context_token = openiLinkContextToken;
|
||||
}
|
||||
try {
|
||||
const res = await httpClient.post(url, {
|
||||
...this.gotOption,
|
||||
json: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${openiLinkAppToken}`,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ import {
|
||||
SystemInstance,
|
||||
SystemModel,
|
||||
SystemModelInfo,
|
||||
NotifyStatus,
|
||||
NotifyLogInfo,
|
||||
} from '../data/system';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
import NotificationService from './notify';
|
||||
@@ -391,34 +389,11 @@ export default class SystemService {
|
||||
if (notificationInfo && 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(
|
||||
title,
|
||||
content,
|
||||
notificationInfo,
|
||||
);
|
||||
|
||||
await SystemModel.create({
|
||||
type: AuthDataType.notifyLog,
|
||||
info: {
|
||||
timestamp: Date.now(),
|
||||
title,
|
||||
content,
|
||||
status: isSuccess ? NotifyStatus.success : NotifyStatus.fail,
|
||||
notifyType,
|
||||
},
|
||||
});
|
||||
|
||||
if (isSuccess) {
|
||||
return { code: 200, message: '通知发送成功' };
|
||||
} 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) {
|
||||
if (!command.startsWith(TASK_COMMAND)) {
|
||||
command = `${TASK_COMMAND} ${command}`;
|
||||
|
||||
+12
-1
@@ -195,12 +195,14 @@ export SMTP_SERVER=""
|
||||
## SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||
export SMTP_SSL=""
|
||||
|
||||
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
## smtp_email 填写 SMTP 发件邮箱
|
||||
export SMTP_EMAIL=""
|
||||
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
export SMTP_PASSWORD=""
|
||||
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
|
||||
export SMTP_NAME=""
|
||||
## smtp_email_to 填写 SMTP 收件邮箱,多个用英文;分隔,不填默认发给发件邮箱
|
||||
export SMTP_EMAIL_TO=""
|
||||
|
||||
## 17. PushMe
|
||||
## 官方说明文档:https://push.i-i.me/
|
||||
@@ -259,4 +261,13 @@ export WEBHOOK_METHOD=""
|
||||
## 支持 text/plain、application/json、multipart/form-data、application/x-www-form-urlencoded
|
||||
export WEBHOOK_CONTENT_TYPE=""
|
||||
|
||||
## 23. OpeniLink
|
||||
## 官方文档: https://openilink.com/docs/hub/apps
|
||||
## 在 OpeniLink Hub 后台安装 App 后获取 app_token
|
||||
export OPENILINK_APP_TOKEN=""
|
||||
## OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址
|
||||
export OPENILINK_HUB_URL=""
|
||||
## OpeniLink 的 context_token,用于标识消息会话上下文,可从消息事件中获取
|
||||
export OPENILINK_CONTEXT_TOKEN=""
|
||||
|
||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||
|
||||
+76
-4
@@ -121,7 +121,8 @@ const push_config = {
|
||||
|
||||
SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
|
||||
SMTP_EMAIL: '', // SMTP 发件邮箱
|
||||
SMTP_TO: '', // SMTP 收件邮箱,默认通知将会发给发件邮箱
|
||||
SMTP_TO: '', // SMTP 收件邮箱,兼容旧参数名,默认通知将会发给发件邮箱
|
||||
SMTP_EMAIL_TO: '', // SMTP 收件邮箱,多个分号分隔,默认发给发件邮箱
|
||||
SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写
|
||||
|
||||
@@ -151,6 +152,11 @@ const push_config = {
|
||||
WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken
|
||||
WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
WXPUSHER_UIDS: '', // wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
|
||||
// 官方文档: https://openilink.com/docs/hub/apps
|
||||
OPENILINK_APP_TOKEN: '', // OpeniLink 的 app_token,在 OpeniLink Hub 后台安装 App 后获取
|
||||
OPENILINK_HUB_URL: '', // OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址
|
||||
OPENILINK_CONTEXT_TOKEN: '', // OpeniLink 的 context_token,用于标识消息会话上下文,可从消息事件中获取
|
||||
};
|
||||
|
||||
for (const key in push_config) {
|
||||
@@ -1046,8 +1052,14 @@ function fsBotNotify(text, desp) {
|
||||
}
|
||||
|
||||
async function smtpNotify(text, desp) {
|
||||
const { SMTP_EMAIL, SMTP_TO, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } =
|
||||
push_config;
|
||||
const {
|
||||
SMTP_EMAIL,
|
||||
SMTP_TO,
|
||||
SMTP_EMAIL_TO,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_SERVICE,
|
||||
SMTP_NAME,
|
||||
} = push_config;
|
||||
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
|
||||
return;
|
||||
}
|
||||
@@ -1063,9 +1075,20 @@ async function smtpNotify(text, desp) {
|
||||
});
|
||||
|
||||
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
|
||||
const recipients = [SMTP_EMAIL_TO, SMTP_TO].reduce((list, value) => {
|
||||
if (!value) {
|
||||
return list;
|
||||
}
|
||||
return list.concat(
|
||||
value
|
||||
.split(/[;;]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
}, []);
|
||||
const info = await transporter.sendMail({
|
||||
from: addr,
|
||||
to: SMTP_TO ? SMTP_TO.split(';') : addr,
|
||||
to: recipients.length ? recipients : SMTP_EMAIL,
|
||||
subject: text,
|
||||
html: `${desp.replace(/\n/g, '<br/>')}`,
|
||||
});
|
||||
@@ -1408,6 +1431,54 @@ function wxPusherNotify(text, desp) {
|
||||
});
|
||||
}
|
||||
|
||||
function openiLinkNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
const { OPENILINK_APP_TOKEN, OPENILINK_HUB_URL, OPENILINK_CONTEXT_TOKEN } =
|
||||
push_config;
|
||||
if (OPENILINK_APP_TOKEN) {
|
||||
const baseUrl = OPENILINK_HUB_URL
|
||||
? OPENILINK_HUB_URL.replace(/\/$/, '')
|
||||
: 'https://hub.openilink.com';
|
||||
const body = {
|
||||
type: 'text',
|
||||
content: `${text}\n\n${desp}`,
|
||||
};
|
||||
if (OPENILINK_CONTEXT_TOKEN) {
|
||||
body.context_token = OPENILINK_CONTEXT_TOKEN;
|
||||
}
|
||||
const options = {
|
||||
url: `${baseUrl}/bot/v1/message/send`,
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${OPENILINK_APP_TOKEN}`,
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('OpeniLink 发送通知消息失败!\n', err);
|
||||
} else {
|
||||
if (data.ok) {
|
||||
console.log('OpeniLink 发送通知消息成功!');
|
||||
} else {
|
||||
console.log(`OpeniLink 发送通知消息异常:${data.error}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseString(input, valueFormatFn) {
|
||||
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
|
||||
const matches = {};
|
||||
@@ -1538,6 +1609,7 @@ async function sendNotify(text, desp, params = {}) {
|
||||
qmsgNotify(text, desp), // 自定义通知
|
||||
ntfyNotify(text, desp), // Ntfy
|
||||
wxPusherNotify(text, desp), // wxpusher
|
||||
openiLinkNotify(text, desp), // OpeniLink
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+51
-8
@@ -107,7 +107,8 @@ push_config = {
|
||||
|
||||
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
|
||||
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
|
||||
'SMTP_EMAIL': '', # SMTP 发件邮箱
|
||||
'SMTP_EMAIL_TO': '', # SMTP 收件邮箱,多个分号分隔,默认发给发件邮箱
|
||||
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
|
||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||
|
||||
@@ -135,6 +136,10 @@ push_config = {
|
||||
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
|
||||
'OPENILINK_APP_TOKEN': '', # OpeniLink 的 app_token,在 OpeniLink Hub 后台安装 App 后获取 官方文档: https://openilink.com/docs/hub/apps
|
||||
'OPENILINK_HUB_URL': '', # OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址
|
||||
'OPENILINK_CONTEXT_TOKEN': '', # OpeniLink 的 context_token,用于标识消息会话上下文,可从消息事件中获取
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
@@ -690,6 +695,10 @@ def smtp(title: str, content: str) -> None:
|
||||
return
|
||||
print("SMTP 邮件 服务启动")
|
||||
|
||||
email_to = push_config.get("SMTP_EMAIL_TO") or push_config.get("SMTP_EMAIL")
|
||||
email_to_list = [
|
||||
item.strip() for item in re.split(r"[;;]", email_to) if item.strip()
|
||||
]
|
||||
message = MIMEText(content, "plain", "utf-8")
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
@@ -697,12 +706,7 @@ def smtp(title: str, content: str) -> None:
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
)
|
||||
)
|
||||
message["To"] = formataddr(
|
||||
(
|
||||
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
)
|
||||
)
|
||||
message["To"] = ",".join(email_to_list)
|
||||
message["Subject"] = Header(title, "utf-8")
|
||||
|
||||
try:
|
||||
@@ -716,7 +720,7 @@ def smtp(title: str, content: str) -> None:
|
||||
)
|
||||
smtp_server.sendmail(
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
push_config.get("SMTP_EMAIL"),
|
||||
email_to_list,
|
||||
message.as_bytes(),
|
||||
)
|
||||
smtp_server.close()
|
||||
@@ -898,6 +902,43 @@ def wxpusher_bot(title: str, content: str) -> None:
|
||||
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
||||
|
||||
|
||||
def openilink(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 OpeniLink 推送消息。
|
||||
支持的环境变量:
|
||||
- OPENILINK_APP_TOKEN: 在 OpeniLink Hub 后台安装 App 后获取的 app_token
|
||||
- OPENILINK_HUB_URL: OpeniLink Hub 地址,默认为 https://hub.openilink.com
|
||||
- OPENILINK_CONTEXT_TOKEN: 消息会话上下文 token,可从消息事件中获取
|
||||
"""
|
||||
if not push_config.get("OPENILINK_APP_TOKEN"):
|
||||
return
|
||||
|
||||
print("OpeniLink 服务启动")
|
||||
|
||||
base_url = (
|
||||
push_config.get("OPENILINK_HUB_URL", "").rstrip("/")
|
||||
or "https://hub.openilink.com"
|
||||
)
|
||||
url = f"{base_url}/bot/v1/message/send"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f'Bearer {push_config.get("OPENILINK_APP_TOKEN")}',
|
||||
}
|
||||
data = {
|
||||
"type": "text",
|
||||
"content": f"{title}\n\n{content}",
|
||||
}
|
||||
if push_config.get("OPENILINK_CONTEXT_TOKEN"):
|
||||
data["context_token"] = push_config.get("OPENILINK_CONTEXT_TOKEN")
|
||||
|
||||
response = requests.post(url=url, json=data, headers=headers).json()
|
||||
|
||||
if response.get("ok"):
|
||||
print("OpeniLink 推送成功!")
|
||||
else:
|
||||
print(f'OpeniLink 推送失败!错误信息:{response.get("error")}')
|
||||
|
||||
|
||||
def parse_headers(headers):
|
||||
if not headers:
|
||||
return {}
|
||||
@@ -1063,6 +1104,8 @@ def add_notify_function():
|
||||
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
|
||||
):
|
||||
notify_function.append(wxpusher_bot)
|
||||
if push_config.get("OPENILINK_APP_TOKEN"):
|
||||
notify_function.append(openilink)
|
||||
if not notify_function:
|
||||
print(f"无推送渠道,请检查通知变量是否正确")
|
||||
return notify_function
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import SecuritySettings from './security';
|
||||
import LoginLog from './loginLog';
|
||||
import NotifyLog from './notifyLog';
|
||||
import NotificationSetting from './notification';
|
||||
import Other from './other';
|
||||
import About from './about';
|
||||
@@ -126,7 +125,6 @@ const Setting = () => {
|
||||
const [editedApp, setEditedApp] = useState<any>();
|
||||
const [tabActiveKey, setTabActiveKey] = useState('security');
|
||||
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
||||
const [notifyLogData, setNotifyLogData] = useState<any[]>([]);
|
||||
const [notificationInfo, setNotificationInfo] = useState<any>();
|
||||
const containergRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState<number>(0);
|
||||
@@ -255,8 +253,6 @@ const Setting = () => {
|
||||
getApps();
|
||||
} else if (activeKey === 'login') {
|
||||
getLoginLog();
|
||||
} else if (activeKey === 'notifylog') {
|
||||
getNotifyLog();
|
||||
} else if (activeKey === 'notification') {
|
||||
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(() => {
|
||||
if (isDemoEnv) {
|
||||
getApps();
|
||||
@@ -361,11 +344,6 @@ const Setting = () => {
|
||||
label: intl.get('登录日志'),
|
||||
children: <LoginLog height={height} data={loginLogData} />,
|
||||
},
|
||||
{
|
||||
key: 'notifylog',
|
||||
label: intl.get('通知日志'),
|
||||
children: <NotifyLog height={height} data={notifyLogData} />,
|
||||
},
|
||||
{
|
||||
key: 'dependence',
|
||||
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;
|
||||
@@ -98,6 +98,7 @@ export default {
|
||||
{ value: 'pushPlus', label: 'PushPlus' },
|
||||
{ value: 'wePlusBot', label: intl.get('微加机器人') },
|
||||
{ value: 'wxPusherBot', label: 'wxPusher' },
|
||||
{ value: 'openiLink', label: 'OpeniLink' },
|
||||
{ value: 'chat', label: intl.get('群晖chat') },
|
||||
{ value: 'email', label: intl.get('邮箱') },
|
||||
{ value: 'lark', label: intl.get('飞书机器人') },
|
||||
@@ -387,6 +388,27 @@ export default {
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
openiLink: [
|
||||
{
|
||||
label: 'openiLinkAppToken',
|
||||
tip: intl.get(
|
||||
'OpeniLink的app_token,在OpeniLink Hub后台安装App后获取,参考 https://openilink.com/docs/hub/apps',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'openiLinkHubUrl',
|
||||
tip: intl.get(
|
||||
'OpeniLink Hub地址,默认为 https://hub.openilink.com,自建Hub时填写自己的地址',
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'openiLinkContextToken',
|
||||
tip: intl.get(
|
||||
'OpeniLink的context_token,用于标识消息会话上下文,可从消息事件中获取',
|
||||
),
|
||||
},
|
||||
],
|
||||
lark: [
|
||||
{
|
||||
label: 'larkKey',
|
||||
|
||||
Reference in New Issue
Block a user