W1-W3 实现企业微信个人应用消息通知
这个提交包含在:
@@ -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"
|
||||
|
||||
@@ -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<string, string>, 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 (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="通知渠道"
|
||||
subtitle="维护企微群机器人渠道、业务域路由、测试发送与最近投递记录。目标业务通知会在提交后同步尝试外推。"
|
||||
subtitle="维护企微通知渠道、业务域路由、测试发送与最近投递记录。目标业务通知会在提交后同步尝试外推。"
|
||||
actions={
|
||||
<Button type="button" variant="secondary" onClick={startCreate} data-testid="notification-channels-create">
|
||||
新建渠道
|
||||
@@ -523,7 +527,7 @@ export function NotificationChannelsPage() {
|
||||
{editingId ? "编辑通知渠道" : "新建通知渠道"}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500">
|
||||
通过角色 / 业务域 / 通知类型三维控制 IM 群路由,避免默认全量广播。
|
||||
通过角色 / 业务域 / 通知类型三维控制 IM 路由,避免默认全量广播。
|
||||
</p>
|
||||
</div>
|
||||
{editingId ? (
|
||||
@@ -543,7 +547,7 @@ export function NotificationChannelsPage() {
|
||||
disabled={Boolean(editingId)}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, channelType: event.target.value as "wecom" }))}
|
||||
>
|
||||
<option value="wecom">企微群机器人</option>
|
||||
<option value="wecom">企微</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -572,7 +576,11 @@ export function NotificationChannelsPage() {
|
||||
>
|
||||
<option value="mock">Mock</option>
|
||||
<option value="wecom_webhook">企微 Webhook</option>
|
||||
<option value="wecom_app_message">企微应用消息(个人)</option>
|
||||
</select>
|
||||
{isAppMessageMode ? (
|
||||
<p className="text-xs text-zinc-500">使用企业微信自建应用给已绑定成员发送个人消息。</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -586,90 +594,100 @@ export function NotificationChannelsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="webhookUrl">Webhook URL</Label>
|
||||
<Input
|
||||
id="webhookUrl"
|
||||
value={form.webhookUrl}
|
||||
placeholder={form.mode === "mock" ? "Mock 模式可留空" : "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?..."}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, webhookUrl: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="webhookSecret">Webhook Secret</Label>
|
||||
{editingId && form.secretConfigured ? (
|
||||
<div className="space-y-3 rounded-lg border border-zinc-200 bg-zinc-50 p-4">
|
||||
<p className="text-xs text-zinc-600">
|
||||
当前已配置密钥:{form.secretMasked ?? "******"}。默认保留现有密钥,不会回显明文。
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 text-sm text-zinc-700">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="secretAction"
|
||||
checked={form.secretAction === "keep"}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
secretAction: "keep",
|
||||
secret: "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
保留现有密钥
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="secretAction"
|
||||
checked={form.secretAction === "replace"}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
secretAction: "replace",
|
||||
secret: "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
替换密钥
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="secretAction"
|
||||
checked={form.secretAction === "clear"}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
secretAction: "clear",
|
||||
secret: "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
清空密钥
|
||||
</label>
|
||||
</div>
|
||||
{isWebhookMode ? (
|
||||
<>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="webhookUrl">Webhook URL</Label>
|
||||
<Input
|
||||
id="webhookUrl"
|
||||
value={form.webhookUrl}
|
||||
placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?..."
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, webhookUrl: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-zinc-500">
|
||||
{editingId ? "当前未配置密钥,保存时可新增。" : "可选,配置后将参与签名。"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{form.secretAction === "replace" ? (
|
||||
<Input
|
||||
id="webhookSecret"
|
||||
value={form.secret}
|
||||
placeholder={editingId ? "输入新的 Webhook Secret" : "可选,配置后将参与签名"}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, secret: event.target.value }))}
|
||||
/>
|
||||
) : null}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="webhookSecret">Webhook Secret</Label>
|
||||
{editingId && form.secretConfigured ? (
|
||||
<div className="space-y-3 rounded-lg border border-zinc-200 bg-zinc-50 p-4">
|
||||
<p className="text-xs text-zinc-600">
|
||||
当前已配置密钥:{form.secretMasked ?? "******"}。默认保留现有密钥,不会回显明文。
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 text-sm text-zinc-700">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="secretAction"
|
||||
checked={form.secretAction === "keep"}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
secretAction: "keep",
|
||||
secret: "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
保留现有密钥
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="secretAction"
|
||||
checked={form.secretAction === "replace"}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
secretAction: "replace",
|
||||
secret: "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
替换密钥
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="secretAction"
|
||||
checked={form.secretAction === "clear"}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
secretAction: "clear",
|
||||
secret: "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
清空密钥
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-zinc-500">
|
||||
{editingId ? "当前未配置密钥,保存时可新增。" : "可选,配置后将参与签名。"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{form.secretAction === "clear" ? (
|
||||
<p className="text-xs text-amber-600">保存后将清空当前 Webhook Secret。</p>
|
||||
) : null}
|
||||
</div>
|
||||
{form.secretAction === "replace" ? (
|
||||
<Input
|
||||
id="webhookSecret"
|
||||
value={form.secret}
|
||||
placeholder={editingId ? "输入新的 Webhook Secret" : "可选,配置后将参与签名"}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, secret: event.target.value }))}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{form.secretAction === "clear" ? (
|
||||
<p className="text-xs text-amber-600">保存后将清空当前 Webhook Secret。</p>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-600 md:col-span-2">
|
||||
{isAppMessageMode
|
||||
? "个人应用消息使用现有企业微信应用配置和成员绑定,不需要填写 Webhook URL 或 Secret。"
|
||||
: "Mock 模式只记录本地投递结果,不需要填写 Webhook URL 或 Secret。"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-zinc-700">
|
||||
|
||||
在新工单中引用
屏蔽一个用户