From 2d02106a7b0f89f2a230efccbbc8e0f90e34eaf4 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 22 Aug 2026 12:02:24 +0800 Subject: [PATCH] feat(notification): configure DingTalk robot channel --- .../cards/NotificationChannelCard.vue | 7 +- .../dialog/NotificationChannelInfoDialog.vue | 38 +++++++++-- .../__tests__/useSetupWizard.spec.ts | 2 + src/composables/useSetupWizard.ts | 8 +++ src/locales/en-US.ts | 12 +++- src/locales/zh-CN.ts | 12 +++- src/locales/zh-TW.ts | 12 +++- .../setting/AccountSettingNotification.vue | 3 + .../AccountSettingNotification.spec.ts | 12 ++++ src/views/setup/NotificationSettingsStep.vue | 68 +++++++++++++++++-- 10 files changed, 159 insertions(+), 15 deletions(-) diff --git a/src/components/cards/NotificationChannelCard.vue b/src/components/cards/NotificationChannelCard.vue index 9469026a..43c3bde5 100644 --- a/src/components/cards/NotificationChannelCard.vue +++ b/src/components/cards/NotificationChannelCard.vue @@ -5,7 +5,9 @@ import { useI18n } from 'vue-i18n' import { openSharedDialog } from '@/composables/useSharedDialog' import { useCardAccentColor } from '@/composables/useCardAccentColor' -const NotificationChannelInfoDialog = defineAsyncComponent(() => import('@/components/dialog/NotificationChannelInfoDialog.vue')) +const NotificationChannelInfoDialog = defineAsyncComponent( + () => import('@/components/dialog/NotificationChannelInfoDialog.vue'), +) const { t } = useI18n() const { accentRgb, imageRef, updateAccentColor } = useCardAccentColor() @@ -38,6 +40,7 @@ const notificationTypeNames: { [key: string]: string } = { synologychat: t('notification.synologychat.name'), slack: t('notification.slack.name'), discord: t('notification.discord.name'), + dingtalk: t('notification.dingtalk.name'), webpush: t('notification.webpush.name'), custom: t('setting.notification.custom'), } @@ -79,6 +82,8 @@ const getIcon = computed(() => { return getLogoUrl('slack') case 'discord': return getLogoUrl('discord') + case 'dingtalk': + return getLogoUrl('notification') case 'webpush': return getLogoUrl('chrome') default: diff --git a/src/components/dialog/NotificationChannelInfoDialog.vue b/src/components/dialog/NotificationChannelInfoDialog.vue index 6cee6597..1b9f16ef 100644 --- a/src/components/dialog/NotificationChannelInfoDialog.vue +++ b/src/components/dialog/NotificationChannelInfoDialog.vue @@ -363,12 +363,7 @@ async function logoutWechatClawBot() { } wechatClawBotActionLoading.value = true try { - await manageNotificationChannel( - 'WechatClawBot', - 'logout', - getWechatClawBotRequestParams(), - { feedback: 'silent' }, - ) + await manageNotificationChannel('WechatClawBot', 'logout', getWechatClawBotRequestParams(), { feedback: 'silent' }) $toast.success(t('notification.wechatclawbot.logoutSuccess')) await fetchWechatClawBotStatus({ autoGenerateQrcode: true, @@ -1008,6 +1003,37 @@ onMounted(() => { /> + + + + + + + + + + + { ['telegram', {}, 'notification.telegram.tokenRequired'], ['slack', {}, 'notification.slack.oauthTokenRequired'], ['synologychat', {}, 'notification.synologychat.webhookRequired'], + ['dingtalk', {}, 'notification.dingtalk.webhookRequired'], ['vocechat', {}, 'notification.vocechat.hostRequired'], ['webpush', {}, 'notification.webpush.usernameRequired'], ['qqbot', {}, 'notification.qqbot.appIdRequired'], @@ -399,6 +400,7 @@ describe('useSetupWizard defaults, selection and validation', () => { ['telegram', { TELEGRAM_CHAT_ID: 'chat', TELEGRAM_TOKEN: 'token' }], ['slack', { SLACK_CHANNEL: 'channel', SLACK_OAUTH_TOKEN: 'token' }], ['synologychat', { SYNOLOGYCHAT_WEBHOOK: 'https://chat.example.com' }], + ['dingtalk', { DINGTALK_WEBHOOK: 'https://oapi.dingtalk.com/robot/send?access_token=test' }], ['vocechat', { VOCECHAT_API_KEY: 'key', VOCECHAT_HOST: 'https://voce.example.com' }], ['webpush', { WEBPUSH_USERNAME: 'admin' }], ['qqbot', { QQ_APP_ID: 'app', QQ_APP_SECRET: 'secret' }], diff --git a/src/composables/useSetupWizard.ts b/src/composables/useSetupWizard.ts index b8f6eebe..5d0ea0e1 100644 --- a/src/composables/useSetupWizard.ts +++ b/src/composables/useSetupWizard.ts @@ -358,6 +358,7 @@ export function useSetupWizard() { 'wechat': 'WechatModule', 'wechatclawbot': 'WechatClawBotModule', 'slack': 'SlackModule', + 'dingtalk': 'DingTalkModule', 'synologychat': 'SynologyChatModule', 'qqbot': 'QQBotModule', 'vocechat': 'VoceChatModule', @@ -457,6 +458,7 @@ export function useSetupWizard() { wechatclawbot: '微信 ClawBot', telegram: 'Telegram', slack: 'Slack', + dingtalk: '钉钉', synologychat: 'SynologyChat', qqbot: 'QQ', vocechat: 'VoceChat', @@ -730,6 +732,12 @@ export function useSetupWizard() { validationErrors.value.notification.SYNOLOGYCHAT_WEBHOOK = true } break + case 'dingtalk': + if (!config.DINGTALK_WEBHOOK?.trim()) { + errors.push(t('notification.dingtalk.webhookRequired')) + validationErrors.value.notification.DINGTALK_WEBHOOK = true + } + break case 'vocechat': if (!config.VOCECHAT_HOST?.trim()) { errors.push(t('notification.vocechat.hostRequired')) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 47a0651c..ad294b56 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -545,7 +545,7 @@ export default { notification: { title: 'Notifications', description: - 'Notification channels (WeChat Work, WeChat ClawBot, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope', + 'Notification channels (WeChat Work, WeChat ClawBot, Telegram, Slack, DingTalk, SynologyChat, VoceChat, WebPush), message scope', }, about: { title: 'About', @@ -785,6 +785,15 @@ export default { adminsHint: 'User IDs that can use admin menu and commands, separated by commas', adminsPlaceholder: 'User IDs list, separated by commas', }, + dingtalk: { + name: 'DingTalk', + webhook: 'Custom Robot Webhook', + webhookHint: 'Full webhook URL of the DingTalk group custom robot', + webhookRequired: 'Webhook URL cannot be empty', + secret: 'Signing Secret', + secretHint: + 'Enter the SEC-prefixed secret when signature security is enabled; leave empty for keyword or IP allowlist security', + }, synologychat: { name: 'Synology Chat', webhook: 'Webhook URL', @@ -2482,6 +2491,7 @@ export default { other: 'Other', telegram: 'Telegram', slack: 'Slack', + dingTalk: 'DingTalk', synologyChat: 'SynologyChat', voceChat: 'VoceChat', webPush: 'WebPush', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index d200d0d6..28334487 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -536,7 +536,8 @@ export default { }, notification: { title: '通知', - description: '通知渠道(企业微信、微信 ClawBot、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息发送范围', + description: + '通知渠道(企业微信、微信 ClawBot、Telegram、Slack、钉钉、SynologyChat、VoceChat、WebPush)、消息发送范围', }, about: { title: '关于', @@ -774,6 +775,14 @@ export default { adminsHint: '可使用管理菜单及命令的用户ID列表,多个ID使用,分隔', adminsPlaceholder: '用户ID列表,多个ID使用,分隔', }, + dingtalk: { + name: '钉钉', + webhook: '自定义机器人 Webhook', + webhookHint: '钉钉群自定义机器人的完整 Webhook 地址', + webhookRequired: 'Webhook 地址不能为空', + secret: '加签密钥', + secretHint: '机器人安全设置启用“加签”时填写,以 SEC 开头;使用关键词或 IP 白名单时可留空', + }, synologychat: { name: 'Synology Chat', webhook: '机器人传入URL', @@ -2437,6 +2446,7 @@ export default { other: '其它', telegram: 'Telegram', slack: 'Slack', + dingTalk: '钉钉', synologyChat: 'SynologyChat', voceChat: 'VoceChat', webPush: 'WebPush', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 46af9dcf..ab6c3c28 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -536,7 +536,8 @@ export default { }, notification: { title: '通知', - description: '通知渠道(企業微信、微信 ClawBot、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息發送範圍', + description: + '通知渠道(企業微信、微信 ClawBot、Telegram、Slack、釘釘、SynologyChat、VoceChat、WebPush)、消息發送範圍', }, about: { title: '關於', @@ -774,6 +775,14 @@ export default { adminsHint: '可使用管理菜單及命令的用戶ID列表,多個ID使用,分隔', adminsPlaceholder: '用戶ID列表,多個ID使用,分隔', }, + dingtalk: { + name: '釘釘', + webhook: '自定義機器人 Webhook', + webhookHint: '釘釘群自定義機器人的完整 Webhook 地址', + webhookRequired: 'Webhook 地址不能為空', + secret: '加簽密鑰', + secretHint: '機器人安全設置啟用“加簽”時填寫,以 SEC 開頭;使用關鍵詞或 IP 白名單時可留空', + }, synologychat: { name: 'Synology Chat', webhook: '機器人傳入URL', @@ -2436,6 +2445,7 @@ export default { other: '其它', telegram: 'Telegram', slack: 'Slack', + dingTalk: '釘釘', synologyChat: 'SynologyChat', voceChat: 'VoceChat', webPush: 'WebPush', diff --git a/src/views/setting/AccountSettingNotification.vue b/src/views/setting/AccountSettingNotification.vue index 591a3fe2..8acde490 100644 --- a/src/views/setting/AccountSettingNotification.vue +++ b/src/views/setting/AccountSettingNotification.vue @@ -455,6 +455,9 @@ useSilentSettingRefresh(loadPageData, { Discord + + {{ t('setting.notification.dingTalk') }} + {{ t('setting.notification.synologyChat') }} diff --git a/src/views/setting/__tests__/AccountSettingNotification.spec.ts b/src/views/setting/__tests__/AccountSettingNotification.spec.ts index 9dfe7881..037cea2b 100644 --- a/src/views/setting/__tests__/AccountSettingNotification.spec.ts +++ b/src/views/setting/__tests__/AccountSettingNotification.spec.ts @@ -180,6 +180,18 @@ describe('AccountSettingNotification', () => { expect(mocks.toastSuccess).toHaveBeenCalledWith('通知设置保存成功') }) + it('offers DingTalk as a native notification channel', async () => { + const user = userEvent.setup() + await renderNotificationSettings() + await screen.findByText('Alpha / wechatclawbot') + + const channelCard = getCard('通知渠道') + await user.click(channelCard.getAllByRole('button').at(-1)!) + await user.click(await screen.findByText('钉钉', { selector: '.v-list-item-title' })) + + expect(screen.getByText('通知4 / dingtalk')).toBeInTheDocument() + }) + it('compresses chained ClawBot renames and migrates the original source before saving channels', async () => { const user = userEvent.setup() await renderNotificationSettings() diff --git a/src/views/setup/NotificationSettingsStep.vue b/src/views/setup/NotificationSettingsStep.vue index 4c8119a3..7e58e176 100644 --- a/src/views/setup/NotificationSettingsStep.vue +++ b/src/views/setup/NotificationSettingsStep.vue @@ -118,6 +118,19 @@ const notificationTypes = [ + + + + +
钉钉
+
+
+
+ + + + + + + + + + +