From 2561c66cb831398a64ad9706d29b59d8bd27e49d Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 7 May 2026 19:31:14 +0800 Subject: [PATCH] feat: update workflow categories and admin notifications --- .../settings/admin-settings-client.tsx | 469 ++++++++++++++---- .../agent-workflow-settings-client.tsx | 154 ++++++ app/(chat)/gallery/page.tsx | 59 ++- .../notifications/[id]/route.ts | 63 +++ .../dingtalk-robot/notifications/route.ts | 101 ++++ app/api/admin/dingtalk-robot/send/route.ts | 2 +- app/api/admin/settings/route.ts | 14 + app/api/jobs/dingtalk-notifications/route.ts | 78 +++ app/api/site-settings/route.ts | 26 + .../[id]/CanvasEditorLegacyPageClient.tsx | 31 +- app/canvas/[id]/model3d/page.tsx | 34 +- components/agents/AgentPageDark.tsx | 2 + components/agents/AgentWorkflowPanel.tsx | 4 + components/agents/AgentWorkflowPanelDark.tsx | 4 +- .../agents/AgentWorkflowPanelDarkV2.tsx | 194 ++++---- components/agents/AgentWorkflowPanelNew.tsx | 4 +- components/canvas/CanvasChatPanel.tsx | 25 +- components/canvas/EnhanceProWorkspace.tsx | 326 +++++++++++- components/canvas/RealtimeCanvasDialog.tsx | 15 + components/canvas/nodes/ImageNode.tsx | 28 +- lib/agents/generation-project-state.ts | 4 + lib/agents/workflow-composer.ts | 12 + lib/agents/workflow-ui-config.ts | 286 +++++++++++ .../0078_add_dingtalk_notification_tasks.sql | 21 + lib/db/migrations/meta/_journal.json | 7 + lib/db/schema.ts | 20 + lib/jobs/dingtalk-notification-job.ts | 138 ++++++ lib/model3d/model-alias-config.ts | 38 +- .../dingtalk-notification-draft-config.ts | 42 ++ lib/notifications/dingtalk-robot.ts | 38 +- next-env.d.ts | 2 +- tsconfig.tsbuildinfo | 2 +- types/agent.ts | 18 + 33 files changed, 2006 insertions(+), 255 deletions(-) create mode 100644 app/api/admin/dingtalk-robot/notifications/[id]/route.ts create mode 100644 app/api/admin/dingtalk-robot/notifications/route.ts create mode 100644 app/api/jobs/dingtalk-notifications/route.ts create mode 100644 lib/db/migrations/0078_add_dingtalk_notification_tasks.sql create mode 100644 lib/jobs/dingtalk-notification-job.ts create mode 100644 lib/notifications/dingtalk-notification-draft-config.ts diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index b5a1dc9ed..61a14c449 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -93,6 +93,12 @@ import { normalizeDingTalkRobotNotificationConfig, type DingTalkRobotNotificationConfig, } from "@/lib/notifications/dingtalk-robot-config"; +import { + DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY, + defaultDingTalkNotificationDraftConfig, + normalizeDingTalkNotificationDraftConfig, + type DingTalkNotificationDraftConfig, +} from "@/lib/notifications/dingtalk-notification-draft-config"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Separator } from "@/components/ui/separator"; import { HomeAnimationEmbed } from "@/components/home/home-animation-embed"; @@ -371,6 +377,61 @@ function createEmptyEnhanceProPreset(): EnhanceProPreset { }; } +type AdminDingTalkNotificationTaskStatus = + | "pending" + | "sending" + | "sent" + | "failed" + | "cancelled"; + +type AdminDingTalkNotificationTask = { + id: string; + title: string; + content: string; + status: AdminDingTalkNotificationTaskStatus; + scheduledAt: string; + attempts: number; + sentAt?: string | null; + cancelledAt?: string | null; + lastError?: string | null; + createdAt: string; + updatedAt: string; +}; + +function formatDingTalkNotificationTaskStatus( + status: AdminDingTalkNotificationTaskStatus, +) { + if (status === "pending") return "待发送"; + if (status === "sending") return "发送中"; + if (status === "sent") return "已发送"; + if (status === "failed") return "发送失败"; + if (status === "cancelled") return "已取消"; + return status; +} + +function getDingTalkNotificationTaskStatusClassName( + status: AdminDingTalkNotificationTaskStatus, +) { + if (status === "sent") { + return "border border-emerald-200 bg-emerald-50 text-emerald-700"; + } + if (status === "failed") { + return "border border-red-200 bg-red-50 text-red-600"; + } + if (status === "cancelled") { + return "border border-slate-200 bg-slate-100 text-slate-500"; + } + if (status === "sending") { + return "border border-blue-200 bg-blue-50 text-blue-600"; + } + return "border border-amber-200 bg-amber-50 text-amber-700"; +} + +function toDateTimeLocalInputValue(value: Date) { + const offsetMs = value.getTimezoneOffset() * 60 * 1000; + return new Date(value.getTime() - offsetMs).toISOString().slice(0, 16); +} + function LocalAiSectionToggleButton({ expanded, onClick, @@ -1526,6 +1587,20 @@ export function AdminSettingsClient() { `/api/admin/settings?key=${DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY}`, fetcher ); + const { + data: dingTalkNotificationDraftData, + mutate: mutateDingTalkNotificationDraft, + } = useSWR<{ setting?: { value?: unknown } }>( + `/api/admin/settings?key=${DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY}`, + fetcher + ); + const { + data: dingTalkNotificationTasksData, + mutate: mutateDingTalkNotificationTasks, + } = useSWR<{ tasks?: AdminDingTalkNotificationTask[] }>( + "/api/admin/dingtalk-robot/notifications?limit=30", + fetcher + ); const { data: threeDModelAliasConfigData, mutate: mutateThreeDModelAliasConfig, @@ -2833,34 +2908,26 @@ export function AdminSettingsClient() { tags?: CanvasSkillTag[]; }>("/api/canvas-skill-tags", fetcher); - // 系统通知状态 - const [notification, setNotification] = useState<{ - enabled: boolean; - title: string; - content: string; - }>({ - enabled: false, - title: "", - content: "", - }); + const [dingTalkNotificationDraft, setDingTalkNotificationDraft] = + useState( + defaultDingTalkNotificationDraftConfig, + ); const [dingTalkRobotNotificationConfig, setDingTalkRobotNotificationConfig] = useState( defaultDingTalkRobotNotificationConfig, ); + const [dingTalkNotificationScheduleAt, setDingTalkNotificationScheduleAt] = + useState(""); + const [isSavingDingTalkNotificationDraft, setIsSavingDingTalkNotificationDraft] = + useState(false); const [isSavingDingTalkRobotNotificationConfig, setIsSavingDingTalkRobotNotificationConfig] = useState(false); const [isSendingDingTalkNotification, setIsSendingDingTalkNotification] = useState(false); - - useEffect(() => { - fetch("/api/admin/settings?key=login_notification") - .then((res) => res.json()) - .then((data) => { - if (data.setting?.value) { - setNotification(data.setting.value); - } - }); - }, []); + const [isSchedulingDingTalkNotification, setIsSchedulingDingTalkNotification] = + useState(false); + const [cancellingDingTalkNotificationTaskId, setCancellingDingTalkNotificationTaskId] = + useState(null); useEffect(() => { const value = dingTalkRobotNotificationConfigData?.setting?.value; @@ -2869,19 +2936,35 @@ export function AdminSettingsClient() { ); }, [dingTalkRobotNotificationConfigData]); - const handleSaveNotification = async () => { + useEffect(() => { + const value = dingTalkNotificationDraftData?.setting?.value; + setDingTalkNotificationDraft( + normalizeDingTalkNotificationDraftConfig(value), + ); + }, [dingTalkNotificationDraftData]); + + const handleSaveDingTalkNotificationDraft = async () => { + setIsSavingDingTalkNotificationDraft(true); try { - await fetch("/api/admin/settings", { + const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - key: "login_notification", - value: notification, + key: DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY, + value: dingTalkNotificationDraft, }), }); - toast.success("系统通知已保存"); + + if (!response.ok) { + throw new Error("保存失败"); + } + + toast.success("钉钉通知草稿已保存"); + mutateDingTalkNotificationDraft(); } catch (error) { - toast.error("保存失败"); + toast.error("保存钉钉通知草稿失败"); + } finally { + setIsSavingDingTalkNotificationDraft(false); } }; @@ -2911,16 +2994,16 @@ export function AdminSettingsClient() { }; const handleSendNotificationToDingTalk = async () => { - const title = String(notification.title || "").trim(); - const content = String(notification.content || "").trim(); + const title = String(dingTalkNotificationDraft.title || "").trim(); + const content = String(dingTalkNotificationDraft.content || "").trim(); if (!title) { - toast.error("请先填写系统通知标题"); + toast.error("请先填写钉钉通知标题"); return; } if (!content) { - toast.error("请先填写系统通知内容"); + toast.error("请先填写钉钉通知正文"); return; } @@ -2937,7 +3020,7 @@ export function AdminSettingsClient() { throw new Error(payload?.error || "发送失败"); } - toast.success("当前系统通知已发送到钉钉群"); + toast.success("钉钉通知已发送"); } catch (error) { toast.error( error instanceof Error ? error.message : "发送钉钉群通知失败", @@ -2947,6 +3030,86 @@ export function AdminSettingsClient() { } }; + const handleScheduleDingTalkNotification = async () => { + const title = String(dingTalkNotificationDraft.title || "").trim(); + const content = String(dingTalkNotificationDraft.content || "").trim(); + + if (!title) { + toast.error("请先填写钉钉通知标题"); + return; + } + + if (!content) { + toast.error("请先填写钉钉通知正文"); + return; + } + + if (!dingTalkNotificationScheduleAt) { + toast.error("请选择定时发送时间"); + return; + } + + const scheduledAt = new Date(dingTalkNotificationScheduleAt); + if (Number.isNaN(scheduledAt.getTime())) { + toast.error("定时发送时间格式不正确"); + return; + } + + setIsSchedulingDingTalkNotification(true); + try { + const response = await fetch("/api/admin/dingtalk-robot/notifications", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title, + content, + scheduledAt: scheduledAt.toISOString(), + }), + }); + const payload = await response.json().catch(() => null); + + if (!response.ok || payload?.success === false) { + throw new Error(payload?.error || "创建定时通知失败"); + } + + toast.success("钉钉定时通知已创建"); + setDingTalkNotificationScheduleAt(""); + mutateDingTalkNotificationTasks(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "创建钉钉定时通知失败", + ); + } finally { + setIsSchedulingDingTalkNotification(false); + } + }; + + const handleCancelDingTalkNotificationTask = async (taskId: string) => { + setCancellingDingTalkNotificationTaskId(taskId); + try { + const response = await fetch( + `/api/admin/dingtalk-robot/notifications/${taskId}`, + { + method: "DELETE", + }, + ); + const payload = await response.json().catch(() => null); + + if (!response.ok || payload?.success === false) { + throw new Error(payload?.error || "取消定时通知失败"); + } + + toast.success("定时通知已取消"); + mutateDingTalkNotificationTasks(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "取消钉钉通知任务失败", + ); + } finally { + setCancellingDingTalkNotificationTaskId(null); + } + }; + useEffect(() => { if (questionsData) { setQuestions(questionsData); @@ -7397,7 +7560,7 @@ export function AdminSettingsClient() { - 系统通知 + 钉钉通知 @@ -13510,55 +13673,117 @@ export function AdminSettingsClient() { - {/* 系统通知设置 */} + {/* 钉钉通知设置 */} - 系统通知 + 钉钉通知中心 - 发布全站公告通知,用户进入系统时可见 + 直接粘贴 Markdown 公告内容,支持立即发送和定时发送,仅推送到钉钉群,不再同步站内弹窗。 -
- - setNotification({ ...notification, enabled: checked }) - } - /> - -
- + - setNotification({ ...notification, title: e.target.value }) + setDingTalkNotificationDraft((current) => ({ + ...current, + title: e.target.value, + })) } - placeholder="例如:系统维护公告" + placeholder="例如:系统更新公告|生图引擎性能升级完成" />
- +
+ + + 支持标题、粗体、列表、引用、链接 + +