Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions messages/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@
},
"notifications": {
"title": "Push Notifications",
"description": "Configure WeChat Work robot push notifications",
"description": "Configure Webhook push notifications",
"global": {
"title": "Notification Master Switch",
"description": "Enable or disable all push notification features",
Expand Down Expand Up @@ -426,7 +426,7 @@
"saveError": "Failed to save settings",
"loadError": "Failed to load notification settings",
"webhookRequired": "Please fill in Webhook URL first",
"testSuccess": "Test message sent, please check WeChat Work",
"testSuccess": "Test message sent",
"testFailed": "Test failed",
"testFailedRetry": "Test failed, please retry",
"testError": "Test connection failed",
Expand Down
4 changes: 2 additions & 2 deletions messages/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@
},
"notifications": {
"title": "プッシュ通知",
"description": "WeCom(企業微信)ロボットのプッシュ通知を設定",
"description": "Webhook プッシュ通知を設定",
"global": {
"title": "通知マスタースイッチ",
"description": "すべてのプッシュ通知機能を有効または無効にする",
Expand Down Expand Up @@ -417,7 +417,7 @@
"saveError": "設定の保存に失敗しました",
"loadError": "通知設定の読み込みに失敗しました",
"webhookRequired": "まずWebhook URLを入力してください",
"testSuccess": "テストメッセージを送信しました。WeComを確認してください",
"testSuccess": "テストメッセージを送信しました",
"testFailed": "テストに失敗しました",
"testFailedRetry": "テストに失敗しました。再試行してください",
"testError": "接続テストに失敗しました",
Expand Down
4 changes: 2 additions & 2 deletions messages/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@
},
"notifications": {
"title": "Push-уведомления",
"description": "Настройка push-уведомлений робота WeChat Work",
"description": "Настройка push-уведомлений Webhook",
"global": {
"title": "Главный переключатель уведомлений",
"description": "Включить или отключить все функции push-уведомлений",
Expand Down Expand Up @@ -417,7 +417,7 @@
"saveError": "Не удалось сохранить настройки",
"loadError": "Не удалось загрузить настройки уведомлений",
"webhookRequired": "Сначала заполните Webhook URL",
"testSuccess": "Тестовое сообщение отправлено, проверьте WeChat Work",
"testSuccess": "Тестовое сообщение отправлено",
"testFailed": "Тест не пройден",
"testFailedRetry": "Тест не пройден, попробуйте снова",
"testError": "Ошибка тестирования подключения",
Expand Down
4 changes: 2 additions & 2 deletions messages/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1560,7 +1560,7 @@
},
"notifications": {
"title": "消息推送",
"description": "配置企业微信机器人消息推送",
"description": "配置 Webhook 消息推送",
"global": {
"title": "通知总开关",
"description": "启用或禁用所有消息推送功能",
Expand Down Expand Up @@ -1607,7 +1607,7 @@
"saveError": "保存设置失败",
"loadError": "加载通知设置失败",
"webhookRequired": "请先填写 Webhook URL",
"testSuccess": "测试消息已发送,请检查企业微信",
"testSuccess": "测试消息已发送",
"testFailed": "测试失败",
"testFailedRetry": "测试失败,请重试",
"testError": "测试连接失败",
Expand Down
4 changes: 2 additions & 2 deletions messages/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@
},
"notifications": {
"title": "訊息推送",
"description": "設定企業微信機器人訊息推送",
"description": "設定 Webhook 訊息推送",
"global": {
"title": "通知總開關",
"description": "啟用或停用所有訊息推送功能",
Expand Down Expand Up @@ -417,7 +417,7 @@
"saveError": "儲存設定失敗",
"loadError": "載入通知設定失敗",
"webhookRequired": "請先填寫 Webhook URL",
"testSuccess": "測試訊息已發送,請檢查企業微信",
"testSuccess": "測試訊息已發送",
"testFailed": "測試失敗",
"testFailedRetry": "測試失敗,請重試",
"testError": "測試連線失敗",
Expand Down
14 changes: 8 additions & 6 deletions src/actions/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use server";

import type { NotificationJobType } from "@/lib/constants/notification.constants";
import { logger } from "@/lib/logger";
import { WeChatBot } from "@/lib/wechat/bot";
import { WebhookNotifier } from "@/lib/webhook";
import { buildTestMessage } from "@/lib/webhook/templates/test-messages";
import {
getNotificationSettings,
type NotificationSettings,
Expand Down Expand Up @@ -116,7 +118,8 @@ export async function updateNotificationSettingsAction(
* 测试 Webhook 连通性
*/
export async function testWebhookAction(
webhookUrl: string
webhookUrl: string,
type: NotificationJobType
): Promise<{ success: boolean; error?: string }> {
if (!webhookUrl || !webhookUrl.trim()) {
return { success: false, error: "Webhook URL 不能为空" };
Expand All @@ -135,10 +138,9 @@ export async function testWebhookAction(
}

try {
const bot = new WeChatBot(trimmedUrl);
const result = await bot.testConnection();

return result;
const notifier = new WebhookNotifier(trimmedUrl, { maxRetries: 1 });
const testMessage = buildTestMessage(type);
return notifier.send(testMessage);
} catch (error) {
return {
success: false,
Expand Down
25 changes: 13 additions & 12 deletions src/app/[locale]/settings/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import type { NotificationJobType } from "@/lib/constants/notification.constants";

/**
* 通知设置表单 Schema
Expand Down Expand Up @@ -50,7 +51,7 @@ export default function NotificationsPage() {
const t = useTranslations("settings");
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [testingWebhook, setTestingWebhook] = useState<string | null>(null);
const [testingWebhook, setTestingWebhook] = useState<NotificationJobType | null>(null);

const {
register,
Expand Down Expand Up @@ -129,7 +130,7 @@ export default function NotificationsPage() {
}
};

const handleTestWebhook = async (webhookUrl: string, type: string) => {
const handleTestWebhook = async (webhookUrl: string, type: NotificationJobType) => {
if (!webhookUrl || !webhookUrl.trim()) {
toast.error(t("notifications.form.webhookRequired"));
return;
Expand All @@ -138,7 +139,7 @@ export default function NotificationsPage() {
setTestingWebhook(type);

try {
const result = await testWebhookAction(webhookUrl);
const result = await testWebhookAction(webhookUrl, type);

if (result.success) {
toast.success(t("notifications.form.testSuccess"));
Expand Down Expand Up @@ -229,12 +230,12 @@ export default function NotificationsPage() {
type="button"
variant="outline"
size="sm"
disabled={!enabled || testingWebhook === "circuitBreaker"}
disabled={!enabled || testingWebhook === "circuit-breaker"}
onClick={() =>
handleTestWebhook(watch("circuitBreakerWebhook") || "", "circuitBreaker")
handleTestWebhook(watch("circuitBreakerWebhook") || "", "circuit-breaker")
}
>
{testingWebhook === "circuitBreaker" ? (
{testingWebhook === "circuit-breaker" ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{t("common.testing")}
Expand Down Expand Up @@ -325,12 +326,12 @@ export default function NotificationsPage() {
type="button"
variant="outline"
size="sm"
disabled={!enabled || testingWebhook === "dailyLeaderboard"}
disabled={!enabled || testingWebhook === "daily-leaderboard"}
onClick={() =>
handleTestWebhook(watch("dailyLeaderboardWebhook") || "", "dailyLeaderboard")
handleTestWebhook(watch("dailyLeaderboardWebhook") || "", "daily-leaderboard")
}
>
{testingWebhook === "dailyLeaderboard" ? (
{testingWebhook === "daily-leaderboard" ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{t("common.testing")}
Expand Down Expand Up @@ -419,10 +420,10 @@ export default function NotificationsPage() {
type="button"
variant="outline"
size="sm"
disabled={!enabled || testingWebhook === "costAlert"}
onClick={() => handleTestWebhook(watch("costAlertWebhook") || "", "costAlert")}
disabled={!enabled || testingWebhook === "cost-alert"}
onClick={() => handleTestWebhook(watch("costAlertWebhook") || "", "cost-alert")}
>
{testingWebhook === "costAlert" ? (
{testingWebhook === "cost-alert" ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{t("common.testing")}
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/actions/[...route]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import * as usageLogActions from "@/actions/usage-logs";
// 导入 actions
import * as userActions from "@/actions/users";
import { createActionRoute } from "@/lib/api/action-adapter-openapi";
import { NOTIFICATION_JOB_TYPES } from "@/lib/constants/notification.constants";
// 导入 validation schemas
import {
CreateProviderSchema,
Expand Down Expand Up @@ -809,6 +810,7 @@ const { route: testWebhookRoute, handler: testWebhookHandler } = createActionRou
{
requestSchema: z.object({
webhookUrl: z.string().url(),
type: z.enum(NOTIFICATION_JOB_TYPES),
}),
description: "测试 Webhook 配置",
summary: "测试 Webhook 配置",
Expand Down
2 changes: 1 addition & 1 deletion src/drizzle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export const systemSettings = pgTable('system_settings', {
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
});

// Notification Settings table - 企业微信机器人通知配置
// Notification Settings table - Webhook 通知配置
export const notificationSettings = pgTable('notification_settings', {
id: serial('id').primaryKey(),

Expand Down
10 changes: 10 additions & 0 deletions src/lib/constants/notification.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 通知相关常量
*/
export const NOTIFICATION_JOB_TYPES = [
"circuit-breaker",
"cost-alert",
"daily-leaderboard",
] as const;

export type NotificationJobType = (typeof NOTIFICATION_JOB_TYPES)[number];
29 changes: 13 additions & 16 deletions src/lib/notification/notification-queue.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import type { Job } from "bull";
import Queue from "bull";
import type { NotificationJobType } from "@/lib/constants/notification.constants";
import { logger } from "@/lib/logger";
import { sendWeChatNotification } from "@/lib/wechat/bot";
import {
buildCircuitBreakerAlert,
buildCostAlert,
buildDailyLeaderboard,
buildCircuitBreakerMessage,
buildCostAlertMessage,
buildDailyLeaderboardMessage,
type CircuitBreakerAlertData,
type CostAlertData,
type DailyLeaderboardData,
} from "@/lib/wechat/message-templates";
type StructuredMessage,
sendWebhookMessage,
} from "@/lib/webhook";
import { generateCostAlerts } from "./tasks/cost-alert";
import { generateDailyLeaderboard } from "./tasks/daily-leaderboard";

/**
* 通知任务类型
*/
export type NotificationJobType = "circuit-breaker" | "daily-leaderboard" | "cost-alert";

/**
* 通知任务数据
*/
Expand Down Expand Up @@ -124,11 +121,11 @@ function setupQueueProcessor(queue: Queue.Queue<NotificationJobData>): void {
});

try {
// 构建消息内容
let content: string;
// 构建结构化消息
let message: StructuredMessage;
switch (type) {
case "circuit-breaker":
content = buildCircuitBreakerAlert(data as CircuitBreakerAlertData);
message = buildCircuitBreakerMessage(data as CircuitBreakerAlertData);
break;
case "daily-leaderboard": {
// 动态生成排行榜数据
Expand All @@ -146,7 +143,7 @@ function setupQueueProcessor(queue: Queue.Queue<NotificationJobData>): void {
return { success: true, skipped: true };
}

content = buildDailyLeaderboard(leaderboardData);
message = buildDailyLeaderboardMessage(leaderboardData);
break;
}
case "cost-alert": {
Expand All @@ -166,15 +163,15 @@ function setupQueueProcessor(queue: Queue.Queue<NotificationJobData>): void {
}

// 发送第一个告警(后续可扩展为批量发送)
content = buildCostAlert(alerts[0]);
message = buildCostAlertMessage(alerts[0]);
break;
}
default:
throw new Error(`Unknown notification type: ${type}`);
}

// 发送通知
const result = await sendWeChatNotification(webhookUrl, content);
const result = await sendWebhookMessage(webhookUrl, message);

if (!result.success) {
throw new Error(result.error || "Failed to send notification");
Expand Down
2 changes: 1 addition & 1 deletion src/lib/notification/notifier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { logger } from "@/lib/logger";
import { getRedisClient } from "@/lib/redis/client";
import type { CircuitBreakerAlertData } from "@/lib/wechat/message-templates";
import type { CircuitBreakerAlertData } from "@/lib/webhook";
import { generateCostAlerts } from "./tasks/cost-alert";
import { generateDailyLeaderboard } from "./tasks/daily-leaderboard";

Expand Down
2 changes: 1 addition & 1 deletion src/lib/notification/tasks/cost-alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { and, eq, gte, sql } from "drizzle-orm";
import { db } from "@/drizzle/db";
import { keys, messageRequest, providers } from "@/drizzle/schema";
import { logger } from "@/lib/logger";
import type { CostAlertData } from "@/lib/wechat/message-templates";
import type { CostAlertData } from "@/lib/webhook";

/**
* 生成成本预警数据
Expand Down
2 changes: 1 addition & 1 deletion src/lib/notification/tasks/daily-leaderboard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from "@/lib/logger";
import type { DailyLeaderboardData } from "@/lib/wechat/message-templates";
import type { DailyLeaderboardData } from "@/lib/webhook";
import { findLast24HoursLeaderboard } from "@/repository/leaderboard";

/**
Expand Down
25 changes: 25 additions & 0 deletions src/lib/webhook/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Types

// Notifier
export { sendWebhookMessage, WebhookNotifier } from "./notifier";
// Renderers (for advanced usage)
export { createRenderer, type Renderer } from "./renderers";
// Templates
export {
buildCircuitBreakerMessage,
buildCostAlertMessage,
buildDailyLeaderboardMessage,
} from "./templates";
export type {
CircuitBreakerAlertData,
CostAlertData,
DailyLeaderboardData,
DailyLeaderboardEntry,
MessageLevel,
ProviderType,
Section,
SectionContent,
StructuredMessage,
WebhookPayload,
WebhookResult,
} from "./types";
Loading
Loading