Skip to content

Commit f7d0fe0

Browse files
committed
feat: add feishu notification support
1 parent 58a7db9 commit f7d0fe0

File tree

5 files changed

+287
-37
lines changed

5 files changed

+287
-37
lines changed

backend/src/services/NotificationService.ts

Lines changed: 197 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ interface ResendConfig {
201201
to: string;
202202
}
203203

204+
// 飞书配置接口
205+
interface FeishuConfig {
206+
webhookUrl: string;
207+
}
208+
209+
// 企业微信配置接口
210+
interface WeComConfig {
211+
webhookUrl: string;
212+
}
213+
214+
215+
204216
/**
205217
* 解析通知渠道配置
206218
*/
@@ -258,9 +270,11 @@ function parseChannelConfig<T>(channel: models.NotificationChannel): T {
258270
}
259271
}
260272

261-
/**
262-
* 通过Resend API发送邮件通知
263-
*/
273+
274+
// =================================================================
275+
// Section: 各渠道发送器实现 (Sender Implementations)
276+
// =================================================================
277+
264278
async function sendResendNotification(
265279
channel: models.NotificationChannel,
266280
subject: string,
@@ -427,8 +441,51 @@ async function sendTelegramNotification(
427441
};
428442
}
429443
}
444+
445+
// =================================================================
446+
// Section: 新的通知发送器抽象层 (Refactored Sender Abstraction)
447+
// =================================================================
448+
449+
/**
450+
* 定义了通知发送器的统一接口。
451+
* 每种通知渠道(如邮件、Telegram)都必须实现这个接口。
452+
* "Good code is all about making the data structures, so the code is obvious."
453+
* 这个接口就是我们新的数据结构。
454+
*/
455+
interface NotificationSender {
456+
(
457+
channel: models.NotificationChannel,
458+
subject: string,
459+
content: string
460+
): Promise<{ success: boolean; error?: string }>;
461+
}
462+
463+
/**
464+
* 发送器注册表。
465+
* 这是一个从渠道类型字符串到其发送器实现的映射。
466+
* "Talk is cheap. Show me the code."
467+
* 这段代码取代了原来愚蠢的 if-else 链。
468+
*/
469+
const senderRegistry: Record<string, NotificationSender> = {};
470+
471+
/**
472+
* 注册一个新的通知发送器。
473+
* @param type 渠道类型 (e.g., 'resend', 'telegram')
474+
* @param sender 实现了 NotificationSender 接口的函数
475+
*/
476+
function registerSender(type: string, sender: NotificationSender) {
477+
if (senderRegistry[type]) {
478+
console.warn(`[通知注册] 覆盖已存在的发送器: ${type}`);
479+
}
480+
senderRegistry[type] = sender;
481+
console.log(`[通知注册] 成功注册发送器: ${type}`);
482+
}
483+
484+
430485
/**
431-
* 根据渠道类型发送通知
486+
* 根据渠道类型发送通知 (重构后)
487+
* 这个函数现在只负责查找和调用,不再关心具体实现。
488+
* "The point of interfaces is that you don't have to care."
432489
*/
433490
async function sendNotificationByChannel(
434491
channel: models.NotificationChannel,
@@ -444,23 +501,150 @@ async function sendNotificationByChannel(
444501
return { success: false, error: "通知渠道已禁用" };
445502
}
446503

447-
console.log(`[渠道分发] 渠道ID=${channel.id}的类型为${channel.type}`);
448-
449-
if (channel.type === "resend") {
450-
console.log(`[渠道分发] 使用Resend邮件服务发送通知`);
451-
return await sendResendNotification(channel, subject, content);
452-
} else if (channel.type === "telegram") {
453-
console.log(`[渠道分发] 使用Telegram发送通知`);
454-
return await sendTelegramNotification(channel, subject, content);
504+
const sender = senderRegistry[channel.type];
505+
if (sender) {
506+
console.log(`[渠道分发] 找到类型为 ${channel.type} 的发送器,开始执行`);
507+
return await sender(channel, subject, content);
455508
} else {
456509
console.error(`[渠道分发] 不支持的通知渠道类型: ${channel.type}`);
457510
return { success: false, error: `不支持的通知渠道类型: ${channel.type}` };
458511
}
459512
}
460513

514+
515+
461516
/**
462-
* 发送通知
517+
* 发送飞书通知
463518
*/
519+
async function sendFeishuNotification(
520+
channel: models.NotificationChannel,
521+
subject: string,
522+
content: string
523+
): Promise<{ success: boolean; error?: string }> {
524+
try {
525+
const config = parseChannelConfig<FeishuConfig>(channel);
526+
const webhookUrl = config.webhookUrl;
527+
528+
if (!webhookUrl) {
529+
console.error("[飞书通知] Webhook URL 不能为空");
530+
return { success: false, error: "飞书 Webhook URL 不能为空" };
531+
}
532+
533+
const message = {
534+
msg_type: "interactive",
535+
card: {
536+
header: {
537+
title: {
538+
content: subject,
539+
tag: "plain_text",
540+
},
541+
},
542+
elements: [
543+
{
544+
tag: "div",
545+
text: {
546+
content: content,
547+
tag: "lark_md",
548+
},
549+
},
550+
],
551+
},
552+
};
553+
554+
console.log("[飞书通知] 准备发送通知到:", webhookUrl);
555+
const response = await fetch(webhookUrl, {
556+
method: "POST",
557+
headers: {
558+
"Content-Type": "application/json",
559+
},
560+
body: JSON.stringify(message),
561+
});
562+
563+
const responseData = await response.json();
564+
565+
if (responseData.StatusCode === 0 || responseData.code === 0) {
566+
console.log("[飞书通知] 发送成功");
567+
return { success: true };
568+
} else {
569+
console.error("[飞书通知] 发送失败:", responseData);
570+
return {
571+
success: false,
572+
error: responseData.StatusMessage || responseData.msg || "发送失败",
573+
};
574+
}
575+
} catch (error) {
576+
console.error("发送飞书通知异常:", error);
577+
return {
578+
success: false,
579+
error: error instanceof Error ? error.message : String(error),
580+
};
581+
}
582+
}
583+
584+
// 注册已有的发送器
585+
registerSender("resend", sendResendNotification);
586+
registerSender("telegram", sendTelegramNotification);
587+
registerSender("feishu", sendFeishuNotification);
588+
589+
/**
590+
* 发送企业微信通知
591+
*/
592+
async function sendWeComNotification(
593+
channel: models.NotificationChannel,
594+
subject: string,
595+
content: string
596+
): Promise<{ success: boolean; error?: string }> {
597+
try {
598+
const config = parseChannelConfig<WeComConfig>(channel);
599+
const webhookUrl = config.webhookUrl;
600+
601+
if (!webhookUrl) {
602+
console.error("[企业微信通知] Webhook URL 不能为空");
603+
return { success: false, error: "企业微信 Webhook URL 不能为空" };
604+
}
605+
606+
// 企业微信的 Markdown 格式要求主题是加粗标题
607+
const markdownContent = `**${subject}**\n\n${content}`;
608+
609+
const message = {
610+
msgtype: "markdown",
611+
markdown: {
612+
content: markdownContent,
613+
},
614+
};
615+
616+
console.log("[企业微信通知] 准备发送通知到:", webhookUrl);
617+
const response = await fetch(webhookUrl, {
618+
method: "POST",
619+
headers: {
620+
"Content-Type": "application/json",
621+
},
622+
body: JSON.stringify(message),
623+
});
624+
625+
const responseData = await response.json();
626+
627+
if (responseData.errcode === 0) {
628+
console.log("[企业微信通知] 发送成功");
629+
return { success: true };
630+
} else {
631+
console.error("[企业微信通知] 发送失败:", responseData);
632+
return {
633+
success: false,
634+
error: `错误码: ${responseData.errcode}, 错误信息: ${responseData.errmsg}`,
635+
};
636+
}
637+
} catch (error) {
638+
console.error("发送企业微信通知异常:", error);
639+
return {
640+
success: false,
641+
error: error instanceof Error ? error.message : String(error),
642+
};
643+
}
644+
}
645+
646+
registerSender("wecom", sendWeComNotification);
647+
464648
export async function sendNotification(
465649
type: "monitor" | "agent" | "system",
466650
targetId: number | null,

frontend/src/i18n/en-US.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,9 @@ const enUS = {
472472
"notifications.channels.errors.fromRequired": "Sender is required",
473473
"notifications.channels.errors.toRequired": "Recipient is required",
474474
"notifications.channels.errors.invalidFromEmail": "Invalid sender format",
475+
"notifications.channels.errors.webhookUrlRequired": "Webhook URL is required",
475476
"notifications.channels.apiKey": "Resend API Key",
477+
"notifications.channels.webhookUrl": "Webhook URL",
476478
"notifications.channels.getApiKey": "Get Resend API Key",
477479
"notifications.channels.from": "Sender",
478480
"notifications.channels.fromHint": 'Supported formats: "Your Name <email@example.com>" or "email@example.com"',

frontend/src/i18n/zh-CN.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,9 @@ const zhCN = {
448448
"notifications.channels.errors.fromRequired": "发件人为必填项",
449449
"notifications.channels.errors.toRequired": "收件人为必填项",
450450
"notifications.channels.errors.invalidFromEmail": "发件人格式无效",
451+
"notifications.channels.errors.webhookUrlRequired": "Webhook URL为必填项",
451452
"notifications.channels.apiKey": "Resend API密钥",
453+
"notifications.channels.webhookUrl": "Webhook URL",
452454
"notifications.channels.getApiKey": "获取Resend API密钥",
453455
"notifications.channels.from": "发件人",
454456
"notifications.channels.fromHint":

0 commit comments

Comments
 (0)