feat(notification): configure DingTalk robot channel

This commit is contained in:
jxxghp
2026-08-22 12:02:24 +08:00
parent 3455d91f4b
commit 2d02106a7b
10 changed files with 159 additions and 15 deletions
@@ -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:
@@ -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(() => {
/>
</VCol>
</VRow>
<VRow v-else-if="notificationInfo.type == 'dingtalk'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.name"
:label="t('notification.name')"
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="notificationInfo.config.DINGTALK_WEBHOOK"
:label="t('notification.dingtalk.webhook')"
:hint="t('notification.dingtalk.webhookHint')"
persistent-hint
prepend-inner-icon="mdi-webhook"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="notificationInfo.config.DINGTALK_SECRET"
:label="t('notification.dingtalk.secret')"
:hint="t('notification.dingtalk.secretHint')"
persistent-hint
prepend-inner-icon="mdi-shield-key"
type="password"
/>
</VCol>
</VRow>
<VRow v-else-if="notificationInfo.type == 'vocechat'">
<VCol cols="12" md="6">
<VTextField
@@ -380,6 +380,7 @@ describe('useSetupWizard defaults, selection and validation', () => {
['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' }],
+8
View File
@@ -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'))
+11 -1
View File
@@ -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',
+11 -1
View File
@@ -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',
+11 -1
View File
@@ -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',
@@ -455,6 +455,9 @@ useSilentSettingRefresh(loadPageData, {
<VListItem @click="addNotification('discord')">
<VListItemTitle>Discord</VListItemTitle>
</VListItem>
<VListItem @click="addNotification('dingtalk')">
<VListItemTitle>{{ t('setting.notification.dingTalk') }}</VListItemTitle>
</VListItem>
<VListItem @click="addNotification('synologychat')">
<VListItemTitle>{{ t('setting.notification.synologyChat') }}</VListItemTitle>
</VListItem>
@@ -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()
+63 -5
View File
@@ -118,6 +118,19 @@ const notificationTypes = [
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.notification.type === 'dingtalk' ? 'primary' : 'default'"
:variant="wizardData.notification.type === 'dingtalk' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectNotification('dingtalk')"
>
<VCardText class="text-center">
<VIcon icon="mdi-message-processing" size="48" class="mb-2" />
<div class="text-h6">钉钉</div>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.notification.type === 'qqbot' ? 'primary' : 'default'"
@@ -341,7 +354,9 @@ const notificationTypes = [
:label="t('notification.feishu.appId')"
:hint="t('notification.feishu.appIdHint')"
:error="validationErrors.notification.FEISHU_APP_ID"
:error-messages="validationErrors.notification.FEISHU_APP_ID ? [t('notification.feishu.appIdRequired')] : []"
:error-messages="
validationErrors.notification.FEISHU_APP_ID ? [t('notification.feishu.appIdRequired')] : []
"
persistent-hint
prepend-inner-icon="mdi-application"
required
@@ -353,7 +368,11 @@ const notificationTypes = [
:label="t('notification.feishu.appSecret')"
:hint="t('notification.feishu.appSecretHint')"
:error="validationErrors.notification.FEISHU_APP_SECRET"
:error-messages="validationErrors.notification.FEISHU_APP_SECRET ? [t('notification.feishu.appSecretRequired')] : []"
:error-messages="
validationErrors.notification.FEISHU_APP_SECRET
? [t('notification.feishu.appSecretRequired')]
: []
"
persistent-hint
prepend-inner-icon="mdi-key"
required
@@ -517,9 +536,7 @@ const notificationTypes = [
:hint="t('notification.qqbot.appSecretHint')"
:error="validationErrors.notification.QQ_APP_SECRET"
:error-messages="
validationErrors.notification.QQ_APP_SECRET
? [t('notification.qqbot.appSecretRequired')]
: []
validationErrors.notification.QQ_APP_SECRET ? [t('notification.qqbot.appSecretRequired')] : []
"
persistent-hint
prepend-inner-icon="mdi-key"
@@ -643,6 +660,47 @@ const notificationTypes = [
/>
</VCol>
</VRow>
<VRow v-else-if="wizardData.notification.type === 'dingtalk'">
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.name"
:label="t('notification.name')"
:placeholder="t('notification.name')"
:hint="t('notification.nameHint')"
:error="validationErrors.notification.name"
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
persistent-hint
prepend-inner-icon="mdi-label"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.DINGTALK_WEBHOOK"
:label="t('notification.dingtalk.webhook')"
:hint="t('notification.dingtalk.webhookHint')"
:error="validationErrors.notification.DINGTALK_WEBHOOK"
:error-messages="
validationErrors.notification.DINGTALK_WEBHOOK
? [t('notification.dingtalk.webhookRequired')]
: []
"
persistent-hint
prepend-inner-icon="mdi-webhook"
required
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.DINGTALK_SECRET"
:label="t('notification.dingtalk.secret')"
:hint="t('notification.dingtalk.secretHint')"
persistent-hint
prepend-inner-icon="mdi-shield-key"
type="password"
/>
</VCol>
</VRow>
<VRow v-else-if="wizardData.notification.type === 'vocechat'">
<VCol cols="12" md="6">
<VTextField