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

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import api from '@/api'
import { NotificationConf } from '@/api/types'
import { getLogoUrl } from '@/utils/imageUtils'
import { useToast } from 'vue-toastification'
@@ -45,6 +46,7 @@ const notificationInfo = ref<NotificationConf>({
// 各通知类型的名称字典
const notificationTypeNames: { [key: string]: string } = {
wechat: t('notification.wechat.name'),
wechatclawbot: t('notification.wechatclawbot.name'),
telegram: t('notification.telegram.name'),
qqbot: t('notification.qqbot.name'),
vocechat: t('notification.vocechat.name'),
@@ -68,6 +70,18 @@ const notificationTypes = [
{ 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) {
if (notification.type !== 'wechat') {
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({
get: () => notificationInfo.value.config?.WECHAT_MODE === 'bot',
set: value => {
@@ -101,7 +148,11 @@ function openNotificationInfoDialog() {
// 替换成深复制,避免修改时影响原数据
notificationInfo.value = cloneDeep(props.notification)
ensureWechatConfigDefaults(notificationInfo.value)
ensureWechatClawBotConfigDefaults(notificationInfo.value)
notificationInfoDialog.value = true
if (notificationInfo.value.type === 'wechatclawbot') {
fetchWechatClawBotStatus(true)
}
}
// 保存详情数据
@@ -117,16 +168,137 @@ function saveNotificationInfo() {
return
}
ensureWechatConfigDefaults(notificationInfo.value)
ensureWechatClawBotConfigDefaults(notificationInfo.value)
notificationInfoDialog.value = false
emit('change', notificationInfo.value, props.notification.name)
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(() => {
switch (props.notification.type) {
case 'wechat':
return getLogoUrl('wechat')
case 'wechatclawbot':
return getLogoUrl('wechatclawbot')
case 'telegram':
return getLogoUrl('telegram')
case 'qqbot':
@@ -148,8 +320,15 @@ const getIcon = computed(() => {
// 按钮点击
function onClose() {
clearWechatClawBotTimer()
emit('close')
}
watch(notificationInfoDialog, value => {
if (!value) {
clearWechatClawBotTimer()
}
})
</script>
<template>
<div>
@@ -347,6 +526,137 @@ function onClose() {
</VCol>
</template>
</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'">
<VCol cols="12" md="6">
<VTextField

View File

@@ -91,6 +91,7 @@ const userForm = ref<ExtendedUser>({
},
settings: {
wechat_userid: null,
wechatclawbot_userid: null,
telegram_userid: null,
slack_userid: null,
discord_userid: null,
@@ -503,6 +504,15 @@ onMounted(() => {
prepend-inner-icon="mdi-wechat"
/>
</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">
<VTextField
v-model="userForm.settings.telegram_userid"