feat: 完善QQBot插件与账号能力绑定
This commit is contained in:
parent
ae2fd41e0f
commit
7a93e49af6
@ -30,6 +30,10 @@ QQBOT_REVERSE_WS_TOKEN=
|
|||||||
QQBOT_AUTO_REGISTER_ACCOUNT=true
|
QQBOT_AUTO_REGISTER_ACCOUNT=true
|
||||||
QQBOT_API_TIMEOUT_MS=10000
|
QQBOT_API_TIMEOUT_MS=10000
|
||||||
QQBOT_SEND_RATE_PER_SECOND=1
|
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_BASE_URL=http://127.0.0.1:6099
|
||||||
NAPCAT_WEBUI_TOKEN=
|
NAPCAT_WEBUI_TOKEN=
|
||||||
NAPCAT_WEBUI_TIMEOUT_MS=8000
|
NAPCAT_WEBUI_TIMEOUT_MS=8000
|
||||||
|
|||||||
@ -25,6 +25,20 @@ CREATE TABLE IF NOT EXISTS `qqbot_account` (
|
|||||||
UNIQUE KEY `uk_qqbot_account_self_id` (`self_id`)
|
UNIQUE KEY `uk_qqbot_account_self_id` (`self_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) 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` (
|
CREATE TABLE IF NOT EXISTS `qqbot_napcat_container` (
|
||||||
`id` bigint NOT NULL,
|
`id` bigint NOT NULL,
|
||||||
`name` varchar(120) 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`)
|
UNIQUE KEY `uk_qqbot_dedupe_event_key` (`event_key`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) 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 = (
|
SET @qqbot_sql = (
|
||||||
SELECT IF(
|
SELECT IF(
|
||||||
COUNT(*) = 0,
|
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`)
|
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
|
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
|
ON DUPLICATE KEY UPDATE
|
||||||
`name` = VALUES(`name`),
|
`name` = VALUES(`name`),
|
||||||
`plugin_key` = VALUES(`plugin_key`),
|
`plugin_key` = VALUES(`plugin_key`),
|
||||||
`operation_key` = VALUES(`operation_key`),
|
`operation_key` = VALUES(`operation_key`),
|
||||||
`parser_key` = VALUES(`parser_key`),
|
`parser_key` = VALUES(`parser_key`),
|
||||||
`target_type` = VALUES(`target_type`),
|
`target_type` = VALUES(`target_type`),
|
||||||
|
`enabled` = VALUES(`enabled`),
|
||||||
`remark` = VALUES(`remark`),
|
`remark` = VALUES(`remark`),
|
||||||
`is_deleted` = 0;
|
`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`)
|
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`)
|
||||||
VALUES
|
VALUES
|
||||||
(2041700000000100400, 0, 'QqBot', '/qqbot', NULL, '/qqbot/dashboard', NULL, 'catalog', '{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}', 1, 110),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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),
|
(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`),
|
`sort` = VALUES(`sort`),
|
||||||
`is_deleted` = 0;
|
`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`)
|
INSERT IGNORE INTO `admin_role_menu` (`role_id`, `menu_id`)
|
||||||
SELECT role.`id`, menu.`id`
|
SELECT role.`id`, menu.`id`
|
||||||
FROM `admin_role` role
|
FROM `admin_role` role
|
||||||
|
|||||||
@ -115,6 +115,39 @@ export class AdminMenuService {
|
|||||||
.filter((menu) => !menu.isDeleted && menu.status === 1)
|
.filter((menu) => !menu.isDeleted && menu.status === 1)
|
||||||
.forEach((menu) => menuMap.set(menu.id, menu));
|
.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()];
|
return [...menuMap.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/qqbot/account/qqbot-account-ability.entity.ts
Normal file
48
src/qqbot/account/qqbot-account-ability.entity.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -104,6 +104,58 @@ export class QqbotAccountController {
|
|||||||
return vbenSuccess(await this.accountService.remove(id));
|
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')
|
@Post('kick')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: '断开 QQBot 反向 WS 会话' })
|
@ApiOperation({ summary: '断开 QQBot 反向 WS 会话' })
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { throwVbenError } from '@/common';
|
import { throwVbenError } from '@/common';
|
||||||
|
import {
|
||||||
|
QqbotAccountAbility,
|
||||||
|
type QqbotAccountAbilityType,
|
||||||
|
} from './qqbot-account-ability.entity';
|
||||||
import { QqbotAccount } from './qqbot-account.entity';
|
import { QqbotAccount } from './qqbot-account.entity';
|
||||||
import type {
|
import type {
|
||||||
QqbotAccountBodyDto,
|
QqbotAccountBodyDto,
|
||||||
@ -17,6 +21,8 @@ export class QqbotAccountService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(QqbotAccount)
|
@InjectRepository(QqbotAccount)
|
||||||
private readonly accountRepository: Repository<QqbotAccount>,
|
private readonly accountRepository: Repository<QqbotAccount>,
|
||||||
|
@InjectRepository(QqbotAccountAbility)
|
||||||
|
private readonly accountAbilityRepository: Repository<QqbotAccountAbility>,
|
||||||
private readonly napcatContainerService: QqbotNapcatContainerService,
|
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) {
|
async getDefaultAccount(selfId?: string) {
|
||||||
if (selfId) {
|
if (selfId) {
|
||||||
const account = await this.accountRepository.findOne({
|
const account = await this.accountRepository.findOne({
|
||||||
@ -134,6 +176,10 @@ export class QqbotAccountService {
|
|||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await this.accountRepository.update({ id: existing.id }, payload);
|
await this.accountRepository.update({ id: existing.id }, payload);
|
||||||
|
await this.accountAbilityRepository.update(
|
||||||
|
{ accountId: existing.id },
|
||||||
|
{ selfId },
|
||||||
|
);
|
||||||
return existing.id;
|
return existing.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +250,12 @@ export class QqbotAccountService {
|
|||||||
delete payload.accessToken;
|
delete payload.accessToken;
|
||||||
}
|
}
|
||||||
await this.accountRepository.update({ id: body.id }, payload);
|
await this.accountRepository.update({ id: body.id }, payload);
|
||||||
|
if (payload.selfId) {
|
||||||
|
await this.accountAbilityRepository.update(
|
||||||
|
{ accountId: body.id },
|
||||||
|
{ selfId: payload.selfId },
|
||||||
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,6 +280,10 @@ export class QqbotAccountService {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await this.accountAbilityRepository.update(
|
||||||
|
{ accountId: id },
|
||||||
|
{ isDeleted: true },
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
deletedContainers: containerResult.deletedContainers,
|
deletedContainers: containerResult.deletedContainers,
|
||||||
};
|
};
|
||||||
@ -319,4 +375,92 @@ export class QqbotAccountService {
|
|||||||
typeof body.selfId === 'string' ? body.selfId.trim() : body.selfId,
|
typeof body.selfId === 'string' ? body.selfId.trim() : body.selfId,
|
||||||
} as Partial<QqbotAccount>;
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,9 @@ export class QqbotCommandQueryDto implements QqbotPageQuery {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
selfId?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
pluginKey?: string;
|
pluginKey?: string;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Not, Repository } from 'typeorm';
|
import { Not, Repository } from 'typeorm';
|
||||||
import { throwVbenError } from '@/common';
|
import { throwVbenError } from '@/common';
|
||||||
|
import { QqbotAccountService } from '../account/qqbot-account.service';
|
||||||
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
|
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
|
||||||
import type {
|
import type {
|
||||||
QqbotCommandParserType,
|
QqbotCommandParserType,
|
||||||
@ -24,6 +25,7 @@ export class QqbotCommandService {
|
|||||||
private readonly commandRepository: Repository<QqbotCommand>,
|
private readonly commandRepository: Repository<QqbotCommand>,
|
||||||
@InjectRepository(QqbotCommandLog)
|
@InjectRepository(QqbotCommandLog)
|
||||||
private readonly commandLogRepository: Repository<QqbotCommandLog>,
|
private readonly commandLogRepository: Repository<QqbotCommandLog>,
|
||||||
|
private readonly accountService: QqbotAccountService,
|
||||||
private readonly pluginRegistry: QqbotPluginRegistryService,
|
private readonly pluginRegistry: QqbotPluginRegistryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -39,6 +41,15 @@ export class QqbotCommandService {
|
|||||||
{ keyword: `%${query.keyword}%` },
|
{ 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) {
|
if (query.pluginKey) {
|
||||||
builder.andWhere('command.pluginKey = :pluginKey', {
|
builder.andWhere('command.pluginKey = :pluginKey', {
|
||||||
pluginKey: query.pluginKey,
|
pluginKey: query.pluginKey,
|
||||||
@ -75,10 +86,15 @@ export class QqbotCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listEnabledForMessage(message: QqbotNormalizedMessage) {
|
async listEnabledForMessage(message: QqbotNormalizedMessage) {
|
||||||
|
const boundIds = await this.accountService.getBoundCommandIds(
|
||||||
|
message.selfId,
|
||||||
|
);
|
||||||
|
if (boundIds.length === 0) return [];
|
||||||
return this.commandRepository
|
return this.commandRepository
|
||||||
.createQueryBuilder('command')
|
.createQueryBuilder('command')
|
||||||
.where('command.isDeleted = :isDeleted', { isDeleted: false })
|
.where('command.isDeleted = :isDeleted', { isDeleted: false })
|
||||||
.andWhere('command.enabled = :enabled', { enabled: true })
|
.andWhere('command.enabled = :enabled', { enabled: true })
|
||||||
|
.andWhere('command.id IN (:...boundIds)', { boundIds })
|
||||||
.andWhere('command.targetType IN (:...targetTypes)', {
|
.andWhere('command.targetType IN (:...targetTypes)', {
|
||||||
targetTypes: ['all', message.messageType],
|
targetTypes: ['all', message.messageType],
|
||||||
})
|
})
|
||||||
@ -97,7 +113,7 @@ export class QqbotCommandService {
|
|||||||
|
|
||||||
async save(body: QqbotCommandBodyDto) {
|
async save(body: QqbotCommandBodyDto) {
|
||||||
const payload = await this.normalizeBody(body);
|
const payload = await this.normalizeBody(body);
|
||||||
await this.assertCodeAvailable(payload.code);
|
await this.assertCodeAvailable(payload.code || '');
|
||||||
const saved = await this.commandRepository.save(
|
const saved = await this.commandRepository.save(
|
||||||
this.commandRepository.create(payload),
|
this.commandRepository.create(payload),
|
||||||
);
|
);
|
||||||
@ -110,7 +126,7 @@ export class QqbotCommandService {
|
|||||||
...this.toRawBody(current),
|
...this.toRawBody(current),
|
||||||
...body,
|
...body,
|
||||||
});
|
});
|
||||||
await this.assertCodeAvailable(payload.code, body.id);
|
await this.assertCodeAvailable(payload.code || '', body.id);
|
||||||
await this.commandRepository.update({ id: body.id }, payload);
|
await this.commandRepository.update({ id: body.id }, payload);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -218,9 +234,7 @@ export class QqbotCommandService {
|
|||||||
private stringifyList(value: string[] | string | undefined, fallback = []) {
|
private stringifyList(value: string[] | string | undefined, fallback = []) {
|
||||||
const list = Array.isArray(value)
|
const list = Array.isArray(value)
|
||||||
? value
|
? value
|
||||||
: `${value || ''}`
|
: `${value || ''}`.split(',').map((item) => item.trim());
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim());
|
|
||||||
const normalized = list
|
const normalized = list
|
||||||
.map((item) => `${item || ''}`.trim())
|
.map((item) => `${item || ''}`.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|||||||
@ -22,8 +22,8 @@ export class QqbotConfigService {
|
|||||||
|
|
||||||
async getPermissionConfig(): Promise<QqbotPermissionConfig> {
|
async getPermissionConfig(): Promise<QqbotPermissionConfig> {
|
||||||
const [allowlistEnabled, blocklistEnabled] = await Promise.all([
|
const [allowlistEnabled, blocklistEnabled] = await Promise.all([
|
||||||
this.getBoolean(QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled, false),
|
this.getBooleanConfig(QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled, false),
|
||||||
this.getBoolean(QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled, true),
|
this.getBooleanConfig(QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled, true),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { allowlistEnabled, blocklistEnabled };
|
return { allowlistEnabled, blocklistEnabled };
|
||||||
@ -36,7 +36,7 @@ export class QqbotConfigService {
|
|||||||
|
|
||||||
if (typeof config.allowlistEnabled === 'boolean') {
|
if (typeof config.allowlistEnabled === 'boolean') {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
this.setBoolean(
|
this.setBooleanConfig(
|
||||||
QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled,
|
QQBOT_PERMISSION_CONFIG_KEYS.allowlistEnabled,
|
||||||
config.allowlistEnabled,
|
config.allowlistEnabled,
|
||||||
'QQBot 白名单总开关',
|
'QQBot 白名单总开关',
|
||||||
@ -45,7 +45,7 @@ export class QqbotConfigService {
|
|||||||
}
|
}
|
||||||
if (typeof config.blocklistEnabled === 'boolean') {
|
if (typeof config.blocklistEnabled === 'boolean') {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
this.setBoolean(
|
this.setBooleanConfig(
|
||||||
QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled,
|
QQBOT_PERMISSION_CONFIG_KEYS.blocklistEnabled,
|
||||||
config.blocklistEnabled,
|
config.blocklistEnabled,
|
||||||
'QQBot 黑名单总开关',
|
'QQBot 黑名单总开关',
|
||||||
@ -57,7 +57,7 @@ export class QqbotConfigService {
|
|||||||
return this.getPermissionConfig();
|
return this.getPermissionConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBoolean(configKey: string, defaultValue: boolean) {
|
async getBooleanConfig(configKey: string, defaultValue: boolean) {
|
||||||
const record = await this.configRepository.findOne({
|
const record = await this.configRepository.findOne({
|
||||||
where: { configKey },
|
where: { configKey },
|
||||||
});
|
});
|
||||||
@ -65,7 +65,11 @@ export class QqbotConfigService {
|
|||||||
return record.configValue === 'true';
|
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({
|
const exists = await this.configRepository.findOne({
|
||||||
where: { configKey },
|
where: { configKey },
|
||||||
});
|
});
|
||||||
|
|||||||
93
src/qqbot/plugin/qqbot-event-plugin-registry.service.ts
Normal file
93
src/qqbot/plugin/qqbot-event-plugin-registry.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
key: plugin.key,
|
key: plugin.key,
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
operationCount: plugin.operations.length,
|
operationCount: plugin.operations.length,
|
||||||
|
triggerMode: 'command',
|
||||||
version: plugin.version,
|
version: plugin.version,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
name: operation.name,
|
name: operation.name,
|
||||||
outputSchema: operation.outputSchema,
|
outputSchema: operation.outputSchema,
|
||||||
pluginKey: plugin.key,
|
pluginKey: plugin.key,
|
||||||
|
triggerMode: 'command',
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -58,10 +60,18 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
return {
|
return {
|
||||||
checkedAt: new Date().toISOString(),
|
checkedAt: new Date().toISOString(),
|
||||||
message: '插件未提供健康检查',
|
message: '插件未提供健康检查',
|
||||||
|
name: plugin.name,
|
||||||
|
pluginKey: plugin.key,
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
|
triggerMode: 'command' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return plugin.healthCheck();
|
return {
|
||||||
|
...(await plugin.healthCheck()),
|
||||||
|
name: plugin.name,
|
||||||
|
pluginKey: plugin.key,
|
||||||
|
triggerMode: 'command' as const,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { vbenSuccess } from '@/common';
|
import { vbenSuccess } from '@/common';
|
||||||
|
import { QqbotEventPluginRegistryService } from './qqbot-event-plugin-registry.service';
|
||||||
import { QqbotPluginRegistryService } from './qqbot-plugin-registry.service';
|
import { QqbotPluginRegistryService } from './qqbot-plugin-registry.service';
|
||||||
|
import type { QqbotPluginTriggerMode } from './qqbot-plugin.types';
|
||||||
|
|
||||||
@ApiTags('qqbot-plugin')
|
@ApiTags('qqbot-plugin')
|
||||||
@Controller('qqbot/plugin')
|
@Controller('qqbot/plugin')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class QqbotPluginController {
|
export class QqbotPluginController {
|
||||||
constructor(private readonly pluginRegistry: QqbotPluginRegistryService) {}
|
constructor(
|
||||||
|
private readonly eventPluginRegistry: QqbotEventPluginRegistryService,
|
||||||
|
private readonly pluginRegistry: QqbotPluginRegistryService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get('list')
|
@Get('list')
|
||||||
@ApiOperation({ summary: 'QQBot 插件列表' })
|
@ApiOperation({ summary: 'QQBot 插件列表' })
|
||||||
async list() {
|
@ApiQuery({
|
||||||
return vbenSuccess(this.pluginRegistry.listPlugins());
|
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')
|
@Get('operation/list')
|
||||||
@ApiOperation({ summary: 'QQBot 插件能力列表' })
|
@ApiOperation({ summary: 'QQBot 插件能力列表' })
|
||||||
@ApiQuery({ name: 'pluginKey', required: false, type: String })
|
@ApiQuery({ name: 'pluginKey', required: false, type: String })
|
||||||
async operationList(@Query('pluginKey') pluginKey?: string) {
|
@ApiQuery({
|
||||||
return vbenSuccess(this.pluginRegistry.listOperations(pluginKey));
|
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')
|
@Get('health')
|
||||||
@ApiOperation({ summary: 'QQBot 插件健康检查' })
|
@ApiOperation({ summary: 'QQBot 插件健康检查' })
|
||||||
@ApiQuery({ name: 'pluginKey', required: false, type: String })
|
@ApiQuery({ name: 'pluginKey', required: false, type: String })
|
||||||
async health(@Query('pluginKey') pluginKey?: string) {
|
@ApiQuery({
|
||||||
return vbenSuccess(await this.pluginRegistry.health(pluginKey));
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,14 @@ import type { QqbotCommand } from '../command/qqbot-command.entity';
|
|||||||
export type QqbotPluginHealth = {
|
export type QqbotPluginHealth = {
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
name?: string;
|
||||||
|
pluginKey?: string;
|
||||||
status: QqbotPluginHealthStatus;
|
status: QqbotPluginHealthStatus;
|
||||||
|
triggerMode?: QqbotPluginTriggerMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QqbotPluginTriggerMode = 'command' | 'event';
|
||||||
|
|
||||||
export type QqbotPluginOperationContext = {
|
export type QqbotPluginOperationContext = {
|
||||||
args?: Record<string, any>;
|
args?: Record<string, any>;
|
||||||
command?: QqbotCommand;
|
command?: QqbotCommand;
|
||||||
@ -38,11 +43,34 @@ export type QqbotIntegrationPlugin = {
|
|||||||
version: string;
|
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 = {
|
export type QqbotPluginSummary = {
|
||||||
description?: string;
|
description?: string;
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
operationCount: number;
|
operationCount: number;
|
||||||
|
triggerMode: QqbotPluginTriggerMode;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,4 +82,5 @@ export type QqbotPluginOperationSummary = {
|
|||||||
name: string;
|
name: string;
|
||||||
outputSchema?: Record<string, any>;
|
outputSchema?: Record<string, any>;
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
|
triggerMode: QqbotPluginTriggerMode;
|
||||||
};
|
};
|
||||||
|
|||||||
218
src/qqbot/plugins/repeater/qqbot-repeater.plugin.ts
Normal file
218
src/qqbot/plugins/repeater/qqbot-repeater.plugin.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
||||||
import { QqbotAccountController } from './account/qqbot-account.controller';
|
import { QqbotAccountController } from './account/qqbot-account.controller';
|
||||||
|
import { QqbotAccountAbility } from './account/qqbot-account-ability.entity';
|
||||||
import { QqbotAccount } from './account/qqbot-account.entity';
|
import { QqbotAccount } from './account/qqbot-account.entity';
|
||||||
import { QqbotAccountService } from './account/qqbot-account.service';
|
import { QqbotAccountService } from './account/qqbot-account.service';
|
||||||
import { QqbotNapcatLoginService } from './account/qqbot-napcat-login.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 { QqbotBlocklist } from './permission/qqbot-blocklist.entity';
|
||||||
import { QqbotPermissionController } from './permission/qqbot-permission.controller';
|
import { QqbotPermissionController } from './permission/qqbot-permission.controller';
|
||||||
import { QqbotPermissionService } from './permission/qqbot-permission.service';
|
import { QqbotPermissionService } from './permission/qqbot-permission.service';
|
||||||
|
import { QqbotEventPluginRegistryService } from './plugin/qqbot-event-plugin-registry.service';
|
||||||
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
|
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
|
||||||
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
|
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
|
||||||
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
||||||
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
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 { QqbotRuleController } from './rule/qqbot-rule.controller';
|
||||||
import { QqbotRule } from './rule/qqbot-rule.entity';
|
import { QqbotRule } from './rule/qqbot-rule.entity';
|
||||||
import { QqbotRuleEngineService } from './rule/qqbot-rule-engine.service';
|
import { QqbotRuleEngineService } from './rule/qqbot-rule-engine.service';
|
||||||
@ -52,6 +55,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
|||||||
AdminAuthGuardModule,
|
AdminAuthGuardModule,
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
QqbotAccount,
|
QqbotAccount,
|
||||||
|
QqbotAccountAbility,
|
||||||
QqbotAllowlist,
|
QqbotAllowlist,
|
||||||
QqbotBlocklist,
|
QqbotBlocklist,
|
||||||
QqbotCommand,
|
QqbotCommand,
|
||||||
@ -92,7 +96,9 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
|||||||
QqbotNapcatLoginService,
|
QqbotNapcatLoginService,
|
||||||
QqbotNapcatContainerService,
|
QqbotNapcatContainerService,
|
||||||
QqbotPermissionService,
|
QqbotPermissionService,
|
||||||
|
QqbotEventPluginRegistryService,
|
||||||
QqbotPluginRegistryService,
|
QqbotPluginRegistryService,
|
||||||
|
QqbotRepeaterPluginService,
|
||||||
QqbotRateLimitService,
|
QqbotRateLimitService,
|
||||||
QqbotReplyTemplateService,
|
QqbotReplyTemplateService,
|
||||||
QqbotReverseWsService,
|
QqbotReverseWsService,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||||
import { QqbotCommandEngineService } from '../command/qqbot-command-engine.service';
|
import { QqbotCommandEngineService } from '../command/qqbot-command-engine.service';
|
||||||
import { QqbotPermissionService } from '../permission/qqbot-permission.service';
|
import { QqbotPermissionService } from '../permission/qqbot-permission.service';
|
||||||
|
import { QqbotRepeaterPluginService } from '../plugins/repeater/qqbot-repeater.plugin';
|
||||||
import { QqbotSendService } from '../send/qqbot-send.service';
|
import { QqbotSendService } from '../send/qqbot-send.service';
|
||||||
import { QqbotRuleService } from './qqbot-rule.service';
|
import { QqbotRuleService } from './qqbot-rule.service';
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ export class QqbotRuleEngineService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly commandEngineService: QqbotCommandEngineService,
|
private readonly commandEngineService: QqbotCommandEngineService,
|
||||||
private readonly permissionService: QqbotPermissionService,
|
private readonly permissionService: QqbotPermissionService,
|
||||||
|
private readonly repeaterPluginService: QqbotRepeaterPluginService,
|
||||||
private readonly ruleService: QqbotRuleService,
|
private readonly ruleService: QqbotRuleService,
|
||||||
private readonly sendService: QqbotSendService,
|
private readonly sendService: QqbotSendService,
|
||||||
) {}
|
) {}
|
||||||
@ -44,5 +46,7 @@ export class QqbotRuleEngineService {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.repeaterPluginService.handleMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,9 @@ export class QqbotRuleQueryDto {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
selfId?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
targetType?: QqbotRuleTargetType;
|
targetType?: QqbotRuleTargetType;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { throwVbenError } from '@/common';
|
import { throwVbenError } from '@/common';
|
||||||
|
import { QqbotAccountService } from '../account/qqbot-account.service';
|
||||||
import { QqbotRule } from './qqbot-rule.entity';
|
import { QqbotRule } from './qqbot-rule.entity';
|
||||||
import type {
|
import type {
|
||||||
QqbotRuleBodyDto,
|
QqbotRuleBodyDto,
|
||||||
@ -20,6 +21,7 @@ export class QqbotRuleService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(QqbotRule)
|
@InjectRepository(QqbotRule)
|
||||||
private readonly ruleRepository: Repository<QqbotRule>,
|
private readonly ruleRepository: Repository<QqbotRule>,
|
||||||
|
private readonly accountService: QqbotAccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async page(query: QqbotRuleQueryDto) {
|
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) {
|
if (query.targetType) {
|
||||||
builder.andWhere('rule.targetType = :targetType', {
|
builder.andWhere('rule.targetType = :targetType', {
|
||||||
targetType: query.targetType,
|
targetType: query.targetType,
|
||||||
@ -53,14 +62,22 @@ export class QqbotRuleService {
|
|||||||
.skip(skip)
|
.skip(skip)
|
||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
return { list, pageNo, pageSize, total };
|
return {
|
||||||
|
list,
|
||||||
|
pageNo,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async listEnabledForMessage(message: QqbotNormalizedMessage) {
|
async listEnabledForMessage(message: QqbotNormalizedMessage) {
|
||||||
|
const boundIds = await this.accountService.getBoundRuleIds(message.selfId);
|
||||||
|
if (boundIds.length === 0) return [];
|
||||||
return this.ruleRepository
|
return this.ruleRepository
|
||||||
.createQueryBuilder('rule')
|
.createQueryBuilder('rule')
|
||||||
.where('rule.isDeleted = :isDeleted', { isDeleted: false })
|
.where('rule.isDeleted = :isDeleted', { isDeleted: false })
|
||||||
.andWhere('rule.enabled = :enabled', { enabled: true })
|
.andWhere('rule.enabled = :enabled', { enabled: true })
|
||||||
|
.andWhere('rule.id IN (:...boundIds)', { boundIds })
|
||||||
.andWhere('rule.targetType IN (:...targetTypes)', {
|
.andWhere('rule.targetType IN (:...targetTypes)', {
|
||||||
targetTypes: ['all', message.messageType],
|
targetTypes: ['all', message.messageType],
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user