From 99aa4a1fe7890dbb55828ec9f493f0b8396dc319 Mon Sep 17 00:00:00 2001 From: sunlei Date: Mon, 1 Jun 2026 20:29:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84QQBot=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=85=8D=E7=BD=AE=E5=92=8C=E8=B4=A6=E5=8F=B7=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antdv-next/src/api/qqbot/index.ts | 42 +++- .../src/views/qqbot/account/list.tsx | 29 ++- .../src/views/qqbot/modules/options.ts | 8 + .../src/views/qqbot/permission/list.tsx | 237 +++++++++++++++--- 4 files changed, 264 insertions(+), 52 deletions(-) diff --git a/apps/web-antdv-next/src/api/qqbot/index.ts b/apps/web-antdv-next/src/api/qqbot/index.ts index 1825768..3b5461a 100644 --- a/apps/web-antdv-next/src/api/qqbot/index.ts +++ b/apps/web-antdv-next/src/api/qqbot/index.ts @@ -80,7 +80,7 @@ export namespace QqbotApi { priority: number; remark?: string; replyContent: string; - targetType: 'all' | 'group' | 'private'; + targetType: 'all' | 'channel' | 'group' | 'private'; } export interface RuleBody { @@ -93,7 +93,7 @@ export namespace QqbotApi { priority?: number; remark?: string; replyContent: string; - targetType?: 'all' | 'group' | 'private'; + targetType?: 'all' | 'channel' | 'group' | 'private'; } export interface Conversation { @@ -105,7 +105,7 @@ export namespace QqbotApi { selfId: string; targetId: string; targetName?: string; - targetType: 'group' | 'private'; + targetType: 'channel' | 'group' | 'private'; } export interface Message { @@ -113,7 +113,7 @@ export namespace QqbotApi { eventTime: string; id: string; messageText: string; - messageType: 'group' | 'private'; + messageType: 'channel' | 'group' | 'private'; senderNickname?: string; selfId: string; targetId: string; @@ -129,25 +129,34 @@ export namespace QqbotApi { selfId: string; status: 'failed' | 'pending' | 'success'; targetId: string; - targetType: 'group' | 'private'; + targetType: 'channel' | 'group' | 'private'; + } + + export interface PermissionConfig { + allowlistEnabled: boolean; + blocklistEnabled: boolean; } export interface Permission { enabled: boolean; id: string; + preciseUser: boolean; remark?: string; selfId?: string; targetId: string; - targetType: 'all' | 'group' | 'private'; + targetType: 'channel' | 'group' | 'private' | 'qq'; + userId?: string; } export interface PermissionBody { enabled?: boolean; id?: string; + preciseUser?: boolean; remark?: string; selfId?: string; targetId: string; - targetType: 'all' | 'group' | 'private'; + targetType: 'channel' | 'group' | 'private' | 'qq'; + userId?: string; } export type Query = Recordable; @@ -179,7 +188,9 @@ export function updateQqbotAccount(data: QqbotApi.AccountBody) { } export function deleteQqbotAccount(id: string) { - return requestClient.post(`/qqbot/account/delete?id=${id}`); + return requestClient.post<{ deletedContainers: number }>( + `/qqbot/account/delete?id=${id}`, + ); } export function kickQqbotAccount(selfId: string) { @@ -291,6 +302,21 @@ export function getQqbotPermissionList( ); } +export function getQqbotPermissionConfig() { + return requestClient.get( + '/qqbot/permission/config', + ); +} + +export function updateQqbotPermissionConfig( + data: Partial, +) { + return requestClient.post( + '/qqbot/permission/config', + data, + ); +} + export function createQqbotPermission( kind: 'allowlist' | 'blocklist', data: QqbotApi.PermissionBody, diff --git a/apps/web-antdv-next/src/views/qqbot/account/list.tsx b/apps/web-antdv-next/src/views/qqbot/account/list.tsx index 6b6642f..755bca2 100644 --- a/apps/web-antdv-next/src/views/qqbot/account/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/account/list.tsx @@ -141,6 +141,23 @@ export default defineComponent({ onClick: openScanRefresh, permissionCodes: ['QqBot:Account:RefreshLogin'], }, + { + confirm: (row) => + `确认删除账号「${row.selfId}」吗?该操作会同时删除该账号专属的 NapCat 容器。`, + danger: true, + key: 'delete', + label: '删除', + onClick: async (row, context) => { + const result = await deleteQqbotAccount(row.id); + message.success( + result.deletedContainers > 0 + ? `账号删除成功,已删除 ${result.deletedContainers} 个 NapCat 容器` + : '账号删除成功', + ); + await context.reload(); + }, + permissionCodes: ['QqBot:Account:Delete'], + }, { disabled: (row) => row.connectStatus !== 'online', key: 'kick', @@ -158,18 +175,6 @@ export default defineComponent({ onClick: openEdit, permissionCodes: ['QqBot:Account:Edit'], }, - { - confirm: (row) => `确认删除账号「${row.selfId}」吗?`, - danger: true, - key: 'delete', - label: '删除', - onClick: async (row, context) => { - await deleteQqbotAccount(row.id); - message.success('账号删除成功'); - await context.reload(); - }, - permissionCodes: ['QqBot:Account:Delete'], - }, ]; const [registerTable, tableApi] = useKtTable({ api, diff --git a/apps/web-antdv-next/src/views/qqbot/modules/options.ts b/apps/web-antdv-next/src/views/qqbot/modules/options.ts index 9058500..cfc448c 100644 --- a/apps/web-antdv-next/src/views/qqbot/modules/options.ts +++ b/apps/web-antdv-next/src/views/qqbot/modules/options.ts @@ -2,11 +2,13 @@ export const qqbotTargetTypeOptions = [ { label: '全部', value: 'all' }, { label: '私聊', value: 'private' }, { label: '群聊', value: 'group' }, + { label: '频道', value: 'channel' }, ]; export const qqbotMessageTypeOptions = [ { label: '私聊', value: 'private' }, { label: '群聊', value: 'group' }, + { label: '频道', value: 'channel' }, ]; export const qqbotRuleMatchOptions = [ @@ -17,6 +19,12 @@ export const qqbotRuleMatchOptions = [ export const qqbotRuleTargetOptions = qqbotTargetTypeOptions; +export const qqbotPermissionTargetOptions = [ + { label: 'QQ号', value: 'qq' }, + { label: '群聊', value: 'group' }, + { label: '频道', value: 'channel' }, +]; + const qqbotDefaultSendStatusOption = { color: 'default', label: '等待中', diff --git a/apps/web-antdv-next/src/views/qqbot/permission/list.tsx b/apps/web-antdv-next/src/views/qqbot/permission/list.tsx index d5035c2..b19c1cc 100644 --- a/apps/web-antdv-next/src/views/qqbot/permission/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/permission/list.tsx @@ -7,12 +7,20 @@ import type { KtTableRowAction, } from '#/components/ktTable'; -import { computed, defineComponent, reactive, ref, watch } from 'vue'; +import { + computed, + defineComponent, + onMounted, + reactive, + ref, + watch, +} from 'vue'; import { Page } from '@vben/common-ui'; import { Plus } from '@vben/icons'; import { + Button, Form, FormItem, Input, @@ -27,14 +35,20 @@ import { import { createQqbotPermission, deleteQqbotPermission, + getQqbotPermissionConfig, getQqbotPermissionList, updateQqbotPermission, + updateQqbotPermissionConfig, } from '#/api/qqbot'; import { KtTable, useKtTable } from '#/components/ktTable'; -import { getOptionLabel, qqbotTargetTypeOptions } from '../modules/options'; +import { + getOptionLabel, + qqbotPermissionTargetOptions, +} from '../modules/options'; const AKtTable = KtTable as any; +const AButton = Button as any; const AInput = Input as any; const AModal = Modal as any; const ASelect = Select as any; @@ -42,20 +56,33 @@ const ASwitch = Switch as any; const ATabs = Tabs as any; type PermissionKind = 'allowlist' | 'blocklist'; +type PermissionTargetType = QqbotApi.PermissionBody['targetType']; +const permissionTargetTabItems = qqbotPermissionTargetOptions.map((item) => ({ + key: item.value, + label: item.label, +})); export default defineComponent({ name: 'QqBotPermissionList', setup() { const activeKind = ref('allowlist'); + const activeTargetType = ref('qq'); + const configSaving = ref(false); const saving = ref(false); const modalOpen = ref(false); const editingId = ref(); + const permissionConfig = reactive({ + allowlistEnabled: false, + blocklistEnabled: true, + }); const form = reactive({ enabled: true, + preciseUser: false, remark: '', selfId: '', targetId: '', - targetType: 'private', + targetType: 'qq', + userId: '', }); const columns: Array> = [ { dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 }, @@ -66,12 +93,22 @@ export default defineComponent({ width: 110, }, { dataIndex: 'targetId', key: 'targetId', title: '目标 ID', width: 160 }, + { + dataIndex: 'preciseUser', + key: 'preciseUser', + title: '精确 QQ', + width: 100, + }, + { dataIndex: 'userId', key: 'userId', title: 'QQ 号', width: 150 }, { dataIndex: 'enabled', key: 'enabled', title: '状态', width: 100 }, { dataIndex: 'remark', key: 'remark', title: '备注', width: 260 }, ]; const api: KtTableApi = { list: async (params) => - await getQqbotPermissionList(activeKind.value, params), + await getQqbotPermissionList(activeKind.value, { + ...params, + targetType: activeTargetType.value, + }), }; const buttons: Array> = [ { @@ -116,58 +153,99 @@ export default defineComponent({ fieldName: 'selfId', label: 'Self ID', }, - { - component: 'Select', - componentProps: { - allowClear: true, - options: qqbotTargetTypeOptions, - }, - fieldName: 'targetType', - label: '目标类型', - }, { component: 'Input', componentProps: { allowClear: true, placeholder: '目标 ID' }, fieldName: 'targetId', label: '目标 ID', }, + { + component: 'Input', + componentProps: { allowClear: true, placeholder: 'QQ 号' }, + fieldName: 'userId', + label: 'QQ 号', + }, ], }, rowActions, tableTitle: '权限名单', }); + const activeTargetLabel = computed(() => getPermissionTargetLabel()); const modalTitle = computed( () => - `${editingId.value ? '编辑' : '新增'}${activeKind.value === 'allowlist' ? '白名单' : '黑名单'}`, + `${editingId.value ? '编辑' : '新增'}${activeTargetLabel.value}${activeKind.value === 'allowlist' ? '白名单' : '黑名单'}`, ); + const targetIdLabel = computed(() => { + if (activeTargetType.value === 'group') return '群号'; + if (activeTargetType.value === 'channel') return '频道 ID'; + return 'QQ 号'; + }); - watch(activeKind, async () => { + onMounted(() => { + void loadConfig(); + }); + + watch([activeKind, activeTargetType], async () => { await tableApi.reset(); }); + async function loadConfig() { + Object.assign(permissionConfig, await getQqbotPermissionConfig()); + } + + async function saveConfig() { + configSaving.value = true; + try { + Object.assign( + permissionConfig, + await updateQqbotPermissionConfig(permissionConfig), + ); + message.success('权限配置保存成功'); + } finally { + configSaving.value = false; + } + } + function openCreate() { editingId.value = undefined; Object.assign(form, { enabled: true, + preciseUser: false, remark: '', selfId: '', targetId: '', - targetType: 'private', + targetType: activeTargetType.value, + userId: '', }); modalOpen.value = true; } function openEdit(row: QqbotApi.Permission) { editingId.value = row.id; - Object.assign(form, { ...row }); + activeTargetType.value = normalizePermissionTargetType(row.targetType); + Object.assign(form, { + ...row, + preciseUser: !!row.preciseUser, + targetType: activeTargetType.value, + userId: row.userId || '', + }); modalOpen.value = true; } async function submitPermission() { - if (form.targetType !== 'all' && !form.targetId.trim()) { - message.warning('请填写目标 ID'); + form.targetType = activeTargetType.value; + if (!form.targetId.trim()) { + message.warning(`请填写${targetIdLabel.value}`); return; } + if (isPreciseAvailable() && form.preciseUser && !form.userId?.trim()) { + message.warning('开启精确到 QQ 号后必须填写 QQ 号'); + return; + } + if (!isPreciseAvailable()) { + form.preciseUser = false; + form.userId = ''; + } saving.value = true; try { @@ -185,9 +263,65 @@ export default defineComponent({ } } + function getPermissionTargetLabel(value = activeTargetType.value) { + return getOptionLabel(qqbotPermissionTargetOptions, value); + } + + function isPreciseAvailable() { + return ( + activeTargetType.value === 'group' || + activeTargetType.value === 'channel' + ); + } + + function normalizePermissionTargetType( + value?: string, + ): PermissionTargetType { + if (value === 'group' || value === 'channel' || value === 'qq') { + return value; + } + return 'qq'; + } + return () => (
+
+
+ + 白名单过滤: + { + permissionConfig.allowlistEnabled = value; + }, + }} + /> + + + 黑名单过滤: + { + permissionConfig.blocklistEnabled = value; + }, + }} + /> + +
+ + 保存配置 + +
+ { + activeTargetType.value = value; + }, + }} + /> { - form.targetType = value; - }, - }} - options={qqbotTargetTypeOptions} - value={form.targetType} + disabled + options={qqbotPermissionTargetOptions} + value={activeTargetType.value} /> - + { form.targetId = value; }, }} - placeholder="私聊填 QQ 号,群聊填群号" + placeholder={`请填写${targetIdLabel.value}`} value={form.targetId} /> + {isPreciseAvailable() && ( + <> + + { + form.preciseUser = value; + if (!value) form.userId = ''; + }, + }} + /> + + {form.preciseUser && ( + + { + form.userId = value; + }, + }} + placeholder="请填写需要精确匹配的 QQ 号" + value={form.userId} + /> + + )} + + )}