From b1ae41fa99c33a6cbc660da03428683951d83c36 Mon Sep 17 00:00:00 2001 From: shunli Date: Mon, 11 May 2026 09:17:00 +0800 Subject: [PATCH] =?UTF-8?q?W1-W3=20=E5=AE=9E=E7=8E=B0=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E4=B8=AA=E4=BA=BA=E5=BA=94=E7=94=A8=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/notificationChannels.ts | 2 +- client/src/pages/NotificationChannelsPage.tsx | 194 +++++++------- ...wecom-personal-app-message-r5-closeout.csv | 6 + server/src/routes/notifications.js | 45 ++-- .../src/services/integrations/dispatcher.js | 1 + server/src/services/integrations/im/wecom.js | 238 +++++++++++++++++- .../services/integrations/wecomAccessToken.js | 166 ++++++++++++ server/src/services/notificationDispatcher.js | 21 +- server/src/services/notifications/channels.js | 50 +++- .../tests/unit/routes/notifications.test.js | 83 ++++++ .../services/integrations/dispatcher.test.js | 112 +++++++++ .../services/notificationDispatcher.test.js | 62 +++++ 12 files changed, 869 insertions(+), 111 deletions(-) create mode 100644 issues/2026-05-11_08-56-44-wecom-personal-app-message-r5-closeout.csv create mode 100644 server/src/services/integrations/wecomAccessToken.js diff --git a/client/src/api/notificationChannels.ts b/client/src/api/notificationChannels.ts index 1184deca..86ab80f6 100644 --- a/client/src/api/notificationChannels.ts +++ b/client/src/api/notificationChannels.ts @@ -1,7 +1,7 @@ import { apiRequestData } from "./http"; export type NotificationChannelType = "wecom" | "wechat_work"; -export type NotificationChannelMode = "mock" | "wecom_webhook"; +export type NotificationChannelMode = "mock" | "wecom_webhook" | "wecom_app_message"; export type NotificationDomainScope = | "workflow" | "reimbursement" diff --git a/client/src/pages/NotificationChannelsPage.tsx b/client/src/pages/NotificationChannelsPage.tsx index 4cc0a044..f16297cb 100644 --- a/client/src/pages/NotificationChannelsPage.tsx +++ b/client/src/pages/NotificationChannelsPage.tsx @@ -170,6 +170,7 @@ function formatChannelType(channelType: string) { } function formatMode(mode: string) { + if (mode === "wecom_app_message") return "企微应用消息"; return mode === "wecom_webhook" ? "Webhook" : "Mock"; } @@ -185,6 +186,7 @@ function formatSummary(values: string[], labelMap: Record, empty } function buildPayload(form: ChannelFormState): NotificationChannelPayload { + const isWebhookMode = form.mode === "wecom_webhook"; const templateJson: NotificationChannelTemplate = { title: form.title.trim() || "{{title}}", body: form.body.trim() || "{{body}}\n\n查看详情:{{detailUrl}}", @@ -194,7 +196,7 @@ function buildPayload(form: ChannelFormState): NotificationChannelPayload { const configJson: NotificationChannelWriteConfig = { mode: form.mode, - webhookUrl: form.webhookUrl.trim() || null, + webhookUrl: isWebhookMode ? form.webhookUrl.trim() || null : null, timeoutMs: Math.max(100, Number(form.timeoutMs || 5000)), routing: { roleCodes: form.roleCodes, @@ -203,9 +205,9 @@ function buildPayload(form: ChannelFormState): NotificationChannelPayload { }, }; - if (form.secretAction === "replace") { + if (isWebhookMode && form.secretAction === "replace") { configJson.secret = form.secret.trim() || null; - } else if (form.secretAction === "clear") { + } else if (isWebhookMode && form.secretAction === "clear") { configJson.clearSecret = true; } @@ -247,7 +249,7 @@ export function NotificationChannelsPage() { const saveMutation = useMutation({ mutationFn: async () => { - if (editingId && form.secretAction === "replace" && !form.secret.trim()) { + if (editingId && form.mode === "wecom_webhook" && form.secretAction === "replace" && !form.secret.trim()) { throw new Error("请输入新的 Webhook Secret,或切换为保留现有密钥。"); } @@ -323,12 +325,14 @@ export function NotificationChannelsPage() { } const channels = channelsQuery.data?.items ?? []; + const isWebhookMode = form.mode === "wecom_webhook"; + const isAppMessageMode = form.mode === "wecom_app_message"; return (
新建渠道 @@ -523,7 +527,7 @@ export function NotificationChannelsPage() { {editingId ? "编辑通知渠道" : "新建通知渠道"}

- 通过角色 / 业务域 / 通知类型三维控制 IM 群路由,避免默认全量广播。 + 通过角色 / 业务域 / 通知类型三维控制 IM 路由,避免默认全量广播。

{editingId ? ( @@ -543,7 +547,7 @@ export function NotificationChannelsPage() { disabled={Boolean(editingId)} onChange={(event) => setForm((prev) => ({ ...prev, channelType: event.target.value as "wecom" }))} > - + @@ -572,7 +576,11 @@ export function NotificationChannelsPage() { > + + {isAppMessageMode ? ( +

使用企业微信自建应用给已绑定成员发送个人消息。

+ ) : null}
@@ -586,90 +594,100 @@ export function NotificationChannelsPage() { />
-
- - setForm((prev) => ({ ...prev, webhookUrl: event.target.value }))} - /> -
- -
- - {editingId && form.secretConfigured ? ( -
-

- 当前已配置密钥:{form.secretMasked ?? "******"}。默认保留现有密钥,不会回显明文。 -

-
- - - -
+ {isWebhookMode ? ( + <> +
+ + setForm((prev) => ({ ...prev, webhookUrl: event.target.value }))} + />
- ) : ( -

- {editingId ? "当前未配置密钥,保存时可新增。" : "可选,配置后将参与签名。"} -

- )} - {form.secretAction === "replace" ? ( - setForm((prev) => ({ ...prev, secret: event.target.value }))} - /> - ) : null} +
+ + {editingId && form.secretConfigured ? ( +
+

+ 当前已配置密钥:{form.secretMasked ?? "******"}。默认保留现有密钥,不会回显明文。 +

+
+ + + +
+
+ ) : ( +

+ {editingId ? "当前未配置密钥,保存时可新增。" : "可选,配置后将参与签名。"} +

+ )} - {form.secretAction === "clear" ? ( -

保存后将清空当前 Webhook Secret。

- ) : null} -
+ {form.secretAction === "replace" ? ( + setForm((prev) => ({ ...prev, secret: event.target.value }))} + /> + ) : null} + + {form.secretAction === "clear" ? ( +

保存后将清空当前 Webhook Secret。

+ ) : null} +
+ + ) : ( +
+ {isAppMessageMode + ? "个人应用消息使用现有企业微信应用配置和成员绑定,不需要填写 Webhook URL 或 Secret。" + : "Mock 模式只记录本地投递结果,不需要填写 Webhook URL 或 Secret。"} +
+ )}