feat: 完善QQBot权限配置和账号删除操作

This commit is contained in:
sunlei 2026-06-01 20:29:10 +08:00
parent 9fc876b006
commit 99aa4a1fe7
4 changed files with 264 additions and 52 deletions

View File

@ -80,7 +80,7 @@ export namespace QqbotApi {
priority: number; priority: number;
remark?: string; remark?: string;
replyContent: string; replyContent: string;
targetType: 'all' | 'group' | 'private'; targetType: 'all' | 'channel' | 'group' | 'private';
} }
export interface RuleBody { export interface RuleBody {
@ -93,7 +93,7 @@ export namespace QqbotApi {
priority?: number; priority?: number;
remark?: string; remark?: string;
replyContent: string; replyContent: string;
targetType?: 'all' | 'group' | 'private'; targetType?: 'all' | 'channel' | 'group' | 'private';
} }
export interface Conversation { export interface Conversation {
@ -105,7 +105,7 @@ export namespace QqbotApi {
selfId: string; selfId: string;
targetId: string; targetId: string;
targetName?: string; targetName?: string;
targetType: 'group' | 'private'; targetType: 'channel' | 'group' | 'private';
} }
export interface Message { export interface Message {
@ -113,7 +113,7 @@ export namespace QqbotApi {
eventTime: string; eventTime: string;
id: string; id: string;
messageText: string; messageText: string;
messageType: 'group' | 'private'; messageType: 'channel' | 'group' | 'private';
senderNickname?: string; senderNickname?: string;
selfId: string; selfId: string;
targetId: string; targetId: string;
@ -129,25 +129,34 @@ export namespace QqbotApi {
selfId: string; selfId: string;
status: 'failed' | 'pending' | 'success'; status: 'failed' | 'pending' | 'success';
targetId: string; targetId: string;
targetType: 'group' | 'private'; targetType: 'channel' | 'group' | 'private';
}
export interface PermissionConfig {
allowlistEnabled: boolean;
blocklistEnabled: boolean;
} }
export interface Permission { export interface Permission {
enabled: boolean; enabled: boolean;
id: string; id: string;
preciseUser: boolean;
remark?: string; remark?: string;
selfId?: string; selfId?: string;
targetId: string; targetId: string;
targetType: 'all' | 'group' | 'private'; targetType: 'channel' | 'group' | 'private' | 'qq';
userId?: string;
} }
export interface PermissionBody { export interface PermissionBody {
enabled?: boolean; enabled?: boolean;
id?: string; id?: string;
preciseUser?: boolean;
remark?: string; remark?: string;
selfId?: string; selfId?: string;
targetId: string; targetId: string;
targetType: 'all' | 'group' | 'private'; targetType: 'channel' | 'group' | 'private' | 'qq';
userId?: string;
} }
export type Query = Recordable<any>; export type Query = Recordable<any>;
@ -179,7 +188,9 @@ export function updateQqbotAccount(data: QqbotApi.AccountBody) {
} }
export function deleteQqbotAccount(id: string) { export function deleteQqbotAccount(id: string) {
return requestClient.post<boolean>(`/qqbot/account/delete?id=${id}`); return requestClient.post<{ deletedContainers: number }>(
`/qqbot/account/delete?id=${id}`,
);
} }
export function kickQqbotAccount(selfId: string) { export function kickQqbotAccount(selfId: string) {
@ -291,6 +302,21 @@ export function getQqbotPermissionList(
); );
} }
export function getQqbotPermissionConfig() {
return requestClient.get<QqbotApi.PermissionConfig>(
'/qqbot/permission/config',
);
}
export function updateQqbotPermissionConfig(
data: Partial<QqbotApi.PermissionConfig>,
) {
return requestClient.post<QqbotApi.PermissionConfig>(
'/qqbot/permission/config',
data,
);
}
export function createQqbotPermission( export function createQqbotPermission(
kind: 'allowlist' | 'blocklist', kind: 'allowlist' | 'blocklist',
data: QqbotApi.PermissionBody, data: QqbotApi.PermissionBody,

View File

@ -141,6 +141,23 @@ export default defineComponent({
onClick: openScanRefresh, onClick: openScanRefresh,
permissionCodes: ['QqBot:Account:RefreshLogin'], 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', disabled: (row) => row.connectStatus !== 'online',
key: 'kick', key: 'kick',
@ -158,18 +175,6 @@ export default defineComponent({
onClick: openEdit, onClick: openEdit,
permissionCodes: ['QqBot:Account:Edit'], 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<QqbotApi.Account>({ const [registerTable, tableApi] = useKtTable<QqbotApi.Account>({
api, api,

View File

@ -2,11 +2,13 @@ export const qqbotTargetTypeOptions = [
{ label: '全部', value: 'all' }, { label: '全部', value: 'all' },
{ label: '私聊', value: 'private' }, { label: '私聊', value: 'private' },
{ label: '群聊', value: 'group' }, { label: '群聊', value: 'group' },
{ label: '频道', value: 'channel' },
]; ];
export const qqbotMessageTypeOptions = [ export const qqbotMessageTypeOptions = [
{ label: '私聊', value: 'private' }, { label: '私聊', value: 'private' },
{ label: '群聊', value: 'group' }, { label: '群聊', value: 'group' },
{ label: '频道', value: 'channel' },
]; ];
export const qqbotRuleMatchOptions = [ export const qqbotRuleMatchOptions = [
@ -17,6 +19,12 @@ export const qqbotRuleMatchOptions = [
export const qqbotRuleTargetOptions = qqbotTargetTypeOptions; export const qqbotRuleTargetOptions = qqbotTargetTypeOptions;
export const qqbotPermissionTargetOptions = [
{ label: 'QQ号', value: 'qq' },
{ label: '群聊', value: 'group' },
{ label: '频道', value: 'channel' },
];
const qqbotDefaultSendStatusOption = { const qqbotDefaultSendStatusOption = {
color: 'default', color: 'default',
label: '等待中', label: '等待中',

View File

@ -7,12 +7,20 @@ import type {
KtTableRowAction, KtTableRowAction,
} from '#/components/ktTable'; } 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 { Page } from '@vben/common-ui';
import { Plus } from '@vben/icons'; import { Plus } from '@vben/icons';
import { import {
Button,
Form, Form,
FormItem, FormItem,
Input, Input,
@ -27,14 +35,20 @@ import {
import { import {
createQqbotPermission, createQqbotPermission,
deleteQqbotPermission, deleteQqbotPermission,
getQqbotPermissionConfig,
getQqbotPermissionList, getQqbotPermissionList,
updateQqbotPermission, updateQqbotPermission,
updateQqbotPermissionConfig,
} from '#/api/qqbot'; } from '#/api/qqbot';
import { KtTable, useKtTable } from '#/components/ktTable'; import { KtTable, useKtTable } from '#/components/ktTable';
import { getOptionLabel, qqbotTargetTypeOptions } from '../modules/options'; import {
getOptionLabel,
qqbotPermissionTargetOptions,
} from '../modules/options';
const AKtTable = KtTable as any; const AKtTable = KtTable as any;
const AButton = Button as any;
const AInput = Input as any; const AInput = Input as any;
const AModal = Modal as any; const AModal = Modal as any;
const ASelect = Select as any; const ASelect = Select as any;
@ -42,20 +56,33 @@ const ASwitch = Switch as any;
const ATabs = Tabs as any; const ATabs = Tabs as any;
type PermissionKind = 'allowlist' | 'blocklist'; type PermissionKind = 'allowlist' | 'blocklist';
type PermissionTargetType = QqbotApi.PermissionBody['targetType'];
const permissionTargetTabItems = qqbotPermissionTargetOptions.map((item) => ({
key: item.value,
label: item.label,
}));
export default defineComponent({ export default defineComponent({
name: 'QqBotPermissionList', name: 'QqBotPermissionList',
setup() { setup() {
const activeKind = ref<PermissionKind>('allowlist'); const activeKind = ref<PermissionKind>('allowlist');
const activeTargetType = ref<PermissionTargetType>('qq');
const configSaving = ref(false);
const saving = ref(false); const saving = ref(false);
const modalOpen = ref(false); const modalOpen = ref(false);
const editingId = ref<string>(); const editingId = ref<string>();
const permissionConfig = reactive<QqbotApi.PermissionConfig>({
allowlistEnabled: false,
blocklistEnabled: true,
});
const form = reactive<QqbotApi.PermissionBody>({ const form = reactive<QqbotApi.PermissionBody>({
enabled: true, enabled: true,
preciseUser: false,
remark: '', remark: '',
selfId: '', selfId: '',
targetId: '', targetId: '',
targetType: 'private', targetType: 'qq',
userId: '',
}); });
const columns: Array<TableColumnType<QqbotApi.Permission>> = [ const columns: Array<TableColumnType<QqbotApi.Permission>> = [
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 }, { dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 150 },
@ -66,12 +93,22 @@ export default defineComponent({
width: 110, width: 110,
}, },
{ dataIndex: 'targetId', key: 'targetId', title: '目标 ID', width: 160 }, { 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: 'enabled', key: 'enabled', title: '状态', width: 100 },
{ dataIndex: 'remark', key: 'remark', title: '备注', width: 260 }, { dataIndex: 'remark', key: 'remark', title: '备注', width: 260 },
]; ];
const api: KtTableApi<QqbotApi.Permission> = { const api: KtTableApi<QqbotApi.Permission> = {
list: async (params) => list: async (params) =>
await getQqbotPermissionList(activeKind.value, params), await getQqbotPermissionList(activeKind.value, {
...params,
targetType: activeTargetType.value,
}),
}; };
const buttons: Array<KtTableButton<QqbotApi.Permission>> = [ const buttons: Array<KtTableButton<QqbotApi.Permission>> = [
{ {
@ -116,58 +153,99 @@ export default defineComponent({
fieldName: 'selfId', fieldName: 'selfId',
label: 'Self ID', label: 'Self ID',
}, },
{
component: 'Select',
componentProps: {
allowClear: true,
options: qqbotTargetTypeOptions,
},
fieldName: 'targetType',
label: '目标类型',
},
{ {
component: 'Input', component: 'Input',
componentProps: { allowClear: true, placeholder: '目标 ID' }, componentProps: { allowClear: true, placeholder: '目标 ID' },
fieldName: 'targetId', fieldName: 'targetId',
label: '目标 ID', label: '目标 ID',
}, },
{
component: 'Input',
componentProps: { allowClear: true, placeholder: 'QQ 号' },
fieldName: 'userId',
label: 'QQ 号',
},
], ],
}, },
rowActions, rowActions,
tableTitle: '权限名单', tableTitle: '权限名单',
}); });
const activeTargetLabel = computed(() => getPermissionTargetLabel());
const modalTitle = computed( 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(); 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() { function openCreate() {
editingId.value = undefined; editingId.value = undefined;
Object.assign(form, { Object.assign(form, {
enabled: true, enabled: true,
preciseUser: false,
remark: '', remark: '',
selfId: '', selfId: '',
targetId: '', targetId: '',
targetType: 'private', targetType: activeTargetType.value,
userId: '',
}); });
modalOpen.value = true; modalOpen.value = true;
} }
function openEdit(row: QqbotApi.Permission) { function openEdit(row: QqbotApi.Permission) {
editingId.value = row.id; 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; modalOpen.value = true;
} }
async function submitPermission() { async function submitPermission() {
if (form.targetType !== 'all' && !form.targetId.trim()) { form.targetType = activeTargetType.value;
message.warning('请填写目标 ID'); if (!form.targetId.trim()) {
message.warning(`请填写${targetIdLabel.value}`);
return; return;
} }
if (isPreciseAvailable() && form.preciseUser && !form.userId?.trim()) {
message.warning('开启精确到 QQ 号后必须填写 QQ 号');
return;
}
if (!isPreciseAvailable()) {
form.preciseUser = false;
form.userId = '';
}
saving.value = true; saving.value = true;
try { 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 () => ( return () => (
<Page autoContentHeight> <Page autoContentHeight>
<div style={{ display: 'grid', gap: '12px' }}> <div style={{ display: 'grid', gap: '12px' }}>
<div
style={{
alignItems: 'center',
display: 'flex',
gap: '20px',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', gap: '20px' }}>
<span>
<ASwitch
checked={permissionConfig.allowlistEnabled}
{...{
'onUpdate:checked': (value: boolean) => {
permissionConfig.allowlistEnabled = value;
},
}}
/>
</span>
<span>
<ASwitch
checked={permissionConfig.blocklistEnabled}
{...{
'onUpdate:checked': (value: boolean) => {
permissionConfig.blocklistEnabled = value;
},
}}
/>
</span>
</div>
<AButton loading={configSaving.value} onClick={saveConfig}>
</AButton>
</div>
<ATabs <ATabs
activeKey={activeKind.value} activeKey={activeKind.value}
items={[ items={[
@ -200,6 +334,15 @@ export default defineComponent({
}, },
}} }}
/> />
<ATabs
activeKey={activeTargetType.value}
items={permissionTargetTabItems}
{...{
'onUpdate:activeKey': (value: PermissionTargetType) => {
activeTargetType.value = value;
},
}}
/>
<AKtTable <AKtTable
onRegister={registerTable} onRegister={registerTable}
v-slots={{ v-slots={{
@ -213,7 +356,16 @@ export default defineComponent({
); );
} }
if (column.key === 'targetType') { if (column.key === 'targetType') {
return getOptionLabel(qqbotTargetTypeOptions, row.targetType); return getPermissionTargetLabel(row.targetType);
}
if (column.key === 'preciseUser') {
if (row.targetType === 'qq' || row.targetType === 'private') {
return '-';
}
return row.preciseUser ? '是' : '否';
}
if (column.key === 'userId') {
return row.preciseUser ? row.userId || '-' : '-';
} }
return undefined; return undefined;
}, },
@ -246,29 +398,50 @@ export default defineComponent({
</FormItem> </FormItem>
<FormItem label="目标类型"> <FormItem label="目标类型">
<ASelect <ASelect
{...{ disabled
'onUpdate:value': ( options={qqbotPermissionTargetOptions}
value: QqbotApi.PermissionBody['targetType'], value={activeTargetType.value}
) => {
form.targetType = value;
},
}}
options={qqbotTargetTypeOptions}
value={form.targetType}
/> />
</FormItem> </FormItem>
<FormItem label="目标 ID"> <FormItem label={targetIdLabel.value}>
<AInput <AInput
disabled={form.targetType === 'all'}
{...{ {...{
'onUpdate:value': (value: string) => { 'onUpdate:value': (value: string) => {
form.targetId = value; form.targetId = value;
}, },
}} }}
placeholder="私聊填 QQ 号,群聊填群号" placeholder={`请填写${targetIdLabel.value}`}
value={form.targetId} value={form.targetId}
/> />
</FormItem> </FormItem>
{isPreciseAvailable() && (
<>
<FormItem label="精确 QQ">
<ASwitch
checked={form.preciseUser}
{...{
'onUpdate:checked': (value: boolean) => {
form.preciseUser = value;
if (!value) form.userId = '';
},
}}
/>
</FormItem>
{form.preciseUser && (
<FormItem label="QQ 号">
<AInput
{...{
'onUpdate:value': (value: string) => {
form.userId = value;
},
}}
placeholder="请填写需要精确匹配的 QQ 号"
value={form.userId}
/>
</FormItem>
)}
</>
)}
<FormItem label="启用"> <FormItem label="启用">
<ASwitch <ASwitch
checked={form.enabled} checked={form.enabled}