feat: 完善QQBot插件与账号能力绑定

This commit is contained in:
sunlei 2026-06-02 19:21:22 +08:00
parent ae2fd41e0f
commit 7a93e49af6
18 changed files with 1031 additions and 22 deletions

View File

@ -30,6 +30,10 @@ QQBOT_REVERSE_WS_TOKEN=
QQBOT_AUTO_REGISTER_ACCOUNT=true
QQBOT_API_TIMEOUT_MS=10000
QQBOT_SEND_RATE_PER_SECOND=1
QQBOT_REPEATER_THRESHOLD=2
QQBOT_REPEATER_MAX_TEXT_LENGTH=300
QQBOT_REPEATER_STATE_TTL_MS=600000
QQBOT_REPEATER_CONFIG_CACHE_TTL_MS=2000
NAPCAT_WEBUI_BASE_URL=http://127.0.0.1:6099
NAPCAT_WEBUI_TOKEN=
NAPCAT_WEBUI_TIMEOUT_MS=8000

View File

@ -25,6 +25,20 @@ CREATE TABLE IF NOT EXISTS `qqbot_account` (
UNIQUE KEY `uk_qqbot_account_self_id` (`self_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `qqbot_account_ability` (
`id` bigint NOT NULL,
`account_id` bigint NOT NULL,
`self_id` varchar(64) NOT NULL,
`ability_type` varchar(32) NOT NULL,
`ability_key` varchar(128) NOT NULL,
`is_deleted` tinyint(1) NOT NULL DEFAULT 0,
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_qqbot_account_ability` (`account_id`, `ability_type`, `ability_key`),
KEY `idx_qqbot_account_ability_self` (`self_id`, `ability_type`, `is_deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `qqbot_napcat_container` (
`id` bigint NOT NULL,
`name` varchar(120) NOT NULL,
@ -246,6 +260,192 @@ CREATE TABLE IF NOT EXISTS `qqbot_dedupe` (
UNIQUE KEY `uk_qqbot_dedupe_event_key` (`event_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'INSERT INTO `qqbot_account_ability` (`id`, `account_id`, `self_id`, `ability_type`, `ability_key`, `is_deleted`)
SELECT CAST((UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 - 1288834974657) * 4194304 + 100000 + ROW_NUMBER() OVER (ORDER BY `legacy`.`account_id`, `legacy`.`ability_key`) AS UNSIGNED) AS `id`,
`legacy`.`account_id`,
`legacy`.`self_id`,
''command'' AS `ability_type`,
`legacy`.`ability_key`,
0 AS `is_deleted`
FROM (
SELECT `account`.`id` AS `account_id`,
`account`.`self_id`,
TRIM(CASE JSON_TYPE(`binding`.`raw_item`)
WHEN ''STRING'' THEN JSON_UNQUOTE(`binding`.`raw_item`)
WHEN ''OBJECT'' THEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.id'')), JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.key'')), '''')
ELSE ''''
END) AS `ability_key`,
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.enabled'')), ''true'') AS `binding_enabled`
FROM `qqbot_account` `account`
JOIN JSON_TABLE(IF(JSON_VALID(`account`.`command_bindings`), `account`.`command_bindings`, ''[]''), ''$[*]'' COLUMNS (`raw_item` json PATH ''$'')) AS `binding`
WHERE `account`.`is_deleted` = 0
) `legacy`
WHERE `legacy`.`ability_key` <> ''''
AND `legacy`.`binding_enabled` <> ''false''
ON DUPLICATE KEY UPDATE `self_id` = VALUES(`self_id`), `is_deleted` = 0',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_account'
AND column_name = 'command_bindings'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'INSERT INTO `qqbot_account_ability` (`id`, `account_id`, `self_id`, `ability_type`, `ability_key`, `is_deleted`)
SELECT CAST((UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 - 1288834974657) * 4194304 + 200000 + ROW_NUMBER() OVER (ORDER BY `legacy`.`account_id`, `legacy`.`ability_key`) AS UNSIGNED) AS `id`,
`legacy`.`account_id`,
`legacy`.`self_id`,
''rule'' AS `ability_type`,
`legacy`.`ability_key`,
0 AS `is_deleted`
FROM (
SELECT `account`.`id` AS `account_id`,
`account`.`self_id`,
TRIM(CASE JSON_TYPE(`binding`.`raw_item`)
WHEN ''STRING'' THEN JSON_UNQUOTE(`binding`.`raw_item`)
WHEN ''OBJECT'' THEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.id'')), JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.key'')), '''')
ELSE ''''
END) AS `ability_key`,
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.enabled'')), ''true'') AS `binding_enabled`
FROM `qqbot_account` `account`
JOIN JSON_TABLE(IF(JSON_VALID(`account`.`rule_bindings`), `account`.`rule_bindings`, ''[]''), ''$[*]'' COLUMNS (`raw_item` json PATH ''$'')) AS `binding`
WHERE `account`.`is_deleted` = 0
) `legacy`
WHERE `legacy`.`ability_key` <> ''''
AND `legacy`.`binding_enabled` <> ''false''
ON DUPLICATE KEY UPDATE `self_id` = VALUES(`self_id`), `is_deleted` = 0',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_account'
AND column_name = 'rule_bindings'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'INSERT INTO `qqbot_account_ability` (`id`, `account_id`, `self_id`, `ability_type`, `ability_key`, `is_deleted`)
SELECT CAST((UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 - 1288834974657) * 4194304 + 300000 + ROW_NUMBER() OVER (ORDER BY `legacy`.`account_id`, `legacy`.`ability_key`) AS UNSIGNED) AS `id`,
`legacy`.`account_id`,
`legacy`.`self_id`,
''event_plugin'' AS `ability_type`,
`legacy`.`ability_key`,
0 AS `is_deleted`
FROM (
SELECT `account`.`id` AS `account_id`,
`account`.`self_id`,
TRIM(CASE JSON_TYPE(`binding`.`raw_item`)
WHEN ''STRING'' THEN JSON_UNQUOTE(`binding`.`raw_item`)
WHEN ''OBJECT'' THEN COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.id'')), JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.key'')), '''')
ELSE ''''
END) AS `ability_key`,
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`binding`.`raw_item`, ''$.enabled'')), ''true'') AS `binding_enabled`
FROM `qqbot_account` `account`
JOIN JSON_TABLE(IF(JSON_VALID(`account`.`event_plugin_bindings`), `account`.`event_plugin_bindings`, ''[]''), ''$[*]'' COLUMNS (`raw_item` json PATH ''$'')) AS `binding`
WHERE `account`.`is_deleted` = 0
) `legacy`
WHERE `legacy`.`ability_key` <> ''''
AND `legacy`.`binding_enabled` <> ''false''
ON DUPLICATE KEY UPDATE `self_id` = VALUES(`self_id`), `is_deleted` = 0',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_account'
AND column_name = 'event_plugin_bindings'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE `qqbot_rule` DROP COLUMN `self_id`',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_rule'
AND column_name = 'self_id'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE `qqbot_command` DROP COLUMN `self_id`',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_command'
AND column_name = 'self_id'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE `qqbot_account` DROP COLUMN `command_bindings`',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_account'
AND column_name = 'command_bindings'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE `qqbot_account` DROP COLUMN `rule_bindings`',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_account'
AND column_name = 'rule_bindings'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE `qqbot_account` DROP COLUMN `event_plugin_bindings`',
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'qqbot_account'
AND column_name = 'event_plugin_bindings'
);
PREPARE qqbot_stmt FROM @qqbot_sql;
EXECUTE qqbot_stmt;
DEALLOCATE PREPARE qqbot_stmt;
SET @qqbot_sql = (
SELECT IF(
COUNT(*) = 0,
@ -315,26 +515,45 @@ ON DUPLICATE KEY UPDATE
INSERT INTO `qqbot_command` (`id`, `code`, `name`, `aliases`, `prefixes`, `plugin_key`, `operation_key`, `parser_key`, `target_type`, `default_params`, `reply_template`, `error_template`, `enabled`, `priority`, `cooldown_ms`, `remark`)
VALUES
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!",""]', 'ff14Market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 0, 0, 1500, '默认示例命令;默认查询范围为中国,可按需改为具体服务器')
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!",""]', 'ff14Market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 1, 0, 1500, '默认示例命令;请在账号配置中绑定后启用')
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`),
`plugin_key` = VALUES(`plugin_key`),
`operation_key` = VALUES(`operation_key`),
`parser_key` = VALUES(`parser_key`),
`target_type` = VALUES(`target_type`),
`enabled` = VALUES(`enabled`),
`remark` = VALUES(`remark`),
`is_deleted` = 0;
UPDATE `admin_menu`
SET `name` = 'QqBotAccountConfigButton'
WHERE `id` = 2041700000000120406
AND `name` = 'QqBotAccountConfig';
INSERT INTO `admin_dict` (`id`, `dict_code`, `label`, `value`, `children_code`, `sort`, `status`)
VALUES
(2041700000000300401, 'QQBOT_PLUGIN_TRIGGER_MODE', '命令', 'command', NULL, 1, 1),
(2041700000000300402, 'QQBOT_PLUGIN_TRIGGER_MODE', '事件', 'event', NULL, 2, 1)
ON DUPLICATE KEY UPDATE
`label` = VALUES(`label`),
`children_code` = VALUES(`children_code`),
`sort` = VALUES(`sort`),
`status` = VALUES(`status`),
`is_deleted` = 0;
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`)
VALUES
(2041700000000100400, 0, 'QqBot', '/qqbot', NULL, '/qqbot/dashboard', NULL, 'catalog', '{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}', 1, 110),
(2041700000000100401, 2041700000000100400, 'QqBotDashboard', '/qqbot/dashboard', '/qqbot/dashboard/list', NULL, 'QqBot:Dashboard:List', 'menu', '{"icon":"lucide:gauge","title":"工作台"}', 1, 0),
(2041700000000100402, 2041700000000100400, 'QqBotAccount', '/qqbot/account', '/qqbot/account/list', NULL, 'QqBot:Account:List', 'menu', '{"icon":"lucide:radio-receiver","title":"账号连接"}', 1, 1),
(2041700000000100410, 2041700000000100400, 'QqBotAccountConfig', '/qqbot/account/config', '/qqbot/account/config', NULL, 'QqBot:Account:Config', 'menu', '{"activePath":"/qqbot/account","hideInMenu":true,"title":"账号功能配置"}', 1, 0),
(2041700000000120401, 2041700000000100402, 'QqBotAccountCreate', NULL, NULL, NULL, 'QqBot:Account:Create', 'button', '{"title":"common.create"}', 1, 0),
(2041700000000120402, 2041700000000100402, 'QqBotAccountEdit', NULL, NULL, NULL, 'QqBot:Account:Edit', 'button', '{"title":"common.edit"}', 1, 0),
(2041700000000120403, 2041700000000100402, 'QqBotAccountDelete', NULL, NULL, NULL, 'QqBot:Account:Delete', 'button', '{"title":"common.delete"}', 1, 0),
(2041700000000120404, 2041700000000100402, 'QqBotAccountKick', NULL, NULL, NULL, 'QqBot:Account:Kick', 'button', '{"title":"断开连接"}', 1, 0),
(2041700000000120405, 2041700000000100402, 'QqBotAccountRefreshLogin', NULL, NULL, NULL, 'QqBot:Account:RefreshLogin', 'button', '{"title":"更新登录"}', 1, 0),
(2041700000000120406, 2041700000000100402, 'QqBotAccountConfigButton', NULL, NULL, NULL, 'QqBot:Account:Config', 'button', '{"title":"配置"}', 1, 0),
(2041700000000100403, 2041700000000100400, 'QqBotRule', '/qqbot/rule', '/qqbot/rule/list', NULL, 'QqBot:Rule:List', 'menu', '{"icon":"lucide:workflow","title":"自动回复规则"}', 1, 2),
(2041700000000120411, 2041700000000100403, 'QqBotRuleCreate', NULL, NULL, NULL, 'QqBot:Rule:Create', 'button', '{"title":"common.create"}', 1, 0),
(2041700000000120412, 2041700000000100403, 'QqBotRuleEdit', NULL, NULL, NULL, 'QqBot:Rule:Edit', 'button', '{"title":"common.edit"}', 1, 0),
@ -368,6 +587,11 @@ ON DUPLICATE KEY UPDATE
`sort` = VALUES(`sort`),
`is_deleted` = 0;
UPDATE `admin_menu`
SET `status` = 0,
`is_deleted` = 1
WHERE `name` IN ('QqBotEventPlugin', 'QqBotEventPluginToggle');
INSERT IGNORE INTO `admin_role_menu` (`role_id`, `menu_id`)
SELECT role.`id`, menu.`id`
FROM `admin_role` role

View File

@ -115,6 +115,39 @@ export class AdminMenuService {
.filter((menu) => !menu.isDeleted && menu.status === 1)
.forEach((menu) => menuMap.set(menu.id, menu));
});
return this.includeAncestorMenus([...menuMap.values()]);
}
private async includeAncestorMenus(menus: AdminMenu[]) {
const menuMap = new Map<string, AdminMenu>();
menus.forEach((menu) => menuMap.set(menu.id, menu));
const pendingParentIds = new Set<string>();
const collectMissingParent = (pid?: null | string) => {
if (!pid || pid === '0' || menuMap.has(pid)) return;
pendingParentIds.add(pid);
};
menus.forEach((menu) => collectMissingParent(menu.pid));
while (pendingParentIds.size > 0) {
const ids = [...pendingParentIds];
pendingParentIds.clear();
const parents = await this.menuRepository.find({
where: {
id: In(ids),
isDeleted: false,
status: 1,
},
});
parents.forEach((parent) => {
if (menuMap.has(parent.id)) return;
menuMap.set(parent.id, parent);
collectMissingParent(parent.pid);
});
}
return [...menuMap.values()];
}

View File

@ -0,0 +1,48 @@
import {
BeforeInsert,
Column,
CreateDateColumn,
Entity,
Index,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
import { ensureSnowflakeId } from '@/common';
export type QqbotAccountAbilityType = 'command' | 'event_plugin' | 'rule';
@Entity('qqbot_account_ability')
@Index('uk_qqbot_account_ability', ['accountId', 'abilityType', 'abilityKey'], {
unique: true,
})
@Index('idx_qqbot_account_ability_self', ['selfId', 'abilityType', 'isDeleted'])
export class QqbotAccountAbility {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'account_id', type: 'bigint' })
accountId: string;
@Column({ length: 64, name: 'self_id' })
selfId: string;
@Column({ length: 32, name: 'ability_type' })
abilityType: QqbotAccountAbilityType;
@Column({ length: 128, name: 'ability_key' })
abilityKey: string;
@Column({ default: false, name: 'is_deleted' })
isDeleted: boolean;
@CreateDateColumn({ name: 'create_time' })
createTime: Date;
@UpdateDateColumn({ name: 'update_time' })
updateTime: Date;
@BeforeInsert()
createId() {
ensureSnowflakeId(this);
}
}

View File

@ -104,6 +104,58 @@ export class QqbotAccountController {
return vbenSuccess(await this.accountService.remove(id));
}
@Post('bind/command')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '绑定账号在线命令' })
@ApiQuery({ name: 'selfId', type: String })
@ApiQuery({ name: 'commandId', type: String })
async bindCommand(
@Query('selfId') selfId: string,
@Query('commandId') commandId: string,
) {
return vbenSuccess(
await this.accountService.bindCommand(selfId, commandId),
);
}
@Post('unbind/command')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '解绑账号在线命令' })
@ApiQuery({ name: 'selfId', type: String })
@ApiQuery({ name: 'commandId', type: String })
async unbindCommand(
@Query('selfId') selfId: string,
@Query('commandId') commandId: string,
) {
return vbenSuccess(
await this.accountService.unbindCommand(selfId, commandId),
);
}
@Post('bind/rule')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '绑定账号自动回复规则' })
@ApiQuery({ name: 'selfId', type: String })
@ApiQuery({ name: 'ruleId', type: String })
async bindRule(
@Query('selfId') selfId: string,
@Query('ruleId') ruleId: string,
) {
return vbenSuccess(await this.accountService.bindRule(selfId, ruleId));
}
@Post('unbind/rule')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '解绑账号自动回复规则' })
@ApiQuery({ name: 'selfId', type: String })
@ApiQuery({ name: 'ruleId', type: String })
async unbindRule(
@Query('selfId') selfId: string,
@Query('ruleId') ruleId: string,
) {
return vbenSuccess(await this.accountService.unbindRule(selfId, ruleId));
}
@Post('kick')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '断开 QQBot 反向 WS 会话' })

View File

@ -2,6 +2,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { throwVbenError } from '@/common';
import {
QqbotAccountAbility,
type QqbotAccountAbilityType,
} from './qqbot-account-ability.entity';
import { QqbotAccount } from './qqbot-account.entity';
import type {
QqbotAccountBodyDto,
@ -17,6 +21,8 @@ export class QqbotAccountService {
constructor(
@InjectRepository(QqbotAccount)
private readonly accountRepository: Repository<QqbotAccount>,
@InjectRepository(QqbotAccountAbility)
private readonly accountAbilityRepository: Repository<QqbotAccountAbility>,
private readonly napcatContainerService: QqbotNapcatContainerService,
) {}
@ -60,6 +66,42 @@ export class QqbotAccountService {
});
}
async getBoundCommandIds(selfId: string) {
return this.getBoundAbilityKeys(selfId, 'command');
}
async getBoundRuleIds(selfId: string) {
return this.getBoundAbilityKeys(selfId, 'rule');
}
async getBoundEventPluginKeys(selfId: string) {
return this.getBoundAbilityKeys(selfId, 'event_plugin');
}
async bindCommand(selfId: string, commandId: string) {
return this.bindAbility(selfId, commandId, 'command');
}
async bindRule(selfId: string, ruleId: string) {
return this.bindAbility(selfId, ruleId, 'rule');
}
async bindEventPlugin(selfId: string, pluginKey: string) {
return this.bindAbility(selfId, pluginKey, 'event_plugin');
}
async unbindCommand(selfId: string, commandId: string) {
return this.unbindAbility(selfId, commandId, 'command');
}
async unbindRule(selfId: string, ruleId: string) {
return this.unbindAbility(selfId, ruleId, 'rule');
}
async unbindEventPlugin(selfId: string, pluginKey: string) {
return this.unbindAbility(selfId, pluginKey, 'event_plugin');
}
async getDefaultAccount(selfId?: string) {
if (selfId) {
const account = await this.accountRepository.findOne({
@ -134,6 +176,10 @@ export class QqbotAccountService {
if (existing) {
await this.accountRepository.update({ id: existing.id }, payload);
await this.accountAbilityRepository.update(
{ accountId: existing.id },
{ selfId },
);
return existing.id;
}
@ -204,6 +250,12 @@ export class QqbotAccountService {
delete payload.accessToken;
}
await this.accountRepository.update({ id: body.id }, payload);
if (payload.selfId) {
await this.accountAbilityRepository.update(
{ accountId: body.id },
{ selfId: payload.selfId },
);
}
return true;
}
@ -228,6 +280,10 @@ export class QqbotAccountService {
isDeleted: true,
},
);
await this.accountAbilityRepository.update(
{ accountId: id },
{ isDeleted: true },
);
return {
deletedContainers: containerResult.deletedContainers,
};
@ -319,4 +375,92 @@ export class QqbotAccountService {
typeof body.selfId === 'string' ? body.selfId.trim() : body.selfId,
} as Partial<QqbotAccount>;
}
private async bindAbility(
selfId: string,
abilityKey: string,
type: QqbotAccountAbilityType,
) {
const account = await this.assertConfigurableAccount(selfId);
const normalizedKey = this.normalizeAbilityId(abilityKey);
const existing = await this.accountAbilityRepository.findOne({
where: {
abilityKey: normalizedKey,
abilityType: type,
accountId: account.id,
},
});
if (existing) {
await this.accountAbilityRepository.update(
{ id: existing.id },
{ isDeleted: false, selfId: account.selfId },
);
return true;
}
await this.accountAbilityRepository.save(
this.accountAbilityRepository.create({
abilityKey: normalizedKey,
abilityType: type,
accountId: account.id,
isDeleted: false,
selfId: account.selfId,
}),
);
return true;
}
private async unbindAbility(
selfId: string,
abilityKey: string,
type: QqbotAccountAbilityType,
) {
const account = await this.assertConfigurableAccount(selfId);
const normalizedKey = this.normalizeAbilityId(abilityKey);
await this.accountAbilityRepository.update(
{
abilityKey: normalizedKey,
abilityType: type,
accountId: account.id,
},
{ isDeleted: true, selfId: account.selfId },
);
return true;
}
private async assertConfigurableAccount(selfId: string) {
const normalizedSelfId = `${selfId || ''}`.trim();
if (!normalizedSelfId) throwVbenError('请选择所属 QQBot 账号');
const account = await this.findBySelfId(normalizedSelfId);
if (!account || !account.enabled) {
throwVbenError(`QQBot 账号不存在或已停用:${normalizedSelfId}`);
}
return account;
}
private normalizeAbilityId(abilityId: string) {
const normalizedId = `${abilityId || ''}`.trim();
if (!normalizedId) throwVbenError('绑定能力 ID 不能为空');
return normalizedId;
}
private async getBoundAbilityKeys(
selfId: string,
abilityType: QqbotAccountAbilityType,
) {
const account = await this.findBySelfId(`${selfId || ''}`.trim());
if (!account || !account.enabled || account.isDeleted) return [];
const bindings = await this.accountAbilityRepository.find({
order: {
createTime: 'ASC',
},
where: {
abilityType,
accountId: account.id,
isDeleted: false,
},
});
return bindings.map((item) => item.abilityKey);
}
}

View File

@ -17,6 +17,9 @@ export class QqbotCommandQueryDto implements QqbotPageQuery {
@ApiPropertyOptional()
keyword?: string;
@ApiPropertyOptional()
selfId?: string;
@ApiPropertyOptional()
pluginKey?: string;

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
import { throwVbenError } from '@/common';
import { QqbotAccountService } from '../account/qqbot-account.service';
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
import type {
QqbotCommandParserType,
@ -24,6 +25,7 @@ export class QqbotCommandService {
private readonly commandRepository: Repository<QqbotCommand>,
@InjectRepository(QqbotCommandLog)
private readonly commandLogRepository: Repository<QqbotCommandLog>,
private readonly accountService: QqbotAccountService,
private readonly pluginRegistry: QqbotPluginRegistryService,
) {}
@ -39,6 +41,15 @@ export class QqbotCommandService {
{ keyword: `%${query.keyword}%` },
);
}
if (query.selfId) {
const boundIds = await this.accountService.getBoundCommandIds(
query.selfId,
);
if (boundIds.length === 0) {
return { list: [], pageNo, pageSize, total: 0 };
}
builder.andWhere('command.id IN (:...boundIds)', { boundIds });
}
if (query.pluginKey) {
builder.andWhere('command.pluginKey = :pluginKey', {
pluginKey: query.pluginKey,
@ -75,10 +86,15 @@ export class QqbotCommandService {
}
async listEnabledForMessage(message: QqbotNormalizedMessage) {
const boundIds = await this.accountService.getBoundCommandIds(
message.selfId,
);
if (boundIds.length === 0) return [];
return this.commandRepository
.createQueryBuilder('command')
.where('command.isDeleted = :isDeleted', { isDeleted: false })
.andWhere('command.enabled = :enabled', { enabled: true })
.andWhere('command.id IN (:...boundIds)', { boundIds })
.andWhere('command.targetType IN (:...targetTypes)', {
targetTypes: ['all', message.messageType],
})
@ -97,7 +113,7 @@ export class QqbotCommandService {
async save(body: QqbotCommandBodyDto) {
const payload = await this.normalizeBody(body);
await this.assertCodeAvailable(payload.code);
await this.assertCodeAvailable(payload.code || '');
const saved = await this.commandRepository.save(
this.commandRepository.create(payload),
);
@ -110,7 +126,7 @@ export class QqbotCommandService {
...this.toRawBody(current),
...body,
});
await this.assertCodeAvailable(payload.code, body.id);
await this.assertCodeAvailable(payload.code || '', body.id);
await this.commandRepository.update({ id: body.id }, payload);
return true;
}
@ -218,9 +234,7 @@ export class QqbotCommandService {
private stringifyList(value: string[] | string | undefined, fallback = []) {
const list = Array.isArray(value)
? value
: `${value || ''}`
.split(',')
.map((item) => item.trim());
: `${value || ''}`.split(',').map((item) => item.trim());
const normalized = list
.map((item) => `${item || ''}`.trim())
.filter(Boolean);

View File

@ -22,8 +22,8 @@ export class QqbotConfigService {
async getPermissionConfig(): Promise<QqbotPermissionConfig> {
const [allowlistEnabled, blocklistEnabled] = await Promise.all([
this.getBoolean(QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled, false),
this.getBoolean(QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled, true),
this.getBooleanConfig(QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled, false),
this.getBooleanConfig(QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled, true),
]);
return { allowlistEnabled, blocklistEnabled };
@ -36,7 +36,7 @@ export class QqbotConfigService {
if (typeof config.allowlistEnabled === 'boolean') {
tasks.push(
this.setBoolean(
this.setBooleanConfig(
QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled,
config.allowlistEnabled,
'QQBot 白名单总开关',
@ -45,7 +45,7 @@ export class QqbotConfigService {
}
if (typeof config.blocklistEnabled === 'boolean') {
tasks.push(
this.setBoolean(
this.setBooleanConfig(
QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled,
config.blocklistEnabled,
'QQBot 黑名单总开关',
@ -57,7 +57,7 @@ export class QqbotConfigService {
return this.getPermissionConfig();
}
private async getBoolean(configKey: string, defaultValue: boolean) {
async getBooleanConfig(configKey: string, defaultValue: boolean) {
const record = await this.configRepository.findOne({
where: { configKey },
});
@ -65,7 +65,11 @@ export class QqbotConfigService {
return record.configValue === 'true';
}
private async setBoolean(configKey: string, value: boolean, remark: string) {
async setBooleanConfig(
configKey: string,
value: boolean,
remark: string,
) {
const exists = await this.configRepository.findOne({
where: { configKey },
});

View File

@ -0,0 +1,93 @@
import { Injectable } from '@nestjs/common';
import { throwVbenError } from '@/common';
import { QqbotAccountService } from '../account/qqbot-account.service';
import { QqbotRepeaterPluginService } from '../plugins/repeater/qqbot-repeater.plugin';
import type {
QqbotEventPluginDefinition,
QqbotPluginHealth,
QqbotPluginOperationSummary,
} from './qqbot-plugin.types';
@Injectable()
export class QqbotEventPluginRegistryService {
constructor(
private readonly accountService: QqbotAccountService,
private readonly repeaterPlugin: QqbotRepeaterPluginService,
) {}
listDefinitions(pluginKey?: string): QqbotEventPluginDefinition[] {
return this.getDefinitions(pluginKey);
}
async listPlugins(selfId?: string) {
const accounts = selfId
? [await this.accountService.findBySelfId(selfId)]
: await this.accountService.allEnabled();
return Promise.all(
accounts
.filter((account): account is NonNullable<typeof account> => !!account)
.map((account) =>
this.repeaterPlugin.getSummary({
accountName: account.name,
connectStatus: account.connectStatus,
selfId: account.selfId,
}),
),
);
}
listOperations(pluginKey?: string): QqbotPluginOperationSummary[] {
return this.getDefinitions(pluginKey).map((definition) => ({
description: definition.description,
inputSchema: {
triggerType: definition.triggerType,
},
key: definition.triggerType,
name: definition.triggerType === 'message' ? '消息事件' : definition.name,
outputSchema: undefined,
pluginKey: definition.key,
triggerMode: 'event',
}));
}
async health(pluginKey?: string): Promise<QqbotPluginHealth[]> {
return this.getDefinitions(pluginKey).map((definition) => ({
checkedAt: new Date().toISOString(),
message: definition.remark || '事件插件由账号配置绑定后触发',
name: definition.name,
pluginKey: definition.key,
status: 'healthy',
triggerMode: 'event',
}));
}
async bind(pluginKey: string, selfId: string) {
this.assertPlugin(pluginKey);
if (pluginKey === 'repeater') {
return this.repeaterPlugin.bind(selfId);
}
await this.accountService.bindEventPlugin(selfId, pluginKey);
return true;
}
async unbind(pluginKey: string, selfId: string) {
this.assertPlugin(pluginKey);
if (pluginKey === 'repeater') {
return this.repeaterPlugin.unbind(selfId);
}
await this.accountService.unbindEventPlugin(selfId, pluginKey);
return true;
}
private assertPlugin(pluginKey: string) {
if (pluginKey === 'repeater') return;
throwVbenError(`QQBot 事件插件不存在:${pluginKey}`);
}
private getDefinitions(pluginKey?: string): QqbotEventPluginDefinition[] {
const definitions = [this.repeaterPlugin.getDefinition()];
return pluginKey
? definitions.filter((definition) => definition.key === pluginKey)
: definitions;
}
}

View File

@ -32,6 +32,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
key: plugin.key,
name: plugin.name,
operationCount: plugin.operations.length,
triggerMode: 'command',
version: plugin.version,
}));
}
@ -46,6 +47,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
name: operation.name,
outputSchema: operation.outputSchema,
pluginKey: plugin.key,
triggerMode: 'command',
})),
);
}
@ -58,10 +60,18 @@ export class QqbotPluginRegistryService implements OnModuleInit {
return {
checkedAt: new Date().toISOString(),
message: '插件未提供健康检查',
name: plugin.name,
pluginKey: plugin.key,
status: 'healthy',
triggerMode: 'command' as const,
};
}
return plugin.healthCheck();
return {
...(await plugin.healthCheck()),
name: plugin.name,
pluginKey: plugin.key,
triggerMode: 'command' as const,
};
}),
);
}

View File

@ -1,32 +1,135 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
import { vbenSuccess } from '@/common';
import { QqbotEventPluginRegistryService } from './qqbot-event-plugin-registry.service';
import { QqbotPluginRegistryService } from './qqbot-plugin-registry.service';
import type { QqbotPluginTriggerMode } from './qqbot-plugin.types';
@ApiTags('qqbot-plugin')
@Controller('qqbot/plugin')
@UseGuards(JwtAuthGuard)
export class QqbotPluginController {
constructor(private readonly pluginRegistry: QqbotPluginRegistryService) {}
constructor(
private readonly eventPluginRegistry: QqbotEventPluginRegistryService,
private readonly pluginRegistry: QqbotPluginRegistryService,
) {}
@Get('list')
@ApiOperation({ summary: 'QQBot 插件列表' })
async list() {
return vbenSuccess(this.pluginRegistry.listPlugins());
@ApiQuery({
enum: ['command', 'event'],
name: 'triggerMode',
required: false,
})
async list(@Query('triggerMode') triggerMode?: QqbotPluginTriggerMode) {
return vbenSuccess([
...(this.includesTriggerMode('command', triggerMode)
? this.pluginRegistry.listPlugins()
: []),
...(this.includesTriggerMode('event', triggerMode)
? this.eventPluginRegistry.listDefinitions().map((definition) => ({
description: definition.description,
key: definition.key,
name: definition.name,
operationCount: 1,
triggerMode: 'event' as const,
version: definition.version,
}))
: []),
]);
}
@Get('operation/list')
@ApiOperation({ summary: 'QQBot 插件能力列表' })
@ApiQuery({ name: 'pluginKey', required: false, type: String })
async operationList(@Query('pluginKey') pluginKey?: string) {
return vbenSuccess(this.pluginRegistry.listOperations(pluginKey));
@ApiQuery({
enum: ['command', 'event'],
name: 'triggerMode',
required: false,
})
async operationList(
@Query('pluginKey') pluginKey?: string,
@Query('triggerMode') triggerMode?: QqbotPluginTriggerMode,
) {
return vbenSuccess([
...(this.includesTriggerMode('command', triggerMode)
? this.pluginRegistry.listOperations(pluginKey)
: []),
...(this.includesTriggerMode('event', triggerMode)
? this.eventPluginRegistry.listOperations(pluginKey)
: []),
]);
}
@Get('health')
@ApiOperation({ summary: 'QQBot 插件健康检查' })
@ApiQuery({ name: 'pluginKey', required: false, type: String })
async health(@Query('pluginKey') pluginKey?: string) {
return vbenSuccess(await this.pluginRegistry.health(pluginKey));
@ApiQuery({
enum: ['command', 'event'],
name: 'triggerMode',
required: false,
})
async health(
@Query('pluginKey') pluginKey?: string,
@Query('triggerMode') triggerMode?: QqbotPluginTriggerMode,
) {
const [commandHealth, eventHealth] = await Promise.all([
this.includesTriggerMode('command', triggerMode)
? this.pluginRegistry.health(pluginKey)
: Promise.resolve([]),
this.includesTriggerMode('event', triggerMode)
? this.eventPluginRegistry.health(pluginKey)
: Promise.resolve([]),
]);
return vbenSuccess([...commandHealth, ...eventHealth]);
}
@Get('event/list')
@ApiOperation({ summary: 'QQBot 事件触发插件列表' })
@ApiQuery({ name: 'selfId', required: false, type: String })
async eventList(@Query('selfId') selfId?: string) {
return vbenSuccess(await this.eventPluginRegistry.listPlugins(selfId));
}
@Post('event/bind')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '绑定 QQBot 事件触发插件' })
@ApiQuery({ name: 'pluginKey', type: String })
@ApiQuery({ name: 'selfId', type: String })
async eventBind(
@Query('pluginKey') pluginKey: string,
@Query('selfId') selfId: string,
) {
return vbenSuccess(await this.eventPluginRegistry.bind(pluginKey, selfId));
}
@Post('event/unbind')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '解绑 QQBot 事件触发插件' })
@ApiQuery({ name: 'pluginKey', type: String })
@ApiQuery({ name: 'selfId', type: String })
async eventUnbind(
@Query('pluginKey') pluginKey: string,
@Query('selfId') selfId: string,
) {
return vbenSuccess(
await this.eventPluginRegistry.unbind(pluginKey, selfId),
);
}
private includesTriggerMode(
target: QqbotPluginTriggerMode,
triggerMode?: QqbotPluginTriggerMode,
) {
return !triggerMode || triggerMode === target;
}
}

View File

@ -7,9 +7,14 @@ import type { QqbotCommand } from '../command/qqbot-command.entity';
export type QqbotPluginHealth = {
checkedAt: string;
message?: string;
name?: string;
pluginKey?: string;
status: QqbotPluginHealthStatus;
triggerMode?: QqbotPluginTriggerMode;
};
export type QqbotPluginTriggerMode = 'command' | 'event';
export type QqbotPluginOperationContext = {
args?: Record<string, any>;
command?: QqbotCommand;
@ -38,11 +43,34 @@ export type QqbotIntegrationPlugin = {
version: string;
};
export type QqbotEventPluginSummary = {
accountName?: string;
bound: boolean;
connectStatus?: string;
description?: string;
key: string;
name: string;
remark?: string;
selfId: string;
triggerType: 'message';
version: string;
};
export type QqbotEventPluginDefinition = {
description?: string;
key: string;
name: string;
remark?: string;
triggerType: 'message';
version: string;
};
export type QqbotPluginSummary = {
description?: string;
key: string;
name: string;
operationCount: number;
triggerMode: QqbotPluginTriggerMode;
version: string;
};
@ -54,4 +82,5 @@ export type QqbotPluginOperationSummary = {
name: string;
outputSchema?: Record<string, any>;
pluginKey: string;
triggerMode: QqbotPluginTriggerMode;
};

View File

@ -0,0 +1,218 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { QqbotAccountService } from '../../account/qqbot-account.service';
import type {
QqbotEventPluginDefinition,
QqbotEventPluginSummary,
} from '../../plugin/qqbot-plugin.types';
import type { QqbotNormalizedMessage } from '../../qqbot.types';
import { QqbotSendService } from '../../send/qqbot-send.service';
const QQBOT_REPEATER_VERSION = '1.0.0';
const QQBOT_REPEATER_PLUGIN_KEY = 'repeater';
type QqbotRepeaterConversationState = {
count: number;
lastText: string;
repeatedText: string;
updatedAt: number;
};
@Injectable()
export class QqbotRepeaterPluginService {
private readonly logger = new Logger(QqbotRepeaterPluginService.name);
private readonly states = new Map<string, QqbotRepeaterConversationState>();
private readonly boundCache = new Map<
string,
{
expiresAt: number;
value: boolean;
}
>();
private readonly pluginDescription =
'监听同一会话内连续重复的普通文本消息,达到阈值后自动复读一次。';
private readonly pluginName = '复读机';
private readonly pluginRemark =
'连续重复达到阈值后触发命令、CQ 码和机器人自身消息不触发。';
constructor(
private readonly configService: ConfigService,
private readonly accountService: QqbotAccountService,
private readonly sendService: QqbotSendService,
) {}
async getSummary(params: {
accountName?: string;
connectStatus?: string;
selfId: string;
}): Promise<QqbotEventPluginSummary> {
const definition = this.getDefinition();
return {
accountName: params.accountName,
bound: await this.isBound(params.selfId),
connectStatus: params.connectStatus,
description: definition.description,
key: definition.key,
name: definition.name,
remark: definition.remark,
selfId: params.selfId,
triggerType: definition.triggerType,
version: definition.version,
};
}
getDefinition(): QqbotEventPluginDefinition {
return {
description: this.pluginDescription,
key: QQBOT_REPEATER_PLUGIN_KEY,
name: this.pluginName,
remark: this.pluginRemark,
triggerType: 'message',
version: QQBOT_REPEATER_VERSION,
};
}
async bind(selfId: string) {
await this.accountService.bindEventPlugin(
selfId,
QQBOT_REPEATER_PLUGIN_KEY,
);
this.clearBoundCache(selfId);
return this.getSummary({ selfId });
}
async unbind(selfId: string) {
await this.accountService.unbindEventPlugin(
selfId,
QQBOT_REPEATER_PLUGIN_KEY,
);
this.clearBoundCache(selfId);
return this.getSummary({ selfId });
}
clearBoundCache(selfId: string) {
this.boundCache.delete(`${selfId || ''}`.trim());
}
async handleMessage(message: QqbotNormalizedMessage) {
if (!(await this.isBound(message.selfId))) return false;
const text = this.normalizeText(message.messageText);
if (!this.canRepeat(message, text)) {
this.resetState(message);
return false;
}
const key = this.buildStateKey(message);
const state = this.getNextState(key, text);
if (!this.shouldRepeat(state, text)) return false;
state.repeatedText = text;
try {
await this.sendService.sendText({
channelId: message.channelId,
guildId: message.rawEvent.guild_id
? `${message.rawEvent.guild_id}`
: undefined,
message: text,
selfId: message.selfId,
targetId: message.targetId,
targetType: message.messageType,
});
return true;
} catch (err) {
const errMsg = err instanceof Error ? err.message : '复读失败';
this.logger.warn(`QQBot 复读机发送失败: ${errMsg}`);
return false;
}
}
private async isBound(selfId: string) {
const normalizedSelfId = `${selfId || ''}`.trim();
if (!normalizedSelfId) return false;
const now = Date.now();
const cached = this.boundCache.get(normalizedSelfId);
if (cached && cached.expiresAt > now) {
return cached.value;
}
const value = (
await this.accountService.getBoundEventPluginKeys(normalizedSelfId)
).includes(QQBOT_REPEATER_PLUGIN_KEY);
this.boundCache.set(normalizedSelfId, {
expiresAt: now + this.getConfigCacheTtlMs(),
value,
});
return value;
}
private canRepeat(message: QqbotNormalizedMessage, text: string) {
if (!text) return false;
if (message.userId === message.selfId) return false;
if (/^[!/]/.test(text)) return false;
if (text.includes('[CQ:')) return false;
return text.length <= this.getMaxTextLength();
}
private getNextState(key: string, text: string) {
const current = this.states.get(key);
const now = Date.now();
const next =
current?.lastText === text
? { ...current, count: current.count + 1, updatedAt: now }
: {
count: 1,
lastText: text,
repeatedText: '',
updatedAt: now,
};
this.states.set(key, next);
this.pruneStates(now);
return next;
}
private shouldRepeat(state: QqbotRepeaterConversationState, text: string) {
return state.count >= this.getThreshold() && state.repeatedText !== text;
}
private normalizeText(value: string) {
return `${value || ''}`.trim().replace(/\s+/g, ' ');
}
private resetState(message: QqbotNormalizedMessage) {
this.states.delete(this.buildStateKey(message));
}
private buildStateKey(message: QqbotNormalizedMessage) {
return [message.selfId, message.messageType, message.targetId].join(':');
}
private getThreshold() {
const value = Number(this.configService.get('QQBOT_REPEATER_THRESHOLD'));
return Number.isInteger(value) && value > 1 ? value : 2;
}
private getMaxTextLength() {
const value = Number(
this.configService.get('QQBOT_REPEATER_MAX_TEXT_LENGTH'),
);
return Number.isInteger(value) && value > 0 ? value : 300;
}
private getStateTtlMs() {
const value = Number(this.configService.get('QQBOT_REPEATER_STATE_TTL_MS'));
return Number.isInteger(value) && value > 0 ? value : 10 * 60 * 1000;
}
private getConfigCacheTtlMs() {
const value = Number(
this.configService.get('QQBOT_REPEATER_CONFIG_CACHE_TTL_MS'),
);
return Number.isInteger(value) && value > 0 ? value : 2000;
}
private pruneStates(now: number) {
const ttl = this.getStateTtlMs();
for (const [key, state] of this.states.entries()) {
if (now - state.updatedAt > ttl) this.states.delete(key);
}
}
}

View File

@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { QqbotAccountController } from './account/qqbot-account.controller';
import { QqbotAccountAbility } from './account/qqbot-account-ability.entity';
import { QqbotAccount } from './account/qqbot-account.entity';
import { QqbotAccountService } from './account/qqbot-account.service';
import { QqbotNapcatLoginService } from './account/qqbot-napcat-login.service';
@ -33,10 +34,12 @@ import { QqbotAllowlist } from './permission/qqbot-allowlist.entity';
import { QqbotBlocklist } from './permission/qqbot-blocklist.entity';
import { QqbotPermissionController } from './permission/qqbot-permission.controller';
import { QqbotPermissionService } from './permission/qqbot-permission.service';
import { QqbotEventPluginRegistryService } from './plugin/qqbot-event-plugin-registry.service';
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
import { QqbotRepeaterPluginService } from './plugins/repeater/qqbot-repeater.plugin';
import { QqbotRuleController } from './rule/qqbot-rule.controller';
import { QqbotRule } from './rule/qqbot-rule.entity';
import { QqbotRuleEngineService } from './rule/qqbot-rule-engine.service';
@ -52,6 +55,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
AdminAuthGuardModule,
TypeOrmModule.forFeature([
QqbotAccount,
QqbotAccountAbility,
QqbotAllowlist,
QqbotBlocklist,
QqbotCommand,
@ -92,7 +96,9 @@ import { QqbotSendService } from './send/qqbot-send.service';
QqbotNapcatLoginService,
QqbotNapcatContainerService,
QqbotPermissionService,
QqbotEventPluginRegistryService,
QqbotPluginRegistryService,
QqbotRepeaterPluginService,
QqbotRateLimitService,
QqbotReplyTemplateService,
QqbotReverseWsService,

View File

@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import type { QqbotNormalizedMessage } from '../qqbot.types';
import { QqbotCommandEngineService } from '../command/qqbot-command-engine.service';
import { QqbotPermissionService } from '../permission/qqbot-permission.service';
import { QqbotRepeaterPluginService } from '../plugins/repeater/qqbot-repeater.plugin';
import { QqbotSendService } from '../send/qqbot-send.service';
import { QqbotRuleService } from './qqbot-rule.service';
@ -12,6 +13,7 @@ export class QqbotRuleEngineService {
constructor(
private readonly commandEngineService: QqbotCommandEngineService,
private readonly permissionService: QqbotPermissionService,
private readonly repeaterPluginService: QqbotRepeaterPluginService,
private readonly ruleService: QqbotRuleService,
private readonly sendService: QqbotSendService,
) {}
@ -44,5 +46,7 @@ export class QqbotRuleEngineService {
}
return;
}
await this.repeaterPluginService.handleMessage(message);
}
}

View File

@ -45,6 +45,9 @@ export class QqbotRuleQueryDto {
@ApiPropertyOptional()
keyword?: string;
@ApiPropertyOptional()
selfId?: string;
@ApiPropertyOptional()
targetType?: QqbotRuleTargetType;

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { throwVbenError } from '@/common';
import { QqbotAccountService } from '../account/qqbot-account.service';
import { QqbotRule } from './qqbot-rule.entity';
import type {
QqbotRuleBodyDto,
@ -20,6 +21,7 @@ export class QqbotRuleService {
constructor(
@InjectRepository(QqbotRule)
private readonly ruleRepository: Repository<QqbotRule>,
private readonly accountService: QqbotAccountService,
) {}
async page(query: QqbotRuleQueryDto) {
@ -36,6 +38,13 @@ export class QqbotRuleService {
},
);
}
if (query.selfId) {
const boundIds = await this.accountService.getBoundRuleIds(query.selfId);
if (boundIds.length === 0) {
return { list: [], pageNo, pageSize, total: 0 };
}
builder.andWhere('rule.id IN (:...boundIds)', { boundIds });
}
if (query.targetType) {
builder.andWhere('rule.targetType = :targetType', {
targetType: query.targetType,
@ -53,14 +62,22 @@ export class QqbotRuleService {
.skip(skip)
.take(pageSize)
.getManyAndCount();
return { list, pageNo, pageSize, total };
return {
list,
pageNo,
pageSize,
total,
};
}
async listEnabledForMessage(message: QqbotNormalizedMessage) {
const boundIds = await this.accountService.getBoundRuleIds(message.selfId);
if (boundIds.length === 0) return [];
return this.ruleRepository
.createQueryBuilder('rule')
.where('rule.isDeleted = :isDeleted', { isDeleted: false })
.andWhere('rule.enabled = :enabled', { enabled: true })
.andWhere('rule.id IN (:...boundIds)', { boundIds })
.andWhere('rule.targetType IN (:...targetTypes)', {
targetTypes: ['all', message.messageType],
})