feat: add dingtalk robot update notifications
This commit is contained in:
parent
94a22f0cc6
commit
38c10d616b
@ -87,6 +87,12 @@ import {
|
|||||||
normalizeCanvasImageTaskQueueConfig,
|
normalizeCanvasImageTaskQueueConfig,
|
||||||
type CanvasImageTaskQueueConfig,
|
type CanvasImageTaskQueueConfig,
|
||||||
} from "@/lib/canvas/image-task-config";
|
} from "@/lib/canvas/image-task-config";
|
||||||
|
import {
|
||||||
|
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||||
|
defaultDingTalkRobotNotificationConfig,
|
||||||
|
normalizeDingTalkRobotNotificationConfig,
|
||||||
|
type DingTalkRobotNotificationConfig,
|
||||||
|
} from "@/lib/notifications/dingtalk-robot-config";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
||||||
@ -1513,6 +1519,13 @@ export function AdminSettingsClient() {
|
|||||||
`/api/admin/settings?key=${WORLDLABS_KEY_POOL_CONFIG_KEY}`,
|
`/api/admin/settings?key=${WORLDLABS_KEY_POOL_CONFIG_KEY}`,
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
data: dingTalkRobotNotificationConfigData,
|
||||||
|
mutate: mutateDingTalkRobotNotificationConfig,
|
||||||
|
} = useSWR<{ setting?: { value?: unknown } }>(
|
||||||
|
`/api/admin/settings?key=${DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY}`,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
data: threeDModelAliasConfigData,
|
data: threeDModelAliasConfigData,
|
||||||
mutate: mutateThreeDModelAliasConfig,
|
mutate: mutateThreeDModelAliasConfig,
|
||||||
@ -2830,6 +2843,14 @@ export function AdminSettingsClient() {
|
|||||||
title: "",
|
title: "",
|
||||||
content: "",
|
content: "",
|
||||||
});
|
});
|
||||||
|
const [dingTalkRobotNotificationConfig, setDingTalkRobotNotificationConfig] =
|
||||||
|
useState<DingTalkRobotNotificationConfig>(
|
||||||
|
defaultDingTalkRobotNotificationConfig,
|
||||||
|
);
|
||||||
|
const [isSavingDingTalkRobotNotificationConfig, setIsSavingDingTalkRobotNotificationConfig] =
|
||||||
|
useState(false);
|
||||||
|
const [isSendingDingTalkNotification, setIsSendingDingTalkNotification] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/admin/settings?key=login_notification")
|
fetch("/api/admin/settings?key=login_notification")
|
||||||
@ -2841,6 +2862,13 @@ export function AdminSettingsClient() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const value = dingTalkRobotNotificationConfigData?.setting?.value;
|
||||||
|
setDingTalkRobotNotificationConfig(
|
||||||
|
normalizeDingTalkRobotNotificationConfig(value),
|
||||||
|
);
|
||||||
|
}, [dingTalkRobotNotificationConfigData]);
|
||||||
|
|
||||||
const handleSaveNotification = async () => {
|
const handleSaveNotification = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch("/api/admin/settings", {
|
await fetch("/api/admin/settings", {
|
||||||
@ -2857,6 +2885,68 @@ export function AdminSettingsClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveDingTalkRobotNotificationConfig = async () => {
|
||||||
|
setIsSavingDingTalkRobotNotificationConfig(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/admin/settings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||||
|
value: dingTalkRobotNotificationConfig,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("保存失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("钉钉群机器人配置已保存");
|
||||||
|
mutateDingTalkRobotNotificationConfig();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("保存钉钉群机器人配置失败");
|
||||||
|
} finally {
|
||||||
|
setIsSavingDingTalkRobotNotificationConfig(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendNotificationToDingTalk = async () => {
|
||||||
|
const title = String(notification.title || "").trim();
|
||||||
|
const content = String(notification.content || "").trim();
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
toast.error("请先填写系统通知标题");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
toast.error("请先填写系统通知内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSendingDingTalkNotification(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/admin/dingtalk-robot/send", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title, content }),
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
|
||||||
|
if (!response.ok || payload?.success === false) {
|
||||||
|
throw new Error(payload?.error || "发送失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("当前系统通知已发送到钉钉群");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "发送钉钉群通知失败",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSendingDingTalkNotification(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (questionsData) {
|
if (questionsData) {
|
||||||
setQuestions(questionsData);
|
setQuestions(questionsData);
|
||||||
@ -13421,7 +13511,7 @@ export function AdminSettingsClient() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 系统通知设置 */}
|
{/* 系统通知设置 */}
|
||||||
<TabsContent value="notification" className="mt-6">
|
<TabsContent value="notification" className="mt-6 space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">系统通知</CardTitle>
|
<CardTitle className="text-xl">系统通知</CardTitle>
|
||||||
@ -13471,6 +13561,91 @@ export function AdminSettingsClient() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">钉钉群机器人通知</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
配置钉钉自定义机器人后,可将当前系统通知一键发送到钉钉群
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="dingtalk-robot-enabled"
|
||||||
|
checked={dingTalkRobotNotificationConfig.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setDingTalkRobotNotificationConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
enabled: checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="dingtalk-robot-enabled">
|
||||||
|
启用钉钉群机器人通知
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="dingtalk-robot-webhook">Webhook 地址</Label>
|
||||||
|
<Input
|
||||||
|
id="dingtalk-robot-webhook"
|
||||||
|
value={dingTalkRobotNotificationConfig.webhookUrl}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDingTalkRobotNotificationConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
webhookUrl: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="https://oapi.dingtalk.com/robot/send?access_token=..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="dingtalk-robot-secret">
|
||||||
|
加签密钥(可选)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="dingtalk-robot-secret"
|
||||||
|
type="password"
|
||||||
|
value={dingTalkRobotNotificationConfig.secret}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDingTalkRobotNotificationConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
secret: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="如机器人启用了安全加签,请填写"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
建议在钉钉群中创建“自定义机器人”,将 Webhook 与加签密钥配置到这里。发送时会复用当前系统通知的标题与正文。
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveDingTalkRobotNotificationConfig}
|
||||||
|
disabled={isSavingDingTalkRobotNotificationConfig}
|
||||||
|
>
|
||||||
|
{isSavingDingTalkRobotNotificationConfig ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
保存机器人配置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleSendNotificationToDingTalk}
|
||||||
|
disabled={isSendingDingTalkNotification}
|
||||||
|
>
|
||||||
|
{isSendingDingTalkNotification ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Bell className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
发送当前系统通知到钉钉群
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* AI 模型配置 */}
|
{/* AI 模型配置 */}
|
||||||
|
|||||||
67
app/api/admin/dingtalk-robot/send/route.ts
Normal file
67
app/api/admin/dingtalk-robot/send/route.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { getSystemSetting } from "@/lib/db/queries";
|
||||||
|
import { ChatSDKError } from "@/lib/errors";
|
||||||
|
import { requireAdmin } from "@/lib/middleware/admin";
|
||||||
|
import {
|
||||||
|
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||||
|
defaultDingTalkRobotNotificationConfig,
|
||||||
|
normalizeDingTalkRobotNotificationConfig,
|
||||||
|
} from "@/lib/notifications/dingtalk-robot-config";
|
||||||
|
import { sendDingTalkRobotNotification } from "@/lib/notifications/dingtalk-robot";
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
title: z.string().trim().min(1, "通知标题不能为空").max(100, "通知标题过长"),
|
||||||
|
content: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "通知内容不能为空")
|
||||||
|
.max(5000, "通知内容过长"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
await requireAdmin();
|
||||||
|
|
||||||
|
const body = bodySchema.parse(await request.json());
|
||||||
|
const setting = await getSystemSetting({
|
||||||
|
key: DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
const config = setting?.value
|
||||||
|
? normalizeDingTalkRobotNotificationConfig(setting.value)
|
||||||
|
: defaultDingTalkRobotNotificationConfig;
|
||||||
|
|
||||||
|
const result = await sendDingTalkRobotNotification({
|
||||||
|
config,
|
||||||
|
title: body.title,
|
||||||
|
content: body.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ChatSDKError) {
|
||||||
|
return error.toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error.issues[0]?.message || "参数校验失败",
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "发送钉钉通知失败",
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,6 +51,10 @@ import {
|
|||||||
CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY,
|
CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY,
|
||||||
normalizeCanvasImageTaskQueueConfig,
|
normalizeCanvasImageTaskQueueConfig,
|
||||||
} from "@/lib/canvas/image-task-config";
|
} from "@/lib/canvas/image-task-config";
|
||||||
|
import {
|
||||||
|
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||||
|
normalizeDingTalkRobotNotificationConfig,
|
||||||
|
} from "@/lib/notifications/dingtalk-robot-config";
|
||||||
|
|
||||||
const settingSchema = z.object({
|
const settingSchema = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
@ -98,6 +102,12 @@ export async function GET(request: Request) {
|
|||||||
setting: setting ? { ...setting, value } : { key, value },
|
setting: setting ? { ...setting, value } : { key, value },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||||
|
const value = normalizeDingTalkRobotNotificationConfig(setting?.value);
|
||||||
|
return Response.json({
|
||||||
|
setting: setting ? { ...setting, value } : { key, value },
|
||||||
|
});
|
||||||
|
}
|
||||||
return Response.json({ setting });
|
return Response.json({ setting });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +242,10 @@ export async function POST(request: Request) {
|
|||||||
nextValue = normalizeCanvasImageTaskQueueConfig(value);
|
nextValue = normalizeCanvasImageTaskQueueConfig(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||||
|
nextValue = normalizeDingTalkRobotNotificationConfig(value);
|
||||||
|
}
|
||||||
|
|
||||||
if (key === THIRD_PARTY_SQLSERVER_CONFIG_KEY) {
|
if (key === THIRD_PARTY_SQLSERVER_CONFIG_KEY) {
|
||||||
const existingSetting = await getSystemSetting({
|
const existingSetting = await getSystemSetting({
|
||||||
key: THIRD_PARTY_SQLSERVER_CONFIG_KEY,
|
key: THIRD_PARTY_SQLSERVER_CONFIG_KEY,
|
||||||
|
|||||||
37
lib/notifications/dingtalk-robot-config.ts
Normal file
37
lib/notifications/dingtalk-robot-config.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export const DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY =
|
||||||
|
"dingtalk_robot_notification_config";
|
||||||
|
|
||||||
|
export type DingTalkRobotNotificationConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
webhookUrl: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultDingTalkRobotNotificationConfig: DingTalkRobotNotificationConfig =
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
webhookUrl: "",
|
||||||
|
secret: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRobotString(value: unknown) {
|
||||||
|
return String(value || "")
|
||||||
|
.replace(/[\u2028\u2029]/g, "")
|
||||||
|
.replace(/[\u0000-\u001F\u007F]/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDingTalkRobotNotificationConfig(
|
||||||
|
value: unknown,
|
||||||
|
): DingTalkRobotNotificationConfig {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return defaultDingTalkRobotNotificationConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = value as Partial<DingTalkRobotNotificationConfig>;
|
||||||
|
return {
|
||||||
|
enabled: raw.enabled === true,
|
||||||
|
webhookUrl: normalizeRobotString(raw.webhookUrl),
|
||||||
|
secret: normalizeRobotString(raw.secret),
|
||||||
|
};
|
||||||
|
}
|
||||||
134
lib/notifications/dingtalk-robot.ts
Normal file
134
lib/notifications/dingtalk-robot.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { createHmac } from "node:crypto";
|
||||||
|
import {
|
||||||
|
defaultDingTalkRobotNotificationConfig,
|
||||||
|
normalizeDingTalkRobotNotificationConfig,
|
||||||
|
type DingTalkRobotNotificationConfig,
|
||||||
|
} from "./dingtalk-robot-config";
|
||||||
|
|
||||||
|
function buildSignedWebhookUrl(webhookUrl: string, secret: string) {
|
||||||
|
const normalizedUrl = String(webhookUrl || "").trim();
|
||||||
|
const normalizedSecret = String(secret || "").trim();
|
||||||
|
|
||||||
|
if (!normalizedSecret) {
|
||||||
|
return normalizedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now().toString();
|
||||||
|
const stringToSign = `${timestamp}\n${normalizedSecret}`;
|
||||||
|
const sign = createHmac("sha256", normalizedSecret)
|
||||||
|
.update(stringToSign, "utf8")
|
||||||
|
.digest("base64");
|
||||||
|
|
||||||
|
const url = new URL(normalizedUrl);
|
||||||
|
url.searchParams.set("timestamp", timestamp);
|
||||||
|
url.searchParams.set("sign", sign);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMarkdownText(params: { title: string; content: string }) {
|
||||||
|
const normalizedTitle = String(params.title || "").trim();
|
||||||
|
const normalizedContent = String(params.content || "").trim();
|
||||||
|
const sentAt = new Intl.DateTimeFormat("zh-CN", {
|
||||||
|
timeZone: "Asia/Shanghai",
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
}).format(new Date());
|
||||||
|
|
||||||
|
const body = normalizedContent
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
return [`### ${normalizedTitle}`, "", body, "", `> 发送时间:${sentAt}`].join(
|
||||||
|
"\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendDingTalkRobotNotification(params: {
|
||||||
|
config: DingTalkRobotNotificationConfig;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}) {
|
||||||
|
const config = normalizeDingTalkRobotNotificationConfig(params.config);
|
||||||
|
|
||||||
|
if (!config.enabled) {
|
||||||
|
throw new Error("钉钉群机器人通知未启用");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.webhookUrl) {
|
||||||
|
throw new Error("钉钉群机器人 Webhook 未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = String(params.title || "").trim();
|
||||||
|
const content = String(params.content || "").trim();
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
throw new Error("通知标题不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
throw new Error("通知内容不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestUrl = buildSignedWebhookUrl(config.webhookUrl, config.secret);
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
msgtype: "markdown",
|
||||||
|
markdown: {
|
||||||
|
title,
|
||||||
|
text: buildMarkdownText({ title, content }),
|
||||||
|
},
|
||||||
|
at: {
|
||||||
|
isAtAll: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(15_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
let responseData: any = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = responseText ? JSON.parse(responseText) : null;
|
||||||
|
} catch {
|
||||||
|
responseData = responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`钉钉群机器人请求失败:${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
responseData &&
|
||||||
|
typeof responseData === "object" &&
|
||||||
|
"errcode" in responseData &&
|
||||||
|
responseData.errcode !== 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
String(responseData.errmsg || "钉钉群机器人返回失败"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
response: responseData,
|
||||||
|
configUsed: {
|
||||||
|
...defaultDingTalkRobotNotificationConfig,
|
||||||
|
...config,
|
||||||
|
secret: config.secret ? "***" : "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user