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
5 changes: 5 additions & 0 deletions messages/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,11 @@
"dingtalkSecretPlaceholder": "Optional, used for signing",
"customHeaders": "Custom Headers (JSON)",
"customHeadersPlaceholder": "{\"X-Token\":\"...\"}",
"errors": {
"headersInvalidJson": "Headers must be valid JSON",
"headersMustBeObject": "Headers must be a JSON object",
"headersValueMustBeString": "Header values must be strings"
},
"types": {
"wechat": "WeCom",
"feishu": "Feishu",
Expand Down
5 changes: 5 additions & 0 deletions messages/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@
"dingtalkSecretPlaceholder": "任意(署名用)",
"customHeaders": "カスタムヘッダー(JSON)",
"customHeadersPlaceholder": "{\"X-Token\":\"...\"}",
"errors": {
"headersInvalidJson": "Headers は有効な JSON である必要があります",
"headersMustBeObject": "Headers は JSON オブジェクトである必要があります",
"headersValueMustBeString": "Headers の値は文字列である必要があります"
},
"types": {
"wechat": "WeCom",
"feishu": "Feishu",
Expand Down
5 changes: 5 additions & 0 deletions messages/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@
"dingtalkSecretPlaceholder": "Необязательно, для подписи",
"customHeaders": "Пользовательские заголовки (JSON)",
"customHeadersPlaceholder": "{\"X-Token\":\"...\"}",
"errors": {
"headersInvalidJson": "Заголовки должны быть корректным JSON",
"headersMustBeObject": "Заголовки должны быть JSON-объектом",
"headersValueMustBeString": "Значения заголовков должны быть строками"
},
"types": {
"wechat": "WeCom",
"feishu": "Feishu",
Expand Down
5 changes: 5 additions & 0 deletions messages/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1678,6 +1678,11 @@
"dingtalkSecretPlaceholder": "可选,用于签名",
"customHeaders": "自定义 Headers(JSON)",
"customHeadersPlaceholder": "{\"X-Token\":\"...\"}",
"errors": {
"headersInvalidJson": "Headers 不是有效 JSON",
"headersMustBeObject": "Headers 必须是 JSON 对象",
"headersValueMustBeString": "Headers 的值必须为字符串"
},
"types": {
"wechat": "企业微信",
"feishu": "飞书",
Expand Down
5 changes: 5 additions & 0 deletions messages/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@
"dingtalkSecretPlaceholder": "可選,用於簽名",
"customHeaders": "自訂 Headers(JSON)",
"customHeadersPlaceholder": "{\"X-Token\":\"...\"}",
"errors": {
"headersInvalidJson": "Headers 不是有效 JSON",
"headersMustBeObject": "Headers 必須是 JSON 物件",
"headersValueMustBeString": "Headers 的值必須為字串"
},
"types": {
"wechat": "企業微信",
"feishu": "飛書",
Expand Down
9 changes: 5 additions & 4 deletions src/actions/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type UpdateNotificationSettingsInput,
updateNotificationSettings,
} from "@/repository/notifications";
import type { ActionResult } from "./types";

/**
* 获取通知设置
Expand All @@ -28,11 +29,11 @@ export async function getNotificationSettingsAction(): Promise<NotificationSetti
*/
export async function updateNotificationSettingsAction(
payload: UpdateNotificationSettingsInput
): Promise<{ success: boolean; data?: NotificationSettings; error?: string }> {
): Promise<ActionResult<NotificationSettings>> {
try {
const session = await getSession();
if (!session || session.user.role !== "admin") {
return { success: false, error: "无权限执行此操作" };
return { ok: false, error: "无权限执行此操作" };
}

const updated = await updateNotificationSettings(payload);
Expand All @@ -50,10 +51,10 @@ export async function updateNotificationSettingsAction(
});
}

return { success: true, data: updated };
return { ok: true, data: updated };
} catch (error) {
return {
success: false,
ok: false,
error: error instanceof Error ? error.message : "更新通知设置失败",
};
}
Expand Down
30 changes: 22 additions & 8 deletions src/actions/webhook-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,27 @@ function trimToNull(value: string | null | undefined): string | null {
return trimmed ? trimmed : null;
}

function parseCustomTemplate(value: string | null | undefined): Record<string, unknown> | null {
const trimmed = trimToNull(value);
if (!trimmed) return null;
function parseCustomTemplate(value: unknown): Record<string, unknown> | null {
if (value === null || value === undefined) {
return null;
}

if (typeof value === "string") {
const trimmed = trimToNull(value);
if (!trimmed) return null;

const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("自定义模板必须是 JSON 对象");
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("自定义模板必须是 JSON 对象");
}
return parsed as Record<string, unknown>;
}
return parsed as Record<string, unknown>;

if (typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}

throw new Error("自定义模板必须是 JSON 对象");
}

function validateProviderConfig(params: {
Expand Down Expand Up @@ -66,6 +78,8 @@ const NotificationTypeSchema = z.enum(["circuit_breaker", "daily_leaderboard", "

export type NotificationType = z.infer<typeof NotificationTypeSchema>;

const CustomTemplateSchema = z.union([z.string().trim(), z.record(z.string(), z.unknown())]);

const BaseTargetSchema = z.object({
name: z.string().trim().min(1, "目标名称不能为空").max(100, "目标名称不能超过100个字符"),
providerType: ProviderTypeSchema,
Expand All @@ -77,7 +91,7 @@ const BaseTargetSchema = z.object({

dingtalkSecret: z.string().trim().optional().nullable(),

customTemplate: z.string().trim().optional().nullable(),
customTemplate: CustomTemplateSchema.optional().nullable(),
customHeaders: z.record(z.string(), z.string()).optional().nullable(),

proxyUrl: z.string().trim().optional().nullable(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { logger } from "@/lib/logger";
import { setOnboardingCompleted, shouldShowOnboarding } from "@/lib/onboarding";
import { cn } from "@/lib/utils";
import {
Expand Down Expand Up @@ -135,7 +136,7 @@ export function WebhookMigrationDialog() {
}
} catch (error) {
// Don't block user if settings fetch fails, but log for debugging
console.warn("[WebhookMigrationDialog] Failed to load notification settings:", error);
logger.warn("[WebhookMigrationDialog] Failed to load notification settings", { error });
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,52 @@ function toJsonString(value: unknown): string {
}
}

function parseHeadersJson(value: string | null | undefined): Record<string, string> | null {
type TranslateFn = (key: string) => string;

function parseTemplateJson(
value: string | null | undefined,
t: TranslateFn
): Record<string, unknown> | null {
const trimmed = value?.trim();
if (!trimmed) return null;

const parsed = JSON.parse(trimmed) as unknown;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed) as unknown;
} catch {
throw new Error(t("notifications.templateEditor.jsonInvalid"));
}

if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(t("notifications.templateEditor.jsonInvalid"));
}

return parsed as Record<string, unknown>;
}

function parseHeadersJson(
value: string | null | undefined,
t: TranslateFn
): Record<string, string> | null {
const trimmed = value?.trim();
if (!trimmed) return null;

let parsed: unknown;
try {
parsed = JSON.parse(trimmed) as unknown;
} catch {
throw new Error(t("notifications.targetDialog.errors.headersInvalidJson"));
}

if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Headers 必须是 JSON 对象");
throw new Error(t("notifications.targetDialog.errors.headersMustBeObject"));
}

const record = parsed as Record<string, unknown>;
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(record)) {
if (typeof v !== "string") {
throw new Error("Headers 的值必须为字符串");
throw new Error(t("notifications.targetDialog.errors.headersValueMustBeString"));
}
out[k] = v;
}
Expand Down Expand Up @@ -160,8 +192,9 @@ export function WebhookTargetDialog({
telegramBotToken: values.telegramBotToken || null,
telegramChatId: values.telegramChatId || null,
dingtalkSecret: values.dingtalkSecret || null,
customTemplate: values.customTemplate || null,
customHeaders: parseHeadersJson(values.customHeaders),
customTemplate:
normalizedType === "custom" ? parseTemplateJson(values.customTemplate, t) : null,
customHeaders: parseHeadersJson(values.customHeaders, t),
proxyUrl: values.proxyUrl || null,
proxyFallbackToDirect: values.proxyFallbackToDirect,
isEnabled: values.isEnabled,
Expand Down
Loading
Loading