feat: add wechat clawbot notification setup UI

This commit is contained in:
jxxghp
2026-05-10 21:47:35 +08:00
parent f495e13667
commit 4f9dce70d3
11 changed files with 534 additions and 24 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import api from '@/api'
import { NotificationConf } from '@/api/types' import { NotificationConf } from '@/api/types'
import { getLogoUrl } from '@/utils/imageUtils' import { getLogoUrl } from '@/utils/imageUtils'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
@@ -45,6 +46,7 @@ const notificationInfo = ref<NotificationConf>({
// 各通知类型的名称字典 // 各通知类型的名称字典
const notificationTypeNames: { [key: string]: string } = { const notificationTypeNames: { [key: string]: string } = {
wechat: t('notification.wechat.name'), wechat: t('notification.wechat.name'),
wechatclawbot: t('notification.wechatclawbot.name'),
telegram: t('notification.telegram.name'), telegram: t('notification.telegram.name'),
qqbot: t('notification.qqbot.name'), qqbot: t('notification.qqbot.name'),
vocechat: t('notification.vocechat.name'), vocechat: t('notification.vocechat.name'),
@@ -68,6 +70,18 @@ const notificationTypes = [
{ value: '其它', title: t('notificationSwitch.other') }, { value: '其它', title: t('notificationSwitch.other') },
] ]
interface WechatClawBotStatus {
connected?: boolean
account_id?: string | null
qrcode?: string | null
qrcode_url?: string | null
qrcode_status?: string | null
qrcode_updated_at?: number | null
known_targets?: Array<{ userid: string; username: string; last_active?: number | null }>
default_target?: string | null
base_url?: string | null
}
function ensureWechatConfigDefaults(notification: NotificationConf) { function ensureWechatConfigDefaults(notification: NotificationConf) {
if (notification.type !== 'wechat') { if (notification.type !== 'wechat') {
return return
@@ -83,6 +97,39 @@ function ensureWechatConfigDefaults(notification: NotificationConf) {
} }
} }
function ensureWechatClawBotConfigDefaults(notification: NotificationConf) {
if (notification.type !== 'wechatclawbot') {
return
}
if (!notification.config) {
notification.config = {}
}
if (!notification.config.WECHATCLAWBOT_BASE_URL) {
notification.config.WECHATCLAWBOT_BASE_URL = 'https://ilinkai.weixin.qq.com'
}
if (!notification.config.WECHATCLAWBOT_POLL_TIMEOUT) {
notification.config.WECHATCLAWBOT_POLL_TIMEOUT = 25
}
}
const wechatClawBotLoading = ref(false)
const wechatClawBotActionLoading = ref(false)
const wechatClawBotStatus = ref<WechatClawBotStatus | null>(null)
let wechatClawBotTimer: number | null = null
function getWechatClawBotRequestParams(extraParams: Record<string, any> = {}) {
const config = notificationInfo.value.config || {}
return {
source: notificationInfo.value.name,
fallback_source: props.notification.name,
WECHATCLAWBOT_BASE_URL: config.WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET: config.WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS: config.WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT: config.WECHATCLAWBOT_POLL_TIMEOUT,
...extraParams,
}
}
const isWechatBotMode = computed({ const isWechatBotMode = computed({
get: () => notificationInfo.value.config?.WECHAT_MODE === 'bot', get: () => notificationInfo.value.config?.WECHAT_MODE === 'bot',
set: value => { set: value => {
@@ -101,7 +148,11 @@ function openNotificationInfoDialog() {
// 替换成深复制,避免修改时影响原数据 // 替换成深复制,避免修改时影响原数据
notificationInfo.value = cloneDeep(props.notification) notificationInfo.value = cloneDeep(props.notification)
ensureWechatConfigDefaults(notificationInfo.value) ensureWechatConfigDefaults(notificationInfo.value)
ensureWechatClawBotConfigDefaults(notificationInfo.value)
notificationInfoDialog.value = true notificationInfoDialog.value = true
if (notificationInfo.value.type === 'wechatclawbot') {
fetchWechatClawBotStatus(true)
}
} }
// 保存详情数据 // 保存详情数据
@@ -117,16 +168,137 @@ function saveNotificationInfo() {
return return
} }
ensureWechatConfigDefaults(notificationInfo.value) ensureWechatConfigDefaults(notificationInfo.value)
ensureWechatClawBotConfigDefaults(notificationInfo.value)
notificationInfoDialog.value = false notificationInfoDialog.value = false
emit('change', notificationInfo.value, props.notification.name) emit('change', notificationInfo.value, props.notification.name)
emit('done') emit('done')
} }
function clearWechatClawBotTimer() {
if (wechatClawBotTimer) {
window.clearTimeout(wechatClawBotTimer)
wechatClawBotTimer = null
}
}
function scheduleWechatClawBotRefresh() {
clearWechatClawBotTimer()
if (!notificationInfoDialog.value || notificationInfo.value.type !== 'wechatclawbot') {
return
}
const connected = wechatClawBotStatus.value?.connected
const pendingStatus = ['waiting', 'scanned'].includes((wechatClawBotStatus.value?.qrcode_status || '').toLowerCase())
if (connected || pendingStatus) {
wechatClawBotTimer = window.setTimeout(() => {
fetchWechatClawBotStatus(false)
}, connected ? 10000 : 3000)
}
}
async function fetchWechatClawBotStatus(autoGenerateQrcode = false) {
if (notificationInfo.value.type !== 'wechatclawbot' || !notificationInfo.value.name) {
return
}
wechatClawBotLoading.value = true
try {
const result: { [key: string]: any } = await api.get('notification/wechatclawbot/status', {
params: getWechatClawBotRequestParams({ auto_generate_qrcode: autoGenerateQrcode }),
})
if (result.success) {
wechatClawBotStatus.value = result.data
scheduleWechatClawBotRefresh()
} else {
wechatClawBotStatus.value = null
clearWechatClawBotTimer()
$toast.error(result.message || t('notification.wechatclawbot.statusLoadFailed'))
}
} catch (error) {
console.error(error)
clearWechatClawBotTimer()
$toast.error(t('notification.wechatclawbot.statusLoadFailed'))
} finally {
wechatClawBotLoading.value = false
}
}
async function refreshWechatClawBotQrcode() {
if (!notificationInfo.value.name) {
return
}
wechatClawBotActionLoading.value = true
try {
const result: { [key: string]: any } = await api.post('notification/wechatclawbot/refresh', null, {
params: getWechatClawBotRequestParams(),
})
if (result.success) {
wechatClawBotStatus.value = result.data
scheduleWechatClawBotRefresh()
$toast.success(t('notification.wechatclawbot.qrcodeRefreshSuccess'))
} else {
$toast.error(result.message || t('notification.wechatclawbot.qrcodeRefreshFailed'))
}
} catch (error) {
console.error(error)
$toast.error(t('notification.wechatclawbot.qrcodeRefreshFailed'))
} finally {
wechatClawBotActionLoading.value = false
}
}
async function logoutWechatClawBot() {
if (!notificationInfo.value.name) {
return
}
wechatClawBotActionLoading.value = true
try {
const result: { [key: string]: any } = await api.post('notification/wechatclawbot/logout', null, {
params: getWechatClawBotRequestParams(),
})
if (result.success) {
$toast.success(result.message || t('notification.wechatclawbot.logoutSuccess'))
await fetchWechatClawBotStatus(true)
} else {
$toast.error(result.message || t('notification.wechatclawbot.logoutFailed'))
}
} catch (error) {
console.error(error)
$toast.error(t('notification.wechatclawbot.logoutFailed'))
} finally {
wechatClawBotActionLoading.value = false
}
}
function formatWechatClawBotTime(timestamp?: number | null) {
if (!timestamp) {
return ''
}
return new Date(timestamp * 1000).toLocaleString()
}
const wechatClawBotStatusText = computed(() => {
const status = (wechatClawBotStatus.value?.qrcode_status || '').toLowerCase()
if (wechatClawBotStatus.value?.connected) {
return t('notification.wechatclawbot.connected')
}
if (status === 'scanned') {
return t('notification.wechatclawbot.scanned')
}
if (status === 'expired') {
return t('notification.wechatclawbot.expired')
}
if (status === 'confirmed') {
return t('notification.wechatclawbot.confirmed')
}
return t('notification.wechatclawbot.waiting')
})
// 根据存储类型选择图标 // 根据存储类型选择图标
const getIcon = computed(() => { const getIcon = computed(() => {
switch (props.notification.type) { switch (props.notification.type) {
case 'wechat': case 'wechat':
return getLogoUrl('wechat') return getLogoUrl('wechat')
case 'wechatclawbot':
return getLogoUrl('wechatclawbot')
case 'telegram': case 'telegram':
return getLogoUrl('telegram') return getLogoUrl('telegram')
case 'qqbot': case 'qqbot':
@@ -148,8 +320,15 @@ const getIcon = computed(() => {
// 按钮点击 // 按钮点击
function onClose() { function onClose() {
clearWechatClawBotTimer()
emit('close') emit('close')
} }
watch(notificationInfoDialog, value => {
if (!value) {
clearWechatClawBotTimer()
}
})
</script> </script>
<template> <template>
<div> <div>
@@ -347,6 +526,137 @@ function onClose() {
</VCol> </VCol>
</template> </template>
</VRow> </VRow>
<VRow v-else-if="notificationInfo.type == 'wechatclawbot'">
<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" md="6">
<VTextField
v-model="notificationInfo.config.WECHATCLAWBOT_BASE_URL"
:label="t('notification.wechatclawbot.baseUrl')"
:hint="t('notification.wechatclawbot.baseUrlHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHATCLAWBOT_DEFAULT_TARGET"
:label="t('notification.wechatclawbot.defaultTarget')"
:placeholder="t('notification.wechatclawbot.defaultTargetPlaceholder')"
:hint="t('notification.wechatclawbot.defaultTargetHint')"
persistent-hint
prepend-inner-icon="mdi-account-arrow-right"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHATCLAWBOT_ADMINS"
:label="t('notification.wechatclawbot.admins')"
:placeholder="t('notification.wechatclawbot.adminsPlaceholder')"
:hint="t('notification.wechatclawbot.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="notificationInfo.config.WECHATCLAWBOT_POLL_TIMEOUT"
:label="t('notification.wechatclawbot.pollTimeout')"
:hint="t('notification.wechatclawbot.pollTimeoutHint')"
persistent-hint
type="number"
prepend-inner-icon="mdi-timer-outline"
/>
</VCol>
<VCol cols="12">
<VCard variant="tonal" class="pa-4">
<div class="d-flex flex-wrap align-center justify-space-between gap-3 mb-3">
<div>
<div class="text-subtitle-1 font-weight-medium">{{ t('notification.wechatclawbot.loginStatus') }}</div>
<div class="text-body-2 text-medium-emphasis">{{ wechatClawBotStatusText }}</div>
</div>
<div class="d-flex flex-wrap gap-2">
<VBtn
size="small"
variant="tonal"
:loading="wechatClawBotLoading"
@click.stop="fetchWechatClawBotStatus(true)"
>
{{ t('common.refresh') }}
</VBtn>
<VBtn
size="small"
color="primary"
variant="tonal"
:loading="wechatClawBotActionLoading"
@click.stop="refreshWechatClawBotQrcode"
>
{{ t('notification.wechatclawbot.refreshQrcode') }}
</VBtn>
<VBtn
size="small"
color="error"
variant="tonal"
:loading="wechatClawBotActionLoading"
:disabled="!wechatClawBotStatus?.connected"
@click.stop="logoutWechatClawBot"
>
{{ t('notification.wechatclawbot.logout') }}
</VBtn>
</div>
</div>
<VRow>
<VCol cols="12" md="5">
<div class="rounded text-center p-3 border h-100 d-flex align-center justify-center min-h-[16rem]">
<VImg
v-if="wechatClawBotStatus?.qrcode_url"
:src="wechatClawBotStatus.qrcode_url"
width="220"
height="220"
class="mx-auto"
/>
<VProgressCircular v-else-if="wechatClawBotLoading" indeterminate color="primary" />
<div v-else class="text-body-2 text-medium-emphasis">
{{ t('notification.wechatclawbot.noQrcode') }}
</div>
</div>
</VCol>
<VCol cols="12" md="7">
<VAlert variant="tonal" :type="wechatClawBotStatus?.connected ? 'success' : 'info'" class="mb-3">
<div class="text-body-2">{{ t('notification.wechatclawbot.scanHint') }}</div>
<div v-if="wechatClawBotStatus?.account_id" class="mt-2">
{{ t('notification.wechatclawbot.accountId') }}: {{ wechatClawBotStatus.account_id }}
</div>
<div v-if="wechatClawBotStatus?.qrcode_updated_at" class="mt-2">
{{ t('notification.wechatclawbot.qrcodeUpdatedAt') }}:
{{ formatWechatClawBotTime(wechatClawBotStatus.qrcode_updated_at) }}
</div>
</VAlert>
<div class="text-subtitle-2 mb-2">{{ t('notification.wechatclawbot.knownTargets') }}</div>
<VList v-if="wechatClawBotStatus?.known_targets?.length" density="compact" class="border rounded">
<VListItem
v-for="item in wechatClawBotStatus.known_targets"
:key="item.userid"
:title="item.username || item.userid"
:subtitle="`${item.userid}${item.last_active ? ` · ${formatWechatClawBotTime(item.last_active)}` : ''}`"
/>
</VList>
<div v-else class="text-body-2 text-medium-emphasis">
{{ t('notification.wechatclawbot.noKnownTargets') }}
</div>
</VCol>
</VRow>
</VCard>
</VCol>
</VRow>
<VRow v-else-if="notificationInfo.type == 'telegram'"> <VRow v-else-if="notificationInfo.type == 'telegram'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

View File

@@ -91,6 +91,7 @@ const userForm = ref<ExtendedUser>({
}, },
settings: { settings: {
wechat_userid: null, wechat_userid: null,
wechatclawbot_userid: null,
telegram_userid: null, telegram_userid: null,
slack_userid: null, slack_userid: null,
discord_userid: null, discord_userid: null,
@@ -503,6 +504,15 @@ onMounted(() => {
prepend-inner-icon="mdi-wechat" prepend-inner-icon="mdi-wechat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6">
<VTextField
v-model="userForm.settings.wechatclawbot_userid"
density="comfortable"
clearable
:label="t('dialog.userAddEdit.wechatClawBot')"
prepend-inner-icon="mdi-robot-happy-outline"
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="userForm.settings.telegram_userid" v-model="userForm.settings.telegram_userid"

View File

@@ -329,6 +329,7 @@ export function useSetupWizard() {
notification: { notification: {
'telegram': 'TelegramModule', 'telegram': 'TelegramModule',
'wechat': 'WechatModule', 'wechat': 'WechatModule',
'wechatclawbot': 'WechatClawBotModule',
'slack': 'SlackModule', 'slack': 'SlackModule',
'synologychat': 'SynologyChatModule', 'synologychat': 'SynologyChatModule',
'qqbot': 'QQBotModule', 'qqbot': 'QQBotModule',
@@ -423,7 +424,17 @@ export function useSetupWizard() {
wizardData.value.notification.type = type wizardData.value.notification.type = type
// 如果名称为空或为默认名称,则设置默认名称 // 如果名称为空或为默认名称,则设置默认名称
if (!wizardData.value.notification.name || wizardData.value.notification.name.includes('通知')) { if (!wizardData.value.notification.name || wizardData.value.notification.name.includes('通知')) {
wizardData.value.notification.name = `${type} 通知` const displayNameMap: Record<string, string> = {
wechat: '企业微信',
wechatclawbot: '微信 ClawBot',
telegram: 'Telegram',
slack: 'Slack',
synologychat: 'SynologyChat',
qqbot: 'QQ',
vocechat: 'VoceChat',
webpush: 'WebPush',
}
wizardData.value.notification.name = `${displayNameMap[type] || type} 通知`
} }
wizardData.value.notification.enabled = true wizardData.value.notification.enabled = true
// 不清空config和switchs保留用户已输入的值 // 不清空config和switchs保留用户已输入的值
@@ -656,6 +667,8 @@ export function useSetupWizard() {
validationErrors.value.notification.WECHAT_APP_SECRET = true validationErrors.value.notification.WECHAT_APP_SECRET = true
} }
break break
case 'wechatclawbot':
break
case 'telegram': case 'telegram':
if (!config.TELEGRAM_TOKEN?.trim()) { if (!config.TELEGRAM_TOKEN?.trim()) {
errors.push(t('notification.telegram.tokenRequired')) errors.push(t('notification.telegram.tokenRequired'))
@@ -854,7 +867,7 @@ export function useSetupWizard() {
case 5: // 媒体服务器测试 - 只有选择了媒体服务器才测试 case 5: // 媒体服务器测试 - 只有选择了媒体服务器才测试
return !!wizardData.value.mediaServer.type return !!wizardData.value.mediaServer.type
case 6: // 消息通知测试 - 只有选择了通知才测试 case 6: // 消息通知测试 - 只有选择了通知才测试
return !!wizardData.value.notification.type return !!wizardData.value.notification.type && wizardData.value.notification.type !== 'wechatclawbot'
default: default:
return false return false
} }

View File

@@ -353,7 +353,8 @@ export default {
}, },
notification: { notification: {
title: 'Notifications', title: 'Notifications',
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope', description:
'Notification channels (WeChat Work, WeChat ClawBot, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
}, },
about: { about: {
title: 'About', title: 'About',
@@ -472,6 +473,38 @@ export default {
adminsHint: 'User IDs that can use admin menu and commands, separated by commas', adminsHint: 'User IDs that can use admin menu and commands, separated by commas',
adminsPlaceholder: 'User IDs list, separated by commas', adminsPlaceholder: 'User IDs list, separated by commas',
}, },
wechatclawbot: {
name: 'WeChat ClawBot',
baseUrl: 'iLink Base URL',
baseUrlHint: 'iLink service URL for WeChat ClawBot, keep default in most cases',
defaultTarget: 'Default Target',
defaultTargetHint: 'Optional target userid; leave empty to notify interacted users',
defaultTargetPlaceholder: 'userid (optional)',
admins: 'Admin Whitelist',
adminsHint: 'User IDs allowed to run slash commands, separated by commas',
adminsPlaceholder: 'User IDs list, separated by commas',
pollTimeout: 'Poll Timeout (seconds)',
pollTimeoutHint: 'Long polling timeout, recommended 20-30 seconds',
loginStatus: 'Login Status',
connected: 'Connected',
waiting: 'Waiting for QR scan',
scanned: 'Scanned, waiting for confirmation',
confirmed: 'Confirmed, establishing connection',
expired: 'QR code expired',
refreshQrcode: 'Refresh QR Code',
logout: 'Logout',
noQrcode: 'No QR code yet. Refresh or save config first.',
scanHint: 'Scan with WeChat to bind. Save and enable this channel before first use.',
accountId: 'Account ID',
qrcodeUpdatedAt: 'QR Updated At',
knownTargets: 'Recent Interacted Users',
noKnownTargets: 'No interaction records yet',
statusLoadFailed: 'Failed to load WeChat ClawBot status',
qrcodeRefreshSuccess: 'WeChat ClawBot QR code refreshed',
qrcodeRefreshFailed: 'Failed to refresh WeChat ClawBot QR code',
logoutSuccess: 'WeChat ClawBot logged out',
logoutFailed: 'Failed to logout WeChat ClawBot',
},
telegram: { telegram: {
name: 'Telegram', name: 'Telegram',
token: 'Bot Token', token: 'Bot Token',
@@ -1732,7 +1765,8 @@ export default {
timeSaveSuccess: 'Notification send time saved successfully', timeSaveSuccess: 'Notification send time saved successfully',
timeSaveFailed: 'Failed to save notification send time!', timeSaveFailed: 'Failed to save notification send time!',
channel: 'Notification', channel: 'Notification',
wechat: 'WeChat', wechat: 'WeChat Work',
wechatClawBot: 'WeChat ClawBot',
resourceDownload: 'Resource Download', resourceDownload: 'Resource Download',
mediaImport: 'Media Import', mediaImport: 'Media Import',
subscription: 'Subscription', subscription: 'Subscription',
@@ -1816,7 +1850,7 @@ export default {
animeCategory: 'Anime', animeCategory: 'Anime',
downloadUser: 'Remote Search Auto Download User List', downloadUser: 'Remote Search Auto Download User List',
downloadUserHint: downloadUserHint:
'Whether to automatically download when searching with Telegram, WeChat, etc., comma separated, set to all to represent all users auto-download', 'Whether to auto-download when searching with Telegram, WeChat Work, etc., comma separated, set to all for all users',
multipleNameSearch: 'Multiple Name Resource Search', multipleNameSearch: 'Multiple Name Resource Search',
multipleNameSearchHint: multipleNameSearchHint:
'Search site resources using multiple names (Chinese, English, etc.) and merge search results, will increase site access frequency', 'Search site resources using multiple names (Chinese, English, etc.) and merge search results, will increase site access frequency',
@@ -2061,7 +2095,8 @@ export default {
resetDefaultAvatar: 'Reset Default Avatar', resetDefaultAvatar: 'Reset Default Avatar',
restoreCurrentAvatar: 'Restore Current Avatar', restoreCurrentAvatar: 'Restore Current Avatar',
notifications: 'Notifications', notifications: 'Notifications',
wechat: 'WeChat UserID', wechat: 'WeChat Work UserID',
wechatClawBot: 'WeChat ClawBot UserID',
telegram: 'Telegram UserID', telegram: 'Telegram UserID',
slack: 'Slack UserID', slack: 'Slack UserID',
discord: 'Discord UserID', discord: 'Discord UserID',
@@ -2787,7 +2822,8 @@ export default {
nickname: 'Nickname', nickname: 'Nickname',
nicknamePlaceholder: 'Display nickname, takes precedence over username', nicknamePlaceholder: 'Display nickname, takes precedence over username',
accountBinding: 'Account Binding', accountBinding: 'Account Binding',
wechatUser: 'WeChat User', wechatUser: 'WeChat Work User',
wechatClawBotUser: 'WeChat ClawBot User',
telegramUser: 'Telegram User', telegramUser: 'Telegram User',
slackUser: 'Slack User', slackUser: 'Slack User',
discordUser: 'Discord User', discordUser: 'Discord User',

View File

@@ -351,7 +351,7 @@ export default {
}, },
notification: { notification: {
title: '通知', title: '通知',
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息发送范围', description: '通知渠道(企业微信、微信 ClawBot、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息发送范围',
}, },
about: { about: {
title: '关于', title: '关于',
@@ -468,6 +468,38 @@ export default {
adminsHint: '可使用管理菜单及命令的用户ID列表多个ID使用,分隔', adminsHint: '可使用管理菜单及命令的用户ID列表多个ID使用,分隔',
adminsPlaceholder: '用户ID列表多个ID使用,分隔', adminsPlaceholder: '用户ID列表多个ID使用,分隔',
}, },
wechatclawbot: {
name: '微信 ClawBot',
baseUrl: 'iLink 地址',
baseUrlHint: '微信 ClawBot iLink 服务地址,通常使用默认值',
defaultTarget: '默认通知目标',
defaultTargetHint: '可填写用户 userid不填则默认发给已互动用户',
defaultTargetPlaceholder: '用户 userid可选',
admins: '管理员白名单',
adminsHint: '允许执行斜杠命令的用户ID列表多个ID使用,分隔',
adminsPlaceholder: '用户ID列表多个ID使用,分隔',
pollTimeout: '轮询超时(秒)',
pollTimeoutHint: '长轮询请求超时时间,建议 20-30 秒',
loginStatus: '登录状态',
connected: '已连接',
waiting: '等待扫码',
scanned: '已扫码,待确认',
confirmed: '已确认,正在建立连接',
expired: '二维码已过期',
refreshQrcode: '刷新二维码',
logout: '退出登录',
noQrcode: '暂无二维码,请先刷新或保存配置后重试',
scanHint: '使用微信扫码绑定后,状态会自动刷新。首次使用请先保存并启用该通知渠道。',
accountId: '账号ID',
qrcodeUpdatedAt: '二维码更新时间',
knownTargets: '最近互动用户',
noKnownTargets: '暂无互动用户记录',
statusLoadFailed: '获取微信 ClawBot 状态失败',
qrcodeRefreshSuccess: '微信 ClawBot 二维码已刷新',
qrcodeRefreshFailed: '刷新微信 ClawBot 二维码失败',
logoutSuccess: '微信 ClawBot 已退出登录',
logoutFailed: '微信 ClawBot 退出登录失败',
},
telegram: { telegram: {
name: 'Telegram', name: 'Telegram',
token: 'Bot Token', token: 'Bot Token',
@@ -1703,7 +1735,8 @@ export default {
timeSaveSuccess: '通知发送时间保存成功', timeSaveSuccess: '通知发送时间保存成功',
timeSaveFailed: '通知发送时间保存失败!', timeSaveFailed: '通知发送时间保存失败!',
channel: '通知', channel: '通知',
wechat: '微信', wechat: '企业微信',
wechatClawBot: '微信 ClawBot',
resourceDownload: '资源下载', resourceDownload: '资源下载',
mediaImport: '整理入库', mediaImport: '整理入库',
subscription: '订阅', subscription: '订阅',
@@ -1781,7 +1814,7 @@ export default {
tvCategory: '电视剧', tvCategory: '电视剧',
animeCategory: '动漫', animeCategory: '动漫',
downloadUser: '远程搜索自动下载用户', downloadUser: '远程搜索自动下载用户',
downloadUserHint: '使用Telegram、微信等搜索时是否自动下载使用逗号分割设置为 all 代表所有用户自动择优下载', downloadUserHint: '使用Telegram、企业微信等搜索时是否自动下载,使用逗号分割,设置为 all 代表所有用户自动择优下载',
multipleNameSearch: '多名称资源搜索', multipleNameSearch: '多名称资源搜索',
multipleNameSearchHint: '使用多个名称(中文、英文等)搜索站点资源并合并搜索结果,会增加站点访问频率', multipleNameSearchHint: '使用多个名称(中文、英文等)搜索站点资源并合并搜索结果,会增加站点访问频率',
downloadSubtitle: '下载站点字幕', downloadSubtitle: '下载站点字幕',
@@ -2021,7 +2054,8 @@ export default {
resetDefaultAvatar: '重置默认头像', resetDefaultAvatar: '重置默认头像',
restoreCurrentAvatar: '还原当前头像', restoreCurrentAvatar: '还原当前头像',
notifications: '通知', notifications: '通知',
wechat: '微信ID', wechat: '企业微信ID',
wechatClawBot: '微信 ClawBot ID',
telegram: 'Telegram ID', telegram: 'Telegram ID',
slack: 'Slack ID', slack: 'Slack ID',
discord: 'Discord ID', discord: 'Discord ID',
@@ -2740,7 +2774,8 @@ export default {
nickname: '昵称', nickname: '昵称',
nicknamePlaceholder: '显示昵称,优先于用户名显示', nicknamePlaceholder: '显示昵称,优先于用户名显示',
accountBinding: '账号绑定', accountBinding: '账号绑定',
wechatUser: '微信用户', wechatUser: '企业微信用户',
wechatClawBotUser: '微信 ClawBot 用户',
telegramUser: 'Telegram用户', telegramUser: 'Telegram用户',
slackUser: 'Slack用户', slackUser: 'Slack用户',
discordUser: 'Discord用户', discordUser: 'Discord用户',

View File

@@ -352,7 +352,7 @@ export default {
}, },
notification: { notification: {
title: '通知', title: '通知',
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息發送範圍', description: '通知渠道(企業微信、微信 ClawBot、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息發送範圍',
}, },
about: { about: {
title: '關於', title: '關於',
@@ -469,6 +469,38 @@ export default {
adminsHint: '可使用管理菜單及命令的用戶ID列表多個ID使用,分隔', adminsHint: '可使用管理菜單及命令的用戶ID列表多個ID使用,分隔',
adminsPlaceholder: '用戶ID列表多個ID使用,分隔', adminsPlaceholder: '用戶ID列表多個ID使用,分隔',
}, },
wechatclawbot: {
name: '微信 ClawBot',
baseUrl: 'iLink 地址',
baseUrlHint: '微信 ClawBot iLink 服務地址,通常使用預設值',
defaultTarget: '預設通知目標',
defaultTargetHint: '可填寫使用者 userid不填則預設發給已互動使用者',
defaultTargetPlaceholder: '使用者 userid可選',
admins: '管理員白名單',
adminsHint: '允許執行斜線命令的用戶ID列表多個ID使用,分隔',
adminsPlaceholder: '用戶ID列表多個ID使用,分隔',
pollTimeout: '輪詢超時(秒)',
pollTimeoutHint: '長輪詢請求超時時間,建議 20-30 秒',
loginStatus: '登入狀態',
connected: '已連線',
waiting: '等待掃碼',
scanned: '已掃碼,待確認',
confirmed: '已確認,正在建立連線',
expired: '二維碼已過期',
refreshQrcode: '刷新二維碼',
logout: '退出登入',
noQrcode: '暫無二維碼,請先刷新或保存配置後再試',
scanHint: '使用微信掃碼綁定後,狀態會自動刷新。首次使用請先保存並啟用該通知渠道。',
accountId: '帳號ID',
qrcodeUpdatedAt: '二維碼更新時間',
knownTargets: '最近互動用戶',
noKnownTargets: '暫無互動用戶記錄',
statusLoadFailed: '獲取微信 ClawBot 狀態失敗',
qrcodeRefreshSuccess: '微信 ClawBot 二維碼已刷新',
qrcodeRefreshFailed: '刷新微信 ClawBot 二維碼失敗',
logoutSuccess: '微信 ClawBot 已退出登入',
logoutFailed: '微信 ClawBot 退出登入失敗',
},
telegram: { telegram: {
name: 'Telegram', name: 'Telegram',
token: 'Bot Token', token: 'Bot Token',
@@ -1705,7 +1737,8 @@ export default {
timeSaveSuccess: '通知發送時間保存成功', timeSaveSuccess: '通知發送時間保存成功',
timeSaveFailed: '通知發送時間保存失敗!', timeSaveFailed: '通知發送時間保存失敗!',
channel: '通知', channel: '通知',
wechat: '微信', wechat: '企業微信',
wechatClawBot: '微信 ClawBot',
resourceDownload: '資源下載', resourceDownload: '資源下載',
mediaImport: '整理入庫', mediaImport: '整理入庫',
subscription: '訂閱', subscription: '訂閱',
@@ -1791,7 +1824,7 @@ export default {
mediaSourceHint: '搜索媒體信息時使用的數據源以及排序', mediaSourceHint: '搜索媒體信息時使用的數據源以及排序',
filterRuleGroupHint: '搜索媒體信息時按選定的過濾規則組對結果進行過濾', filterRuleGroupHint: '搜索媒體信息時按選定的過濾規則組對結果進行過濾',
downloadUserPlaceholder: '用戶ID1,用戶ID2', downloadUserPlaceholder: '用戶ID1,用戶ID2',
downloadUserHint: '使用Telegram、微信等搜索時是否自動下載使用逗號分割設置為 all 代表所有用戶自動擇優下載', downloadUserHint: '使用Telegram、企業微信等搜索時是否自動下載,使用逗號分割,設置為 all 代表所有用戶自動擇優下載',
downloadLabelPlaceholder: 'MOVIEPILOT', downloadLabelPlaceholder: 'MOVIEPILOT',
}, },
directory: { directory: {
@@ -2023,7 +2056,8 @@ export default {
resetDefaultAvatar: '重置默認頭像', resetDefaultAvatar: '重置默認頭像',
restoreCurrentAvatar: '還原當前頭像', restoreCurrentAvatar: '還原當前頭像',
notifications: '通知', notifications: '通知',
wechat: '微信UserID', wechat: '企業微信 UserID',
wechatClawBot: '微信 ClawBot ID',
telegram: 'Telegram UserID', telegram: 'Telegram UserID',
slack: 'Slack UserID', slack: 'Slack UserID',
discord: 'Discord UserID', discord: 'Discord UserID',
@@ -2742,7 +2776,8 @@ export default {
nickname: '暱稱', nickname: '暱稱',
nicknamePlaceholder: '顯示暱稱,優先於用戶名顯示', nicknamePlaceholder: '顯示暱稱,優先於用戶名顯示',
accountBinding: '賬號綁定', accountBinding: '賬號綁定',
wechatUser: '微信用戶', wechatUser: '企業微信用戶',
wechatClawBotUser: '微信 ClawBot 用戶',
telegramUser: 'Telegram用戶', telegramUser: 'Telegram用戶',
slackUser: 'Slack用戶', slackUser: 'Slack用戶',
discordUser: 'Discord用戶', discordUser: 'Discord用戶',

View File

@@ -13,6 +13,7 @@ import plexLogo from '@/assets/images/logos/plex.png'
import trimemediaLogo from '@/assets/images/logos/trimemedia.png' import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
import ugreenLogo from '@/assets/images/logos/ugreen.png' import ugreenLogo from '@/assets/images/logos/ugreen.png'
import wechatLogo from '@/assets/images/logos/wechat.png' import wechatLogo from '@/assets/images/logos/wechat.png'
import clawbotLogo from '@/assets/images/logos/clawbot.png'
import telegramLogo from '@/assets/images/logos/telegram.webp' import telegramLogo from '@/assets/images/logos/telegram.webp'
import slackLogo from '@/assets/images/logos/slack.webp' import slackLogo from '@/assets/images/logos/slack.webp'
import discordLogo from '@/assets/images/logos/discord.png' import discordLogo from '@/assets/images/logos/discord.png'
@@ -44,6 +45,7 @@ const logoMap: Record<string, string> = {
trimemedia: trimemediaLogo, trimemedia: trimemediaLogo,
ugreen: ugreenLogo, ugreen: ugreenLogo,
wechat: wechatLogo, wechat: wechatLogo,
wechatclawbot: clawbotLogo,
telegram: telegramLogo, telegram: telegramLogo,
slack: slackLogo, slack: slackLogo,
discord: discordLogo, discord: discordLogo,

View File

@@ -299,12 +299,15 @@ onMounted(() => {
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
<VMenu :activator="'parent'" :close-on-content-click="true"> <VMenu :activator="'parent'" :close-on-content-click="true">
<VList> <VList>
<VListItem @click="addNotification('wechat')"> <VListItem @click="addNotification('wechat')">
<VListItemTitle>{{ t('setting.notification.wechat') }}</VListItemTitle> <VListItemTitle>{{ t('setting.notification.wechat') }}</VListItemTitle>
</VListItem> </VListItem>
<VListItem @click="addNotification('telegram')"> <VListItem @click="addNotification('wechatclawbot')">
<VListItemTitle>{{ t('setting.notification.telegram') }}</VListItemTitle> <VListItemTitle>{{ t('setting.notification.wechatClawBot') }}</VListItemTitle>
</VListItem> </VListItem>
<VListItem @click="addNotification('telegram')">
<VListItemTitle>{{ t('setting.notification.telegram') }}</VListItemTitle>
</VListItem>
<VListItem @click="addNotification('slack')"> <VListItem @click="addNotification('slack')">
<VListItemTitle>{{ t('setting.notification.slack') }}</VListItemTitle> <VListItemTitle>{{ t('setting.notification.slack') }}</VListItemTitle>
</VListItem> </VListItem>

View File

@@ -49,7 +49,20 @@ const notificationTypes = [
> >
<VCardText class="text-center"> <VCardText class="text-center">
<VImg :src="getLogoUrl('wechat')" height="48" width="48" class="mx-auto mb-2" /> <VImg :src="getLogoUrl('wechat')" height="48" width="48" class="mx-auto mb-2" />
<div class="text-h6">微信</div> <div class="text-h6">企业微信</div>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.notification.type === 'wechatclawbot' ? 'primary' : 'default'"
:variant="wizardData.notification.type === 'wechatclawbot' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectNotification('wechatclawbot')"
>
<VCardText class="text-center">
<VImg :src="getLogoUrl('wechatclawbot')" height="48" width="48" class="mx-auto mb-2" />
<div class="text-h6">WeChat ClawBot</div>
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
@@ -251,6 +264,50 @@ const notificationTypes = [
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow v-else-if="wizardData.notification.type === 'wechatclawbot'">
<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.WECHATCLAWBOT_BASE_URL"
:label="t('notification.wechatclawbot.baseUrl')"
:hint="t('notification.wechatclawbot.baseUrlHint')"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.WECHATCLAWBOT_DEFAULT_TARGET"
:label="t('notification.wechatclawbot.defaultTarget')"
:placeholder="t('notification.wechatclawbot.defaultTargetPlaceholder')"
:hint="t('notification.wechatclawbot.defaultTargetHint')"
persistent-hint
prepend-inner-icon="mdi-account-arrow-right"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.notification.config.WECHATCLAWBOT_ADMINS"
:label="t('notification.wechatclawbot.admins')"
:placeholder="t('notification.wechatclawbot.adminsPlaceholder')"
:hint="t('notification.wechatclawbot.adminsHint')"
persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/>
</VCol>
</VRow>
<VRow v-else-if="wizardData.notification.type === 'telegram'"> <VRow v-else-if="wizardData.notification.type === 'telegram'">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField

View File

@@ -436,6 +436,15 @@ watch(
prepend-inner-icon="mdi-wechat" prepend-inner-icon="mdi-wechat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6">
<VTextField
v-model="accountInfo.settings.wechatclawbot_userid"
density="comfortable"
clearable
:label="t('profile.wechatClawBotUser')"
prepend-inner-icon="mdi-robot-happy-outline"
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="accountInfo.settings.telegram_userid" v-model="accountInfo.settings.telegram_userid"