refactor: 收敛QQBot插件与NapCat架构
This commit is contained in:
parent
1f73673ebe
commit
3f664ca4c9
@ -19,7 +19,7 @@
|
|||||||
| Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. |
|
| Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. |
|
||||||
| System Event | `admin_notice`, `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | Admin notices and MySQL actionable events; Loki remains log query source. |
|
| System Event | `admin_notice`, `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | Admin notices and MySQL actionable events; Loki remains log query source. |
|
||||||
| Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. |
|
| Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. |
|
||||||
| QQBot Core | `qqbot_account`, `qqbot_account_ability`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_allowlist`, `qqbot_blocklist`, `qqbot_command`, `qqbot_command_alias`, `qqbot_command_log`, `qqbot_config`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event`, `qqbot_dedupe`, `qqbot_account_napcat`, `qqbot_napcat_container` | Batch 4 | Account, connection, permission, command, message, send queue, and transitional registered-entity compatibility tables until Batches 5-7 replace plugin/NapCat internals. |
|
| QQBot Core | `qqbot_account`, `qqbot_account_ability`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_allowlist`, `qqbot_blocklist`, `qqbot_command`, `qqbot_command_alias`, `qqbot_command_log`, `qqbot_config`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event`, `qqbot_dedupe` | Batch 4 | Account, connection, permission, command, message, send queue, and dedupe state. |
|
||||||
| NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Device identity and login challenge state. |
|
| NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Container runtime, account binding, device identity, login challenge, and cleanup state. |
|
||||||
| QQBot Plugin Platform | `qqbot_plugin`, `qqbot_plugin_version`, `qqbot_plugin_installation`, `qqbot_plugin_operation`, `qqbot_plugin_event_handler`, `qqbot_plugin_account_binding`, `qqbot_plugin_config`, `qqbot_plugin_asset`, `qqbot_plugin_runtime_event` | Batch 5 | Manifest, install lifecycle, runtime health, bindings. |
|
| QQBot Plugin Platform | `qqbot_plugin`, `qqbot_plugin_version`, `qqbot_plugin_installation`, `qqbot_plugin_operation`, `qqbot_plugin_event_handler`, `qqbot_plugin_account_binding`, `qqbot_plugin_config`, `qqbot_plugin_asset`, `qqbot_plugin_runtime_event` | Batch 5 | Manifest, install lifecycle, runtime health, bindings. |
|
||||||
| Plugin-Owned Data | plugin namespace tables | Batch 6 | Table names start with registered plugin namespace. |
|
| Plugin-Owned Data | plugin namespace tables | Batch 6 | Table names start with registered plugin namespace. |
|
||||||
|
|||||||
@ -40,9 +40,10 @@ CREATE TABLE IF NOT EXISTS `qqbot_account_ability` (
|
|||||||
KEY `idx_qqbot_account_ability_self` (`self_id`, `ability_type`, `is_deleted`)
|
KEY `idx_qqbot_account_ability_self` (`self_id`, `ability_type`, `is_deleted`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `qqbot_napcat_container` (
|
CREATE TABLE IF NOT EXISTS `napcat_container` (
|
||||||
`id` bigint NOT NULL,
|
`id` bigint NOT NULL,
|
||||||
`name` varchar(120) NOT NULL,
|
`account_id` bigint DEFAULT NULL,
|
||||||
|
`container_name` varchar(120) NOT NULL,
|
||||||
`base_url` varchar(255) NOT NULL,
|
`base_url` varchar(255) NOT NULL,
|
||||||
`webui_port` int DEFAULT NULL,
|
`webui_port` int DEFAULT NULL,
|
||||||
`webui_token` varchar(255) DEFAULT NULL,
|
`webui_token` varchar(255) DEFAULT NULL,
|
||||||
@ -58,15 +59,34 @@ CREATE TABLE IF NOT EXISTS `qqbot_napcat_container` (
|
|||||||
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
`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),
|
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_qqbot_napcat_container_name` (`name`),
|
UNIQUE KEY `uk_napcat_container_name` (`container_name`),
|
||||||
KEY `idx_qqbot_napcat_container_status` (`status`, `is_deleted`)
|
KEY `idx_napcat_container_status` (`status`, `is_deleted`),
|
||||||
|
KEY `idx_napcat_container_account` (`account_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_napcat` (
|
CREATE TABLE IF NOT EXISTS `napcat_device_identity` (
|
||||||
|
`id` bigint NOT NULL,
|
||||||
|
`account_id` bigint NOT NULL,
|
||||||
|
`container_id` bigint DEFAULT NULL,
|
||||||
|
`data_dir` varchar(512) NOT NULL,
|
||||||
|
`hostname` varchar(128) NOT NULL,
|
||||||
|
`machine_id_path` varchar(512) NOT NULL,
|
||||||
|
`mac_address` varchar(64) NOT NULL,
|
||||||
|
`verification_status` varchar(32) NOT NULL,
|
||||||
|
`last_login_evidence` json DEFAULT NULL,
|
||||||
|
`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_napcat_device_identity_account` (`account_id`),
|
||||||
|
KEY `idx_napcat_device_identity_container` (`container_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `napcat_account_binding` (
|
||||||
`id` bigint NOT NULL,
|
`id` bigint NOT NULL,
|
||||||
`account_id` bigint NOT NULL,
|
`account_id` bigint NOT NULL,
|
||||||
`container_id` bigint NOT NULL,
|
`container_id` bigint NOT NULL,
|
||||||
`bind_status` varchar(32) NOT NULL DEFAULT 'pending',
|
`device_identity_id` bigint DEFAULT NULL,
|
||||||
|
`status` varchar(32) NOT NULL DEFAULT 'pending',
|
||||||
`is_primary` tinyint(1) NOT NULL DEFAULT 1,
|
`is_primary` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
`last_login_at` datetime DEFAULT NULL,
|
`last_login_at` datetime DEFAULT NULL,
|
||||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||||
@ -74,8 +94,51 @@ CREATE TABLE IF NOT EXISTS `qqbot_account_napcat` (
|
|||||||
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
`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),
|
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_qqbot_account_napcat_account` (`account_id`, `is_deleted`),
|
UNIQUE KEY `uk_napcat_account_binding_account` (`account_id`),
|
||||||
KEY `idx_qqbot_account_napcat_container` (`container_id`, `is_deleted`)
|
KEY `idx_napcat_account_binding_container` (`container_id`, `is_deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `napcat_login_session` (
|
||||||
|
`id` bigint NOT NULL,
|
||||||
|
`account_id` bigint DEFAULT NULL,
|
||||||
|
`session_key` varchar(128) NOT NULL,
|
||||||
|
`login_stage` varchar(64) NOT NULL,
|
||||||
|
`status` varchar(32) NOT NULL,
|
||||||
|
`progress_message` varchar(255) NOT NULL,
|
||||||
|
`session_payload` json DEFAULT NULL,
|
||||||
|
`expires_at` datetime DEFAULT NULL,
|
||||||
|
`completed_at` datetime DEFAULT NULL,
|
||||||
|
`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_napcat_login_session_key` (`session_key`),
|
||||||
|
KEY `idx_napcat_login_session_account` (`account_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `napcat_login_challenge` (
|
||||||
|
`id` bigint NOT NULL,
|
||||||
|
`session_id` bigint NOT NULL,
|
||||||
|
`challenge_type` varchar(64) NOT NULL,
|
||||||
|
`status` varchar(32) NOT NULL,
|
||||||
|
`challenge_url` text,
|
||||||
|
`challenge_payload` json DEFAULT NULL,
|
||||||
|
`resolved_at` datetime DEFAULT NULL,
|
||||||
|
`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`),
|
||||||
|
KEY `idx_napcat_login_challenge_session` (`session_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `napcat_runtime_cleanup` (
|
||||||
|
`id` bigint NOT NULL,
|
||||||
|
`session_id` bigint NOT NULL,
|
||||||
|
`cleanup_type` varchar(64) NOT NULL,
|
||||||
|
`status` varchar(32) NOT NULL,
|
||||||
|
`error_message` text,
|
||||||
|
`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`),
|
||||||
|
KEY `idx_napcat_runtime_cleanup_session` (`session_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_config` (
|
CREATE TABLE IF NOT EXISTS `qqbot_config` (
|
||||||
@ -366,141 +429,6 @@ PREPARE qqbot_stmt FROM @qqbot_sql;
|
|||||||
EXECUTE qqbot_stmt;
|
EXECUTE qqbot_stmt;
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
DEALLOCATE PREPARE qqbot_stmt;
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `webui_port` int DEFAULT NULL',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'webui_port'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `webui_token` varchar(255) DEFAULT NULL',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'webui_token'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `image` varchar(255) NOT NULL DEFAULT ''''',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'image'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `data_dir` varchar(500) NOT NULL DEFAULT ''''',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'data_dir'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `reverse_ws_url` varchar(500) NOT NULL DEFAULT ''''',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'reverse_ws_url'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `status` varchar(32) NOT NULL DEFAULT ''creating''',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'status'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `last_started_at` datetime DEFAULT NULL',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'last_started_at'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `last_checked_at` datetime DEFAULT NULL',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'last_checked_at'
|
|
||||||
);
|
|
||||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
|
||||||
EXECUTE qqbot_stmt;
|
|
||||||
DEALLOCATE PREPARE qqbot_stmt;
|
|
||||||
|
|
||||||
SET @qqbot_sql = (
|
|
||||||
SELECT IF(
|
|
||||||
COUNT(*) = 0,
|
|
||||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `last_error` varchar(500) DEFAULT NULL',
|
|
||||||
'SELECT 1'
|
|
||||||
)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_napcat_container'
|
|
||||||
AND column_name = 'last_error'
|
|
||||||
);
|
|
||||||
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,
|
||||||
|
|||||||
@ -877,33 +877,8 @@ CREATE TABLE IF NOT EXISTS qqbot_plugin_runtime_event (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_container (
|
CREATE TABLE IF NOT EXISTS napcat_container (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
account_id BIGINT NOT NULL,
|
account_id BIGINT NULL,
|
||||||
container_name VARCHAR(128) NOT NULL,
|
container_name VARCHAR(120) NOT NULL,
|
||||||
image_name VARCHAR(255) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_napcat_container_name (container_name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_account_napcat (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
account_id BIGINT NOT NULL,
|
|
||||||
container_id BIGINT NOT NULL,
|
|
||||||
bind_status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
|
||||||
is_primary TINYINT NOT NULL DEFAULT 1,
|
|
||||||
last_login_at DATETIME NULL,
|
|
||||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_qqbot_account_napcat_account (account_id, is_deleted),
|
|
||||||
KEY idx_qqbot_account_napcat_container (container_id, is_deleted)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_napcat_container (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
name VARCHAR(120) NOT NULL,
|
|
||||||
base_url VARCHAR(255) NOT NULL,
|
base_url VARCHAR(255) NOT NULL,
|
||||||
webui_port INT NULL,
|
webui_port INT NULL,
|
||||||
webui_token VARCHAR(255) NULL,
|
webui_token VARCHAR(255) NULL,
|
||||||
@ -918,8 +893,9 @@ CREATE TABLE IF NOT EXISTS qqbot_napcat_container (
|
|||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
is_deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_qqbot_napcat_container_name (name),
|
UNIQUE KEY uk_napcat_container_name (container_name),
|
||||||
KEY idx_qqbot_napcat_container_status (status, is_deleted)
|
KEY idx_napcat_container_status (status, is_deleted),
|
||||||
|
KEY idx_napcat_container_account (account_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_device_identity (
|
CREATE TABLE IF NOT EXISTS napcat_device_identity (
|
||||||
@ -942,11 +918,16 @@ CREATE TABLE IF NOT EXISTS napcat_account_binding (
|
|||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
account_id BIGINT NOT NULL,
|
account_id BIGINT NOT NULL,
|
||||||
container_id BIGINT NOT NULL,
|
container_id BIGINT NOT NULL,
|
||||||
device_identity_id BIGINT NOT NULL,
|
device_identity_id BIGINT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||||
|
is_primary TINYINT NOT NULL DEFAULT 1,
|
||||||
|
last_login_at DATETIME NULL,
|
||||||
|
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
is_deleted TINYINT NOT NULL DEFAULT 0,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_napcat_account_binding_account (account_id)
|
UNIQUE KEY uk_napcat_account_binding_account (account_id),
|
||||||
|
KEY idx_napcat_account_binding_container (container_id, is_deleted)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_login_session (
|
CREATE TABLE IF NOT EXISTS napcat_login_session (
|
||||||
|
|||||||
@ -117,45 +117,36 @@ INSERT INTO admin_menu (
|
|||||||
1,
|
1,
|
||||||
110
|
110
|
||||||
),
|
),
|
||||||
(
|
(2041700000000100401, 2041700000000100400, 'QqBotDashboard', '/qqbot/dashboard', '/qqbot/dashboard/list', NULL, 'QqBot:Dashboard:List', 'menu', '{"icon":"lucide:gauge","title":"工作台"}', 1, 0),
|
||||||
2041700000000100402,
|
(2041700000000100402, 2041700000000100400, 'QqBotAccount', '/qqbot/account', '/qqbot/account/list', NULL, 'QqBot:Account:List', 'menu', '{"icon":"lucide:radio-receiver","title":"账号连接"}', 1, 1),
|
||||||
2041700000000100400,
|
(2041700000000100410, 2041700000000100400, 'QqBotAccountConfig', '/qqbot/account/config', '/qqbot/account/config', NULL, 'QqBot:Account:Config', 'menu', '{"activePath":"/qqbot/account","hideInMenu":true,"title":"账号功能配置"}', 1, 0),
|
||||||
'QqBotAccount',
|
(2041700000000120401, 2041700000000100402, 'QqBotAccountCreate', NULL, NULL, NULL, 'QqBot:Account:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
'/qqbot/account',
|
(2041700000000120402, 2041700000000100402, 'QqBotAccountEdit', NULL, NULL, NULL, 'QqBot:Account:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
'/qqbot/account/list',
|
(2041700000000120403, 2041700000000100402, 'QqBotAccountDelete', NULL, NULL, NULL, 'QqBot:Account:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
NULL,
|
(2041700000000120404, 2041700000000100402, 'QqBotAccountKick', NULL, NULL, NULL, 'QqBot:Account:Kick', 'button', '{"title":"断开连接"}', 1, 0),
|
||||||
'QqBot:Account:List',
|
(2041700000000120405, 2041700000000100402, 'QqBotAccountRefreshLogin', NULL, NULL, NULL, 'QqBot:Account:RefreshLogin', 'button', '{"title":"更新登录"}', 1, 0),
|
||||||
'menu',
|
(2041700000000120406, 2041700000000100402, 'QqBotAccountConfigButton', NULL, NULL, NULL, 'QqBot:Account:Config', 'button', '{"title":"配置"}', 1, 0),
|
||||||
'{"icon":"lucide:radio-receiver","title":"账号连接"}',
|
(2041700000000100403, 2041700000000100400, 'QqBotRule', '/qqbot/rule', '/qqbot/rule/list', NULL, 'QqBot:Rule:List', 'menu', '{"icon":"lucide:workflow","title":"自动回复规则"}', 1, 2),
|
||||||
1,
|
(2041700000000120411, 2041700000000100403, 'QqBotRuleCreate', NULL, NULL, NULL, 'QqBot:Rule:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
1
|
(2041700000000120412, 2041700000000100403, 'QqBotRuleEdit', NULL, NULL, NULL, 'QqBot:Rule:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
),
|
(2041700000000120413, 2041700000000100403, 'QqBotRuleDelete', NULL, NULL, NULL, 'QqBot:Rule:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
(
|
(2041700000000120414, 2041700000000100403, 'QqBotRuleToggle', NULL, NULL, NULL, 'QqBot:Rule:Toggle', 'button', '{"title":"启停"}', 1, 0),
|
||||||
2041700000000100408,
|
(2041700000000100408, 2041700000000100400, 'QqBotCommand', '/qqbot/command', '/qqbot/command/list', NULL, 'QqBot:Command:List', 'menu', '{"icon":"lucide:square-terminal","title":"在线命令"}', 1, 3),
|
||||||
2041700000000100400,
|
(2041700000000120441, 2041700000000100408, 'QqBotCommandCreate', NULL, NULL, NULL, 'QqBot:Command:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
'QqBotCommand',
|
(2041700000000120442, 2041700000000100408, 'QqBotCommandEdit', NULL, NULL, NULL, 'QqBot:Command:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
'/qqbot/command',
|
(2041700000000120443, 2041700000000100408, 'QqBotCommandDelete', NULL, NULL, NULL, 'QqBot:Command:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
'/qqbot/command/list',
|
(2041700000000120444, 2041700000000100408, 'QqBotCommandToggle', NULL, NULL, NULL, 'QqBot:Command:Toggle', 'button', '{"title":"启停"}', 1, 0),
|
||||||
NULL,
|
(2041700000000120445, 2041700000000100408, 'QqBotCommandTest', NULL, NULL, NULL, 'QqBot:Command:Test', 'button', '{"title":"测试命令"}', 1, 0),
|
||||||
'QqBot:Command:List',
|
(2041700000000100409, 2041700000000100400, 'QqBotPlugin', '/qqbot/plugin', '/qqbot/plugin/list', NULL, 'QqBot:Plugin:List', 'menu', '{"icon":"lucide:plug","title":"插件能力"}', 1, 4),
|
||||||
'menu',
|
(2041700000000100404, 2041700000000100400, 'QqBotConversation', '/qqbot/conversation', '/qqbot/conversation/list', NULL, 'QqBot:Conversation:List', 'menu', '{"icon":"lucide:messages-square","title":"会话管理"}', 1, 5),
|
||||||
'{"icon":"lucide:square-terminal","title":"在线命令"}',
|
(2041700000000100405, 2041700000000100400, 'QqBotMessage', '/qqbot/message', '/qqbot/message/list', NULL, 'QqBot:Message:List', 'menu', '{"icon":"lucide:message-square-text","title":"消息日志"}', 1, 6),
|
||||||
1,
|
(2041700000000100406, 2041700000000100400, 'QqBotSendLog', '/qqbot/sendLog', '/qqbot/sendLog/list', NULL, 'QqBot:SendLog:List', 'menu', '{"icon":"lucide:send","title":"发送日志"}', 1, 7),
|
||||||
3
|
(2041700000000120421, 2041700000000100406, 'QqBotSendPrivate', NULL, NULL, NULL, 'QqBot:Send:Private', 'button', '{"title":"发送私聊"}', 1, 0),
|
||||||
),
|
(2041700000000120422, 2041700000000100406, 'QqBotSendGroup', NULL, NULL, NULL, 'QqBot:Send:Group', 'button', '{"title":"发送群聊"}', 1, 0),
|
||||||
(
|
(2041700000000100407, 2041700000000100400, 'QqBotPermission', '/qqbot/permission', '/qqbot/permission/list', NULL, 'QqBot:Permission:List', 'menu', '{"icon":"lucide:shield-check","title":"权限名单"}', 1, 8),
|
||||||
2041700000000100409,
|
(2041700000000120431, 2041700000000100407, 'QqBotPermissionCreate', NULL, NULL, NULL, 'QqBot:Permission:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
2041700000000100400,
|
(2041700000000120432, 2041700000000100407, 'QqBotPermissionEdit', NULL, NULL, NULL, 'QqBot:Permission:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
'QqBotPlugin',
|
(2041700000000120433, 2041700000000100407, 'QqBotPermissionDelete', NULL, NULL, NULL, 'QqBot:Permission:Delete', 'button', '{"title":"common.delete"}', 1, 0)
|
||||||
'/qqbot/plugin',
|
|
||||||
'/qqbot/plugin/list',
|
|
||||||
NULL,
|
|
||||||
'QqBot:Plugin:List',
|
|
||||||
'menu',
|
|
||||||
'{"icon":"lucide:plug","title":"插件能力"}',
|
|
||||||
1,
|
|
||||||
4
|
|
||||||
)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
pid = VALUES(pid),
|
pid = VALUES(pid),
|
||||||
path = VALUES(path),
|
path = VALUES(path),
|
||||||
@ -280,8 +271,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.song.search',
|
'bangdream.song.search',
|
||||||
'bangdream_song',
|
'bangdream_song',
|
||||||
'bd',
|
'bd',
|
||||||
'BangDream 查歌',
|
'',
|
||||||
'["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -291,8 +282,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.song.chart',
|
'bangdream.song.chart',
|
||||||
'bangdream_song_chart',
|
'bangdream_song_chart',
|
||||||
'bangdream_song_chart',
|
'bangdream_song_chart',
|
||||||
'BangDream 查谱面',
|
'',
|
||||||
'["查谱面","谱面","bd谱面"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -302,8 +293,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.song.random',
|
'bangdream.song.random',
|
||||||
'bangdream_song_random',
|
'bangdream_song_random',
|
||||||
'bangdream_song_random',
|
'bangdream_song_random',
|
||||||
'BangDream 随机曲',
|
'',
|
||||||
'["随机曲","随机","bd随机"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -313,8 +304,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.song.meta',
|
'bangdream.song.meta',
|
||||||
'bangdream_song_meta',
|
'bangdream_song_meta',
|
||||||
'bangdream_song_meta',
|
'bangdream_song_meta',
|
||||||
'BangDream 分数表',
|
'',
|
||||||
'["查询分数表","查分数表","查询分数榜","查分数榜","bd分数表"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -324,8 +315,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.card.search',
|
'bangdream.card.search',
|
||||||
'bangdream_card',
|
'bangdream_card',
|
||||||
'bangdream_card',
|
'bangdream_card',
|
||||||
'BangDream 查卡',
|
'',
|
||||||
'["查卡","查卡牌","bd查卡"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -335,8 +326,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.card.illustration',
|
'bangdream.card.illustration',
|
||||||
'bangdream_card_illustration',
|
'bangdream_card_illustration',
|
||||||
'bangdream_card_illustration',
|
'bangdream_card_illustration',
|
||||||
'BangDream 查卡面',
|
'',
|
||||||
'["查卡面","查卡插画","查插画","bd卡面"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -346,8 +337,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.character.search',
|
'bangdream.character.search',
|
||||||
'bangdream_character',
|
'bangdream_character',
|
||||||
'bangdream_character',
|
'bangdream_character',
|
||||||
'BangDream 查角色',
|
'',
|
||||||
'["查角色","bd角色"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -357,8 +348,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.event.search',
|
'bangdream.event.search',
|
||||||
'bangdream_event',
|
'bangdream_event',
|
||||||
'bangdream_event',
|
'bangdream_event',
|
||||||
'BangDream 查活动',
|
'',
|
||||||
'["查活动","bd活动"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -368,8 +359,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.event.stage',
|
'bangdream.event.stage',
|
||||||
'bangdream_event_stage',
|
'bangdream_event_stage',
|
||||||
'bangdream_event_stage',
|
'bangdream_event_stage',
|
||||||
'BangDream 查试炼',
|
'',
|
||||||
'["查试炼","查stage","查舞台","查festival","查5v5"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -379,8 +370,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.player.search',
|
'bangdream.player.search',
|
||||||
'bangdream_player',
|
'bangdream_player',
|
||||||
'bangdream_player',
|
'bangdream_player',
|
||||||
'BangDream 查玩家',
|
'',
|
||||||
'["查玩家","查询玩家","bd玩家"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -390,8 +381,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.gacha.search',
|
'bangdream.gacha.search',
|
||||||
'bangdream_gacha',
|
'bangdream_gacha',
|
||||||
'bangdream_gacha',
|
'bangdream_gacha',
|
||||||
'BangDream 查卡池',
|
'',
|
||||||
'["查卡池","bd卡池"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
@ -401,8 +392,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.gacha.simulate',
|
'bangdream.gacha.simulate',
|
||||||
'bangdream_gacha_simulate',
|
'bangdream_gacha_simulate',
|
||||||
'bangdream_gacha_simulate',
|
'bangdream_gacha_simulate',
|
||||||
'BangDream 抽卡模拟',
|
'',
|
||||||
'["抽卡模拟","bd抽卡"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
@ -412,8 +403,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.cutoff.detail',
|
'bangdream.cutoff.detail',
|
||||||
'bangdream_cutoff_detail',
|
'bangdream_cutoff_detail',
|
||||||
'bangdream_cutoff_detail',
|
'bangdream_cutoff_detail',
|
||||||
'BangDream ycx',
|
'',
|
||||||
'["ycx","预测线","查档线","bd档线"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
@ -423,8 +414,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.cutoff.all',
|
'bangdream.cutoff.all',
|
||||||
'bangdream_cutoff_all',
|
'bangdream_cutoff_all',
|
||||||
'bangdream_cutoff_all',
|
'bangdream_cutoff_all',
|
||||||
'BangDream ycxall',
|
'',
|
||||||
'["ycxall","myycx","全部档线"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
@ -434,8 +425,8 @@ INSERT INTO qqbot_command (
|
|||||||
'bangdream.cutoff.recent',
|
'bangdream.cutoff.recent',
|
||||||
'bangdream_cutoff_recent',
|
'bangdream_cutoff_recent',
|
||||||
'bangdream_cutoff_recent',
|
'bangdream_cutoff_recent',
|
||||||
'BangDream lsycx',
|
'',
|
||||||
'["lsycx","历史档线","近期档线"]',
|
'[]',
|
||||||
'bangdream',
|
'bangdream',
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
|
|||||||
@ -79,6 +79,18 @@ WHERE table_schema = DATABASE()
|
|||||||
AND table_name = 'napcat_account_binding'
|
AND table_name = 'napcat_account_binding'
|
||||||
AND index_name = 'uk_napcat_account_binding_account';
|
AND index_name = 'uk_napcat_account_binding_account';
|
||||||
|
|
||||||
|
SELECT 'index_napcat_account_binding_container' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'napcat_account_binding'
|
||||||
|
AND index_name = 'idx_napcat_account_binding_container';
|
||||||
|
|
||||||
|
SELECT 'index_napcat_container_name' AS check_name, COUNT(*) AS matched_rows
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'napcat_container'
|
||||||
|
AND index_name = 'uk_napcat_container_name';
|
||||||
|
|
||||||
SELECT 'index_napcat_login_session_key' AS check_name, COUNT(*) AS matched_rows
|
SELECT 'index_napcat_login_session_key' AS check_name, COUNT(*) AS matched_rows
|
||||||
FROM information_schema.statistics
|
FROM information_schema.statistics
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { AdminModule } from './modules/admin/admin.module';
|
|||||||
import { AssetModule } from './modules/asset/asset.module';
|
import { AssetModule } from './modules/asset/asset.module';
|
||||||
import { BlogContentModule } from './modules/blog/blog-content.module';
|
import { BlogContentModule } from './modules/blog/blog-content.module';
|
||||||
import { QqbotCoreModule } from './modules/qqbot/core/qqbot-core.module';
|
import { QqbotCoreModule } from './modules/qqbot/core/qqbot-core.module';
|
||||||
|
import { QqbotNapcatModule } from './modules/qqbot/napcat/qqbot-napcat.module';
|
||||||
import { QqbotPluginPlatformModule } from './modules/qqbot/plugin-platform/plugin-platform.module';
|
import { QqbotPluginPlatformModule } from './modules/qqbot/plugin-platform/plugin-platform.module';
|
||||||
import { WordpressMirrorModule } from './modules/wordpress/wordpress-mirror.module';
|
import { WordpressMirrorModule } from './modules/wordpress/wordpress-mirror.module';
|
||||||
import { RuntimeModule } from './runtime';
|
import { RuntimeModule } from './runtime';
|
||||||
@ -74,6 +75,7 @@ import { RuntimeModule } from './runtime';
|
|||||||
WordpressMirrorModule,
|
WordpressMirrorModule,
|
||||||
AssetModule,
|
AssetModule,
|
||||||
QqbotCoreModule,
|
QqbotCoreModule,
|
||||||
|
QqbotNapcatModule,
|
||||||
QqbotPluginPlatformModule,
|
QqbotPluginPlatformModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import type {
|
|||||||
QqbotAccountQueryDto,
|
QqbotAccountQueryDto,
|
||||||
QqbotAccountUpdateDto,
|
QqbotAccountUpdateDto,
|
||||||
} from '../../contract/account/qqbot-account.dto';
|
} from '../../contract/account/qqbot-account.dto';
|
||||||
import { QqbotAccountNapcat } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-account-napcat.entity';
|
import { NapcatAccountBinding } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity';
|
||||||
import { QqbotNapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-napcat-container.entity';
|
import { NapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity';
|
||||||
import { QqbotNapcatContainerService } from '@/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
|
import { QqbotNapcatContainerService } from '@/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
|
||||||
import {
|
import {
|
||||||
QQBOT_DEFAULT_PAGE_NO,
|
QQBOT_DEFAULT_PAGE_NO,
|
||||||
@ -45,10 +45,10 @@ export class QqbotAccountService {
|
|||||||
private readonly accountRepository: Repository<QqbotAccount>,
|
private readonly accountRepository: Repository<QqbotAccount>,
|
||||||
@InjectRepository(QqbotAccountAbility)
|
@InjectRepository(QqbotAccountAbility)
|
||||||
private readonly accountAbilityRepository: Repository<QqbotAccountAbility>,
|
private readonly accountAbilityRepository: Repository<QqbotAccountAbility>,
|
||||||
@InjectRepository(QqbotAccountNapcat)
|
@InjectRepository(NapcatAccountBinding)
|
||||||
private readonly accountNapcatRepository: Repository<QqbotAccountNapcat>,
|
private readonly accountNapcatRepository: Repository<NapcatAccountBinding>,
|
||||||
@InjectRepository(QqbotNapcatContainer)
|
@InjectRepository(NapcatContainer)
|
||||||
private readonly napcatContainerRepository: Repository<QqbotNapcatContainer>,
|
private readonly napcatContainerRepository: Repository<NapcatContainer>,
|
||||||
private readonly napcatContainerService: QqbotNapcatContainerService,
|
private readonly napcatContainerService: QqbotNapcatContainerService,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
@Optional()
|
@Optional()
|
||||||
@ -437,7 +437,7 @@ export class QqbotAccountService {
|
|||||||
.orderBy('binding.isPrimary', 'DESC')
|
.orderBy('binding.isPrimary', 'DESC')
|
||||||
.addOrderBy('binding.updateTime', 'DESC')
|
.addOrderBy('binding.updateTime', 'DESC')
|
||||||
.getMany();
|
.getMany();
|
||||||
const bindingMap = new Map<string, QqbotAccountNapcat>();
|
const bindingMap = new Map<string, NapcatAccountBinding>();
|
||||||
for (const binding of bindings) {
|
for (const binding of bindings) {
|
||||||
if (!bindingMap.has(binding.accountId)) {
|
if (!bindingMap.has(binding.accountId)) {
|
||||||
bindingMap.set(binding.accountId, binding);
|
bindingMap.set(binding.accountId, binding);
|
||||||
@ -447,7 +447,7 @@ export class QqbotAccountService {
|
|||||||
const containerIds = Array.from(
|
const containerIds = Array.from(
|
||||||
new Set(bindings.map((binding) => binding.containerId).filter(Boolean)),
|
new Set(bindings.map((binding) => binding.containerId).filter(Boolean)),
|
||||||
);
|
);
|
||||||
const containerMap = new Map<string, QqbotNapcatContainer>();
|
const containerMap = new Map<string, NapcatContainer>();
|
||||||
if (containerIds.length > 0) {
|
if (containerIds.length > 0) {
|
||||||
const containerBuilder = this.napcatContainerRepository
|
const containerBuilder = this.napcatContainerRepository
|
||||||
.createQueryBuilder('container');
|
.createQueryBuilder('container');
|
||||||
@ -500,7 +500,7 @@ export class QqbotAccountService {
|
|||||||
|
|
||||||
private async syncNapcatRuntimeState(
|
private async syncNapcatRuntimeState(
|
||||||
account: QqbotAccount,
|
account: QqbotAccount,
|
||||||
container?: QqbotNapcatContainer,
|
container?: NapcatContainer,
|
||||||
options: { autoLogin?: boolean } = {},
|
options: { autoLogin?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const runtimeStatus = await this.getNapcatRuntimeStatus(account, container);
|
const runtimeStatus = await this.getNapcatRuntimeStatus(account, container);
|
||||||
@ -561,7 +561,7 @@ export class QqbotAccountService {
|
|||||||
|
|
||||||
private async getNapcatRuntimeStatus(
|
private async getNapcatRuntimeStatus(
|
||||||
account: QqbotAccount,
|
account: QqbotAccount,
|
||||||
container?: QqbotNapcatContainer,
|
container?: NapcatContainer,
|
||||||
): Promise<QqbotNapcatRuntimeStatusSnapshot | undefined> {
|
): Promise<QqbotNapcatRuntimeStatusSnapshot | undefined> {
|
||||||
if (!container) return undefined;
|
if (!container) return undefined;
|
||||||
const cached = this.toCachedNapcatRuntimeStatus(container);
|
const cached = this.toCachedNapcatRuntimeStatus(container);
|
||||||
@ -600,7 +600,7 @@ export class QqbotAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toCachedNapcatRuntimeStatus(
|
private toCachedNapcatRuntimeStatus(
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
): QqbotNapcatRuntimeStatusSnapshot {
|
): QqbotNapcatRuntimeStatusSnapshot {
|
||||||
const containerOnline = container.status === 'running';
|
const containerOnline = container.status === 'running';
|
||||||
const lastError = this.toolsService.toTrimmedString(container.lastError);
|
const lastError = this.toolsService.toTrimmedString(container.lastError);
|
||||||
@ -663,7 +663,7 @@ export class QqbotAccountService {
|
|||||||
|
|
||||||
private async tryAutoLogin(
|
private async tryAutoLogin(
|
||||||
account: QqbotAccount,
|
account: QqbotAccount,
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await this.napcatContainerService.tryAutoLogin(container, {
|
const result = await this.napcatContainerService.tryAutoLogin(container, {
|
||||||
@ -693,7 +693,7 @@ export class QqbotAccountService {
|
|||||||
|
|
||||||
private async applyNapcatOfflineState(
|
private async applyNapcatOfflineState(
|
||||||
account: QqbotAccount,
|
account: QqbotAccount,
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
offlineReason: string,
|
offlineReason: string,
|
||||||
) {
|
) {
|
||||||
await this.markQqLoginOffline(account.selfId, offlineReason);
|
await this.markQqLoginOffline(account.selfId, offlineReason);
|
||||||
@ -704,7 +704,7 @@ export class QqbotAccountService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFreshCachedOfflineReason(container: QqbotNapcatContainer) {
|
private getFreshCachedOfflineReason(container: NapcatContainer) {
|
||||||
if (!this.isFreshRuntimeCheck(container.lastCheckedAt)) return null;
|
if (!this.isFreshRuntimeCheck(container.lastCheckedAt)) return null;
|
||||||
const reason = this.toolsService.toTrimmedString(container.lastError);
|
const reason = this.toolsService.toTrimmedString(container.lastError);
|
||||||
return this.toolsService.isNapcatOfflineLoginMessage(reason)
|
return this.toolsService.isNapcatOfflineLoginMessage(reason)
|
||||||
@ -714,7 +714,7 @@ export class QqbotAccountService {
|
|||||||
|
|
||||||
private isRecentConnectNewerThanRuntimeCheck(
|
private isRecentConnectNewerThanRuntimeCheck(
|
||||||
account: QqbotAccount,
|
account: QqbotAccount,
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
) {
|
) {
|
||||||
const checkedAt = this.toTime(container.lastCheckedAt);
|
const checkedAt = this.toTime(container.lastCheckedAt);
|
||||||
if (!checkedAt) return false;
|
if (!checkedAt) return false;
|
||||||
|
|||||||
@ -1,15 +1,25 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||||
import type { QqbotCommandMatchResult } from '../../contract/qqbot.types';
|
import type { QqbotCommandMatchResult } from '../../contract/qqbot.types';
|
||||||
import type { QqbotNormalizedMessage } from '../../contract/qqbot.types';
|
import type { QqbotNormalizedMessage } from '../../contract/qqbot.types';
|
||||||
|
import {
|
||||||
|
QQBOT_PLUGIN_EXECUTION_PORT,
|
||||||
|
type QqbotPluginExecutionPort,
|
||||||
|
} from '../../domain/plugin-execution.port';
|
||||||
import type { QqbotCommand } from '../../infrastructure/persistence/command/qqbot-command.entity';
|
import type { QqbotCommand } from '../../infrastructure/persistence/command/qqbot-command.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotCommandParserService {
|
export class QqbotCommandParserService {
|
||||||
|
constructor(
|
||||||
|
@Optional()
|
||||||
|
@Inject(QQBOT_PLUGIN_EXECUTION_PORT)
|
||||||
|
private readonly pluginExecution?: QqbotPluginExecutionPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
async match(command: QqbotCommand, message: QqbotNormalizedMessage) {
|
async match(command: QqbotCommand, message: QqbotNormalizedMessage) {
|
||||||
const source = `${message.messageText || ''}`.trim();
|
const source = `${message.messageText || ''}`.trim();
|
||||||
if (!source) return null;
|
if (!source) return null;
|
||||||
|
|
||||||
const aliases = this.getAliases(command);
|
const aliases = await this.getAliases(command);
|
||||||
const prefixes = this.getPrefixes(command);
|
const prefixes = this.getPrefixes(command);
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
for (const prefix of prefixes) {
|
for (const prefix of prefixes) {
|
||||||
@ -27,8 +37,11 @@ export class QqbotCommandParserService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAliases(command: QqbotCommand) {
|
async getAliases(command: QqbotCommand) {
|
||||||
return this.normalizeList(command.aliases, [command.code, command.name]);
|
return this.mergeLists(
|
||||||
|
await this.getManifestAliases(command),
|
||||||
|
this.normalizeList(command.aliases, [command.code, command.name]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrefixes(command: QqbotCommand) {
|
getPrefixes(command: QqbotCommand) {
|
||||||
@ -63,6 +76,29 @@ export class QqbotCommandParserService {
|
|||||||
return [...new Set(list)];
|
return [...new Set(list)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getManifestAliases(command: QqbotCommand) {
|
||||||
|
if (!this.pluginExecution) return [];
|
||||||
|
try {
|
||||||
|
const operation = await this.pluginExecution.getOperationByCommand({
|
||||||
|
operationKey: command.operationKey,
|
||||||
|
pluginKey: command.pluginKey,
|
||||||
|
});
|
||||||
|
return this.normalizeArray(operation?.aliases || []);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeLists(...sources: string[][]) {
|
||||||
|
return [
|
||||||
|
...new Set(sources.flat().map((item) => item.trim()).filter(Boolean)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeArray(value: unknown[]) {
|
||||||
|
return value.map((item) => `${item || ''}`).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
private tryParseJsonArray(value: string) {
|
private tryParseJsonArray(value: string) {
|
||||||
if (!value.startsWith('[')) return [];
|
if (!value.startsWith('[')) return [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Sse,
|
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
@ -15,12 +14,9 @@ import { vbenSuccess } from '@/common';
|
|||||||
import {
|
import {
|
||||||
QqbotAccountBodyDto,
|
QqbotAccountBodyDto,
|
||||||
QqbotAccountQueryDto,
|
QqbotAccountQueryDto,
|
||||||
QqbotAccountScanCaptchaDto,
|
|
||||||
QqbotAccountScanStatusDto,
|
|
||||||
QqbotAccountUpdateDto,
|
QqbotAccountUpdateDto,
|
||||||
} from './qqbot-account.dto';
|
} from './qqbot-account.dto';
|
||||||
import { QqbotAccountService } from '../../application/account/qqbot-account.service';
|
import { QqbotAccountService } from '../../application/account/qqbot-account.service';
|
||||||
import { QqbotNapcatLoginService } from '@/modules/qqbot/napcat/application/login/qqbot-napcat-login.service';
|
|
||||||
import { QqbotReverseWsService } from '../../infrastructure/integration/connection/qqbot-reverse-ws.service';
|
import { QqbotReverseWsService } from '../../infrastructure/integration/connection/qqbot-reverse-ws.service';
|
||||||
|
|
||||||
@ApiTags('QQBot - 账号连接')
|
@ApiTags('QQBot - 账号连接')
|
||||||
@ -29,7 +25,6 @@ import { QqbotReverseWsService } from '../../infrastructure/integration/connecti
|
|||||||
export class QqbotAccountController {
|
export class QqbotAccountController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly accountService: QqbotAccountService,
|
private readonly accountService: QqbotAccountService,
|
||||||
private readonly napcatLoginService: QqbotNapcatLoginService,
|
|
||||||
private readonly reverseWsService: QqbotReverseWsService,
|
private readonly reverseWsService: QqbotReverseWsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -59,58 +54,6 @@ export class QqbotAccountController {
|
|||||||
return vbenSuccess(await this.accountService.update(body));
|
return vbenSuccess(await this.accountService.update(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('scan/create')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '扫码新增 QQBot 账号' })
|
|
||||||
async scanCreate() {
|
|
||||||
return vbenSuccess(await this.napcatLoginService.startCreate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('scan/refresh')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '扫码刷新 QQBot 账号登录态' })
|
|
||||||
@ApiQuery({ name: 'id', type: String })
|
|
||||||
async scanRefresh(@Query('id') id: string) {
|
|
||||||
return vbenSuccess(await this.napcatLoginService.startRefresh(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('scan/status')
|
|
||||||
@ApiOperation({ summary: '查询 QQBot 扫码登录状态' })
|
|
||||||
async scanStatus(@Query() query: QqbotAccountScanStatusDto) {
|
|
||||||
return vbenSuccess(await this.napcatLoginService.status(query.sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Sse('scan/events')
|
|
||||||
@ApiOperation({ summary: '订阅 QQBot 扫码登录进度' })
|
|
||||||
scanEvents(@Query() query: QqbotAccountScanStatusDto) {
|
|
||||||
return this.napcatLoginService.events(query.sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('scan/qrcode/refresh')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '刷新 QQBot 扫码二维码' })
|
|
||||||
async refreshScanQrcode(@Query() query: QqbotAccountScanStatusDto) {
|
|
||||||
return vbenSuccess(
|
|
||||||
await this.napcatLoginService.refreshQrcode(query.sessionId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('scan/captcha/submit')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '提交 QQBot 登录安全验证码' })
|
|
||||||
async submitScanCaptcha(@Body() body: QqbotAccountScanCaptchaDto) {
|
|
||||||
return vbenSuccess(
|
|
||||||
await this.napcatLoginService.submitCaptcha(body.sessionId, body),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('scan/cancel')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '取消 QQBot 扫码登录会话' })
|
|
||||||
async cancelScan(@Query() query: QqbotAccountScanStatusDto) {
|
|
||||||
return vbenSuccess(await this.napcatLoginService.cancel(query.sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: '删除 QQBot 账号' })
|
@ApiOperation({ summary: '删除 QQBot 账号' })
|
||||||
|
|||||||
@ -45,19 +45,3 @@ export class QqbotAccountQueryDto {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
connectStatus?: string;
|
connectStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QqbotAccountScanStatusDto {
|
|
||||||
@ApiProperty()
|
|
||||||
sessionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QqbotAccountScanCaptchaDto extends QqbotAccountScanStatusDto {
|
|
||||||
@ApiProperty()
|
|
||||||
randstr: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
|
||||||
sid?: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
ticket: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -152,12 +152,14 @@ export type QqbotPluginOperationContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotPluginOperation<Input = any, Output = any> = {
|
export type QqbotPluginOperation<Input = any, Output = any> = {
|
||||||
|
aliases?: string[];
|
||||||
cacheTtlMs?: number;
|
cacheTtlMs?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
inputSchema?: Record<string, any>;
|
inputSchema?: Record<string, any>;
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
outputSchema?: Record<string, any>;
|
outputSchema?: Record<string, any>;
|
||||||
|
timeoutMs?: number;
|
||||||
execute: (
|
execute: (
|
||||||
input: Input,
|
input: Input,
|
||||||
context: QqbotPluginOperationContext,
|
context: QqbotPluginOperationContext,
|
||||||
@ -206,6 +208,7 @@ export type QqbotPluginSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotPluginOperationSummary = {
|
export type QqbotPluginOperationSummary = {
|
||||||
|
aliases?: string[];
|
||||||
cacheTtlMs?: number;
|
cacheTtlMs?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
inputSchema?: Record<string, any>;
|
inputSchema?: Record<string, any>;
|
||||||
@ -213,6 +216,7 @@ export type QqbotPluginOperationSummary = {
|
|||||||
name: string;
|
name: string;
|
||||||
outputSchema?: Record<string, any>;
|
outputSchema?: Record<string, any>;
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
|
timeoutMs?: number;
|
||||||
triggerMode: QqbotPluginTriggerMode;
|
triggerMode: QqbotPluginTriggerMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,12 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
||||||
import { DictModule } from '@/modules/admin/platform-config/dict/dict.module';
|
import { DictModule } from '@/modules/admin/platform-config/dict/dict.module';
|
||||||
|
import { QqbotNapcatModule } from '@/modules/qqbot/napcat/qqbot-napcat.module';
|
||||||
import { QqbotPluginPlatformModule } from '@/modules/qqbot/plugin-platform/plugin-platform.module';
|
import { QqbotPluginPlatformModule } from '@/modules/qqbot/plugin-platform/plugin-platform.module';
|
||||||
import { QqbotAccountAbility } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity';
|
import { QqbotAccountAbility } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity';
|
||||||
import { QqbotAccountController } from '@/modules/qqbot/core/contract/account/qqbot-account.controller';
|
import { QqbotAccountController } from '@/modules/qqbot/core/contract/account/qqbot-account.controller';
|
||||||
import { QqbotAccount } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity';
|
import { QqbotAccount } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity';
|
||||||
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
||||||
import { QqbotNapcatLoginService } from '@/modules/qqbot/napcat/application/login/qqbot-napcat-login.service';
|
|
||||||
import { QqbotNapcatWatchdogService } from '@/modules/qqbot/napcat/application/login/qqbot-napcat-watchdog.service';
|
|
||||||
import { QqbotCommandController } from '@/modules/qqbot/core/contract/command/qqbot-command.controller';
|
import { QqbotCommandController } from '@/modules/qqbot/core/contract/command/qqbot-command.controller';
|
||||||
import { QqbotCommand } from '@/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity';
|
import { QqbotCommand } from '@/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity';
|
||||||
import { QqbotCommandEngineService } from '@/modules/qqbot/core/application/command/qqbot-command-engine.service';
|
import { QqbotCommandEngineService } from '@/modules/qqbot/core/application/command/qqbot-command-engine.service';
|
||||||
@ -25,22 +24,11 @@ import { QqbotDashboardService } from '@/modules/qqbot/core/application/dashboar
|
|||||||
import { QqbotDedupe } from '@/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity';
|
import { QqbotDedupe } from '@/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity';
|
||||||
import { QqbotDedupeService } from '@/modules/qqbot/core/application/dedupe/qqbot-dedupe.service';
|
import { QqbotDedupeService } from '@/modules/qqbot/core/application/dedupe/qqbot-dedupe.service';
|
||||||
import { QqbotEventService } from '@/modules/qqbot/core/application/event/qqbot-event.service';
|
import { QqbotEventService } from '@/modules/qqbot/core/application/event/qqbot-event.service';
|
||||||
import { NapcatAccountBinding } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity';
|
|
||||||
import { NapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity';
|
|
||||||
import { NapcatDeviceIdentity } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-device-identity.entity';
|
|
||||||
import { NapcatDeviceIdentityService } from '@/modules/qqbot/napcat/infrastructure/integration/device/napcat-device-identity.service';
|
|
||||||
import { NapcatLoginChallengeEntity } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-login-challenge.entity';
|
|
||||||
import { NapcatLoginSession } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-login-session.entity';
|
|
||||||
import { NapcatLoginStateStoreService } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-login-state-store.service';
|
|
||||||
import { NapcatRuntimeCleanup } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-runtime-cleanup.entity';
|
|
||||||
import { QqbotConversation } from '@/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity';
|
import { QqbotConversation } from '@/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity';
|
||||||
import { QqbotMessageController } from '@/modules/qqbot/core/contract/message/qqbot-message.controller';
|
import { QqbotMessageController } from '@/modules/qqbot/core/contract/message/qqbot-message.controller';
|
||||||
import { QqbotMessage } from '@/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity';
|
import { QqbotMessage } from '@/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity';
|
||||||
import { QqbotMessageService } from '@/modules/qqbot/core/application/message/qqbot-message.service';
|
import { QqbotMessageService } from '@/modules/qqbot/core/application/message/qqbot-message.service';
|
||||||
import { QqbotBusService } from '@/modules/qqbot/core/infrastructure/integration/bus/qqbot-bus.service';
|
import { QqbotBusService } from '@/modules/qqbot/core/infrastructure/integration/bus/qqbot-bus.service';
|
||||||
import { QqbotAccountNapcat } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-account-napcat.entity';
|
|
||||||
import { QqbotNapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-napcat-container.entity';
|
|
||||||
import { QqbotNapcatContainerService } from '@/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
|
|
||||||
import { QqbotAllowlist } from '@/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity';
|
import { QqbotAllowlist } from '@/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity';
|
||||||
import { QqbotBlocklist } from '@/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity';
|
import { QqbotBlocklist } from '@/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity';
|
||||||
import { QqbotPermissionController } from '@/modules/qqbot/core/contract/permission/qqbot-permission.controller';
|
import { QqbotPermissionController } from '@/modules/qqbot/core/contract/permission/qqbot-permission.controller';
|
||||||
@ -67,14 +55,6 @@ export const QQBOT_CORE_ENTITIES = [
|
|||||||
QqbotConversation,
|
QqbotConversation,
|
||||||
QqbotDedupe,
|
QqbotDedupe,
|
||||||
QqbotMessage,
|
QqbotMessage,
|
||||||
NapcatAccountBinding,
|
|
||||||
NapcatContainer,
|
|
||||||
NapcatDeviceIdentity,
|
|
||||||
NapcatLoginChallengeEntity,
|
|
||||||
NapcatLoginSession,
|
|
||||||
NapcatRuntimeCleanup,
|
|
||||||
QqbotAccountNapcat,
|
|
||||||
QqbotNapcatContainer,
|
|
||||||
QqbotRule,
|
QqbotRule,
|
||||||
QqbotSendLog,
|
QqbotSendLog,
|
||||||
];
|
];
|
||||||
@ -100,11 +80,6 @@ export const QQBOT_CORE_PROVIDERS = [
|
|||||||
QqbotDedupeService,
|
QqbotDedupeService,
|
||||||
QqbotEventService,
|
QqbotEventService,
|
||||||
QqbotMessageService,
|
QqbotMessageService,
|
||||||
NapcatDeviceIdentityService,
|
|
||||||
NapcatLoginStateStoreService,
|
|
||||||
QqbotNapcatLoginService,
|
|
||||||
QqbotNapcatWatchdogService,
|
|
||||||
QqbotNapcatContainerService,
|
|
||||||
QqbotPermissionService,
|
QqbotPermissionService,
|
||||||
QqbotRateLimitService,
|
QqbotRateLimitService,
|
||||||
QqbotReplyTemplateService,
|
QqbotReplyTemplateService,
|
||||||
@ -117,9 +92,6 @@ export const QQBOT_CORE_PROVIDERS = [
|
|||||||
export const QQBOT_CORE_EXPORTS = [
|
export const QQBOT_CORE_EXPORTS = [
|
||||||
QqbotAccountService,
|
QqbotAccountService,
|
||||||
QqbotSendService,
|
QqbotSendService,
|
||||||
NapcatDeviceIdentityService,
|
|
||||||
QqbotNapcatLoginService,
|
|
||||||
QqbotNapcatContainerService,
|
|
||||||
QqbotReverseWsService,
|
QqbotReverseWsService,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -128,6 +100,7 @@ export const QQBOT_CORE_EXPORTS = [
|
|||||||
ConfigModule,
|
ConfigModule,
|
||||||
AdminAuthGuardModule,
|
AdminAuthGuardModule,
|
||||||
DictModule,
|
DictModule,
|
||||||
|
forwardRef(() => QqbotNapcatModule),
|
||||||
forwardRef(() => QqbotPluginPlatformModule),
|
forwardRef(() => QqbotPluginPlatformModule),
|
||||||
TypeOrmModule.forFeature(QQBOT_CORE_ENTITIES),
|
TypeOrmModule.forFeature(QQBOT_CORE_ENTITIES),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import * as http from 'http';
|
|
||||||
import * as https from 'https';
|
|
||||||
import { createHash, randomUUID } from 'crypto';
|
import { createHash, randomUUID } from 'crypto';
|
||||||
import { Injectable, Optional } from '@nestjs/common';
|
import { Injectable, Optional } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { throwVbenError, ToolsService } from '@/common';
|
import { throwVbenError, ToolsService } from '@/common';
|
||||||
import { NapcatLoginApiClient } from '../../infrastructure/integration/napcat-login-api.client';
|
import {
|
||||||
|
NapcatLoginApiClient,
|
||||||
|
NapcatWebuiHttpClient,
|
||||||
|
} from '../../infrastructure/integration/napcat-login-api.client';
|
||||||
import type {
|
import type {
|
||||||
NapcatApiResponse,
|
|
||||||
NapcatCaptchaLoginResult,
|
NapcatCaptchaLoginResult,
|
||||||
NapcatCredential,
|
|
||||||
NapcatLoginInfo,
|
NapcatLoginInfo,
|
||||||
NapcatLoginStatus,
|
NapcatLoginStatus,
|
||||||
NapcatQrcode,
|
NapcatQrcode,
|
||||||
@ -60,10 +59,9 @@ export class QqbotNapcatLoginService {
|
|||||||
delete this.sessionEventListenerCache[sessionId];
|
delete this.sessionEventListenerCache[sessionId];
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
private readonly credentials: Record<
|
private readonly webuiClient = new NapcatWebuiHttpClient({
|
||||||
string,
|
getTimeoutMs: () => this.getTimeout(),
|
||||||
{ credential: string; expiresAt: number } | undefined
|
});
|
||||||
> = {};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@ -1274,8 +1272,12 @@ export class QqbotNapcatLoginService {
|
|||||||
path: string,
|
path: string,
|
||||||
body: Record<string, any> = {},
|
body: Record<string, any> = {},
|
||||||
) {
|
) {
|
||||||
const credential = await this.getCredential(container);
|
return this.webuiClient
|
||||||
return this.requestNapcat<T>(container, path, body, credential);
|
.post<T>(container, path, body)
|
||||||
|
.catch((err): never => {
|
||||||
|
const message = this.toolsService.getErrorMessage(err);
|
||||||
|
return throwVbenError(message || 'NapCat 请求失败');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async prepareReloginQrcode(
|
private async prepareReloginQrcode(
|
||||||
@ -1866,7 +1868,7 @@ export class QqbotNapcatLoginService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.credentials[this.getCredentialCacheKey(container)];
|
this.webuiClient.clearCredential(container);
|
||||||
if (options.waitForReady === false) return;
|
if (options.waitForReady === false) return;
|
||||||
|
|
||||||
onProgress?.('napcat-ready-wait', '等待 NapCat WebUI 启动');
|
onProgress?.('napcat-ready-wait', '等待 NapCat WebUI 启动');
|
||||||
@ -1891,112 +1893,13 @@ export class QqbotNapcatLoginService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.credentials[this.getCredentialCacheKey(container)];
|
this.webuiClient.clearCredential(container);
|
||||||
if (options.waitForReady === false) return;
|
if (options.waitForReady === false) return;
|
||||||
|
|
||||||
await this.toolsService.sleep(this.getRestartDelayMs());
|
await this.toolsService.sleep(this.getRestartDelayMs());
|
||||||
await this.getLoginStatus(container, true);
|
await this.getLoginStatus(container, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCredentialCacheKey(container: QqbotNapcatRuntime) {
|
|
||||||
return container.id || container.baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getCredential(container: QqbotNapcatRuntime) {
|
|
||||||
const cacheKey = this.getCredentialCacheKey(container);
|
|
||||||
const cached = this.credentials[cacheKey];
|
|
||||||
if (cached && Date.now() < cached.expiresAt) {
|
|
||||||
return cached.credential;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = this.getWebuiToken(container);
|
|
||||||
const hash = createHash('sha256').update(`${token}.napcat`).digest('hex');
|
|
||||||
const data = await this.requestNapcat<NapcatCredential>(
|
|
||||||
container,
|
|
||||||
'/api/auth/login',
|
|
||||||
{ hash },
|
|
||||||
);
|
|
||||||
if (!data.Credential) {
|
|
||||||
throwVbenError('NapCat WebUI 登录失败');
|
|
||||||
}
|
|
||||||
this.credentials[cacheKey] = {
|
|
||||||
credential: data.Credential,
|
|
||||||
expiresAt: Date.now() + 50 * 60 * 1000,
|
|
||||||
};
|
|
||||||
return data.Credential;
|
|
||||||
}
|
|
||||||
|
|
||||||
private requestNapcat<T>(
|
|
||||||
container: QqbotNapcatRuntime,
|
|
||||||
path: string,
|
|
||||||
body: Record<string, any> = {},
|
|
||||||
credential?: string,
|
|
||||||
): Promise<T> {
|
|
||||||
const baseUrl = container.baseUrl;
|
|
||||||
const target = new URL(path, baseUrl);
|
|
||||||
const payload = JSON.stringify(body);
|
|
||||||
const client = target.protocol === 'https:' ? https : http;
|
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
const req = client.request(
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
...(credential
|
|
||||||
? {
|
|
||||||
Authorization: `Bearer ${credential}`,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
'Content-Length': Buffer.byteLength(payload),
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
hostname: target.hostname,
|
|
||||||
method: 'POST',
|
|
||||||
path: `${target.pathname}${target.search}`,
|
|
||||||
port: target.port,
|
|
||||||
protocol: target.protocol,
|
|
||||||
timeout: this.getTimeout(),
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
||||||
res.on('end', () => {
|
|
||||||
const raw = Buffer.concat(chunks).toString('utf8');
|
|
||||||
let result: NapcatApiResponse<T>;
|
|
||||||
try {
|
|
||||||
result = raw ? JSON.parse(raw) : ({ code: -1 } as any);
|
|
||||||
} catch {
|
|
||||||
reject(new Error('NapCat 返回非 JSON 响应'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.code !== 0) {
|
|
||||||
reject(new Error(result.message || 'NapCat 请求失败'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(result.data as T);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
req.on('error', reject);
|
|
||||||
req.on('timeout', () => {
|
|
||||||
req.destroy(new Error('NapCat 请求超时'));
|
|
||||||
});
|
|
||||||
req.write(payload);
|
|
||||||
req.end();
|
|
||||||
}).catch((err): never => {
|
|
||||||
const message = this.toolsService.getErrorMessage(err);
|
|
||||||
return throwVbenError(message || 'NapCat 请求失败');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getWebuiToken(container: QqbotNapcatRuntime) {
|
|
||||||
const value = container.webuiToken || '';
|
|
||||||
const token = `${value}`.trim();
|
|
||||||
if (!token) {
|
|
||||||
throwVbenError('NapCat WebUI token 未配置');
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSessionTtlMs() {
|
private getSessionTtlMs() {
|
||||||
return Number(
|
return Number(
|
||||||
this.configService.get('NAPCAT_LOGIN_QR_EXPIRE_MS') || 2 * 60 * 1000,
|
this.configService.get('NAPCAT_LOGIN_QR_EXPIRE_MS') || 2 * 60 * 1000,
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Sse,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
||||||
|
import { vbenSuccess } from '@/common';
|
||||||
|
import { QqbotNapcatLoginService } from '../application/login/qqbot-napcat-login.service';
|
||||||
|
import {
|
||||||
|
QqbotNapcatScanCaptchaDto,
|
||||||
|
QqbotNapcatScanStatusDto,
|
||||||
|
} from './qqbot-napcat-login.dto';
|
||||||
|
|
||||||
|
@ApiTags('QQBot - NapCat 登录')
|
||||||
|
@Controller('qqbot/account')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class QqbotNapcatLoginController {
|
||||||
|
constructor(private readonly napcatLoginService: QqbotNapcatLoginService) {}
|
||||||
|
|
||||||
|
@Post('scan/create')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '扫码新增 QQBot 账号' })
|
||||||
|
async scanCreate() {
|
||||||
|
return vbenSuccess(await this.napcatLoginService.startCreate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('scan/refresh')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '扫码刷新 QQBot 账号登录态' })
|
||||||
|
@ApiQuery({ name: 'id', type: String })
|
||||||
|
async scanRefresh(@Query('id') id: string) {
|
||||||
|
return vbenSuccess(await this.napcatLoginService.startRefresh(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('scan/status')
|
||||||
|
@ApiOperation({ summary: '查询 QQBot 扫码登录状态' })
|
||||||
|
async scanStatus(@Query() query: QqbotNapcatScanStatusDto) {
|
||||||
|
return vbenSuccess(await this.napcatLoginService.status(query.sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('scan/events')
|
||||||
|
@ApiOperation({ summary: '订阅 QQBot 扫码登录进度' })
|
||||||
|
scanEvents(@Query() query: QqbotNapcatScanStatusDto) {
|
||||||
|
return this.napcatLoginService.events(query.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('scan/qrcode/refresh')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '刷新 QQBot 扫码二维码' })
|
||||||
|
async refreshScanQrcode(@Query() query: QqbotNapcatScanStatusDto) {
|
||||||
|
return vbenSuccess(
|
||||||
|
await this.napcatLoginService.refreshQrcode(query.sessionId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('scan/captcha/submit')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '提交 QQBot 登录安全验证码' })
|
||||||
|
async submitScanCaptcha(@Body() body: QqbotNapcatScanCaptchaDto) {
|
||||||
|
return vbenSuccess(
|
||||||
|
await this.napcatLoginService.submitCaptcha(body.sessionId, body),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('scan/cancel')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '取消 QQBot 扫码登录会话' })
|
||||||
|
async cancelScan(@Query() query: QqbotNapcatScanStatusDto) {
|
||||||
|
return vbenSuccess(await this.napcatLoginService.cancel(query.sessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/modules/qqbot/napcat/contract/qqbot-napcat-login.dto.ts
Normal file
17
src/modules/qqbot/napcat/contract/qqbot-napcat-login.dto.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class QqbotNapcatScanStatusDto {
|
||||||
|
@ApiProperty()
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QqbotNapcatScanCaptchaDto extends QqbotNapcatScanStatusDto {
|
||||||
|
@ApiProperty()
|
||||||
|
randstr: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
sid?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
ticket: string;
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
export * from './qqbot-napcat.module';
|
||||||
|
export * from './contract/qqbot-napcat-login.controller';
|
||||||
|
export * from './contract/qqbot-napcat-login.dto';
|
||||||
export * from './infrastructure/persistence/napcat-account-binding.entity';
|
export * from './infrastructure/persistence/napcat-account-binding.entity';
|
||||||
export * from './infrastructure/persistence/napcat-container.entity';
|
export * from './infrastructure/persistence/napcat-container.entity';
|
||||||
export * from './infrastructure/persistence/napcat-device-identity.entity';
|
export * from './infrastructure/persistence/napcat-device-identity.entity';
|
||||||
@ -6,8 +9,6 @@ export * from './infrastructure/persistence/napcat-login-session.entity';
|
|||||||
export * from './infrastructure/persistence/napcat-login-state-store.service';
|
export * from './infrastructure/persistence/napcat-login-state-store.service';
|
||||||
export * from './infrastructure/persistence/napcat-runtime-cleanup.entity';
|
export * from './infrastructure/persistence/napcat-runtime-cleanup.entity';
|
||||||
export * from './infrastructure/integration/device/napcat-device-identity.service';
|
export * from './infrastructure/integration/device/napcat-device-identity.service';
|
||||||
export * from './infrastructure/persistence/qqbot-account-napcat.entity';
|
|
||||||
export * from './infrastructure/persistence/qqbot-napcat-container.entity';
|
|
||||||
export * from './infrastructure/integration/napcat-login-api.client';
|
export * from './infrastructure/integration/napcat-login-api.client';
|
||||||
export * from './domain/login/napcat-login-state-machine';
|
export * from './domain/login/napcat-login-state-machine';
|
||||||
export * from './application/login/qqbot-napcat-login.service';
|
export * from './application/login/qqbot-napcat-login.service';
|
||||||
|
|||||||
@ -13,8 +13,8 @@ import {
|
|||||||
} from './napcat-docker-device-options';
|
} from './napcat-docker-device-options';
|
||||||
import { NapcatDeviceIdentityService } from '../device/napcat-device-identity.service';
|
import { NapcatDeviceIdentityService } from '../device/napcat-device-identity.service';
|
||||||
import { QqbotAccount } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity';
|
import { QqbotAccount } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity';
|
||||||
import { QqbotAccountNapcat } from '../../persistence/qqbot-account-napcat.entity';
|
import { NapcatAccountBinding } from '../../persistence/napcat-account-binding.entity';
|
||||||
import { QqbotNapcatContainer } from '../../persistence/qqbot-napcat-container.entity';
|
import { NapcatContainer } from '../../persistence/napcat-container.entity';
|
||||||
import type {
|
import type {
|
||||||
NapcatApiResponse,
|
NapcatApiResponse,
|
||||||
NapcatCredential,
|
NapcatCredential,
|
||||||
@ -52,10 +52,10 @@ type NapcatAutoLoginResult = {
|
|||||||
export class QqbotNapcatContainerService {
|
export class QqbotNapcatContainerService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@InjectRepository(QqbotNapcatContainer)
|
@InjectRepository(NapcatContainer)
|
||||||
private readonly containerRepository: Repository<QqbotNapcatContainer>,
|
private readonly containerRepository: Repository<NapcatContainer>,
|
||||||
@InjectRepository(QqbotAccountNapcat)
|
@InjectRepository(NapcatAccountBinding)
|
||||||
private readonly bindingRepository: Repository<QqbotAccountNapcat>,
|
private readonly bindingRepository: Repository<NapcatAccountBinding>,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly deviceIdentityService?: NapcatDeviceIdentityService,
|
private readonly deviceIdentityService?: NapcatDeviceIdentityService,
|
||||||
) {}
|
) {}
|
||||||
@ -169,7 +169,7 @@ export class QqbotNapcatContainerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async resolveRuntimeDeviceIdentity(
|
private async resolveRuntimeDeviceIdentity(
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
selfId: string,
|
selfId: string,
|
||||||
): Promise<NapcatDockerDeviceOptions | undefined> {
|
): Promise<NapcatDockerDeviceOptions | undefined> {
|
||||||
if (
|
if (
|
||||||
@ -199,7 +199,7 @@ export class QqbotNapcatContainerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryAutoLogin(
|
async tryAutoLogin(
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
options: NapcatLoginEnvOptions,
|
options: NapcatLoginEnvOptions,
|
||||||
): Promise<NapcatAutoLoginResult> {
|
): Promise<NapcatAutoLoginResult> {
|
||||||
const selfId = this.toolsService.toTrimmedString(options.selfId);
|
const selfId = this.toolsService.toTrimmedString(options.selfId);
|
||||||
@ -334,8 +334,6 @@ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$NAME"
|
|||||||
const existing = await this.bindingRepository.findOne({
|
const existing = await this.bindingRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
accountId,
|
accountId,
|
||||||
containerId,
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@ -343,8 +341,11 @@ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$NAME"
|
|||||||
{ id: existing.id },
|
{ id: existing.id },
|
||||||
{
|
{
|
||||||
bindStatus: 'bound',
|
bindStatus: 'bound',
|
||||||
|
containerId,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
|
isDeleted: false,
|
||||||
lastLoginAt: new Date(),
|
lastLoginAt: new Date(),
|
||||||
|
remark: '',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await this.removeOtherAccountContainers(accountId, containerId);
|
await this.removeOtherAccountContainers(accountId, containerId);
|
||||||
@ -475,7 +476,7 @@ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$NAME"
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectRuntimeOffline(container: QqbotNapcatContainer) {
|
async detectRuntimeOffline(container: NapcatContainer) {
|
||||||
if (this.getManagedMode() !== 'ssh' || !container.name) return null;
|
if (this.getManagedMode() !== 'ssh' || !container.name) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -549,7 +550,7 @@ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$NAME"
|
|||||||
}
|
}
|
||||||
|
|
||||||
async inspectRuntimeStatus(
|
async inspectRuntimeStatus(
|
||||||
container: QqbotNapcatContainer,
|
container: NapcatContainer,
|
||||||
): Promise<QqbotNapcatRuntimeStatusSnapshot> {
|
): Promise<QqbotNapcatRuntimeStatusSnapshot> {
|
||||||
const checkedAt = new Date();
|
const checkedAt = new Date();
|
||||||
const containerOnline = container.status === 'running';
|
const containerOnline = container.status === 'running';
|
||||||
@ -694,12 +695,12 @@ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$NAME"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeRemoteDockerContainer(container: QqbotNapcatContainer) {
|
private async removeRemoteDockerContainer(container: NapcatContainer) {
|
||||||
const script = this.buildRemoteRemoveScript(container);
|
const script = this.buildRemoteRemoveScript(container);
|
||||||
await this.runProcess('ssh', [...this.getSshArgs(), 'sh -s'], script);
|
await this.runProcess('ssh', [...this.getSshArgs(), 'sh -s'], script);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRemoteRemoveScript(container: QqbotNapcatContainer) {
|
private buildRemoteRemoveScript(container: NapcatContainer) {
|
||||||
const dataDir = this.sh(container.dataDir || '');
|
const dataDir = this.sh(container.dataDir || '');
|
||||||
const name = this.sh(container.name);
|
const name = this.sh(container.name);
|
||||||
const rootDir = this.sh(this.getRootDir());
|
const rootDir = this.sh(this.getRootDir());
|
||||||
@ -725,7 +726,7 @@ fi
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRemoteResetLoginStateScript(container: QqbotNapcatContainer) {
|
private buildRemoteResetLoginStateScript(container: NapcatContainer) {
|
||||||
const dataDir = this.sh(container.dataDir || '');
|
const dataDir = this.sh(container.dataDir || '');
|
||||||
const name = this.sh(container.name);
|
const name = this.sh(container.name);
|
||||||
const rootDir = this.sh(this.getRootDir());
|
const rootDir = this.sh(this.getRootDir());
|
||||||
@ -762,7 +763,7 @@ echo "__KT_PROGRESS__:container-started:NapCat 容器已启动"
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRemoteRecentLogsScript(container: QqbotNapcatContainer) {
|
private buildRemoteRecentLogsScript(container: NapcatContainer) {
|
||||||
return this.buildRemoteRecentLogsByNameScript(container.name);
|
return this.buildRemoteRecentLogsByNameScript(container.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -927,6 +928,7 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
const container = await this.containerRepository.save(
|
const container = await this.containerRepository.save(
|
||||||
this.containerRepository.create({
|
this.containerRepository.create({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
accountId: accountId || null,
|
||||||
dataDir,
|
dataDir,
|
||||||
image,
|
image,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@ -1194,7 +1196,7 @@ ${accountRunFlag}${passwordRunFlag}${deviceRunFlags} -p "$PORT:6099" \\
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private toRuntime(container: QqbotNapcatContainer): QqbotNapcatRuntime {
|
private toRuntime(container: NapcatContainer): QqbotNapcatRuntime {
|
||||||
return {
|
return {
|
||||||
baseUrl: this.normalizeBaseUrl(container.baseUrl),
|
baseUrl: this.normalizeBaseUrl(container.baseUrl),
|
||||||
id: container.id,
|
id: container.id,
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { createHash } from 'crypto';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
export type NewDeviceQrStatus =
|
export type NewDeviceQrStatus =
|
||||||
@ -33,6 +36,142 @@ export type NapcatLoginApiTransport = {
|
|||||||
post(path: string, body: Record<string, unknown>): Promise<unknown>;
|
post(path: string, body: Record<string, unknown>): Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NapcatWebuiRuntime = {
|
||||||
|
baseUrl: string;
|
||||||
|
id?: string;
|
||||||
|
webuiToken?: null | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NapcatApiResponse<T> = {
|
||||||
|
code: number;
|
||||||
|
data?: T;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NapcatCredential = {
|
||||||
|
Credential?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NapcatWebuiHttpClient {
|
||||||
|
private readonly credentials: Record<
|
||||||
|
string,
|
||||||
|
{ credential: string; expiresAt: number } | undefined
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly options: {
|
||||||
|
getTimeoutMs: () => number;
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async post<T>(
|
||||||
|
container: NapcatWebuiRuntime,
|
||||||
|
path: string,
|
||||||
|
body: Record<string, unknown> = {},
|
||||||
|
) {
|
||||||
|
const credential = await this.getCredential(container);
|
||||||
|
return this.request<T>(container, path, body, credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCredential(container: NapcatWebuiRuntime) {
|
||||||
|
delete this.credentials[this.getCredentialCacheKey(container)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCredentialCacheKey(container: NapcatWebuiRuntime) {
|
||||||
|
return container.id || container.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCredential(container: NapcatWebuiRuntime) {
|
||||||
|
const cacheKey = this.getCredentialCacheKey(container);
|
||||||
|
const cached = this.credentials[cacheKey];
|
||||||
|
if (cached && Date.now() < cached.expiresAt) {
|
||||||
|
return cached.credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.getWebuiToken(container);
|
||||||
|
const hash = createHash('sha256').update(`${token}.napcat`).digest('hex');
|
||||||
|
const data = await this.request<NapcatCredential>(
|
||||||
|
container,
|
||||||
|
'/api/auth/login',
|
||||||
|
{ hash },
|
||||||
|
);
|
||||||
|
if (!data.Credential) {
|
||||||
|
throw new Error('NapCat WebUI 登录失败');
|
||||||
|
}
|
||||||
|
this.credentials[cacheKey] = {
|
||||||
|
credential: data.Credential,
|
||||||
|
expiresAt: Date.now() + 50 * 60 * 1000,
|
||||||
|
};
|
||||||
|
return data.Credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
private request<T>(
|
||||||
|
container: NapcatWebuiRuntime,
|
||||||
|
path: string,
|
||||||
|
body: Record<string, unknown> = {},
|
||||||
|
credential?: string,
|
||||||
|
): Promise<T> {
|
||||||
|
const target = new URL(path, container.baseUrl);
|
||||||
|
const payload = JSON.stringify(body);
|
||||||
|
const client = target.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const req = client.request(
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...(credential
|
||||||
|
? {
|
||||||
|
Authorization: `Bearer ${credential}`,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
'Content-Length': Buffer.byteLength(payload),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
hostname: target.hostname,
|
||||||
|
method: 'POST',
|
||||||
|
path: `${target.pathname}${target.search}`,
|
||||||
|
port: target.port,
|
||||||
|
protocol: target.protocol,
|
||||||
|
timeout: this.options.getTimeoutMs(),
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString('utf8');
|
||||||
|
let result: NapcatApiResponse<T>;
|
||||||
|
try {
|
||||||
|
result = raw ? JSON.parse(raw) : ({ code: -1 } as any);
|
||||||
|
} catch {
|
||||||
|
reject(new Error('NapCat 返回非 JSON 响应'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.code !== 0) {
|
||||||
|
reject(new Error(result.message || 'NapCat 请求失败'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(result.data as T);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy(new Error('NapCat 请求超时'));
|
||||||
|
});
|
||||||
|
req.write(payload);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWebuiToken(container: NapcatWebuiRuntime) {
|
||||||
|
const token = `${container.webuiToken || ''}`.trim();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('NapCat WebUI token 未配置');
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NapcatLoginApiClient {
|
export class NapcatLoginApiClient {
|
||||||
constructor(private readonly transport: NapcatLoginApiTransport) {}
|
constructor(private readonly transport: NapcatLoginApiTransport) {}
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { NapcatDeviceIdentity } from './napcat-device-identity.entity';
|
|||||||
import { NapcatLoginChallengeEntity } from './napcat-login-challenge.entity';
|
import { NapcatLoginChallengeEntity } from './napcat-login-challenge.entity';
|
||||||
import { NapcatLoginSession } from './napcat-login-session.entity';
|
import { NapcatLoginSession } from './napcat-login-session.entity';
|
||||||
import { NapcatRuntimeCleanup } from './napcat-runtime-cleanup.entity';
|
import { NapcatRuntimeCleanup } from './napcat-runtime-cleanup.entity';
|
||||||
import { QqbotAccountNapcat } from './qqbot-account-napcat.entity';
|
|
||||||
import { QqbotNapcatContainer } from './qqbot-napcat-container.entity';
|
|
||||||
|
|
||||||
export const NAPCAT_RUNTIME_DOMAIN_CONTRACT = {
|
export const NAPCAT_RUNTIME_DOMAIN_CONTRACT = {
|
||||||
tables: [
|
tables: [
|
||||||
@ -25,6 +23,4 @@ export const NAPCAT_RUNTIME_ENTITIES = [
|
|||||||
NapcatLoginChallengeEntity,
|
NapcatLoginChallengeEntity,
|
||||||
NapcatLoginSession,
|
NapcatLoginSession,
|
||||||
NapcatRuntimeCleanup,
|
NapcatRuntimeCleanup,
|
||||||
QqbotAccountNapcat,
|
|
||||||
QqbotNapcatContainer,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import {
|
|||||||
ensureSnowflakeId,
|
ensureSnowflakeId,
|
||||||
KtCreateDateColumn,
|
KtCreateDateColumn,
|
||||||
KtDateTime,
|
KtDateTime,
|
||||||
|
KtDateTimeColumn,
|
||||||
KtUpdateDateColumn,
|
KtUpdateDateColumn,
|
||||||
} from '@/common';
|
} from '@/common';
|
||||||
import type { QqbotAccountNapcatBindStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
import type { QqbotAccountNapcatBindStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
|
|
||||||
@Entity('napcat_account_binding')
|
@Entity('napcat_account_binding')
|
||||||
@Index('uk_napcat_account_binding_account', ['accountId'], { unique: true })
|
@Index('uk_napcat_account_binding_account', ['accountId'], { unique: true })
|
||||||
|
@Index('idx_napcat_account_binding_container', ['containerId', 'isDeleted'])
|
||||||
export class NapcatAccountBinding {
|
export class NapcatAccountBinding {
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
@PrimaryColumn({ type: 'bigint' })
|
||||||
id: string;
|
id: string;
|
||||||
@ -19,11 +21,33 @@ export class NapcatAccountBinding {
|
|||||||
@Column({ name: 'container_id', type: 'bigint' })
|
@Column({ name: 'container_id', type: 'bigint' })
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
|
||||||
@Column({ name: 'device_identity_id', type: 'bigint' })
|
@Column({
|
||||||
deviceIdentityId: string;
|
default: null,
|
||||||
|
name: 'device_identity_id',
|
||||||
|
nullable: true,
|
||||||
|
type: 'bigint',
|
||||||
|
})
|
||||||
|
deviceIdentityId: null | string;
|
||||||
|
|
||||||
@Column({ length: 32 })
|
@Column({ default: 'pending', length: 32, name: 'status' })
|
||||||
status: QqbotAccountNapcatBindStatus;
|
bindStatus: QqbotAccountNapcatBindStatus;
|
||||||
|
|
||||||
|
@Column({ default: true, name: 'is_primary' })
|
||||||
|
isPrimary: boolean;
|
||||||
|
|
||||||
|
@KtDateTimeColumn({
|
||||||
|
default: null,
|
||||||
|
name: 'last_login_at',
|
||||||
|
nullable: true,
|
||||||
|
type: 'datetime',
|
||||||
|
})
|
||||||
|
lastLoginAt: KtDateTime | null;
|
||||||
|
|
||||||
|
@Column({ default: '', length: 255 })
|
||||||
|
remark: string;
|
||||||
|
|
||||||
|
@Column({ default: false, name: 'is_deleted' })
|
||||||
|
isDeleted: boolean;
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|||||||
@ -3,28 +3,77 @@ import {
|
|||||||
ensureSnowflakeId,
|
ensureSnowflakeId,
|
||||||
KtCreateDateColumn,
|
KtCreateDateColumn,
|
||||||
KtDateTime,
|
KtDateTime,
|
||||||
|
KtDateTimeColumn,
|
||||||
KtUpdateDateColumn,
|
KtUpdateDateColumn,
|
||||||
} from '@/common';
|
} from '@/common';
|
||||||
import type { QqbotNapcatContainerStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
import type { QqbotNapcatContainerStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
|
|
||||||
@Entity('napcat_container')
|
@Entity('napcat_container')
|
||||||
@Index('uk_napcat_container_name', ['containerName'], { unique: true })
|
@Index('uk_napcat_container_name', ['name'], { unique: true })
|
||||||
|
@Index('idx_napcat_container_status', ['status', 'isDeleted'])
|
||||||
|
@Index('idx_napcat_container_account', ['accountId'])
|
||||||
export class NapcatContainer {
|
export class NapcatContainer {
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
@PrimaryColumn({ type: 'bigint' })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column({ name: 'account_id', type: 'bigint' })
|
@Column({ name: 'account_id', nullable: true, type: 'bigint' })
|
||||||
accountId: string;
|
accountId: null | string;
|
||||||
|
|
||||||
@Column({ length: 128, name: 'container_name' })
|
@Column({ length: 120, name: 'container_name' })
|
||||||
containerName: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ length: 255, name: 'image_name' })
|
@Column({ length: 255, name: 'base_url' })
|
||||||
imageName: string;
|
baseUrl: string;
|
||||||
|
|
||||||
@Column({ length: 32 })
|
@Column({ name: 'webui_port', nullable: true, type: 'int' })
|
||||||
|
webuiPort: null | number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
default: null,
|
||||||
|
length: 255,
|
||||||
|
name: 'webui_token',
|
||||||
|
nullable: true,
|
||||||
|
select: false,
|
||||||
|
})
|
||||||
|
webuiToken: null | string;
|
||||||
|
|
||||||
|
@Column({ default: '', length: 255 })
|
||||||
|
image: string;
|
||||||
|
|
||||||
|
@Column({ default: '', length: 500, name: 'data_dir' })
|
||||||
|
dataDir: string;
|
||||||
|
|
||||||
|
@Column({ default: '', length: 500, name: 'reverse_ws_url' })
|
||||||
|
reverseWsUrl: string;
|
||||||
|
|
||||||
|
@Column({ default: 'creating', length: 32 })
|
||||||
status: QqbotNapcatContainerStatus;
|
status: QqbotNapcatContainerStatus;
|
||||||
|
|
||||||
|
@KtDateTimeColumn({
|
||||||
|
default: null,
|
||||||
|
name: 'last_started_at',
|
||||||
|
nullable: true,
|
||||||
|
type: 'datetime',
|
||||||
|
})
|
||||||
|
lastStartedAt: KtDateTime | null;
|
||||||
|
|
||||||
|
@KtDateTimeColumn({
|
||||||
|
default: null,
|
||||||
|
name: 'last_checked_at',
|
||||||
|
nullable: true,
|
||||||
|
type: 'datetime',
|
||||||
|
})
|
||||||
|
lastCheckedAt: KtDateTime | null;
|
||||||
|
|
||||||
|
@Column({ default: null, length: 500, name: 'last_error', nullable: true })
|
||||||
|
lastError: null | string;
|
||||||
|
|
||||||
|
@Column({ default: '', length: 255 })
|
||||||
|
remark: string;
|
||||||
|
|
||||||
|
@Column({ default: false, name: 'is_deleted' })
|
||||||
|
isDeleted: boolean;
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
|
|||||||
@ -55,8 +55,11 @@ export class NapcatLoginStateStoreService {
|
|||||||
where: { sessionKey: sessionId },
|
where: { sessionKey: sessionId },
|
||||||
});
|
});
|
||||||
const session = persisted?.sessionPayload;
|
const session = persisted?.sessionPayload;
|
||||||
if (session) this.cache[session.id] = session;
|
if (!session) return undefined;
|
||||||
return session;
|
|
||||||
|
const hydratedSession = await this.hydratePersistedSession(session);
|
||||||
|
this.cache[hydratedSession.id] = hydratedSession;
|
||||||
|
return hydratedSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(session: QqbotLoginScanSession) {
|
set(session: QqbotLoginScanSession) {
|
||||||
@ -180,6 +183,97 @@ export class NapcatLoginStateStoreService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async hydratePersistedSession(session: QqbotLoginScanSession) {
|
||||||
|
const hydratedSession = { ...session };
|
||||||
|
|
||||||
|
await this.hydrateCaptchaChallenge(hydratedSession);
|
||||||
|
await this.hydrateNewDeviceChallenge(hydratedSession);
|
||||||
|
await this.hydrateRuntimeCleanup(hydratedSession);
|
||||||
|
|
||||||
|
return hydratedSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hydrateCaptchaChallenge(session: QqbotLoginScanSession) {
|
||||||
|
const challenge = await this.findChallenge(session.id, 'captcha');
|
||||||
|
if (!challenge || challenge.status !== 'pending') return;
|
||||||
|
|
||||||
|
const payload = this.toChallengePayload(challenge.challengePayload);
|
||||||
|
if (!session.captchaUrl && challenge.challengeUrl) {
|
||||||
|
session.captchaUrl = challenge.challengeUrl;
|
||||||
|
}
|
||||||
|
if (!session.expectedSelfId && typeof payload.expectedSelfId === 'string') {
|
||||||
|
session.expectedSelfId = payload.expectedSelfId;
|
||||||
|
}
|
||||||
|
session.errorMessage = session.errorMessage || '需要验证码';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hydrateNewDeviceChallenge(session: QqbotLoginScanSession) {
|
||||||
|
const challenge = await this.findChallenge(session.id, 'new-device');
|
||||||
|
if (!challenge || this.isResolvedChallenge(challenge.status)) return;
|
||||||
|
|
||||||
|
const payload = this.toChallengePayload(challenge.challengePayload);
|
||||||
|
session.newDeviceStatus = challenge.status as QqbotLoginScanSession['newDeviceStatus'];
|
||||||
|
if (
|
||||||
|
!session.deviceVerifyUrl &&
|
||||||
|
typeof payload.deviceVerifyUrl === 'string'
|
||||||
|
) {
|
||||||
|
session.deviceVerifyUrl = payload.deviceVerifyUrl;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!session.newDevicePullQrCodeSig &&
|
||||||
|
typeof payload.newDevicePullQrCodeSig === 'string'
|
||||||
|
) {
|
||||||
|
session.newDevicePullQrCodeSig = payload.newDevicePullQrCodeSig;
|
||||||
|
}
|
||||||
|
if (!session.newDeviceQrcode) {
|
||||||
|
session.newDeviceQrcode =
|
||||||
|
typeof payload.newDeviceQrcode === 'string'
|
||||||
|
? payload.newDeviceQrcode
|
||||||
|
: challenge.challengeUrl || undefined;
|
||||||
|
}
|
||||||
|
session.errorMessage = session.errorMessage || '需要新设备验证二维码';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hydrateRuntimeCleanup(session: QqbotLoginScanSession) {
|
||||||
|
if (!this.runtimeCleanupRepository) return;
|
||||||
|
const cleanup = await this.runtimeCleanupRepository.findOne({
|
||||||
|
order: { createTime: 'DESC' },
|
||||||
|
where: {
|
||||||
|
cleanupType: 'password-login-env',
|
||||||
|
sessionId: session.id,
|
||||||
|
status: 'failed',
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
if (!cleanup) return;
|
||||||
|
|
||||||
|
session.status = 'error';
|
||||||
|
session.captchaUrl = undefined;
|
||||||
|
session.errorMessage =
|
||||||
|
cleanup.errorMessage || session.errorMessage || '运行态密码清理失败';
|
||||||
|
session.passwordMd5 = undefined;
|
||||||
|
session.preparingRelogin = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findChallenge(
|
||||||
|
sessionId: string,
|
||||||
|
challengeType: NapcatLoginChallengeType,
|
||||||
|
) {
|
||||||
|
if (!this.loginChallengeRepository) return null;
|
||||||
|
return this.loginChallengeRepository.findOne({
|
||||||
|
order: { createTime: 'DESC' },
|
||||||
|
where: {
|
||||||
|
challengeType,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toChallengePayload(payload: unknown) {
|
||||||
|
return payload && typeof payload === 'object'
|
||||||
|
? (payload as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
|
||||||
private async saveChallenge(input: {
|
private async saveChallenge(input: {
|
||||||
challengePayload: null | Record<string, unknown>;
|
challengePayload: null | Record<string, unknown>;
|
||||||
challengeType: NapcatLoginChallengeType;
|
challengeType: NapcatLoginChallengeType;
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtDateTimeColumn,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
import type { QqbotAccountNapcatBindStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
|
||||||
|
|
||||||
@Entity('qqbot_account_napcat')
|
|
||||||
@Index('idx_qqbot_account_napcat_account', ['accountId', 'isDeleted'])
|
|
||||||
@Index('idx_qqbot_account_napcat_container', ['containerId', 'isDeleted'])
|
|
||||||
export class QqbotAccountNapcat {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', type: 'bigint' })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'container_id', type: 'bigint' })
|
|
||||||
containerId: string;
|
|
||||||
|
|
||||||
@Column({ default: 'pending', length: 32, name: 'bind_status' })
|
|
||||||
bindStatus: QqbotAccountNapcatBindStatus;
|
|
||||||
|
|
||||||
@Column({ default: true, name: 'is_primary' })
|
|
||||||
isPrimary: boolean;
|
|
||||||
|
|
||||||
@KtDateTimeColumn({
|
|
||||||
default: null,
|
|
||||||
name: 'last_login_at',
|
|
||||||
nullable: true,
|
|
||||||
type: 'datetime',
|
|
||||||
})
|
|
||||||
lastLoginAt: KtDateTime | null;
|
|
||||||
|
|
||||||
@Column({ default: '', length: 255 })
|
|
||||||
remark: string;
|
|
||||||
|
|
||||||
@Column({ default: false, name: 'is_deleted' })
|
|
||||||
isDeleted: boolean;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtDateTimeColumn,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
import type { QqbotNapcatContainerStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
|
||||||
|
|
||||||
@Entity('qqbot_napcat_container')
|
|
||||||
@Index('uk_qqbot_napcat_container_name', ['name'], { unique: true })
|
|
||||||
@Index('idx_qqbot_napcat_container_status', ['status', 'isDeleted'])
|
|
||||||
export class QqbotNapcatContainer {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 120 })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, name: 'base_url' })
|
|
||||||
baseUrl: string;
|
|
||||||
|
|
||||||
@Column({ name: 'webui_port', nullable: true, type: 'int' })
|
|
||||||
webuiPort: null | number;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
default: null,
|
|
||||||
length: 255,
|
|
||||||
name: 'webui_token',
|
|
||||||
nullable: true,
|
|
||||||
select: false,
|
|
||||||
})
|
|
||||||
webuiToken: null | string;
|
|
||||||
|
|
||||||
@Column({ default: '', length: 255 })
|
|
||||||
image: string;
|
|
||||||
|
|
||||||
@Column({ default: '', length: 500, name: 'data_dir' })
|
|
||||||
dataDir: string;
|
|
||||||
|
|
||||||
@Column({ default: '', length: 500, name: 'reverse_ws_url' })
|
|
||||||
reverseWsUrl: string;
|
|
||||||
|
|
||||||
@Column({ default: 'creating', length: 32 })
|
|
||||||
status: QqbotNapcatContainerStatus;
|
|
||||||
|
|
||||||
@KtDateTimeColumn({
|
|
||||||
default: null,
|
|
||||||
name: 'last_started_at',
|
|
||||||
nullable: true,
|
|
||||||
type: 'datetime',
|
|
||||||
})
|
|
||||||
lastStartedAt: KtDateTime | null;
|
|
||||||
|
|
||||||
@KtDateTimeColumn({
|
|
||||||
default: null,
|
|
||||||
name: 'last_checked_at',
|
|
||||||
nullable: true,
|
|
||||||
type: 'datetime',
|
|
||||||
})
|
|
||||||
lastCheckedAt: KtDateTime | null;
|
|
||||||
|
|
||||||
@Column({ default: null, length: 500, name: 'last_error', nullable: true })
|
|
||||||
lastError: null | string;
|
|
||||||
|
|
||||||
@Column({ default: '', length: 255 })
|
|
||||||
remark: string;
|
|
||||||
|
|
||||||
@Column({ default: false, name: 'is_deleted' })
|
|
||||||
isDeleted: boolean;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
src/modules/qqbot/napcat/qqbot-napcat.module.ts
Normal file
46
src/modules/qqbot/napcat/qqbot-napcat.module.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
||||||
|
import { QqbotCoreModule } from '@/modules/qqbot/core/qqbot-core.module';
|
||||||
|
import { QqbotNapcatLoginService } from './application/login/qqbot-napcat-login.service';
|
||||||
|
import { QqbotNapcatWatchdogService } from './application/login/qqbot-napcat-watchdog.service';
|
||||||
|
import { QqbotNapcatLoginController } from './contract/qqbot-napcat-login.controller';
|
||||||
|
import { QqbotNapcatContainerService } from './infrastructure/integration/container/qqbot-napcat-container.service';
|
||||||
|
import { NapcatDeviceIdentityService } from './infrastructure/integration/device/napcat-device-identity.service';
|
||||||
|
import {
|
||||||
|
NAPCAT_RUNTIME_ENTITIES,
|
||||||
|
} from './infrastructure/persistence';
|
||||||
|
import { NapcatLoginStateStoreService } from './infrastructure/persistence/napcat-login-state-store.service';
|
||||||
|
|
||||||
|
export const QQBOT_NAPCAT_ENTITIES = [...NAPCAT_RUNTIME_ENTITIES];
|
||||||
|
|
||||||
|
export const QQBOT_NAPCAT_CONTROLLERS = [QqbotNapcatLoginController];
|
||||||
|
|
||||||
|
export const QQBOT_NAPCAT_PROVIDERS = [
|
||||||
|
NapcatDeviceIdentityService,
|
||||||
|
NapcatLoginStateStoreService,
|
||||||
|
QqbotNapcatContainerService,
|
||||||
|
QqbotNapcatLoginService,
|
||||||
|
QqbotNapcatWatchdogService,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const QQBOT_NAPCAT_EXPORTS = [
|
||||||
|
NapcatDeviceIdentityService,
|
||||||
|
NapcatLoginStateStoreService,
|
||||||
|
QqbotNapcatContainerService,
|
||||||
|
QqbotNapcatLoginService,
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule,
|
||||||
|
AdminAuthGuardModule,
|
||||||
|
forwardRef(() => QqbotCoreModule),
|
||||||
|
TypeOrmModule.forFeature(QQBOT_NAPCAT_ENTITIES),
|
||||||
|
],
|
||||||
|
controllers: QQBOT_NAPCAT_CONTROLLERS,
|
||||||
|
providers: QQBOT_NAPCAT_PROVIDERS,
|
||||||
|
exports: [TypeOrmModule, ...QQBOT_NAPCAT_EXPORTS],
|
||||||
|
})
|
||||||
|
export class QqbotNapcatModule {}
|
||||||
@ -1,15 +1,7 @@
|
|||||||
# QQBot NapCat Schema
|
# QQBot NapCat Schema
|
||||||
|
|
||||||
NapCat owns container runtime, account binding, device identity, login session,
|
NapCat owns container runtime, account binding, device identity, login session,
|
||||||
challenge, and cleanup persistence.
|
challenge, and cleanup persistence through the v3 `napcat_*` tables.
|
||||||
|
|
||||||
Current compatibility tables:
|
|
||||||
|
|
||||||
- `qqbot_account_napcat`
|
|
||||||
- `qqbot_napcat_container`
|
|
||||||
- `napcat_device_identity`
|
|
||||||
|
|
||||||
Target v3 tables:
|
|
||||||
|
|
||||||
- `napcat_container`
|
- `napcat_container`
|
||||||
- `napcat_device_identity`
|
- `napcat_device_identity`
|
||||||
@ -21,7 +13,7 @@ Target v3 tables:
|
|||||||
Verification SQL:
|
Verification SQL:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
SELECT COUNT(*) FROM napcat_container WHERE is_deleted = 0;
|
||||||
SELECT COUNT(*) FROM napcat_device_identity;
|
SELECT COUNT(*) FROM napcat_device_identity;
|
||||||
SELECT COUNT(*) FROM qqbot_account_napcat WHERE is_deleted = 0;
|
SELECT COUNT(*) FROM napcat_account_binding WHERE is_deleted = 0;
|
||||||
SELECT COUNT(*) FROM qqbot_napcat_container WHERE is_deleted = 0;
|
|
||||||
```
|
```
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import {
|
|||||||
type QqbotPluginManifest,
|
type QqbotPluginManifest,
|
||||||
} from '../domain/manifest';
|
} from '../domain/manifest';
|
||||||
import type { QqbotPluginWorkerRuntime } from '../infrastructure/integration/runtime';
|
import type { QqbotPluginWorkerRuntime } from '../infrastructure/integration/runtime';
|
||||||
|
import { QqbotPluginPackageReaderService } from '../infrastructure/integration/package/plugin-package-reader.service';
|
||||||
|
import { QqbotEventPluginRegistryService } from './registry/qqbot-event-plugin-registry.service';
|
||||||
|
import { QqbotPluginRegistryService } from './registry/qqbot-plugin-registry.service';
|
||||||
import {
|
import {
|
||||||
QqbotPlugin,
|
QqbotPlugin,
|
||||||
QqbotPluginAccountBinding,
|
QqbotPluginAccountBinding,
|
||||||
@ -106,6 +109,12 @@ export class QqbotPluginPlatformService {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@Inject(QQBOT_PLUGIN_RUNTIME_FACTORY)
|
@Inject(QQBOT_PLUGIN_RUNTIME_FACTORY)
|
||||||
private readonly runtimeFactory?: QqbotPluginRuntimeFactory,
|
private readonly runtimeFactory?: QqbotPluginRuntimeFactory,
|
||||||
|
@Optional()
|
||||||
|
private readonly pluginRegistry?: QqbotPluginRegistryService,
|
||||||
|
@Optional()
|
||||||
|
private readonly eventPluginRegistry?: QqbotEventPluginRegistryService,
|
||||||
|
@Optional()
|
||||||
|
private readonly packageReader?: QqbotPluginPackageReaderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async listInstallations() {
|
async listInstallations() {
|
||||||
@ -163,8 +172,16 @@ export class QqbotPluginPlatformService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadPackage(body: InstallLocalBody) {
|
||||||
|
return {
|
||||||
|
...this.requirePackageReader().readPackage(body),
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async installLocal(body: InstallLocalBody) {
|
async installLocal(body: InstallLocalBody) {
|
||||||
const manifest = parseQqbotPluginManifest(body.manifest);
|
const pluginPackage = this.requirePackageReader().readPackage(body);
|
||||||
|
const manifest = pluginPackage.manifest;
|
||||||
const plugin = await this.pluginRepository.save({
|
const plugin = await this.pluginRepository.save({
|
||||||
pluginKey: manifest.pluginKey,
|
pluginKey: manifest.pluginKey,
|
||||||
pluginName: manifest.name,
|
pluginName: manifest.name,
|
||||||
@ -173,7 +190,7 @@ export class QqbotPluginPlatformService {
|
|||||||
});
|
});
|
||||||
const version = await this.versionRepository.save({
|
const version = await this.versionRepository.save({
|
||||||
manifestJson: manifest,
|
manifestJson: manifest,
|
||||||
packageHash: body.packageHash || 'local-dev-package',
|
packageHash: pluginPackage.packageHash,
|
||||||
pluginId: plugin.id,
|
pluginId: plugin.id,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
});
|
});
|
||||||
@ -181,7 +198,7 @@ export class QqbotPluginPlatformService {
|
|||||||
await this.persistManifestCapabilities(plugin.id, manifest);
|
await this.persistManifestCapabilities(plugin.id, manifest);
|
||||||
|
|
||||||
return this.installationRepository.save({
|
return this.installationRepository.save({
|
||||||
installedPath: body.packagePath || '',
|
installedPath: pluginPackage.packagePath,
|
||||||
pluginId: plugin.id,
|
pluginId: plugin.id,
|
||||||
runtimeStatus: 'stopped',
|
runtimeStatus: 'stopped',
|
||||||
status: 'installed',
|
status: 'installed',
|
||||||
@ -414,6 +431,7 @@ export class QqbotPluginPlatformService {
|
|||||||
) {
|
) {
|
||||||
const activeOperation = enabled;
|
const activeOperation = enabled;
|
||||||
const activeEvent = enabled;
|
const activeEvent = enabled;
|
||||||
|
const pluginKey = await this.getPluginKey(installation.pluginId);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.operationRepository.update(
|
this.operationRepository.update(
|
||||||
{ pluginId: installation.pluginId },
|
{ pluginId: installation.pluginId },
|
||||||
@ -424,6 +442,16 @@ export class QqbotPluginPlatformService {
|
|||||||
{ enabled: activeEvent },
|
{ enabled: activeEvent },
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
this.pluginRegistry?.setPluginActive(pluginKey, enabled);
|
||||||
|
this.eventPluginRegistry?.setPluginActive(pluginKey, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPluginKey(pluginId: string) {
|
||||||
|
const findOne = this.pluginRepository.findOne?.bind(
|
||||||
|
this.pluginRepository,
|
||||||
|
);
|
||||||
|
const plugin = findOne ? await findOne({ where: { id: pluginId } }) : null;
|
||||||
|
return plugin?.pluginKey || pluginId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async stopWorker(installationId: string) {
|
private async stopWorker(installationId: string) {
|
||||||
@ -501,4 +529,11 @@ export class QqbotPluginPlatformService {
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private requirePackageReader() {
|
||||||
|
if (!this.packageReader) {
|
||||||
|
throwVbenError('插件包读取器未初始化');
|
||||||
|
}
|
||||||
|
return this.packageReader;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
import type {
|
||||||
|
QqbotPlugin,
|
||||||
|
QqbotPluginInstallation,
|
||||||
|
} from '@/modules/qqbot/plugin-platform/infrastructure/persistence';
|
||||||
|
|
||||||
|
type PluginStateRow = Pick<QqbotPlugin, 'id' | 'pluginKey'>;
|
||||||
|
type InstallationStateRow = Pick<
|
||||||
|
QqbotPluginInstallation,
|
||||||
|
'pluginId' | 'status'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function resolveInactivePluginKeys(
|
||||||
|
plugins: PluginStateRow[],
|
||||||
|
installations: InstallationStateRow[],
|
||||||
|
) {
|
||||||
|
const pluginKeysById = new Map(
|
||||||
|
plugins.map((plugin) => [plugin.id, plugin.pluginKey] as const),
|
||||||
|
);
|
||||||
|
const statesByPluginKey = new Map<
|
||||||
|
string,
|
||||||
|
{ hasEnabled: boolean; hasInactive: boolean }
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const installation of installations) {
|
||||||
|
const pluginKey = pluginKeysById.get(installation.pluginId);
|
||||||
|
if (!pluginKey) continue;
|
||||||
|
const state = statesByPluginKey.get(pluginKey) || {
|
||||||
|
hasEnabled: false,
|
||||||
|
hasInactive: false,
|
||||||
|
};
|
||||||
|
if (installation.status === 'enabled') {
|
||||||
|
state.hasEnabled = true;
|
||||||
|
} else {
|
||||||
|
state.hasInactive = true;
|
||||||
|
}
|
||||||
|
statesByPluginKey.set(pluginKey, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...statesByPluginKey.entries()]
|
||||||
|
.filter(([, state]) => !state.hasEnabled && state.hasInactive)
|
||||||
|
.map(([pluginKey]) => pluginKey);
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
import { formatKtDateTime, throwVbenError } from '@/common';
|
import { formatKtDateTime, throwVbenError } from '@/common';
|
||||||
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
||||||
import type {
|
import type {
|
||||||
@ -9,36 +11,75 @@ import type {
|
|||||||
} from '@/modules/qqbot/core/contract/qqbot.types';
|
} from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
import {
|
import {
|
||||||
QqbotBuiltinPluginPackageLoaderService,
|
QqbotBuiltinPluginPackageLoaderService,
|
||||||
type QqbotRepeaterPluginPackage,
|
type QqbotEventPluginPackage,
|
||||||
} from '@/modules/qqbot/plugin-platform/infrastructure/integration/package/builtin-plugin-package-loader.service';
|
} from '@/modules/qqbot/plugin-platform/infrastructure/integration/package/builtin-plugin-package-loader.service';
|
||||||
|
import {
|
||||||
|
QqbotPlugin,
|
||||||
|
QqbotPluginInstallation,
|
||||||
|
} from '@/modules/qqbot/plugin-platform/infrastructure/persistence';
|
||||||
|
import { resolveInactivePluginKeys } from './plugin-installation-state';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotEventPluginRegistryService {
|
export class QqbotEventPluginRegistryService implements OnModuleInit {
|
||||||
private repeaterPlugin?: QqbotRepeaterPluginPackage;
|
private readonly eventPlugins = new Map<string, QqbotEventPluginPackage>();
|
||||||
|
private readonly inactivePluginKeys = new Set<string>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly accountService: QqbotAccountService,
|
private readonly accountService: QqbotAccountService,
|
||||||
private readonly builtinPluginLoader: QqbotBuiltinPluginPackageLoaderService,
|
private readonly builtinPluginLoader: QqbotBuiltinPluginPackageLoaderService,
|
||||||
|
@Optional()
|
||||||
|
@InjectRepository(QqbotPlugin)
|
||||||
|
private readonly pluginRepository?: Repository<QqbotPlugin>,
|
||||||
|
@Optional()
|
||||||
|
@InjectRepository(QqbotPluginInstallation)
|
||||||
|
private readonly installationRepository?: Repository<QqbotPluginInstallation>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.loadBuiltinEventPlugins();
|
||||||
|
await this.hydrateInactivePluginKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEventPlugin(plugin: QqbotEventPluginPackage) {
|
||||||
|
const definition = plugin.getDefinition();
|
||||||
|
if (!definition.key) {
|
||||||
|
throwVbenError('QQBot 事件插件必须包含 key');
|
||||||
|
}
|
||||||
|
if (this.eventPlugins.has(definition.key)) {
|
||||||
|
throwVbenError(`QQBot 事件插件重复:${definition.key}`);
|
||||||
|
}
|
||||||
|
this.eventPlugins.set(definition.key, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
listDefinitions(pluginKey?: string): QqbotEventPluginDefinition[] {
|
listDefinitions(pluginKey?: string): QqbotEventPluginDefinition[] {
|
||||||
return this.getDefinitions(pluginKey);
|
return this.getDefinitions(pluginKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPluginActive(pluginKey: string, active: boolean) {
|
||||||
|
if (active) {
|
||||||
|
this.inactivePluginKeys.delete(pluginKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inactivePluginKeys.add(pluginKey);
|
||||||
|
}
|
||||||
|
|
||||||
async listPlugins(selfId?: string) {
|
async listPlugins(selfId?: string) {
|
||||||
|
const plugins = this.getActiveEventPlugins();
|
||||||
const accounts = selfId
|
const accounts = selfId
|
||||||
? [await this.accountService.findBySelfId(selfId)]
|
? [await this.accountService.findBySelfId(selfId)]
|
||||||
: await this.accountService.allEnabled();
|
: await this.accountService.allEnabled();
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
accounts
|
accounts
|
||||||
.filter((account): account is NonNullable<typeof account> => !!account)
|
.filter((account): account is NonNullable<typeof account> => !!account)
|
||||||
.map((account) =>
|
.flatMap((account) =>
|
||||||
this.getRepeaterPlugin().getSummary({
|
plugins.map((plugin) =>
|
||||||
|
plugin.getSummary({
|
||||||
accountName: account.name,
|
accountName: account.name,
|
||||||
connectStatus: account.connectStatus,
|
connectStatus: account.connectStatus,
|
||||||
selfId: account.selfId,
|
selfId: account.selfId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,41 +109,80 @@ export class QqbotEventPluginRegistryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async dispatchMessage(message: QqbotNormalizedMessage) {
|
async dispatchMessage(message: QqbotNormalizedMessage) {
|
||||||
return this.getRepeaterPlugin().handleMessage(message);
|
let handled = false;
|
||||||
|
for (const plugin of this.getActiveEventPlugins()) {
|
||||||
|
const definition = plugin.getDefinition();
|
||||||
|
if (definition.triggerType !== 'message') continue;
|
||||||
|
handled = (await plugin.handleMessage(message)) || handled;
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
async bind(pluginKey: string, selfId: string) {
|
async bind(pluginKey: string, selfId: string) {
|
||||||
this.assertPlugin(pluginKey);
|
return this.requirePlugin(pluginKey).bind(selfId);
|
||||||
if (pluginKey === 'repeater') {
|
|
||||||
return this.getRepeaterPlugin().bind(selfId);
|
|
||||||
}
|
|
||||||
await this.accountService.bindEventPlugin(selfId, pluginKey);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unbind(pluginKey: string, selfId: string) {
|
async unbind(pluginKey: string, selfId: string) {
|
||||||
this.assertPlugin(pluginKey);
|
return this.requirePlugin(pluginKey).unbind(selfId);
|
||||||
if (pluginKey === 'repeater') {
|
|
||||||
return this.getRepeaterPlugin().unbind(selfId);
|
|
||||||
}
|
|
||||||
await this.accountService.unbindEventPlugin(selfId, pluginKey);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertPlugin(pluginKey: string) {
|
private requirePlugin(pluginKey: string) {
|
||||||
if (pluginKey === 'repeater') return;
|
this.ensureEventPluginsLoaded();
|
||||||
|
const plugin = this.eventPlugins.get(pluginKey);
|
||||||
|
if (!plugin) {
|
||||||
throwVbenError(`QQBot 事件插件不存在:${pluginKey}`);
|
throwVbenError(`QQBot 事件插件不存在:${pluginKey}`);
|
||||||
}
|
}
|
||||||
|
if (!this.isPluginActive(pluginKey)) {
|
||||||
|
throwVbenError(`QQBot 事件插件未启用:${pluginKey}`);
|
||||||
|
}
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
private getDefinitions(pluginKey?: string): QqbotEventPluginDefinition[] {
|
private getDefinitions(pluginKey?: string): QqbotEventPluginDefinition[] {
|
||||||
const definitions = [this.getRepeaterPlugin().getDefinition()];
|
const definitions = this.getActiveEventPlugins().map((plugin) =>
|
||||||
|
plugin.getDefinition(),
|
||||||
|
);
|
||||||
return pluginKey
|
return pluginKey
|
||||||
? definitions.filter((definition) => definition.key === pluginKey)
|
? definitions.filter((definition) => definition.key === pluginKey)
|
||||||
: definitions;
|
: definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRepeaterPlugin() {
|
private isPluginActive(pluginKey: string) {
|
||||||
this.repeaterPlugin ||= this.builtinPluginLoader.loadRepeaterPlugin();
|
return !this.inactivePluginKeys.has(pluginKey);
|
||||||
return this.repeaterPlugin;
|
}
|
||||||
|
|
||||||
|
private getActiveEventPlugins() {
|
||||||
|
this.ensureEventPluginsLoaded();
|
||||||
|
return [...this.eventPlugins.entries()]
|
||||||
|
.filter(([pluginKey]) => this.isPluginActive(pluginKey))
|
||||||
|
.map(([, plugin]) => plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureEventPluginsLoaded() {
|
||||||
|
if (this.eventPlugins.size) return;
|
||||||
|
this.loadBuiltinEventPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadBuiltinEventPlugins() {
|
||||||
|
for (const plugin of this.builtinPluginLoader.loadEventPlugins()) {
|
||||||
|
if (!this.eventPlugins.has(plugin.getDefinition().key)) {
|
||||||
|
this.registerEventPlugin(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hydrateInactivePluginKeys() {
|
||||||
|
if (!this.pluginRepository || !this.installationRepository) return;
|
||||||
|
|
||||||
|
const [plugins, installations] = await Promise.all([
|
||||||
|
this.pluginRepository.find(),
|
||||||
|
this.installationRepository.find(),
|
||||||
|
]);
|
||||||
|
for (const pluginKey of resolveInactivePluginKeys(
|
||||||
|
plugins,
|
||||||
|
installations,
|
||||||
|
)) {
|
||||||
|
this.setPluginActive(pluginKey, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
import { formatKtDateTime, throwVbenError } from '@/common';
|
import { formatKtDateTime, throwVbenError } from '@/common';
|
||||||
import type {
|
import type {
|
||||||
QqbotIntegrationPlugin,
|
QqbotIntegrationPlugin,
|
||||||
@ -8,21 +10,34 @@ import type {
|
|||||||
QqbotPluginSummary,
|
QqbotPluginSummary,
|
||||||
} from '@/modules/qqbot/core/contract/qqbot.types';
|
} from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
import { QqbotBuiltinPluginPackageLoaderService } from '@/modules/qqbot/plugin-platform/infrastructure/integration/package/builtin-plugin-package-loader.service';
|
import { QqbotBuiltinPluginPackageLoaderService } from '@/modules/qqbot/plugin-platform/infrastructure/integration/package/builtin-plugin-package-loader.service';
|
||||||
|
import {
|
||||||
|
QqbotPlugin,
|
||||||
|
QqbotPluginInstallation,
|
||||||
|
} from '@/modules/qqbot/plugin-platform/infrastructure/persistence';
|
||||||
|
import { resolveInactivePluginKeys } from './plugin-installation-state';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotPluginRegistryService implements OnModuleInit {
|
export class QqbotPluginRegistryService implements OnModuleInit {
|
||||||
|
private readonly inactivePluginKeys = new Set<string>();
|
||||||
private readonly pluginAliases = new Map<string, string>();
|
private readonly pluginAliases = new Map<string, string>();
|
||||||
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Optional()
|
@Optional()
|
||||||
private readonly builtinPluginLoader?: QqbotBuiltinPluginPackageLoaderService,
|
private readonly builtinPluginLoader?: QqbotBuiltinPluginPackageLoaderService,
|
||||||
|
@Optional()
|
||||||
|
@InjectRepository(QqbotPlugin)
|
||||||
|
private readonly pluginRepository?: Repository<QqbotPlugin>,
|
||||||
|
@Optional()
|
||||||
|
@InjectRepository(QqbotPluginInstallation)
|
||||||
|
private readonly installationRepository?: Repository<QqbotPluginInstallation>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
async onModuleInit() {
|
||||||
for (const plugin of this.builtinPluginLoader?.loadCommandPlugins() || []) {
|
for (const plugin of this.builtinPluginLoader?.loadCommandPlugins() || []) {
|
||||||
this.register(plugin);
|
this.register(plugin);
|
||||||
}
|
}
|
||||||
|
await this.hydrateInactivePluginKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
register(plugin: QqbotIntegrationPlugin) {
|
register(plugin: QqbotIntegrationPlugin) {
|
||||||
@ -39,8 +54,19 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPluginActive(pluginKey: string, active: boolean) {
|
||||||
|
const canonicalKey = this.resolveCanonicalPluginKey(pluginKey);
|
||||||
|
if (active) {
|
||||||
|
this.inactivePluginKeys.delete(canonicalKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inactivePluginKeys.add(canonicalKey);
|
||||||
|
}
|
||||||
|
|
||||||
listPlugins(): QqbotPluginSummary[] {
|
listPlugins(): QqbotPluginSummary[] {
|
||||||
return [...this.plugins.values()].map((plugin) => ({
|
return [...this.plugins.values()]
|
||||||
|
.filter((plugin) => this.isPluginActive(plugin.key))
|
||||||
|
.map((plugin) => ({
|
||||||
description: plugin.description,
|
description: plugin.description,
|
||||||
key: plugin.key,
|
key: plugin.key,
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
@ -53,6 +79,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
listOperations(pluginKey?: string): QqbotPluginOperationSummary[] {
|
listOperations(pluginKey?: string): QqbotPluginOperationSummary[] {
|
||||||
return this.getPlugins(pluginKey).flatMap((plugin) =>
|
return this.getPlugins(pluginKey).flatMap((plugin) =>
|
||||||
plugin.operations.map((operation) => ({
|
plugin.operations.map((operation) => ({
|
||||||
|
aliases: operation.aliases,
|
||||||
cacheTtlMs: operation.cacheTtlMs,
|
cacheTtlMs: operation.cacheTtlMs,
|
||||||
description: operation.description,
|
description: operation.description,
|
||||||
inputSchema: operation.inputSchema,
|
inputSchema: operation.inputSchema,
|
||||||
@ -60,6 +87,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
name: operation.name,
|
name: operation.name,
|
||||||
outputSchema: operation.outputSchema,
|
outputSchema: operation.outputSchema,
|
||||||
pluginKey: plugin.key,
|
pluginKey: plugin.key,
|
||||||
|
timeoutMs: operation.timeoutMs,
|
||||||
triggerMode: 'command',
|
triggerMode: 'command',
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@ -96,7 +124,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
context: QqbotPluginOperationContext = {},
|
context: QqbotPluginOperationContext = {},
|
||||||
) {
|
) {
|
||||||
const operation = this.getOperation(pluginKey, operationKey);
|
const operation = this.getOperation(pluginKey, operationKey);
|
||||||
return operation.execute(input, context);
|
return this.executeWithTimeout(operation, input, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertOperation(pluginKey?: string, operationKey?: string) {
|
assertOperation(pluginKey?: string, operationKey?: string) {
|
||||||
@ -109,6 +137,9 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
private getOperation(pluginKey: string, operationKey: string) {
|
private getOperation(pluginKey: string, operationKey: string) {
|
||||||
const plugin = this.getPluginByKey(pluginKey);
|
const plugin = this.getPluginByKey(pluginKey);
|
||||||
if (!plugin) throwVbenError(`QQBot 插件不存在:${pluginKey}`);
|
if (!plugin) throwVbenError(`QQBot 插件不存在:${pluginKey}`);
|
||||||
|
if (!this.isPluginActive(plugin.key)) {
|
||||||
|
throwVbenError(`QQBot 插件未启用:${plugin.key}`);
|
||||||
|
}
|
||||||
|
|
||||||
const operation = plugin.operations.find(
|
const operation = plugin.operations.find(
|
||||||
(item) => item.key === operationKey,
|
(item) => item.key === operationKey,
|
||||||
@ -119,10 +150,44 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
return operation;
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async executeWithTimeout(
|
||||||
|
operation: QqbotIntegrationPlugin['operations'][number],
|
||||||
|
input: Record<string, any>,
|
||||||
|
context: QqbotPluginOperationContext,
|
||||||
|
) {
|
||||||
|
const timeoutMs = Number(operation.timeoutMs || 0);
|
||||||
|
const execution = Promise.resolve().then(() =>
|
||||||
|
operation.execute(input, context),
|
||||||
|
);
|
||||||
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return execution;
|
||||||
|
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
const timeout = new Promise<never>((_, reject) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
throwVbenError(`QQBot 插件能力执行超时:${operation.key}`);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}, timeoutMs);
|
||||||
|
timer.unref?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await Promise.race([execution, timeout]);
|
||||||
|
} finally {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getPlugins(pluginKey?: string) {
|
private getPlugins(pluginKey?: string) {
|
||||||
if (!pluginKey) return [...this.plugins.values()];
|
if (!pluginKey) {
|
||||||
|
return [...this.plugins.values()].filter((plugin) =>
|
||||||
|
this.isPluginActive(plugin.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
const plugin = this.getPluginByKey(pluginKey);
|
const plugin = this.getPluginByKey(pluginKey);
|
||||||
return plugin ? [plugin] : [];
|
return plugin && this.isPluginActive(plugin.key) ? [plugin] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPluginByKey(pluginKey: string) {
|
private getPluginByKey(pluginKey: string) {
|
||||||
@ -131,4 +196,27 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
this.plugins.get(this.pluginAliases.get(pluginKey) || '')
|
this.plugins.get(this.pluginAliases.get(pluginKey) || '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isPluginActive(pluginKey: string) {
|
||||||
|
return !this.inactivePluginKeys.has(this.resolveCanonicalPluginKey(pluginKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveCanonicalPluginKey(pluginKey: string) {
|
||||||
|
return this.pluginAliases.get(pluginKey) || pluginKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hydrateInactivePluginKeys() {
|
||||||
|
if (!this.pluginRepository || !this.installationRepository) return;
|
||||||
|
|
||||||
|
const [plugins, installations] = await Promise.all([
|
||||||
|
this.pluginRepository.find(),
|
||||||
|
this.installationRepository.find(),
|
||||||
|
]);
|
||||||
|
for (const pluginKey of resolveInactivePluginKeys(
|
||||||
|
plugins,
|
||||||
|
installations,
|
||||||
|
)) {
|
||||||
|
this.setPluginActive(pluginKey, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export class QqbotPluginPlatformController {
|
|||||||
packagePath?: string;
|
packagePath?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return vbenSuccess(this.service.validateManifest(body));
|
return vbenSuccess(this.service.uploadPackage(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('install')
|
@Post('install')
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DictService } from '@/modules/admin/platform-config/dict/dict.service';
|
|
||||||
import type { QqbotPluginExecutionInput } from '@/modules/qqbot/core/domain/plugin-execution.port';
|
|
||||||
import {
|
|
||||||
buildQqbotFf14MarketCatalog,
|
|
||||||
buildQqbotFf14MarketCatalogFromTree,
|
|
||||||
isQqbotFf14LocationName,
|
|
||||||
parseQqbotFf14MarketPriceInput,
|
|
||||||
QQBOT_FF14_MARKET_DICT_CODES,
|
|
||||||
splitQqbotFf14WorldPath,
|
|
||||||
type QqbotFf14MarketCatalog,
|
|
||||||
} from '@/modules/qqbot/plugins/ff14-market/src';
|
|
||||||
import { parseQqbotFflogsCharacterInput } from '@/modules/qqbot/plugins/fflogs/src';
|
|
||||||
import type { QqbotPluginInputNormalizerPort } from '../../../application/argument/plugin-input-normalizer.port';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class QqbotPluginInputNormalizerService
|
|
||||||
implements QqbotPluginInputNormalizerPort
|
|
||||||
{
|
|
||||||
constructor(private readonly dictService: DictService) {}
|
|
||||||
|
|
||||||
async normalizeInput(input: QqbotPluginExecutionInput) {
|
|
||||||
const parserKey = `${input.context?.command?.parserKey || 'plain'}`.trim();
|
|
||||||
const rawArgs = `${input.input?.raw ?? input.input?.text ?? ''}`.trim();
|
|
||||||
if (parserKey === 'ff14Price') {
|
|
||||||
return {
|
|
||||||
...input.input,
|
|
||||||
...this.removeEmpty(
|
|
||||||
parseQqbotFf14MarketPriceInput(
|
|
||||||
rawArgs,
|
|
||||||
await this.getFf14MarketCatalog(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (parserKey === 'fflogsCharacter') {
|
|
||||||
const catalog = await this.getFf14MarketCatalog();
|
|
||||||
return {
|
|
||||||
...input.input,
|
|
||||||
...this.removeEmpty(
|
|
||||||
parseQqbotFflogsCharacterInput(rawArgs, {
|
|
||||||
resolveKnownWorld: (candidate) => {
|
|
||||||
if (!isQqbotFf14LocationName(catalog, candidate)) return null;
|
|
||||||
const worldPath = splitQqbotFf14WorldPath(candidate);
|
|
||||||
return { serverSlug: worldPath.world || candidate };
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return input.input;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getFf14MarketCatalog(): Promise<QqbotFf14MarketCatalog> {
|
|
||||||
const treeCatalog = buildQqbotFf14MarketCatalogFromTree(
|
|
||||||
await this.dictService.relationTree({
|
|
||||||
dictCode: QQBOT_FF14_MARKET_DICT_CODES.region,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (treeCatalog.dataCenters.length > 0) return treeCatalog;
|
|
||||||
|
|
||||||
const [regions, dataCenters, worlds] = await Promise.all([
|
|
||||||
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.region),
|
|
||||||
this.dictService.getDictItemsByKey(
|
|
||||||
QQBOT_FF14_MARKET_DICT_CODES.dataCenter,
|
|
||||||
),
|
|
||||||
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.world),
|
|
||||||
]);
|
|
||||||
return buildQqbotFf14MarketCatalog({
|
|
||||||
dataCenters,
|
|
||||||
regions,
|
|
||||||
worlds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeEmpty(input: Record<string, any>) {
|
|
||||||
return Object.entries(input).reduce<Record<string, any>>(
|
|
||||||
(result, [key, value]) => {
|
|
||||||
if (value !== undefined && value !== '') result[key] = value;
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,23 +7,49 @@ import { ToolsService } from '@/common';
|
|||||||
import { DictService } from '@/modules/admin/platform-config/dict/dict.service';
|
import { DictService } from '@/modules/admin/platform-config/dict/dict.service';
|
||||||
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
||||||
import { QqbotSendService } from '@/modules/qqbot/core/application/send/qqbot-send.service';
|
import { QqbotSendService } from '@/modules/qqbot/core/application/send/qqbot-send.service';
|
||||||
import type { QqbotIntegrationPlugin } from '@/modules/qqbot/core/contract/qqbot.types';
|
import type {
|
||||||
|
QqbotEventPluginDefinition,
|
||||||
|
QqbotIntegrationPlugin,
|
||||||
|
QqbotNormalizedMessage,
|
||||||
|
} from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
import {
|
import {
|
||||||
parseQqbotPluginManifest,
|
parseQqbotPluginManifest,
|
||||||
type QqbotPluginManifest,
|
type QqbotPluginManifest,
|
||||||
} from '@/modules/qqbot/plugin-platform/domain/manifest';
|
} from '@/modules/qqbot/plugin-platform/domain/manifest';
|
||||||
import { QqbotPluginHttpClientService } from '@/modules/qqbot/plugin-platform/infrastructure/integration/sdk';
|
import { QqbotPluginHttpClientService } from '@/modules/qqbot/plugin-platform/infrastructure/integration/sdk';
|
||||||
import { createPlugin as createBangDreamPlugin } from '@/modules/qqbot/plugins/bangdream/src';
|
import { createPlugin as createBangDreamPlugin } from '@/modules/qqbot/plugins/bangdream/src';
|
||||||
import type { BangDreamManifestOperation } from '@/modules/qqbot/plugins/bangdream/src';
|
import {
|
||||||
|
buildFf14MarketCatalog,
|
||||||
|
buildFf14MarketCatalogFromTree,
|
||||||
|
isFf14LocationName,
|
||||||
|
QQBOT_FF14_MARKET_DICT_CODES,
|
||||||
|
splitFf14WorldPath,
|
||||||
|
} from '@/modules/qqbot/plugins/ff14-market/src/domain/ff14-worlds';
|
||||||
|
import type {
|
||||||
|
BangDreamOperationHandlerName,
|
||||||
|
BangDreamOperationKey,
|
||||||
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream.types';
|
||||||
import type { BangDreamRuntimeIo } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
import type { BangDreamRuntimeIo } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
||||||
import { createPlugin as createFf14MarketPlugin } from '@/modules/qqbot/plugins/ff14-market/src';
|
import { createPlugin as createFf14MarketPlugin } from '@/modules/qqbot/plugins/ff14-market/src';
|
||||||
import { createPlugin as createFflogsPlugin } from '@/modules/qqbot/plugins/fflogs/src';
|
import { createPlugin as createFflogsPlugin } from '@/modules/qqbot/plugins/fflogs/src';
|
||||||
import { createPlugin as createRepeaterPlugin } from '@/modules/qqbot/plugins/repeater/src';
|
import { createPlugin as createRepeaterPlugin } from '@/modules/qqbot/plugins/repeater/src';
|
||||||
|
|
||||||
export type QqbotRepeaterPluginPackage = ReturnType<
|
export type RepeaterPluginPackage = ReturnType<
|
||||||
typeof createRepeaterPlugin
|
typeof createRepeaterPlugin
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type QqbotEventPluginPackage = {
|
||||||
|
bind(selfId: string): Promise<boolean> | boolean;
|
||||||
|
getDefinition(): QqbotEventPluginDefinition;
|
||||||
|
getSummary(input: {
|
||||||
|
accountName?: string;
|
||||||
|
connectStatus?: string;
|
||||||
|
selfId: string;
|
||||||
|
}): Promise<unknown> | unknown;
|
||||||
|
handleMessage(message: QqbotNormalizedMessage): Promise<boolean> | boolean;
|
||||||
|
unbind(selfId: string): Promise<boolean> | boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotBuiltinPluginPackageLoaderService {
|
export class QqbotBuiltinPluginPackageLoaderService {
|
||||||
private readonly logger = new Logger(
|
private readonly logger = new Logger(
|
||||||
@ -47,7 +73,11 @@ export class QqbotBuiltinPluginPackageLoaderService {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
loadRepeaterPlugin(): QqbotRepeaterPluginPackage {
|
loadEventPlugins(): QqbotEventPluginPackage[] {
|
||||||
|
return [this.loadRepeaterPlugin()];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRepeaterPlugin(): RepeaterPluginPackage {
|
||||||
const manifest = this.loadManifest('repeater');
|
const manifest = this.loadManifest('repeater');
|
||||||
return createRepeaterPlugin({
|
return createRepeaterPlugin({
|
||||||
host: {
|
host: {
|
||||||
@ -86,13 +116,15 @@ export class QqbotBuiltinPluginPackageLoaderService {
|
|||||||
normalizeError: (error) =>
|
normalizeError: (error) =>
|
||||||
this.toolsService.getErrorMessage(error, 'BangDream 命令执行失败'),
|
this.toolsService.getErrorMessage(error, 'BangDream 命令执行失败'),
|
||||||
operations: manifest.operations.map((operation) => ({
|
operations: manifest.operations.map((operation) => ({
|
||||||
|
aliases: operation.aliases,
|
||||||
description: operation.description,
|
description: operation.description,
|
||||||
handlerName: operation.handlerName,
|
handlerName: operation.handlerName as BangDreamOperationHandlerName,
|
||||||
inputSchema: operation.inputSchema,
|
inputSchema: operation.inputSchema,
|
||||||
key: operation.key,
|
key: operation.key as BangDreamOperationKey,
|
||||||
name: operation.name,
|
name: operation.name,
|
||||||
outputSchema: operation.outputSchema,
|
outputSchema: operation.outputSchema,
|
||||||
})) as BangDreamManifestOperation[],
|
timeoutMs: operation.timeoutMs,
|
||||||
|
})),
|
||||||
pluginKey: manifest.pluginKey,
|
pluginKey: manifest.pluginKey,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
}) as QqbotIntegrationPlugin;
|
}) as QqbotIntegrationPlugin;
|
||||||
@ -120,6 +152,7 @@ export class QqbotBuiltinPluginPackageLoaderService {
|
|||||||
host: {
|
host: {
|
||||||
getConfig: (key) => this.configService.get(key),
|
getConfig: (key) => this.configService.get(key),
|
||||||
getDictByKey: (dictCode) => this.dictService.getDictByKey(dictCode),
|
getDictByKey: (dictCode) => this.dictService.getDictByKey(dictCode),
|
||||||
|
resolveKnownWorld: (candidate) => this.resolveKnownFf14World(candidate),
|
||||||
requestJson: (options) => this.httpClient.requestJson(options),
|
requestJson: (options) => this.httpClient.requestJson(options),
|
||||||
},
|
},
|
||||||
manifest,
|
manifest,
|
||||||
@ -171,6 +204,35 @@ export class QqbotBuiltinPluginPackageLoaderService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveKnownFf14World(candidate: string) {
|
||||||
|
const catalog = await this.loadFf14MarketCatalog();
|
||||||
|
if (!isFf14LocationName(catalog, candidate)) return null;
|
||||||
|
const worldPath = splitFf14WorldPath(candidate);
|
||||||
|
return { serverSlug: worldPath.world || candidate };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFf14MarketCatalog() {
|
||||||
|
const treeCatalog = buildFf14MarketCatalogFromTree(
|
||||||
|
await this.dictService.relationTree({
|
||||||
|
dictCode: QQBOT_FF14_MARKET_DICT_CODES.region,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (treeCatalog.dataCenters.length > 0) return treeCatalog;
|
||||||
|
|
||||||
|
const [regions, dataCenters, worlds] = await Promise.all([
|
||||||
|
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.region),
|
||||||
|
this.dictService.getDictItemsByKey(
|
||||||
|
QQBOT_FF14_MARKET_DICT_CODES.dataCenter,
|
||||||
|
),
|
||||||
|
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.world),
|
||||||
|
]);
|
||||||
|
return buildFf14MarketCatalog({
|
||||||
|
dataCenters,
|
||||||
|
regions,
|
||||||
|
worlds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private resolvePluginRoot(pluginKey: string) {
|
private resolvePluginRoot(pluginKey: string) {
|
||||||
const sourceRoot = join(
|
const sourceRoot = join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
|
|||||||
@ -0,0 +1,192 @@
|
|||||||
|
import { createHash } from 'crypto';
|
||||||
|
import { existsSync, readFileSync, statSync } from 'fs';
|
||||||
|
import { dirname, extname, isAbsolute, relative, resolve } from 'path';
|
||||||
|
import { Injectable, Optional } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwVbenError } from '@/common';
|
||||||
|
import {
|
||||||
|
parseQqbotPluginManifest,
|
||||||
|
type QqbotPluginManifest,
|
||||||
|
} from '@/modules/qqbot/plugin-platform/domain/manifest';
|
||||||
|
|
||||||
|
type PluginPackageBody = {
|
||||||
|
packageHash?: string;
|
||||||
|
packagePath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PackedPluginFile = {
|
||||||
|
path: string;
|
||||||
|
sha256: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PackedPluginPackage = {
|
||||||
|
contentHash?: unknown;
|
||||||
|
files?: unknown;
|
||||||
|
manifest?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QqbotValidatedPluginPackage = {
|
||||||
|
manifest: QqbotPluginManifest;
|
||||||
|
packageHash: string;
|
||||||
|
packagePath: string;
|
||||||
|
packageSizeBytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PACKAGE_EXTENSION = '.qqbot-plugin.json';
|
||||||
|
const DEFAULT_MAX_PACKAGE_BYTES = 20 * 1024 * 1024;
|
||||||
|
|
||||||
|
const sha256 = (content: Buffer | string) =>
|
||||||
|
createHash('sha256').update(content).digest('hex');
|
||||||
|
|
||||||
|
const stableStringify = (value: unknown): string => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
return `{${Object.keys(record)
|
||||||
|
.filter((key) => record[key] !== undefined)
|
||||||
|
.sort()
|
||||||
|
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
||||||
|
.join(',')}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInsideDirectory = (parent: string, child: string) => {
|
||||||
|
const relativePath = relative(parent, child);
|
||||||
|
return (
|
||||||
|
relativePath === '' ||
|
||||||
|
(!!relativePath &&
|
||||||
|
!relativePath.startsWith('..') &&
|
||||||
|
!isAbsolute(relativePath))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QqbotPluginPackageReaderService {
|
||||||
|
constructor(
|
||||||
|
@Optional()
|
||||||
|
private readonly configService?: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
readPackage(body: PluginPackageBody): QqbotValidatedPluginPackage {
|
||||||
|
const packagePath = this.resolvePackagePath(body.packagePath);
|
||||||
|
const packageSizeBytes = statSync(packagePath).size;
|
||||||
|
const maxPackageBytes = this.getMaxPackageBytes();
|
||||||
|
|
||||||
|
if (packageSizeBytes > maxPackageBytes) {
|
||||||
|
throwVbenError('QQBot 插件包超过大小限制');
|
||||||
|
}
|
||||||
|
|
||||||
|
let packedPlugin: PackedPluginPackage;
|
||||||
|
try {
|
||||||
|
packedPlugin = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
throwVbenError('QQBot 插件包不是合法 JSON', undefined, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = this.normalizePackageFiles(packedPlugin.files);
|
||||||
|
const contentHash = this.normalizeContentHash(packedPlugin.contentHash);
|
||||||
|
const expectedHash = sha256(
|
||||||
|
stableStringify({
|
||||||
|
files,
|
||||||
|
manifest: packedPlugin.manifest,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contentHash !== expectedHash) {
|
||||||
|
throwVbenError('QQBot 插件包 hash 校验失败');
|
||||||
|
}
|
||||||
|
if (body.packageHash && body.packageHash !== expectedHash) {
|
||||||
|
throwVbenError('QQBot 插件包 hash 与请求不一致');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
manifest: parseQqbotPluginManifest(packedPlugin.manifest, {
|
||||||
|
pluginRoot: dirname(packagePath),
|
||||||
|
}),
|
||||||
|
packageHash: expectedHash,
|
||||||
|
packagePath,
|
||||||
|
packageSizeBytes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePackagePath(packagePath?: string) {
|
||||||
|
if (!packagePath) throwVbenError('请选择插件包路径');
|
||||||
|
|
||||||
|
const resolvedPath = resolve(packagePath);
|
||||||
|
const controlledRoots = this.getControlledRoots();
|
||||||
|
const allowed = controlledRoots.some((root) =>
|
||||||
|
isInsideDirectory(root, resolvedPath),
|
||||||
|
);
|
||||||
|
if (!allowed) {
|
||||||
|
throwVbenError('插件包路径不在受控目录内');
|
||||||
|
}
|
||||||
|
if (!existsSync(resolvedPath)) {
|
||||||
|
throwVbenError('插件包文件不存在');
|
||||||
|
}
|
||||||
|
if (!resolvedPath.endsWith(PACKAGE_EXTENSION)) {
|
||||||
|
throwVbenError('插件包文件扩展名不合法');
|
||||||
|
}
|
||||||
|
if (!statSync(resolvedPath).isFile()) {
|
||||||
|
throwVbenError('插件包路径不是文件');
|
||||||
|
}
|
||||||
|
if (extname(resolvedPath) !== '.json') {
|
||||||
|
throwVbenError('插件包文件扩展名不合法');
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getControlledRoots() {
|
||||||
|
const configuredRoots = [
|
||||||
|
this.configService?.get<string>('QQBOT_PLUGIN_PACKAGE_ROOT'),
|
||||||
|
this.configService?.get<string>('QQBOT_PLUGIN_PACKAGE_ROOTS'),
|
||||||
|
]
|
||||||
|
.filter((value): value is string => !!value)
|
||||||
|
.flatMap((value) => value.split(/[;,]/))
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const defaultRoots = [
|
||||||
|
resolve(process.cwd(), '.kt-workspace', 'qqbot-plugin-packages'),
|
||||||
|
resolve(process.cwd(), 'src', 'modules', 'qqbot', 'plugins'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...configuredRoots, ...defaultRoots].map((root) => resolve(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMaxPackageBytes() {
|
||||||
|
const configured = Number(
|
||||||
|
this.configService?.get<string>('QQBOT_PLUGIN_PACKAGE_MAX_BYTES'),
|
||||||
|
);
|
||||||
|
return Number.isFinite(configured) && configured > 0
|
||||||
|
? configured
|
||||||
|
: DEFAULT_MAX_PACKAGE_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePackageFiles(files: unknown): PackedPluginFile[] {
|
||||||
|
if (!Array.isArray(files)) {
|
||||||
|
throwVbenError('QQBot 插件包文件清单不合法');
|
||||||
|
}
|
||||||
|
return (files as unknown[]).map((file) => {
|
||||||
|
const record = file as Partial<PackedPluginFile>;
|
||||||
|
if (typeof record.path !== 'string' || typeof record.sha256 !== 'string') {
|
||||||
|
throwVbenError('QQBot 插件包文件清单不合法');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: record.path,
|
||||||
|
sha256: record.sha256,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeContentHash(contentHash: unknown) {
|
||||||
|
if (typeof contentHash !== 'string' || !contentHash) {
|
||||||
|
throwVbenError('QQBot 插件包缺少 contentHash');
|
||||||
|
}
|
||||||
|
return contentHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { throwVbenError } from '@/common';
|
||||||
|
import {
|
||||||
|
QqbotBuiltinPluginPackageLoaderService,
|
||||||
|
type QqbotEventPluginPackage,
|
||||||
|
} from '../package/builtin-plugin-package-loader.service';
|
||||||
|
import type {
|
||||||
|
QqbotIntegrationPlugin,
|
||||||
|
QqbotPluginOperation,
|
||||||
|
QqbotNormalizedMessage,
|
||||||
|
} from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
|
import type {
|
||||||
|
QqbotPluginRuntimeFactory,
|
||||||
|
} from '@/modules/qqbot/plugin-platform/application/plugin-platform.service';
|
||||||
|
import type {
|
||||||
|
QqbotPluginInstallation,
|
||||||
|
QqbotPluginVersion,
|
||||||
|
} from '@/modules/qqbot/plugin-platform/infrastructure/persistence';
|
||||||
|
import {
|
||||||
|
QqbotPluginWorkerRuntime,
|
||||||
|
} from './worker-runtime';
|
||||||
|
import type {
|
||||||
|
QqbotPluginWorkerDriver,
|
||||||
|
QqbotPluginWorkerRequest,
|
||||||
|
} from './worker-runtime.types';
|
||||||
|
|
||||||
|
type RuntimeCommandPlugin = QqbotIntegrationPlugin & {
|
||||||
|
activate?: () => Promise<unknown> | unknown;
|
||||||
|
dispose?: () => Promise<unknown> | unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QqbotBuiltinPluginWorkerRuntimeFactoryService
|
||||||
|
implements QqbotPluginRuntimeFactory
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly pluginLoader: QqbotBuiltinPluginPackageLoaderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(
|
||||||
|
installation: QqbotPluginInstallation,
|
||||||
|
version: QqbotPluginVersion,
|
||||||
|
) {
|
||||||
|
const pluginKey = getManifestPluginKey(version.manifestJson);
|
||||||
|
return new QqbotPluginWorkerRuntime(
|
||||||
|
new QqbotBuiltinPluginWorkerDriver(this.pluginLoader, pluginKey),
|
||||||
|
{
|
||||||
|
defaultTimeoutMs: getDefaultRuntimeTimeout(version.manifestJson),
|
||||||
|
installationId: installation.id,
|
||||||
|
pluginKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QqbotBuiltinPluginWorkerDriver implements QqbotPluginWorkerDriver {
|
||||||
|
private commandPlugin?: RuntimeCommandPlugin;
|
||||||
|
private eventPlugin?: QqbotEventPluginPackage;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly pluginLoader: QqbotBuiltinPluginPackageLoaderService,
|
||||||
|
private readonly pluginKey: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async request(message: QqbotPluginWorkerRequest): Promise<unknown> {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'load':
|
||||||
|
return this.load();
|
||||||
|
case 'activate':
|
||||||
|
await this.commandPlugin?.activate?.();
|
||||||
|
return { ok: true };
|
||||||
|
case 'health':
|
||||||
|
return this.health();
|
||||||
|
case 'executeOperation':
|
||||||
|
return this.executeOperation(message);
|
||||||
|
case 'handleEvent':
|
||||||
|
return this.handleEvent(message);
|
||||||
|
case 'deactivate':
|
||||||
|
return { ok: true };
|
||||||
|
case 'dispose':
|
||||||
|
await this.commandPlugin?.dispose?.();
|
||||||
|
return { ok: true };
|
||||||
|
default:
|
||||||
|
throwVbenError(`未知插件运行时请求:${message.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
await this.commandPlugin?.dispose?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
private load() {
|
||||||
|
this.commandPlugin = this.pluginLoader
|
||||||
|
.loadCommandPlugins()
|
||||||
|
.find((plugin) => plugin.key === this.pluginKey) as
|
||||||
|
| RuntimeCommandPlugin
|
||||||
|
| undefined;
|
||||||
|
this.eventPlugin = this.pluginLoader
|
||||||
|
.loadEventPlugins()
|
||||||
|
.find((plugin) => plugin.getDefinition().key === this.pluginKey);
|
||||||
|
|
||||||
|
if (!this.commandPlugin && !this.eventPlugin) {
|
||||||
|
throwVbenError(`QQBot 插件运行时不存在:${this.pluginKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
pluginKey: this.pluginKey,
|
||||||
|
triggerMode: this.commandPlugin ? 'command' : 'event',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async health() {
|
||||||
|
if (this.commandPlugin?.healthCheck) {
|
||||||
|
return this.commandPlugin.healthCheck();
|
||||||
|
}
|
||||||
|
if (this.eventPlugin) {
|
||||||
|
return {
|
||||||
|
message: this.eventPlugin.getDefinition().remark || '事件插件可用',
|
||||||
|
status: 'healthy',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throwVbenError(`QQBot 插件运行时未加载:${this.pluginKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeOperation(message: QqbotPluginWorkerRequest) {
|
||||||
|
const operation = this.commandPlugin?.operations.find(
|
||||||
|
(item: QqbotPluginOperation) => item.key === message.operationKey,
|
||||||
|
);
|
||||||
|
if (!operation) {
|
||||||
|
throwVbenError(`QQBot 插件能力不存在:${message.operationKey}`);
|
||||||
|
}
|
||||||
|
return operation.execute(
|
||||||
|
(message.input || {}) as Record<string, unknown>,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(message: QqbotPluginWorkerRequest) {
|
||||||
|
if (!this.eventPlugin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const definition = this.eventPlugin.getDefinition();
|
||||||
|
if (message.eventKey && message.eventKey !== definition.key) return false;
|
||||||
|
if (definition.triggerType !== 'message') return false;
|
||||||
|
return this.eventPlugin.handleMessage(
|
||||||
|
(message.event || {}) as QqbotNormalizedMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManifestPluginKey(manifest: unknown) {
|
||||||
|
const pluginKey =
|
||||||
|
typeof manifest === 'object' && manifest
|
||||||
|
? (manifest as { pluginKey?: unknown }).pluginKey
|
||||||
|
: null;
|
||||||
|
if (typeof pluginKey === 'string' && pluginKey) return pluginKey;
|
||||||
|
throwVbenError('插件 manifest 缺少 pluginKey,无法创建运行时');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultRuntimeTimeout(manifest: unknown) {
|
||||||
|
const runtime =
|
||||||
|
typeof manifest === 'object' && manifest
|
||||||
|
? (manifest as { runtime?: { timeoutMs?: unknown } }).runtime
|
||||||
|
: null;
|
||||||
|
const timeoutMs = Number(runtime?.timeoutMs || 30_000);
|
||||||
|
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 30_000;
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './worker-runtime';
|
export * from './worker-runtime';
|
||||||
export * from './worker-runtime.types';
|
export * from './worker-runtime.types';
|
||||||
|
export * from './builtin-plugin-worker-runtime.factory';
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { DictModule } from '@/modules/admin/platform-config/dict/dict.module';
|
|||||||
import { QQBOT_PLUGIN_EXECUTION_PORT } from '@/modules/qqbot/core/domain/plugin-execution.port';
|
import { QQBOT_PLUGIN_EXECUTION_PORT } from '@/modules/qqbot/core/domain/plugin-execution.port';
|
||||||
import { QqbotCoreModule } from '@/modules/qqbot/core/qqbot-core.module';
|
import { QqbotCoreModule } from '@/modules/qqbot/core/qqbot-core.module';
|
||||||
import { QqbotPluginArgumentParserService } from './application/argument/qqbot-plugin-argument-parser.service';
|
import { QqbotPluginArgumentParserService } from './application/argument/qqbot-plugin-argument-parser.service';
|
||||||
import { QQBOT_PLUGIN_INPUT_NORMALIZER } from './application/argument/plugin-input-normalizer.port';
|
|
||||||
import { QqbotEventPluginRegistryService } from './application/registry/qqbot-event-plugin-registry.service';
|
import { QqbotEventPluginRegistryService } from './application/registry/qqbot-event-plugin-registry.service';
|
||||||
import { QqbotPluginRegistryService } from './application/registry/qqbot-plugin-registry.service';
|
import { QqbotPluginRegistryService } from './application/registry/qqbot-plugin-registry.service';
|
||||||
import { QqbotPluginExecutionAdapter } from './application/plugin-execution.adapter';
|
import { QqbotPluginExecutionAdapter } from './application/plugin-execution.adapter';
|
||||||
@ -14,9 +13,11 @@ import { QqbotPluginPlatformService } from './application/plugin-platform.servic
|
|||||||
import { QqbotPluginPlatformController } from './contract/plugin-platform.controller';
|
import { QqbotPluginPlatformController } from './contract/plugin-platform.controller';
|
||||||
import { QqbotPluginController } from './contract/qqbot-plugin.controller';
|
import { QqbotPluginController } from './contract/qqbot-plugin.controller';
|
||||||
import { QQBOT_PLUGIN_PLATFORM_ENTITIES } from './infrastructure/persistence';
|
import { QQBOT_PLUGIN_PLATFORM_ENTITIES } from './infrastructure/persistence';
|
||||||
import { QqbotPluginInputNormalizerService } from './infrastructure/integration/argument/plugin-input-normalizer.service';
|
|
||||||
import { QqbotBuiltinPluginPackageLoaderService } from './infrastructure/integration/package/builtin-plugin-package-loader.service';
|
import { QqbotBuiltinPluginPackageLoaderService } from './infrastructure/integration/package/builtin-plugin-package-loader.service';
|
||||||
|
import { QqbotPluginPackageReaderService } from './infrastructure/integration/package/plugin-package-reader.service';
|
||||||
import { QqbotPluginHttpClientService } from './infrastructure/integration/sdk';
|
import { QqbotPluginHttpClientService } from './infrastructure/integration/sdk';
|
||||||
|
import { QqbotBuiltinPluginWorkerRuntimeFactoryService } from './infrastructure/integration/runtime';
|
||||||
|
import { QQBOT_PLUGIN_RUNTIME_FACTORY } from './application/plugin-platform.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [QqbotPluginController, QqbotPluginPlatformController],
|
controllers: [QqbotPluginController, QqbotPluginPlatformController],
|
||||||
@ -36,16 +37,17 @@ import { QqbotPluginHttpClientService } from './infrastructure/integration/sdk';
|
|||||||
QqbotEventPluginRegistryService,
|
QqbotEventPluginRegistryService,
|
||||||
QqbotPluginArgumentParserService,
|
QqbotPluginArgumentParserService,
|
||||||
QqbotPluginExecutionAdapter,
|
QqbotPluginExecutionAdapter,
|
||||||
QqbotPluginInputNormalizerService,
|
|
||||||
QqbotBuiltinPluginPackageLoaderService,
|
QqbotBuiltinPluginPackageLoaderService,
|
||||||
{
|
QqbotBuiltinPluginWorkerRuntimeFactoryService,
|
||||||
provide: QQBOT_PLUGIN_INPUT_NORMALIZER,
|
QqbotPluginPackageReaderService,
|
||||||
useExisting: QqbotPluginInputNormalizerService,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: QQBOT_PLUGIN_EXECUTION_PORT,
|
provide: QQBOT_PLUGIN_EXECUTION_PORT,
|
||||||
useExisting: QqbotPluginExecutionAdapter,
|
useExisting: QqbotPluginExecutionAdapter,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: QQBOT_PLUGIN_RUNTIME_FACTORY,
|
||||||
|
useExisting: QqbotBuiltinPluginWorkerRuntimeFactoryService,
|
||||||
|
},
|
||||||
QqbotPluginHttpClientService,
|
QqbotPluginHttpClientService,
|
||||||
QqbotPluginPlatformService,
|
QqbotPluginPlatformService,
|
||||||
QqbotPluginRegistryService,
|
QqbotPluginRegistryService,
|
||||||
|
|||||||
@ -12,14 +12,14 @@ import {
|
|||||||
fuzzySearch,
|
fuzzySearch,
|
||||||
type FuzzySearchResult,
|
type FuzzySearchResult,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/search/fuzzy-search';
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/search/fuzzy-search';
|
||||||
import mainAPI, {
|
import bangdreamCatalogCache, {
|
||||||
waitForMainDataReady,
|
waitForBangDreamCatalogReady,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
} from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import type {
|
import type {
|
||||||
QqbotBangDreamCommandInput,
|
BangDreamCommandInput,
|
||||||
QqbotBangDreamCommandOutput,
|
BangDreamCommandOutput,
|
||||||
QqbotBangDreamOperationKey,
|
BangDreamOperationKey,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/qqbot-bangdream.types';
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream.types';
|
||||||
|
|
||||||
const SOURCE_NAME = 'BangDream 内置插件';
|
const SOURCE_NAME = 'BangDream 内置插件';
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ export class BangDreamCommandContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkHealth() {
|
async checkHealth() {
|
||||||
await waitForMainDataReady();
|
await waitForBangDreamCatalogReady();
|
||||||
const data = mainAPI as { cards?: unknown; songs?: unknown };
|
const data = bangdreamCatalogCache as { cards?: unknown; songs?: unknown };
|
||||||
if (!data.songs || !data.cards) {
|
if (!data.songs || !data.cards) {
|
||||||
throw new Error('BangDream 数据配置未加载');
|
throw new Error('BangDream 数据配置未加载');
|
||||||
}
|
}
|
||||||
@ -79,10 +79,10 @@ export class BangDreamCommandContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toImageReply(
|
toImageReply(
|
||||||
operationKey: QqbotBangDreamOperationKey,
|
operationKey: BangDreamOperationKey,
|
||||||
query: string,
|
query: string,
|
||||||
list: Array<Buffer | string>,
|
list: Array<Buffer | string>,
|
||||||
): QqbotBangDreamCommandOutput {
|
): BangDreamCommandOutput {
|
||||||
const images = list.filter((item): item is Buffer => Buffer.isBuffer(item));
|
const images = list.filter((item): item is Buffer => Buffer.isBuffer(item));
|
||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
const message =
|
const message =
|
||||||
@ -102,7 +102,7 @@ export class BangDreamCommandContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRenderOptions(
|
getRenderOptions(
|
||||||
input: QqbotBangDreamCommandInput,
|
input: BangDreamCommandInput,
|
||||||
defaults: { useEasyBG?: boolean } = {},
|
defaults: { useEasyBG?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@ -125,7 +125,7 @@ export class BangDreamCommandContext {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pickDisplayedServerList(input: QqbotBangDreamCommandInput) {
|
pickDisplayedServerList(input: BangDreamCommandInput) {
|
||||||
const source =
|
const source =
|
||||||
input.displayedServerList ||
|
input.displayedServerList ||
|
||||||
this.readConfig(BANGDREAM_TSUGU_ENV_KEYS.displayedServers);
|
this.readConfig(BANGDREAM_TSUGU_ENV_KEYS.displayedServers);
|
||||||
@ -138,7 +138,7 @@ export class BangDreamCommandContext {
|
|||||||
return servers.length > 0 ? [...new Set(servers)] : defaultServers;
|
return servers.length > 0 ? [...new Set(servers)] : defaultServers;
|
||||||
}
|
}
|
||||||
|
|
||||||
pickMainServer(input: QqbotBangDreamCommandInput, tokens: string[]): Server {
|
pickMainServer(input: BangDreamCommandInput, tokens: string[]): Server {
|
||||||
const explicit = this.firstDefined(
|
const explicit = this.firstDefined(
|
||||||
input.mainServer,
|
input.mainServer,
|
||||||
input.serverName,
|
input.serverName,
|
||||||
@ -171,24 +171,24 @@ export class BangDreamCommandContext {
|
|||||||
return server === undefined ? undefined : (server as Server);
|
return server === undefined ? undefined : (server as Server);
|
||||||
}
|
}
|
||||||
|
|
||||||
requireText(input: QqbotBangDreamCommandInput, message: string) {
|
requireText(input: BangDreamCommandInput, message: string) {
|
||||||
const text = this.pickText(input);
|
const text = this.pickText(input);
|
||||||
if (!text) throw new Error(message);
|
if (!text) throw new Error(message);
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
pickText(input: QqbotBangDreamCommandInput) {
|
pickText(input: BangDreamCommandInput) {
|
||||||
return `${input.query || input.text || input.raw || ''}`.trim();
|
return `${input.query || input.text || input.raw || ''}`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTokens(input: QqbotBangDreamCommandInput) {
|
getTokens(input: BangDreamCommandInput) {
|
||||||
if (Array.isArray(input.args)) {
|
if (Array.isArray(input.args)) {
|
||||||
return input.args.map((item) => `${item}`.trim()).filter(Boolean);
|
return input.args.map((item) => `${item}`.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
return this.pickText(input).split(/\s+/).filter(Boolean);
|
return this.pickText(input).split(/\s+/).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
firstToken(input: QqbotBangDreamCommandInput) {
|
firstToken(input: BangDreamCommandInput) {
|
||||||
return this.getTokens(input)[0];
|
return this.getTokens(input)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,138 @@
|
|||||||
|
import { bestdoriApiPath } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
|
import { bangdreamStaticPatchProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/static-patch.provider';
|
||||||
|
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
||||||
|
import {
|
||||||
|
BANGDREAM_TSUGU_ENV_KEYS,
|
||||||
|
normalizeBangDreamPositiveInteger,
|
||||||
|
} from '@/modules/qqbot/plugins/bangdream/src/config/runtime-options';
|
||||||
|
import {
|
||||||
|
readBangDreamRuntimeConfig,
|
||||||
|
sleepBangDreamRuntime,
|
||||||
|
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
||||||
|
|
||||||
|
const bangdreamCatalogCache: Record<string, any> = {};
|
||||||
|
const REQUIRED_CATALOG_KEYS = [
|
||||||
|
'cards',
|
||||||
|
'characters',
|
||||||
|
'events',
|
||||||
|
'gacha',
|
||||||
|
'songs',
|
||||||
|
];
|
||||||
|
const DEFAULT_CATALOG_READY_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
|
function getCatalogReadyTimeoutMs(): number {
|
||||||
|
return normalizeBangDreamPositiveInteger(
|
||||||
|
readBangDreamRuntimeConfig(BANGDREAM_TSUGU_ENV_KEYS.mainDataReadyTimeoutMs),
|
||||||
|
DEFAULT_CATALOG_READY_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCatalogReady(): boolean {
|
||||||
|
return REQUIRED_CATALOG_KEYS.every((key) => {
|
||||||
|
const collection = bangdreamCatalogCache[key];
|
||||||
|
return collection && Object.keys(collection).length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rejectAfter(ms: number): Promise<never> {
|
||||||
|
await sleepBangDreamRuntime(ms);
|
||||||
|
throw new Error(`BangDream 主数据首次加载超时:${ms}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 BangDream 领域目录数据。
|
||||||
|
*
|
||||||
|
* @param useCache - use缓存参数,未传入时使用默认值。
|
||||||
|
*/
|
||||||
|
async function loadCatalogData(useCache: boolean = false) {
|
||||||
|
logger('catalog', 'loading catalog...');
|
||||||
|
const promiseAll = Object.keys(bestdoriApiPath).map(async (key) => {
|
||||||
|
if (useCache) {
|
||||||
|
return (bangdreamCatalogCache[key] =
|
||||||
|
await bangdreamBestdoriProvider.getJson(bestdoriApiPath[key], {
|
||||||
|
cacheTime: 1 / 0,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return (bangdreamCatalogCache[key] =
|
||||||
|
await bangdreamBestdoriProvider.getJson(bestdoriApiPath[key]));
|
||||||
|
} catch {
|
||||||
|
logger('catalog', `load ${key} failed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promiseAll);
|
||||||
|
|
||||||
|
const cardsCnFix =
|
||||||
|
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
|
||||||
|
'cards-cn-fix.json',
|
||||||
|
);
|
||||||
|
for (const key in cardsCnFix) {
|
||||||
|
bangdreamCatalogCache['cards'][key] = cardsCnFix[key];
|
||||||
|
}
|
||||||
|
const skillsCnFix =
|
||||||
|
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
|
||||||
|
'skills-cn-fix.json',
|
||||||
|
);
|
||||||
|
for (const key in skillsCnFix) {
|
||||||
|
bangdreamCatalogCache['skills'][key] = skillsCnFix[key];
|
||||||
|
}
|
||||||
|
const areaItemFix =
|
||||||
|
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
|
||||||
|
'area-item-fix.json',
|
||||||
|
);
|
||||||
|
for (const key in areaItemFix) {
|
||||||
|
if (bangdreamCatalogCache['areaItems'][key] == undefined) {
|
||||||
|
bangdreamCatalogCache['areaItems'][key] = areaItemFix[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const songNickname = await bangdreamStaticPatchProvider.readExcelRows<{
|
||||||
|
Id: number;
|
||||||
|
Nickname: string;
|
||||||
|
}>('nickname-song.xlsx');
|
||||||
|
for (let i = 0; i < songNickname.length; i++) {
|
||||||
|
const element = songNickname[i];
|
||||||
|
if (bangdreamCatalogCache['songs'][element['Id'].toString()]) {
|
||||||
|
bangdreamCatalogCache['songs'][element['Id'].toString()]['nickname'] =
|
||||||
|
element['Nickname'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger('catalog', '读取 nickname-song.xlsx 失败');
|
||||||
|
}
|
||||||
|
logger('catalog', 'catalog loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialLoadPromise: Promise<void> | undefined;
|
||||||
|
|
||||||
|
function ensureCatalogInitialLoad() {
|
||||||
|
if (!initialLoadPromise) {
|
||||||
|
logger('catalog', 'initializing...');
|
||||||
|
initialLoadPromise = loadCatalogData(true).then(async () => {
|
||||||
|
logger('catalog', 'initializing done');
|
||||||
|
await loadCatalogData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return initialLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待 BangDream 目录数据完成首次加载。
|
||||||
|
*/
|
||||||
|
export async function waitForBangDreamCatalogReady(): Promise<void> {
|
||||||
|
if (isCatalogReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Promise.race([
|
||||||
|
ensureCatalogInitialLoad(),
|
||||||
|
rejectAfter(getCatalogReadyTimeoutMs()),
|
||||||
|
]);
|
||||||
|
if (!isCatalogReady()) {
|
||||||
|
throw new Error('BangDream 主数据未完成关键集合加载');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bangdreamCatalogCache;
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
|
import type { BANGDREAM_BESTDORI_API_PATHS } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
||||||
|
|
||||||
|
export type BangDreamCatalogKey = keyof typeof BANGDREAM_BESTDORI_API_PATHS;
|
||||||
|
export type BangDreamCatalogCollection<T = unknown> = Record<string, T>;
|
||||||
|
|
||||||
|
export class BangDreamCatalogRepository {
|
||||||
|
constructor(
|
||||||
|
private readonly catalog: Record<string, unknown> =
|
||||||
|
bangdreamCatalogCache as Record<string, unknown>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Bestdori 目录集合。
|
||||||
|
*
|
||||||
|
* @param key - 目录集合编码。
|
||||||
|
*/
|
||||||
|
getCollection<T = unknown>(
|
||||||
|
key: BangDreamCatalogKey,
|
||||||
|
): BangDreamCatalogCollection<T> {
|
||||||
|
return (this.catalog[key] ?? {}) as BangDreamCatalogCollection<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取原始目录值,适用于 rates 这类非对象映射集合。
|
||||||
|
*
|
||||||
|
* @param key - 目录集合编码。
|
||||||
|
*/
|
||||||
|
getValue<T = unknown>(key: BangDreamCatalogKey): T {
|
||||||
|
return (this.catalog[key] ?? {}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 ID 获取 Bestdori 目录实体。
|
||||||
|
*
|
||||||
|
* @param key - 目录集合编码。
|
||||||
|
* @param id - 实体 ID。
|
||||||
|
*/
|
||||||
|
getEntity<T = unknown>(
|
||||||
|
key: BangDreamCatalogKey,
|
||||||
|
id: number | string,
|
||||||
|
): T | undefined {
|
||||||
|
return this.getCollection<T>(key)[String(id)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取集合中可转成数字的实体 ID。
|
||||||
|
*
|
||||||
|
* @param key - 目录集合编码。
|
||||||
|
*/
|
||||||
|
getNumericIds(key: BangDreamCatalogKey): number[] {
|
||||||
|
return Object.keys(this.getCollection(key))
|
||||||
|
.map(Number)
|
||||||
|
.filter((id) => Number.isFinite(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bangdreamCatalogRepository = new BangDreamCatalogRepository();
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
import type {
|
||||||
|
BangDreamCommandInput,
|
||||||
|
BangDreamOperationKey,
|
||||||
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream.types';
|
||||||
|
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
||||||
|
|
||||||
|
export type BangDreamExecutionStage =
|
||||||
|
| 'handler'
|
||||||
|
| 'catalog'
|
||||||
|
| 'operation'
|
||||||
|
| 'output'
|
||||||
|
| 'start';
|
||||||
|
|
||||||
|
export type BangDreamOperationLifecycleContext = {
|
||||||
|
handlerName?: string;
|
||||||
|
imageCount?: number;
|
||||||
|
input: BangDreamCommandInput;
|
||||||
|
operationKey: BangDreamOperationKey;
|
||||||
|
query?: string;
|
||||||
|
stage: BangDreamExecutionStage;
|
||||||
|
startedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BangDreamOperationLifecycleObserver = {
|
||||||
|
afterOutput?: (
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
afterResolve?: (
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
beforeParse?: (
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
beforeRender?: (
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
name: string;
|
||||||
|
onError?: (
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
error: unknown,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
order?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BangDreamLifecycleObserverMethod =
|
||||||
|
| 'afterOutput'
|
||||||
|
| 'afterResolve'
|
||||||
|
| 'beforeParse'
|
||||||
|
| 'beforeRender';
|
||||||
|
|
||||||
|
export class BangDreamOperationLifecycle {
|
||||||
|
private readonly observers: BangDreamOperationLifecycleObserver[];
|
||||||
|
|
||||||
|
constructor(observers: readonly BangDreamOperationLifecycleObserver[] = []) {
|
||||||
|
this.observers = [...observers].sort(
|
||||||
|
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeParse(context: BangDreamOperationLifecycleContext) {
|
||||||
|
await this.emit('beforeParse', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterResolve(context: BangDreamOperationLifecycleContext) {
|
||||||
|
await this.emit('afterResolve', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeRender(context: BangDreamOperationLifecycleContext) {
|
||||||
|
await this.emit('beforeRender', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterOutput(context: BangDreamOperationLifecycleContext) {
|
||||||
|
await this.emit('afterOutput', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onError(
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
error: unknown,
|
||||||
|
) {
|
||||||
|
for (const observer of this.observers) {
|
||||||
|
await observer.onError?.(context, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async emit(
|
||||||
|
method: BangDreamLifecycleObserverMethod,
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
) {
|
||||||
|
for (const observer of this.observers) {
|
||||||
|
const handler = observer[method];
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
await handler(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBangDreamOperationLifecycleContext(
|
||||||
|
operationKey: BangDreamOperationKey,
|
||||||
|
input: BangDreamCommandInput,
|
||||||
|
): BangDreamOperationLifecycleContext {
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
operationKey,
|
||||||
|
query: extractBangDreamInputText(input),
|
||||||
|
stage: 'start',
|
||||||
|
startedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBangDreamOperationLogObserver(): BangDreamOperationLifecycleObserver {
|
||||||
|
return {
|
||||||
|
afterOutput: (context) => {
|
||||||
|
logger(
|
||||||
|
'operation',
|
||||||
|
formatBangDreamOperationLifecycleObserverMessage('success', context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
beforeParse: (context) => {
|
||||||
|
logger(
|
||||||
|
'operation',
|
||||||
|
formatBangDreamOperationLifecycleObserverMessage('start', context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
name: 'BangDreamOperationLogObserver',
|
||||||
|
onError: (context, error) => {
|
||||||
|
logger(
|
||||||
|
'operation',
|
||||||
|
`${formatBangDreamOperationLifecycleObserverMessage('error', context)} error=${getOperationLifecycleErrorMessage(error)}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBangDreamOperationLifecycleObserverMessage(
|
||||||
|
status: 'error' | 'start' | 'success',
|
||||||
|
context: BangDreamOperationLifecycleContext,
|
||||||
|
) {
|
||||||
|
const durationMs = Date.now() - context.startedAt;
|
||||||
|
return [
|
||||||
|
`status=${status}`,
|
||||||
|
`operation=${context.operationKey}`,
|
||||||
|
`stage=${context.stage}`,
|
||||||
|
context.handlerName ? `handler=${context.handlerName}` : '',
|
||||||
|
context.query ? `query=${context.query}` : '',
|
||||||
|
context.imageCount === undefined ? '' : `imageCount=${context.imageCount}`,
|
||||||
|
`durationMs=${durationMs}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBangDreamInputText(input: BangDreamCommandInput) {
|
||||||
|
const direct = `${input.query || input.text || input.raw || ''}`.trim();
|
||||||
|
if (direct) return direct;
|
||||||
|
return Array.isArray(input.args) ? input.args.join(' ').trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOperationLifecycleErrorMessage(error: unknown) {
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
if (typeof error === 'string') return error;
|
||||||
|
if (error === undefined || error === null) return '';
|
||||||
|
return `${error}`;
|
||||||
|
}
|
||||||
@ -1,145 +0,0 @@
|
|||||||
import type {
|
|
||||||
QqbotBangDreamCommandInput,
|
|
||||||
QqbotBangDreamOperationKey,
|
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/qqbot-bangdream.types';
|
|
||||||
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
|
||||||
|
|
||||||
export type BangDreamExecutionStage =
|
|
||||||
| 'handler'
|
|
||||||
| 'mainData'
|
|
||||||
| 'operation'
|
|
||||||
| 'output'
|
|
||||||
| 'start';
|
|
||||||
|
|
||||||
export type BangDreamHookContext = {
|
|
||||||
handlerName?: string;
|
|
||||||
imageCount?: number;
|
|
||||||
input: QqbotBangDreamCommandInput;
|
|
||||||
operationKey: QqbotBangDreamOperationKey;
|
|
||||||
query?: string;
|
|
||||||
stage: BangDreamExecutionStage;
|
|
||||||
startedAt: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BangDreamHook = {
|
|
||||||
afterOutput?: (context: BangDreamHookContext) => Promise<void> | void;
|
|
||||||
afterResolve?: (context: BangDreamHookContext) => Promise<void> | void;
|
|
||||||
beforeParse?: (context: BangDreamHookContext) => Promise<void> | void;
|
|
||||||
beforeRender?: (context: BangDreamHookContext) => Promise<void> | void;
|
|
||||||
name: string;
|
|
||||||
onError?: (
|
|
||||||
context: BangDreamHookContext,
|
|
||||||
error: unknown,
|
|
||||||
) => Promise<void> | void;
|
|
||||||
order?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BangDreamSimpleHookMethod =
|
|
||||||
| 'afterOutput'
|
|
||||||
| 'afterResolve'
|
|
||||||
| 'beforeParse'
|
|
||||||
| 'beforeRender';
|
|
||||||
|
|
||||||
export class BangDreamHookRegistry {
|
|
||||||
private readonly hooks: BangDreamHook[];
|
|
||||||
|
|
||||||
constructor(hooks: readonly BangDreamHook[] = []) {
|
|
||||||
this.hooks = [...hooks].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
async beforeParse(context: BangDreamHookContext) {
|
|
||||||
await this.emit('beforeParse', context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async afterResolve(context: BangDreamHookContext) {
|
|
||||||
await this.emit('afterResolve', context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async beforeRender(context: BangDreamHookContext) {
|
|
||||||
await this.emit('beforeRender', context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async afterOutput(context: BangDreamHookContext) {
|
|
||||||
await this.emit('afterOutput', context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onError(context: BangDreamHookContext, error: unknown) {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
await hook.onError?.(context, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async emit(
|
|
||||||
method: BangDreamSimpleHookMethod,
|
|
||||||
context: BangDreamHookContext,
|
|
||||||
) {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
const handler = hook[method];
|
|
||||||
if (typeof handler === 'function') {
|
|
||||||
await handler(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBangDreamHookContext(
|
|
||||||
operationKey: QqbotBangDreamOperationKey,
|
|
||||||
input: QqbotBangDreamCommandInput,
|
|
||||||
): BangDreamHookContext {
|
|
||||||
return {
|
|
||||||
input,
|
|
||||||
operationKey,
|
|
||||||
query: extractBangDreamInputText(input),
|
|
||||||
stage: 'start',
|
|
||||||
startedAt: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBangDreamLogHook(): BangDreamHook {
|
|
||||||
return {
|
|
||||||
afterOutput: (context) => {
|
|
||||||
logger('operation', formatBangDreamHookMessage('success', context));
|
|
||||||
},
|
|
||||||
beforeParse: (context) => {
|
|
||||||
logger('operation', formatBangDreamHookMessage('start', context));
|
|
||||||
},
|
|
||||||
name: 'BangDreamLogHook',
|
|
||||||
onError: (context, error) => {
|
|
||||||
logger(
|
|
||||||
'operation',
|
|
||||||
`${formatBangDreamHookMessage('error', context)} error=${getHookErrorMessage(error)}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBangDreamHookMessage(
|
|
||||||
status: 'error' | 'start' | 'success',
|
|
||||||
context: BangDreamHookContext,
|
|
||||||
) {
|
|
||||||
const durationMs = Date.now() - context.startedAt;
|
|
||||||
return [
|
|
||||||
`status=${status}`,
|
|
||||||
`operation=${context.operationKey}`,
|
|
||||||
`stage=${context.stage}`,
|
|
||||||
context.handlerName ? `handler=${context.handlerName}` : '',
|
|
||||||
context.query ? `query=${context.query}` : '',
|
|
||||||
context.imageCount === undefined ? '' : `imageCount=${context.imageCount}`,
|
|
||||||
`durationMs=${durationMs}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractBangDreamInputText(input: QqbotBangDreamCommandInput) {
|
|
||||||
const direct = `${input.query || input.text || input.raw || ''}`.trim();
|
|
||||||
if (direct) return direct;
|
|
||||||
return Array.isArray(input.args) ? input.args.join(' ').trim() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHookErrorMessage(error: unknown) {
|
|
||||||
if (error instanceof Error) return error.message;
|
|
||||||
if (typeof error === 'string') return error;
|
|
||||||
if (error === undefined || error === null) return '';
|
|
||||||
return `${error}`;
|
|
||||||
}
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
import { bestdoriApiPath } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
|
||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
|
||||||
import { bangDreamStaticPatchProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/static-patch.provider';
|
|
||||||
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
|
||||||
import {
|
|
||||||
BANGDREAM_TSUGU_ENV_KEYS,
|
|
||||||
normalizeBangDreamPositiveInteger,
|
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/config/runtime-options';
|
|
||||||
import {
|
|
||||||
readBangDreamRuntimeConfig,
|
|
||||||
sleepBangDreamRuntime,
|
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
|
||||||
|
|
||||||
const mainAPI: Record<string, any> = {}; //main对象,用于存放所有api数据,数据来源于Bestdori网站
|
|
||||||
const REQUIRED_MAIN_DATA_KEYS = [
|
|
||||||
'cards',
|
|
||||||
'characters',
|
|
||||||
'events',
|
|
||||||
'gacha',
|
|
||||||
'songs',
|
|
||||||
];
|
|
||||||
const DEFAULT_MAIN_DATA_READY_TIMEOUT_MS = 15000;
|
|
||||||
|
|
||||||
function getMainDataReadyTimeoutMs(): number {
|
|
||||||
return normalizeBangDreamPositiveInteger(
|
|
||||||
readBangDreamRuntimeConfig(BANGDREAM_TSUGU_ENV_KEYS.mainDataReadyTimeoutMs),
|
|
||||||
DEFAULT_MAIN_DATA_READY_TIMEOUT_MS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMainDataReady(): boolean {
|
|
||||||
return REQUIRED_MAIN_DATA_KEYS.every((key) => {
|
|
||||||
const collection = mainAPI[key];
|
|
||||||
return collection && Object.keys(collection).length > 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rejectAfter(ms: number): Promise<never> {
|
|
||||||
await sleepBangDreamRuntime(ms);
|
|
||||||
throw new Error(`BangDream 主数据首次加载超时:${ms}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
//加载mainAPI
|
|
||||||
/**
|
|
||||||
* 在BangDream 领域模型层中加载主数据API。
|
|
||||||
*
|
|
||||||
* @param useCache - use缓存参数,未传入时使用默认值。
|
|
||||||
*/
|
|
||||||
async function loadMainAPI(useCache: boolean = false) {
|
|
||||||
logger('mainAPI', 'loading mainAPI...');
|
|
||||||
const promiseAll = Object.keys(bestdoriApiPath).map(async (key) => {
|
|
||||||
if (useCache) {
|
|
||||||
return (mainAPI[key] = await bangDreamBestdoriProvider.getJson(
|
|
||||||
bestdoriApiPath[key],
|
|
||||||
{ cacheTime: 1 / 0 },
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return (mainAPI[key] = await bangDreamBestdoriProvider.getJson(
|
|
||||||
bestdoriApiPath[key],
|
|
||||||
));
|
|
||||||
} catch {
|
|
||||||
logger('mainAPI', `load ${key} failed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promiseAll);
|
|
||||||
|
|
||||||
const cardsCnFix =
|
|
||||||
await bangDreamStaticPatchProvider.readJson<Record<string, unknown>>(
|
|
||||||
'cards-cn-fix.json',
|
|
||||||
);
|
|
||||||
for (const key in cardsCnFix) {
|
|
||||||
mainAPI['cards'][key] = cardsCnFix[key];
|
|
||||||
}
|
|
||||||
const skillsCnFix =
|
|
||||||
await bangDreamStaticPatchProvider.readJson<Record<string, unknown>>(
|
|
||||||
'skills-cn-fix.json',
|
|
||||||
);
|
|
||||||
for (const key in skillsCnFix) {
|
|
||||||
mainAPI['skills'][key] = skillsCnFix[key];
|
|
||||||
}
|
|
||||||
const areaItemFix =
|
|
||||||
await bangDreamStaticPatchProvider.readJson<Record<string, unknown>>(
|
|
||||||
'area-item-fix.json',
|
|
||||||
);
|
|
||||||
for (const key in areaItemFix) {
|
|
||||||
if (mainAPI['areaItems'][key] == undefined) {
|
|
||||||
mainAPI['areaItems'][key] = areaItemFix[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const songNickname = await bangDreamStaticPatchProvider.readExcelRows<{
|
|
||||||
Id: number;
|
|
||||||
Nickname: string;
|
|
||||||
}>('nickname-song.xlsx');
|
|
||||||
for (let i = 0; i < songNickname.length; i++) {
|
|
||||||
const element = songNickname[i];
|
|
||||||
if (mainAPI['songs'][element['Id'].toString()]) {
|
|
||||||
mainAPI['songs'][element['Id'].toString()]['nickname'] =
|
|
||||||
element['Nickname'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logger('mainAPI', '读取 nickname-song.xlsx 失败');
|
|
||||||
}
|
|
||||||
logger('mainAPI', 'mainAPI loaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialLoadPromise: Promise<void> | undefined;
|
|
||||||
|
|
||||||
function ensureMainDataInitialLoad() {
|
|
||||||
if (!initialLoadPromise) {
|
|
||||||
logger('mainAPI', 'initializing...');
|
|
||||||
initialLoadPromise = loadMainAPI(true).then(async () => {
|
|
||||||
logger('mainAPI', 'initializing done');
|
|
||||||
await loadMainAPI();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return initialLoadPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 等待 BangDream 主数据完成首次加载。
|
|
||||||
*/
|
|
||||||
export async function waitForMainDataReady(): Promise<void> {
|
|
||||||
if (isMainDataReady()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await Promise.race([
|
|
||||||
ensureMainDataInitialLoad(),
|
|
||||||
rejectAfter(getMainDataReadyTimeoutMs()),
|
|
||||||
]);
|
|
||||||
if (!isMainDataReady()) {
|
|
||||||
throw new Error('BangDream 主数据未完成关键集合加载');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default mainAPI;
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
|
||||||
import type { BANGDREAM_BESTDORI_API_PATHS } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
|
||||||
|
|
||||||
export type BangDreamMainDataKey = keyof typeof BANGDREAM_BESTDORI_API_PATHS;
|
|
||||||
export type BangDreamMainDataCollection<T = unknown> = Record<string, T>;
|
|
||||||
|
|
||||||
export class BangDreamMainDataRepository {
|
|
||||||
constructor(
|
|
||||||
private readonly store: Record<string, unknown> = mainAPI as Record<
|
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Bestdori 主数据集合。
|
|
||||||
*
|
|
||||||
* @param key - 主数据集合编码。
|
|
||||||
*/
|
|
||||||
getCollection<T = unknown>(
|
|
||||||
key: BangDreamMainDataKey,
|
|
||||||
): BangDreamMainDataCollection<T> {
|
|
||||||
return (this.store[key] ?? {}) as BangDreamMainDataCollection<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取原始主数据值,适用于 rates 这类非对象映射集合。
|
|
||||||
*
|
|
||||||
* @param key - 主数据集合编码。
|
|
||||||
*/
|
|
||||||
getValue<T = unknown>(key: BangDreamMainDataKey): T {
|
|
||||||
return (this.store[key] ?? {}) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按 ID 获取 Bestdori 主数据实体。
|
|
||||||
*
|
|
||||||
* @param key - 主数据集合编码。
|
|
||||||
* @param id - 实体 ID。
|
|
||||||
*/
|
|
||||||
getEntity<T = unknown>(
|
|
||||||
key: BangDreamMainDataKey,
|
|
||||||
id: number | string,
|
|
||||||
): T | undefined {
|
|
||||||
return this.getCollection<T>(key)[String(id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取集合中可转成数字的实体 ID。
|
|
||||||
*
|
|
||||||
* @param key - 主数据集合编码。
|
|
||||||
*/
|
|
||||||
getNumericIds(key: BangDreamMainDataKey): number[] {
|
|
||||||
return Object.keys(this.getCollection(key))
|
|
||||||
.map(Number)
|
|
||||||
.filter((id) => Number.isFinite(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bangDreamMainDataRepository = new BangDreamMainDataRepository();
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import type { BangDreamCardArtAttribute } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card-art.layout';
|
import type { BangDreamCardArtAttribute } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card-art.layout';
|
||||||
import {
|
import {
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
|
|
||||||
export class CardArtResourceRepository {
|
export class CardArtResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
import {
|
import {
|
||||||
@ -35,7 +35,7 @@ function toServerCode(server: Server | undefined): string {
|
|||||||
|
|
||||||
export class CardResourceRepository {
|
export class CardResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
import { Gacha } from '@/modules/qqbot/plugins/bangdream/src/domain/gacha/gacha.model';
|
import { Gacha } from '@/modules/qqbot/plugins/bangdream/src/domain/gacha/gacha.model';
|
||||||
import { Event } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
|
import { Event } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
|
||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
import { BANGDREAM_CARD_TYPE_NAME } from '@/modules/qqbot/plugins/bangdream/src/config/dictionary/default-dictionary';
|
import { BANGDREAM_CARD_TYPE_NAME } from '@/modules/qqbot/plugins/bangdream/src/config/dictionary/default-dictionary';
|
||||||
@ -94,7 +94,7 @@ export class Card {
|
|||||||
*/
|
*/
|
||||||
constructor(cardId: number) {
|
constructor(cardId: number) {
|
||||||
this.cardId = cardId;
|
this.cardId = cardId;
|
||||||
const cardData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
const cardData = bangdreamCatalogRepository.getEntity<Record<string, any>>(
|
||||||
'cards',
|
'cards',
|
||||||
cardId,
|
cardId,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { Card } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card.model';
|
import { Card } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card.model';
|
||||||
import {
|
import {
|
||||||
bangDreamMainDataRepository,
|
bangdreamCatalogRepository,
|
||||||
type BangDreamMainDataCollection,
|
type BangDreamCatalogCollection,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
} from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
|
|
||||||
export class CardRepository {
|
export class CardRepository {
|
||||||
/**
|
/**
|
||||||
* 获取卡牌主数据集合。
|
* 获取卡牌主数据集合。
|
||||||
*/
|
*/
|
||||||
getSource(): BangDreamMainDataCollection {
|
getSource(): BangDreamCatalogCollection {
|
||||||
return bangDreamMainDataRepository.getCollection('cards');
|
return bangdreamCatalogRepository.getCollection('cards');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
import { Card, Stat } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card.model';
|
import { Card, Stat } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card.model';
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export class AreaItem {
|
|||||||
*/
|
*/
|
||||||
constructor(areaItemId: number) {
|
constructor(areaItemId: number) {
|
||||||
this.areaItemId = areaItemId;
|
this.areaItemId = areaItemId;
|
||||||
const areaItemData = mainAPI['areaItems'][areaItemId.toString()];
|
const areaItemData = bangdreamCatalogCache['areaItems'][areaItemId.toString()];
|
||||||
if (areaItemData == undefined) {
|
if (areaItemData == undefined) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
|
|
||||||
export class AttributeResourceRepository {
|
export class AttributeResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
|
|
||||||
export class BandResourceRepository {
|
export class BandResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import { convertSvgToPngBuffer } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-image';
|
import { convertSvgToPngBuffer } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-image';
|
||||||
@ -18,8 +18,8 @@ export class Band {
|
|||||||
*/
|
*/
|
||||||
constructor(bandId: number) {
|
constructor(bandId: number) {
|
||||||
this.bandId = bandId;
|
this.bandId = bandId;
|
||||||
const bandData = mainAPI['singer'][bandId.toString()];
|
const bandData = bangdreamCatalogCache['singer'][bandId.toString()];
|
||||||
if (mainAPI['bands'][bandId.toString()] != undefined) {
|
if (bangdreamCatalogCache['bands'][bandId.toString()] != undefined) {
|
||||||
this.hasIcon = true;
|
this.hasIcon = true;
|
||||||
}
|
}
|
||||||
if (bandData == undefined) {
|
if (bandData == undefined) {
|
||||||
@ -36,7 +36,7 @@ export class Band {
|
|||||||
*/
|
*/
|
||||||
getMembers() {
|
getMembers() {
|
||||||
const members = [];
|
const members = [];
|
||||||
const characterList = mainAPI['characters'];
|
const characterList = bangdreamCatalogCache['characters'];
|
||||||
for (const characterID in characterList) {
|
for (const characterID in characterList) {
|
||||||
const character = new Character(parseInt(characterID));
|
const character = new Character(parseInt(characterID));
|
||||||
if (character.bandId == this.bandId) {
|
if (character.bandId == this.bandId) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import {
|
import {
|
||||||
getServerByPriority,
|
getServerByPriority,
|
||||||
@ -17,7 +17,7 @@ function toServerCode(server: Server | undefined): string {
|
|||||||
|
|
||||||
export class CostumeResourceRepository {
|
export class CostumeResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
@ -23,7 +23,7 @@ export class Costume {
|
|||||||
*/
|
*/
|
||||||
constructor(costumeId: number) {
|
constructor(costumeId: number) {
|
||||||
this.costumeId = costumeId;
|
this.costumeId = costumeId;
|
||||||
const costumeData = mainAPI['costumes'][costumeId.toString()];
|
const costumeData = bangdreamCatalogCache['costumes'][costumeId.toString()];
|
||||||
if (costumeData == undefined) {
|
if (costumeData == undefined) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ const LEGACY_ANIMATED_TEXTURE_NAMES = new Set([
|
|||||||
|
|
||||||
export class DegreeResourceRepository {
|
export class DegreeResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
import { Canvas, Image, loadImage } from 'skia-canvas';
|
import { Canvas, Image, loadImage } from 'skia-canvas';
|
||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import { readJSONFromBuffer } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { readJSONFromBuffer } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
import { degreeResourceRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/degree-resource.repository';
|
import { degreeResourceRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/degree-resource.repository';
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export class Degree {
|
|||||||
*/
|
*/
|
||||||
constructor(degreeId) {
|
constructor(degreeId) {
|
||||||
this.degreeId = degreeId;
|
this.degreeId = degreeId;
|
||||||
const degreeData = mainAPI['degrees'][degreeId.toString()];
|
const degreeData = bangdreamCatalogCache['degrees'][degreeId.toString()];
|
||||||
if (degreeData == undefined) {
|
if (degreeData == undefined) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
@ -14,7 +14,7 @@ function toServerCode(server: Server | undefined): string {
|
|||||||
|
|
||||||
export class ItemResourceRepository {
|
export class ItemResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
getServerByPriority,
|
getServerByPriority,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import { itemResourceRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/item-resource.repository';
|
import { itemResourceRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/item-resource.repository';
|
||||||
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
import { BANGDREAM_ITEM_TYPE_PREFIXES } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
import { BANGDREAM_ITEM_TYPE_PREFIXES } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
||||||
@ -47,7 +47,7 @@ export class Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//如果是其他物品
|
//如果是其他物品
|
||||||
const itemData = mainAPI['items'][itemId];
|
const itemData = bangdreamCatalogCache['items'][itemId];
|
||||||
if (itemData == undefined) {
|
if (itemData == undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { getBangDreamAssetPath } from '@/modules/qqbot/plugins/bangdream/src/theme/asset-manifest';
|
import { getBangDreamAssetPath } from '@/modules/qqbot/plugins/bangdream/src/theme/asset-manifest';
|
||||||
|
|
||||||
export class ServerResourceRepository {
|
export class ServerResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
|
|
||||||
export class Skill {
|
export class Skill {
|
||||||
skillId: number;
|
skillId: number;
|
||||||
@ -18,13 +18,13 @@ export class Skill {
|
|||||||
*/
|
*/
|
||||||
constructor(skillId: number) {
|
constructor(skillId: number) {
|
||||||
this.skillId = skillId;
|
this.skillId = skillId;
|
||||||
if (mainAPI['skills'][this.skillId.toString()] == undefined) {
|
if (bangdreamCatalogCache['skills'][this.skillId.toString()] == undefined) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isExist = true;
|
this.isExist = true;
|
||||||
this.skillId = this.skillId;
|
this.skillId = this.skillId;
|
||||||
this.data = mainAPI['skills'][this.skillId.toString()];
|
this.data = bangdreamCatalogCache['skills'][this.skillId.toString()];
|
||||||
this.simpleDescription = this.data['simpleDescription'];
|
this.simpleDescription = this.data['simpleDescription'];
|
||||||
this.description = this.data['description'];
|
this.description = this.data['description'];
|
||||||
this.duration = this.data['duration'];
|
this.duration = this.data['duration'];
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { formatNumber } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
|
|
||||||
export class CharacterResourceRepository {
|
export class CharacterResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import {
|
import {
|
||||||
match,
|
match,
|
||||||
FuzzySearchResult,
|
FuzzySearchResult,
|
||||||
@ -31,7 +31,7 @@ export async function drawCharacterList(
|
|||||||
): Promise<Array<Buffer | string>> {
|
): Promise<Array<Buffer | string>> {
|
||||||
//计算模糊搜索结果
|
//计算模糊搜索结果
|
||||||
const tempCharacterList: Array<Character> = []; //最终输出的角色列表
|
const tempCharacterList: Array<Character> = []; //最终输出的角色列表
|
||||||
const characterIdList: Array<number> = Object.keys(mainAPI['characters']).map(
|
const characterIdList: Array<number> = Object.keys(bangdreamCatalogCache['characters']).map(
|
||||||
Number,
|
Number,
|
||||||
); //所有卡牌ID列表
|
); //所有卡牌ID列表
|
||||||
for (let i = 0; i < characterIdList.length; i++) {
|
for (let i = 0; i < characterIdList.length; i++) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import { characterResourceRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character-resource.repository';
|
import { characterResourceRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character-resource.repository';
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ export class Character {
|
|||||||
* @param characterId - 角色 ID。
|
* @param characterId - 角色 ID。
|
||||||
*/
|
*/
|
||||||
constructor(characterId: number) {
|
constructor(characterId: number) {
|
||||||
const characterData = mainAPI['characters'][characterId.toString()];
|
const characterData = bangdreamCatalogCache['characters'][characterId.toString()];
|
||||||
if (characterData == undefined) {
|
if (characterData == undefined) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export type QqbotBangDreamOperationKey =
|
export type BangDreamOperationKey =
|
||||||
| 'bangdream.card.illustration'
|
| 'bangdream.card.illustration'
|
||||||
| 'bangdream.card.search'
|
| 'bangdream.card.search'
|
||||||
| 'bangdream.character.search'
|
| 'bangdream.character.search'
|
||||||
@ -15,7 +15,7 @@ export type QqbotBangDreamOperationKey =
|
|||||||
| 'bangdream.song.random'
|
| 'bangdream.song.random'
|
||||||
| 'bangdream.song.search';
|
| 'bangdream.song.search';
|
||||||
|
|
||||||
export type QqbotBangDreamOperationHandlerName =
|
export type BangDreamOperationHandlerName =
|
||||||
| 'getCardIllustration'
|
| 'getCardIllustration'
|
||||||
| 'getCutoffAll'
|
| 'getCutoffAll'
|
||||||
| 'getCutoffDetail'
|
| 'getCutoffDetail'
|
||||||
@ -32,7 +32,7 @@ export type QqbotBangDreamOperationHandlerName =
|
|||||||
| 'searchSong'
|
| 'searchSong'
|
||||||
| 'simulateGacha';
|
| 'simulateGacha';
|
||||||
|
|
||||||
export type QqbotBangDreamCommandInput = {
|
export type BangDreamCommandInput = {
|
||||||
args?: string[];
|
args?: string[];
|
||||||
cardId?: number | string;
|
cardId?: number | string;
|
||||||
compress?: boolean | string;
|
compress?: boolean | string;
|
||||||
@ -55,9 +55,9 @@ export type QqbotBangDreamCommandInput = {
|
|||||||
useEasyBG?: boolean | string;
|
useEasyBG?: boolean | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotBangDreamCommandOutput = {
|
export type BangDreamCommandOutput = {
|
||||||
imageCount: number;
|
imageCount: number;
|
||||||
operationKey: QqbotBangDreamOperationKey;
|
operationKey: BangDreamOperationKey;
|
||||||
query: string;
|
query: string;
|
||||||
replyText: string;
|
replyText: string;
|
||||||
source: string;
|
source: string;
|
||||||
@ -15,7 +15,7 @@ import { drawCutoffChart } from '@/modules/qqbot/plugins/bangdream/src/domain/cu
|
|||||||
import { serverNameFullList } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { serverNameFullList } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
import { drawEventDataBlock } from '@/modules/qqbot/plugins/bangdream/src/theme/detail-block.renderer';
|
import { drawEventDataBlock } from '@/modules/qqbot/plugins/bangdream/src/theme/detail-block.renderer';
|
||||||
import { statusName } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { statusName } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import { BangDreamEventStatus } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
import { BangDreamEventStatus } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +33,7 @@ export async function drawCutoffDetail(
|
|||||||
mainServer: Server,
|
mainServer: Server,
|
||||||
compress: boolean,
|
compress: boolean,
|
||||||
): Promise<Array<Buffer | string>> {
|
): Promise<Array<Buffer | string>> {
|
||||||
const eventData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
const eventData = bangdreamCatalogRepository.getEntity<Record<string, any>>(
|
||||||
'events',
|
'events',
|
||||||
eventId,
|
eventId,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ export interface CutoffEventTopData {
|
|||||||
|
|
||||||
export class CutoffEventTopRepository {
|
export class CutoffEventTopRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import { bangDreamHhwxTrackerProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/hhwx-tracker.provider';
|
import { bangdreamHhwxTrackerProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/hhwx-tracker.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import {
|
import {
|
||||||
preferHhwxSource,
|
preferHhwxSource,
|
||||||
reportDataSourceProblem,
|
reportDataSourceProblem,
|
||||||
@ -91,15 +91,15 @@ export class Cutoff {
|
|||||||
if (this.server != Server.cn) {
|
if (this.server != Server.cn) {
|
||||||
// 非国服不使用HHWX
|
// 非国服不使用HHWX
|
||||||
this.useHHWX = false;
|
this.useHHWX = false;
|
||||||
return bangDreamBestdoriProvider;
|
return bangdreamBestdoriProvider;
|
||||||
}
|
}
|
||||||
const provider = !reverse
|
const provider = !reverse
|
||||||
? this.useHHWX
|
? this.useHHWX
|
||||||
? bangDreamHhwxTrackerProvider
|
? bangdreamHhwxTrackerProvider
|
||||||
: bangDreamBestdoriProvider
|
: bangdreamBestdoriProvider
|
||||||
: this.useHHWX
|
: this.useHHWX
|
||||||
? bangDreamBestdoriProvider
|
? bangdreamBestdoriProvider
|
||||||
: bangDreamHhwxTrackerProvider;
|
: bangdreamHhwxTrackerProvider;
|
||||||
if (reverse && this.server == Server.cn) this.useHHWX = !this.useHHWX;
|
if (reverse && this.server == Server.cn) this.useHHWX = !this.useHHWX;
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
@ -236,7 +236,7 @@ export class Cutoff {
|
|||||||
}
|
}
|
||||||
//rate
|
//rate
|
||||||
const rateDataList =
|
const rateDataList =
|
||||||
bangDreamMainDataRepository.getValue<
|
bangdreamCatalogRepository.getValue<
|
||||||
Array<{ server: number; type: string; tier: number; rate: number }>
|
Array<{ server: number; type: string; tier: number; rate: number }>
|
||||||
>('rates');
|
>('rates');
|
||||||
const rateData = rateDataList.find((element) => {
|
const rateData = rateDataList.find((element) => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Canvas, Image, loadImage } from 'skia-canvas';
|
import { Canvas, Image, loadImage } from 'skia-canvas';
|
||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import {
|
import {
|
||||||
getServerByPriority,
|
getServerByPriority,
|
||||||
Server,
|
Server,
|
||||||
@ -35,7 +35,7 @@ type RewardWithId = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class EventDataRepository {
|
export class EventDataRepository {
|
||||||
constructor(private readonly provider = bangDreamBestdoriProvider) {}
|
constructor(private readonly provider = bangdreamBestdoriProvider) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取活动远端详情数据。
|
* 获取活动远端详情数据。
|
||||||
@ -268,10 +268,10 @@ export class EventDataRepository {
|
|||||||
const rewardId = this.pickRewardId(event.rankingRewards, 'deco_pins');
|
const rewardId = this.pickRewardId(event.rankingRewards, 'deco_pins');
|
||||||
if (rewardId === undefined) return undefined;
|
if (rewardId === undefined) return undefined;
|
||||||
|
|
||||||
const { bangDreamMainDataRepository } =
|
const { bangdreamCatalogRepository } =
|
||||||
await import('../../application/main-data.repository');
|
await import('../../application/catalog/bangdream-catalog-repository');
|
||||||
const allDeco =
|
const allDeco =
|
||||||
bangDreamMainDataRepository.getCollection<Record<string, any>>('deco');
|
bangdreamCatalogRepository.getCollection<Record<string, any>>('deco');
|
||||||
const decoAssetName = allDeco[rewardId]?.assetBundleName;
|
const decoAssetName = allDeco[rewardId]?.assetBundleName;
|
||||||
if (!decoAssetName) return undefined;
|
if (!decoAssetName) return undefined;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
|
|
||||||
export type EventStageDataType = 'stages' | 'rotationMusics';
|
export type EventStageDataType = 'stages' | 'rotationMusics';
|
||||||
@ -20,7 +20,7 @@ export type EventStageDataRows<T extends EventStageDataType> =
|
|||||||
|
|
||||||
export class EventStageDataRepository {
|
export class EventStageDataRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Canvas, Image } from 'skia-canvas';
|
import { Canvas, Image } from 'skia-canvas';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import { Attribute } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/attribute.model';
|
import { Attribute } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/attribute.model';
|
||||||
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
||||||
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { globalDefaultServer } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
@ -95,7 +95,7 @@ export class Event {
|
|||||||
*/
|
*/
|
||||||
constructor(eventId: number) {
|
constructor(eventId: number) {
|
||||||
this.eventId = eventId;
|
this.eventId = eventId;
|
||||||
const eventData = bangDreamMainDataRepository.getEntity<
|
const eventData = bangdreamCatalogRepository.getEntity<
|
||||||
Record<string, any>
|
Record<string, any>
|
||||||
>('events', eventId);
|
>('events', eventId);
|
||||||
if (eventData == undefined) {
|
if (eventData == undefined) {
|
||||||
@ -337,7 +337,7 @@ export function getPresentEvent(server: Server, time?: number) {
|
|||||||
time = Date.now();
|
time = Date.now();
|
||||||
}
|
}
|
||||||
const eventList: Array<number> = [];
|
const eventList: Array<number> = [];
|
||||||
const eventListMain = bangDreamMainDataRepository.getCollection('events');
|
const eventListMain = bangdreamCatalogRepository.getCollection('events');
|
||||||
for (const key in eventListMain) {
|
for (const key in eventListMain) {
|
||||||
const event = new Event(parseInt(key));
|
const event = new Event(parseInt(key));
|
||||||
//如果在活动进行时
|
//如果在活动进行时
|
||||||
@ -435,7 +435,7 @@ export function getRecentEventListByEventAndServer(
|
|||||||
count: number,
|
count: number,
|
||||||
sameType: boolean = false,
|
sameType: boolean = false,
|
||||||
) {
|
) {
|
||||||
const eventIdList = bangDreamMainDataRepository.getNumericIds('events');
|
const eventIdList = bangdreamCatalogRepository.getNumericIds('events');
|
||||||
const candidates: CutoffRecentEventCandidate[] = eventIdList
|
const candidates: CutoffRecentEventCandidate[] = eventIdList
|
||||||
.map((eventId) => new Event(eventId))
|
.map((eventId) => new Event(eventId))
|
||||||
.map((candidate) => ({
|
.map((candidate) => ({
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import { Event } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
|
import { Event } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
|
||||||
import {
|
import {
|
||||||
bangDreamMainDataRepository,
|
bangdreamCatalogRepository,
|
||||||
type BangDreamMainDataCollection,
|
type BangDreamCatalogCollection,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
} from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
|
|
||||||
export class EventRepository {
|
export class EventRepository {
|
||||||
/**
|
/**
|
||||||
* 获取活动主数据集合。
|
* 获取活动主数据集合。
|
||||||
*/
|
*/
|
||||||
getSource(): BangDreamMainDataCollection {
|
getSource(): BangDreamCatalogCollection {
|
||||||
return bangDreamMainDataRepository.getCollection('events');
|
return bangdreamCatalogRepository.getCollection('events');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取活动 ID 列表。
|
* 获取活动 ID 列表。
|
||||||
*/
|
*/
|
||||||
getEventIds(): number[] {
|
getEventIds(): number[] {
|
||||||
return bangDreamMainDataRepository.getNumericIds('events');
|
return bangdreamCatalogRepository.getNumericIds('events');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import {
|
import {
|
||||||
getServerByPriority,
|
getServerByPriority,
|
||||||
@ -22,7 +22,7 @@ function toServerCode(server: Server | undefined): string {
|
|||||||
|
|
||||||
export class GachaResourceRepository {
|
export class GachaResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import {
|
import {
|
||||||
Server,
|
Server,
|
||||||
@ -79,7 +79,7 @@ export class Gacha {
|
|||||||
*/
|
*/
|
||||||
constructor(gachaId: number) {
|
constructor(gachaId: number) {
|
||||||
this.gachaId = gachaId;
|
this.gachaId = gachaId;
|
||||||
const gachaData = bangDreamMainDataRepository.getEntity<
|
const gachaData = bangdreamCatalogRepository.getEntity<
|
||||||
Record<string, any>
|
Record<string, any>
|
||||||
>('gacha', gachaId);
|
>('gacha', gachaId);
|
||||||
if (gachaData == undefined) {
|
if (gachaData == undefined) {
|
||||||
@ -247,7 +247,7 @@ export async function getPresentGachaList(
|
|||||||
end: number = Date.now(),
|
end: number = Date.now(),
|
||||||
): Promise<Array<Gacha>> {
|
): Promise<Array<Gacha>> {
|
||||||
const gachaList: Array<Gacha> = [];
|
const gachaList: Array<Gacha> = [];
|
||||||
const gachaListMain = bangDreamMainDataRepository.getCollection('gacha');
|
const gachaListMain = bangdreamCatalogRepository.getCollection('gacha');
|
||||||
|
|
||||||
for (const gachaId in gachaListMain) {
|
for (const gachaId in gachaListMain) {
|
||||||
if (Object.prototype.hasOwnProperty.call(gachaListMain, gachaId)) {
|
if (Object.prototype.hasOwnProperty.call(gachaListMain, gachaId)) {
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { Gacha } from '@/modules/qqbot/plugins/bangdream/src/domain/gacha/gacha.model';
|
import { Gacha } from '@/modules/qqbot/plugins/bangdream/src/domain/gacha/gacha.model';
|
||||||
import {
|
import {
|
||||||
bangDreamMainDataRepository,
|
bangdreamCatalogRepository,
|
||||||
type BangDreamMainDataCollection,
|
type BangDreamCatalogCollection,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
} from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
|
|
||||||
export class GachaRepository {
|
export class GachaRepository {
|
||||||
/**
|
/**
|
||||||
* 获取卡池主数据集合。
|
* 获取卡池主数据集合。
|
||||||
*/
|
*/
|
||||||
getSource(): BangDreamMainDataCollection {
|
getSource(): BangDreamCatalogCollection {
|
||||||
return bangDreamMainDataRepository.getCollection('gacha');
|
return bangdreamCatalogRepository.getCollection('gacha');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { assetsRootPath } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
import { assetsRootPath } from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||||
import { readBangDreamAsset } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
import { readBangDreamAsset } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
||||||
|
|
||||||
export class DeckRankResourceRepository {
|
export class DeckRankResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
private readonly localRankRootPath: string = path.join(
|
private readonly localRankRootPath: string = path.join(
|
||||||
assetsRootPath,
|
assetsRootPath,
|
||||||
'Rank',
|
'Rank',
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { resizeImage } from '@/modules/qqbot/plugins/bangdream/src/theme/image-s
|
|||||||
import { Band } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/band.model';
|
import { Band } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/band.model';
|
||||||
import { drawTextWithImages } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-text';
|
import { drawTextWithImages } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-text';
|
||||||
import { starList } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card-rarity.renderer';
|
import { starList } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card-rarity.renderer';
|
||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import {
|
import {
|
||||||
BANGDREAM_DECK_TOTAL_RATING_ID,
|
BANGDREAM_DECK_TOTAL_RATING_ID,
|
||||||
BANGDREAM_STAGE_CHALLENGE_BAND_ID,
|
BANGDREAM_STAGE_CHALLENGE_BAND_ID,
|
||||||
@ -81,7 +81,7 @@ export async function drawPlayerBandRankInList(
|
|||||||
): Promise<Canvas> {
|
): Promise<Canvas> {
|
||||||
const bandRankMap = player.profile.bandRankMap?.entries;
|
const bandRankMap = player.profile.bandRankMap?.entries;
|
||||||
const BandDetails = {};
|
const BandDetails = {};
|
||||||
for (const i in mainAPI['bands']) {
|
for (const i in bangdreamCatalogCache['bands']) {
|
||||||
if (bandRankMap[i] != undefined) {
|
if (bandRankMap[i] != undefined) {
|
||||||
BandDetails[i] = [bandRankMap[i].toString()];
|
BandDetails[i] = [bandRankMap[i].toString()];
|
||||||
} else {
|
} else {
|
||||||
@ -107,7 +107,7 @@ export async function drawPlayerStageChallengeRankInList(
|
|||||||
player.profile.stageChallengeAchievementConditionsMap.entries;
|
player.profile.stageChallengeAchievementConditionsMap.entries;
|
||||||
|
|
||||||
const BandDetails = {};
|
const BandDetails = {};
|
||||||
for (const band in mainAPI['bands']) {
|
for (const band in bangdreamCatalogCache['bands']) {
|
||||||
const level =
|
const level =
|
||||||
stageChallengeAchievementConditionsMap?.[
|
stageChallengeAchievementConditionsMap?.[
|
||||||
BANGDREAM_STAGE_CHALLENGE_BAND_ID[band]
|
BANGDREAM_STAGE_CHALLENGE_BAND_ID[band]
|
||||||
@ -147,7 +147,7 @@ export async function drawPlayerDeckTotalRatingInList(
|
|||||||
const userDeckTotalRatingMap = player.profile.userDeckTotalRatingMap.entries;
|
const userDeckTotalRatingMap = player.profile.userDeckTotalRatingMap.entries;
|
||||||
const BandDetails = {};
|
const BandDetails = {};
|
||||||
|
|
||||||
for (const i in mainAPI['bands']) {
|
for (const i in bangdreamCatalogCache['bands']) {
|
||||||
if (userDeckTotalRatingMap[i] != undefined) {
|
if (userDeckTotalRatingMap[i] != undefined) {
|
||||||
const rankName = userDeckTotalRatingMap[i].rank;
|
const rankName = userDeckTotalRatingMap[i].rank;
|
||||||
let rankId = BANGDREAM_DECK_TOTAL_RATING_ID[rankName];
|
let rankId = BANGDREAM_DECK_TOTAL_RATING_ID[rankName];
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { drawList } from '@/modules/qqbot/plugins/bangdream/src/theme/list-frame
|
|||||||
import { resizeImage } from '@/modules/qqbot/plugins/bangdream/src/theme/image-stack';
|
import { resizeImage } from '@/modules/qqbot/plugins/bangdream/src/theme/image-stack';
|
||||||
import { drawTextWithImages } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-text';
|
import { drawTextWithImages } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-text';
|
||||||
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
import { Character } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character.model';
|
||||||
import mainAPI from '@/modules/qqbot/plugins/bangdream/src/application/main-data-store';
|
import bangdreamCatalogCache from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||||
import {
|
import {
|
||||||
createCharacterDetailIconSpec,
|
createCharacterDetailIconSpec,
|
||||||
createCharacterDetailItemLayout,
|
createCharacterDetailItemLayout,
|
||||||
@ -69,7 +69,7 @@ async function drawCharacterInList(
|
|||||||
export async function drawCharacterRankInList(player: Player, key?: string) {
|
export async function drawCharacterRankInList(player: Player, key?: string) {
|
||||||
const characterRankMap = player.profile.userCharacterRankMap?.entries;
|
const characterRankMap = player.profile.userCharacterRankMap?.entries;
|
||||||
const CharacterDetailsInListOptions = {};
|
const CharacterDetailsInListOptions = {};
|
||||||
for (const i in mainAPI['characters']) {
|
for (const i in bangdreamCatalogCache['characters']) {
|
||||||
if (characterRankMap[i] != undefined) {
|
if (characterRankMap[i] != undefined) {
|
||||||
CharacterDetailsInListOptions[i] = [`${characterRankMap[i].rank}`];
|
CharacterDetailsInListOptions[i] = [`${characterRankMap[i].rank}`];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
@ -16,7 +16,7 @@ export interface PlayerDetailResponse {
|
|||||||
|
|
||||||
export class PlayerDataRepository {
|
export class PlayerDataRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
|
|
||||||
export class PlayerRankingResourceRepository {
|
export class PlayerRankingResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
import { BangDreamServerId as Server } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
import { BangDreamServerId as Server } from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
||||||
import {
|
import {
|
||||||
@ -42,7 +42,7 @@ export interface CnEventEstimateCalculationContext {
|
|||||||
export function getBangDreamEventSchedule(
|
export function getBangDreamEventSchedule(
|
||||||
eventId: number,
|
eventId: number,
|
||||||
): BangDreamEventSchedule | null {
|
): BangDreamEventSchedule | null {
|
||||||
const eventData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
const eventData = bangdreamCatalogRepository.getEntity<Record<string, any>>(
|
||||||
'events',
|
'events',
|
||||||
eventId,
|
eventId,
|
||||||
);
|
);
|
||||||
@ -64,7 +64,7 @@ export function getPresentBangDreamEventId(
|
|||||||
server: number,
|
server: number,
|
||||||
time = Date.now(),
|
time = Date.now(),
|
||||||
): number | null {
|
): number | null {
|
||||||
const eventIds = bangDreamMainDataRepository.getNumericIds('events');
|
const eventIds = bangdreamCatalogRepository.getNumericIds('events');
|
||||||
const activeEventIds: number[] = [];
|
const activeEventIds: number[] = [];
|
||||||
let latestEndedEventId: number | null = null;
|
let latestEndedEventId: number | null = null;
|
||||||
let latestEndedAt = 0;
|
let latestEndedAt = 0;
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export interface FuzzySearchRule {
|
|||||||
match: (keyword: FuzzySearchKeyword, push: FuzzySearchResultWriter) => void;
|
match: (keyword: FuzzySearchKeyword, push: FuzzySearchResultWriter) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FuzzySearchRuleRegistry {
|
export class FuzzySearchRules {
|
||||||
constructor(private readonly rules: readonly FuzzySearchRule[]) {}
|
constructor(private readonly rules: readonly FuzzySearchRule[]) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,10 +61,10 @@ export function createFuzzySearchKeyword(
|
|||||||
*
|
*
|
||||||
* @param config - 搜索别名配置。
|
* @param config - 搜索别名配置。
|
||||||
*/
|
*/
|
||||||
export function createDefaultFuzzySearchRuleRegistry(
|
export function createDefaultFuzzySearchRules(
|
||||||
config: FuzzySearchConfig,
|
config: FuzzySearchConfig,
|
||||||
) {
|
) {
|
||||||
return new FuzzySearchRuleRegistry([
|
return new FuzzySearchRules([
|
||||||
createNumberRule(),
|
createNumberRule(),
|
||||||
createLevelRule(),
|
createLevelRule(),
|
||||||
createRelationRule(),
|
createRelationRule(),
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createDefaultFuzzySearchRuleRegistry,
|
createDefaultFuzzySearchRules,
|
||||||
createFuzzySearchKeyword,
|
createFuzzySearchKeyword,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/search/fuzzy-search-rule.registry';
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/search/fuzzy-search-rules';
|
||||||
import { searchDictionaryRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/search/search-dictionary.repository';
|
import { searchDictionaryRepository } from '@/modules/qqbot/plugins/bangdream/src/domain/search/search-dictionary.repository';
|
||||||
import type {
|
import type {
|
||||||
FuzzySearchConfig,
|
FuzzySearchConfig,
|
||||||
@ -23,8 +23,8 @@ const QUOTE_EDGE_PATTERN = /^["“”『』「」]|["“”『』「」]$/g;
|
|||||||
const RESERVED_MATCH_KEYS = new Set(['_number', '_relationStr', '_all']);
|
const RESERVED_MATCH_KEYS = new Set(['_number', '_relationStr', '_all']);
|
||||||
|
|
||||||
let cachedConfig: FuzzySearchConfig | undefined;
|
let cachedConfig: FuzzySearchConfig | undefined;
|
||||||
let cachedRuleRegistry:
|
let cachedRules:
|
||||||
| ReturnType<typeof createDefaultFuzzySearchRuleRegistry>
|
| ReturnType<typeof createDefaultFuzzySearchRules>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export const config = new Proxy({} as FuzzySearchConfig, {
|
export const config = new Proxy({} as FuzzySearchConfig, {
|
||||||
@ -38,11 +38,11 @@ function getFuzzySearchConfig() {
|
|||||||
return cachedConfig;
|
return cachedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuleRegistry() {
|
function getFuzzySearchRules() {
|
||||||
cachedRuleRegistry ??= createDefaultFuzzySearchRuleRegistry(
|
cachedRules ??= createDefaultFuzzySearchRules(
|
||||||
getFuzzySearchConfig(),
|
getFuzzySearchConfig(),
|
||||||
);
|
);
|
||||||
return cachedRuleRegistry;
|
return cachedRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,7 +105,7 @@ export function fuzzySearch(keyword: string): FuzzySearchResult {
|
|||||||
const push = appendTo(matches);
|
const push = appendTo(matches);
|
||||||
|
|
||||||
for (const rawKeyword of extractKeywords(keyword)) {
|
for (const rawKeyword of extractKeywords(keyword)) {
|
||||||
getRuleRegistry().match(createFuzzySearchKeyword(rawKeyword), push);
|
getFuzzySearchRules().match(createFuzzySearchKeyword(rawKeyword), push);
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { assetErrorImageBuffer } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-image';
|
import { assetErrorImageBuffer } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-image';
|
||||||
import { bangDreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
import { bangdreamBestdoriProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider';
|
||||||
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
import type { BangDreamDataProvider } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bangdream-data-provider';
|
||||||
import {
|
import {
|
||||||
BANGDREAM_DIFFICULTY_NAME_BY_ID,
|
BANGDREAM_DIFFICULTY_NAME_BY_ID,
|
||||||
@ -31,7 +31,7 @@ function getServerCode(server: Server | undefined): string {
|
|||||||
|
|
||||||
export class SongResourceRepository {
|
export class SongResourceRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
private readonly provider: BangDreamDataProvider = bangdreamBestdoriProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
|
||||||
import { bangDreamMainDataRepository } from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
import { bangdreamCatalogRepository } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
import { stringToNumberArray } from '@/modules/qqbot/plugins/bangdream/src/domain/common/model-utils';
|
||||||
import {
|
import {
|
||||||
BANGDREAM_DIFFICULTY_COLORS,
|
BANGDREAM_DIFFICULTY_COLORS,
|
||||||
@ -99,7 +99,7 @@ export class Song {
|
|||||||
*/
|
*/
|
||||||
constructor(songId: number) {
|
constructor(songId: number) {
|
||||||
this.songId = songId;
|
this.songId = songId;
|
||||||
const songData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
const songData = bangdreamCatalogRepository.getEntity<Record<string, any>>(
|
||||||
'songs',
|
'songs',
|
||||||
songId,
|
songId,
|
||||||
);
|
);
|
||||||
@ -131,7 +131,7 @@ export class Song {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//meta数据
|
//meta数据
|
||||||
const metaData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
const metaData = bangdreamCatalogRepository.getEntity<Record<string, any>>(
|
||||||
'meta',
|
'meta',
|
||||||
songId,
|
songId,
|
||||||
);
|
);
|
||||||
@ -321,7 +321,7 @@ export function getPresentSongList(
|
|||||||
end: number = Date.now(),
|
end: number = Date.now(),
|
||||||
): Song[] {
|
): Song[] {
|
||||||
const songList: Array<Song> = [];
|
const songList: Array<Song> = [];
|
||||||
const songListMain = bangDreamMainDataRepository.getCollection('songs');
|
const songListMain = bangdreamCatalogRepository.getCollection('songs');
|
||||||
|
|
||||||
for (const songId in songListMain) {
|
for (const songId in songListMain) {
|
||||||
if (Object.prototype.hasOwnProperty.call(songListMain, songId)) {
|
if (Object.prototype.hasOwnProperty.call(songListMain, songId)) {
|
||||||
@ -368,7 +368,7 @@ export function getMetaRanking(
|
|||||||
withFever: boolean,
|
withFever: boolean,
|
||||||
mainServer: Server,
|
mainServer: Server,
|
||||||
): SongInRank[] {
|
): SongInRank[] {
|
||||||
const songIdList = bangDreamMainDataRepository.getNumericIds('meta');
|
const songIdList = bangdreamCatalogRepository.getNumericIds('meta');
|
||||||
const songRankList: SongInRank[] = [];
|
const songRankList: SongInRank[] = [];
|
||||||
for (let i = 0; i < songIdList.length; i++) {
|
for (let i = 0; i < songIdList.length; i++) {
|
||||||
const songId = songIdList[i];
|
const songId = songIdList[i];
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { Song } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song.model';
|
import { Song } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song.model';
|
||||||
import {
|
import {
|
||||||
bangDreamMainDataRepository,
|
bangdreamCatalogRepository,
|
||||||
type BangDreamMainDataCollection,
|
type BangDreamCatalogCollection,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/application/main-data.repository';
|
} from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-repository';
|
||||||
|
|
||||||
export class SongRepository {
|
export class SongRepository {
|
||||||
/**
|
/**
|
||||||
* 获取歌曲主数据集合。
|
* 获取歌曲主数据集合。
|
||||||
*/
|
*/
|
||||||
getSource(): BangDreamMainDataCollection {
|
getSource(): BangDreamCatalogCollection {
|
||||||
return bangDreamMainDataRepository.getCollection('songs');
|
return bangdreamCatalogRepository.getCollection('songs');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import {
|
|||||||
type BangDreamCommandContextOptions,
|
type BangDreamCommandContextOptions,
|
||||||
} from './application/bangdream-command-context';
|
} from './application/bangdream-command-context';
|
||||||
import {
|
import {
|
||||||
createBangDreamHookContext,
|
createBangDreamOperationLifecycleContext,
|
||||||
createBangDreamLogHook,
|
createBangDreamOperationLogObserver,
|
||||||
BangDreamHookRegistry,
|
BangDreamOperationLifecycle,
|
||||||
} from './application/hook/hook-registry';
|
} from './application/execution/operation-lifecycle';
|
||||||
import {
|
import {
|
||||||
configureBangDreamRuntimeIo,
|
configureBangDreamRuntimeIo,
|
||||||
type BangDreamRuntimeIo,
|
type BangDreamRuntimeIo,
|
||||||
@ -16,14 +16,14 @@ import {
|
|||||||
type BangDreamOperationModule,
|
type BangDreamOperationModule,
|
||||||
} from './operations';
|
} from './operations';
|
||||||
import type {
|
import type {
|
||||||
QqbotBangDreamCommandInput,
|
BangDreamCommandInput,
|
||||||
QqbotBangDreamCommandOutput,
|
BangDreamCommandOutput,
|
||||||
QqbotBangDreamOperationHandlerName,
|
BangDreamOperationHandlerName,
|
||||||
QqbotBangDreamOperationKey,
|
BangDreamOperationKey,
|
||||||
} from './domain/common/qqbot-bangdream.types';
|
} from './domain/common/bangdream.types';
|
||||||
import { waitForMainDataReady } from './application/main-data-store';
|
import { waitForBangDreamCatalogReady } from './application/catalog/bangdream-catalog-cache';
|
||||||
|
|
||||||
export type BangDreamPluginRuntimeOptions = BangDreamCommandContextOptions & {
|
type BangDreamPluginRuntimeOptions = BangDreamCommandContextOptions & {
|
||||||
description?: string;
|
description?: string;
|
||||||
io?: BangDreamRuntimeIo;
|
io?: BangDreamRuntimeIo;
|
||||||
legacyAliases?: string[];
|
legacyAliases?: string[];
|
||||||
@ -34,13 +34,15 @@ export type BangDreamPluginRuntimeOptions = BangDreamCommandContextOptions & {
|
|||||||
version?: string;
|
version?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BangDreamManifestOperation = {
|
type BangDreamManifestOperation = {
|
||||||
|
aliases?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
handlerName: QqbotBangDreamOperationHandlerName;
|
handlerName: BangDreamOperationHandlerName;
|
||||||
inputSchema?: Record<string, any>;
|
inputSchema?: Record<string, any>;
|
||||||
key: QqbotBangDreamOperationKey;
|
key: BangDreamOperationKey;
|
||||||
name?: string;
|
name?: string;
|
||||||
outputSchema?: Record<string, any>;
|
outputSchema?: Record<string, any>;
|
||||||
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BangDreamResolvedOperation = BangDreamManifestOperation & {
|
type BangDreamResolvedOperation = BangDreamManifestOperation & {
|
||||||
@ -50,7 +52,9 @@ type BangDreamResolvedOperation = BangDreamManifestOperation & {
|
|||||||
export function createPlugin(options: BangDreamPluginRuntimeOptions) {
|
export function createPlugin(options: BangDreamPluginRuntimeOptions) {
|
||||||
if (options.io) configureBangDreamRuntimeIo(options.io);
|
if (options.io) configureBangDreamRuntimeIo(options.io);
|
||||||
const context = new BangDreamCommandContext(options);
|
const context = new BangDreamCommandContext(options);
|
||||||
const hookRegistry = new BangDreamHookRegistry([createBangDreamLogHook()]);
|
const lifecycle = new BangDreamOperationLifecycle([
|
||||||
|
createBangDreamOperationLogObserver(),
|
||||||
|
]);
|
||||||
const operationsByKey = resolveBangDreamOperations(options.operations);
|
const operationsByKey = resolveBangDreamOperations(options.operations);
|
||||||
const normalizeError =
|
const normalizeError =
|
||||||
options.normalizeError ||
|
options.normalizeError ||
|
||||||
@ -59,12 +63,12 @@ export function createPlugin(options: BangDreamPluginRuntimeOptions) {
|
|||||||
'BangDream 命令执行失败');
|
'BangDream 命令执行失败');
|
||||||
|
|
||||||
const executeOperation = (
|
const executeOperation = (
|
||||||
operationKey: QqbotBangDreamOperationKey,
|
operationKey: BangDreamOperationKey,
|
||||||
input: QqbotBangDreamCommandInput,
|
input: BangDreamCommandInput,
|
||||||
) =>
|
) =>
|
||||||
executeBangDreamOperation({
|
executeBangDreamOperation({
|
||||||
context,
|
context,
|
||||||
hookRegistry,
|
lifecycle,
|
||||||
input,
|
input,
|
||||||
normalizeError,
|
normalizeError,
|
||||||
operationKey,
|
operationKey,
|
||||||
@ -98,13 +102,15 @@ export function createPlugin(options: BangDreamPluginRuntimeOptions) {
|
|||||||
legacyKeys: options.legacyAliases,
|
legacyKeys: options.legacyAliases,
|
||||||
name: options.name || 'BangDream 查询',
|
name: options.name || 'BangDream 查询',
|
||||||
operations: options.operations.map((operation) => ({
|
operations: options.operations.map((operation) => ({
|
||||||
|
aliases: operation.aliases,
|
||||||
cacheTtlMs: 60_000,
|
cacheTtlMs: 60_000,
|
||||||
description: operation.description,
|
description: operation.description,
|
||||||
inputSchema: operation.inputSchema || getBangDreamInputSchema(),
|
inputSchema: operation.inputSchema || getBangDreamInputSchema(),
|
||||||
key: operation.key,
|
key: operation.key,
|
||||||
name: operation.name || operation.key,
|
name: operation.name || operation.key,
|
||||||
outputSchema: operation.outputSchema || getBangDreamOutputSchema(),
|
outputSchema: operation.outputSchema || getBangDreamOutputSchema(),
|
||||||
execute: async (input: QqbotBangDreamCommandInput) =>
|
timeoutMs: operation.timeoutMs,
|
||||||
|
execute: async (input: BangDreamCommandInput) =>
|
||||||
await executeOperation(operation.key, input),
|
await executeOperation(operation.key, input),
|
||||||
})),
|
})),
|
||||||
version: options.version || '2.0.0',
|
version: options.version || '2.0.0',
|
||||||
@ -134,21 +140,21 @@ function resolveBangDreamOperations(operations: BangDreamManifestOperation[]) {
|
|||||||
|
|
||||||
async function executeBangDreamOperation(options: {
|
async function executeBangDreamOperation(options: {
|
||||||
context: BangDreamCommandContext;
|
context: BangDreamCommandContext;
|
||||||
hookRegistry: BangDreamHookRegistry;
|
lifecycle: BangDreamOperationLifecycle;
|
||||||
input: QqbotBangDreamCommandInput;
|
input: BangDreamCommandInput;
|
||||||
normalizeError: (error: unknown) => string;
|
normalizeError: (error: unknown) => string;
|
||||||
operationKey: QqbotBangDreamOperationKey;
|
operationKey: BangDreamOperationKey;
|
||||||
operationsByKey: Map<QqbotBangDreamOperationKey, BangDreamResolvedOperation>;
|
operationsByKey: Map<BangDreamOperationKey, BangDreamResolvedOperation>;
|
||||||
}): Promise<QqbotBangDreamCommandOutput> {
|
}): Promise<BangDreamCommandOutput> {
|
||||||
const operationContext = createBangDreamHookContext(
|
const operationContext = createBangDreamOperationLifecycleContext(
|
||||||
options.operationKey,
|
options.operationKey,
|
||||||
options.input,
|
options.input,
|
||||||
);
|
);
|
||||||
await options.hookRegistry.beforeParse(operationContext);
|
await options.lifecycle.beforeParse(operationContext);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
operationContext.stage = 'mainData';
|
operationContext.stage = 'catalog';
|
||||||
await waitForMainDataReady();
|
await waitForBangDreamCatalogReady();
|
||||||
|
|
||||||
operationContext.stage = 'operation';
|
operationContext.stage = 'operation';
|
||||||
const operation = options.operationsByKey.get(options.operationKey);
|
const operation = options.operationsByKey.get(options.operationKey);
|
||||||
@ -156,31 +162,24 @@ async function executeBangDreamOperation(options: {
|
|||||||
throw new Error(`BangDream 插件能力不存在:${options.operationKey}`);
|
throw new Error(`BangDream 插件能力不存在:${options.operationKey}`);
|
||||||
}
|
}
|
||||||
operationContext.handlerName = operation.handlerName;
|
operationContext.handlerName = operation.handlerName;
|
||||||
await options.hookRegistry.afterResolve(operationContext);
|
await options.lifecycle.afterResolve(operationContext);
|
||||||
|
|
||||||
operationContext.stage = 'handler';
|
operationContext.stage = 'handler';
|
||||||
await options.hookRegistry.beforeRender(operationContext);
|
await options.lifecycle.beforeRender(operationContext);
|
||||||
const output = await operation.execute(options.input, options.context);
|
const output = await operation.execute(options.input, options.context);
|
||||||
|
|
||||||
operationContext.stage = 'output';
|
operationContext.stage = 'output';
|
||||||
operationContext.imageCount = output.imageCount;
|
operationContext.imageCount = output.imageCount;
|
||||||
operationContext.query = output.query || operationContext.query;
|
operationContext.query = output.query || operationContext.query;
|
||||||
await options.hookRegistry.afterOutput(operationContext);
|
await options.lifecycle.afterOutput(operationContext);
|
||||||
return output;
|
return output;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = options.normalizeError(error);
|
const message = options.normalizeError(error);
|
||||||
await options.hookRegistry.onError(operationContext, message);
|
await options.lifecycle.onError(operationContext, message);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
|
||||||
QqbotBangDreamCommandInput,
|
|
||||||
QqbotBangDreamCommandOutput,
|
|
||||||
QqbotBangDreamOperationHandlerName,
|
|
||||||
QqbotBangDreamOperationKey,
|
|
||||||
} from './domain/common/qqbot-bangdream.types';
|
|
||||||
|
|
||||||
function getBangDreamInputSchema() {
|
function getBangDreamInputSchema() {
|
||||||
return {
|
return {
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { getJsonAndSave } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/file-cache.client';
|
import { fetchRemoteResourceJson } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/remote-resource.client';
|
||||||
import {
|
import {
|
||||||
getCacheDirectory,
|
getCacheDirectory,
|
||||||
getFileNameFromUrl,
|
getFileNameFromUrl,
|
||||||
@ -32,7 +32,8 @@ async function callAPIAndCacheResponse(
|
|||||||
const cacheDir = getCacheDirectory(url);
|
const cacheDir = getCacheDirectory(url);
|
||||||
const fileName = getFileNameFromUrl(url);
|
const fileName = getFileNameFromUrl(url);
|
||||||
return await runWithCacheClientRetry({
|
return await runWithCacheClientRetry({
|
||||||
action: () => getJsonAndSave(url, cacheDir, fileName, cacheTime),
|
action: () =>
|
||||||
|
fetchRemoteResourceJson(url, cacheDir, fileName, cacheTime),
|
||||||
onFailure: (attempt, _retryCount, error) => {
|
onFailure: (attempt, _retryCount, error) => {
|
||||||
if (isCacheClientNotFound(error)) {
|
if (isCacheClientNotFound(error)) {
|
||||||
logger(
|
logger(
|
||||||
|
|||||||
@ -104,4 +104,4 @@ export function createBestdoriProvider(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bangDreamBestdoriProvider = createBestdoriProvider();
|
export const bangdreamBestdoriProvider = createBestdoriProvider();
|
||||||
|
|||||||
@ -75,4 +75,4 @@ export function createHhwxTrackerProvider(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bangDreamHhwxTrackerProvider = createHhwxTrackerProvider();
|
export const bangdreamHhwxTrackerProvider = createHhwxTrackerProvider();
|
||||||
|
|||||||
@ -120,4 +120,4 @@ export async function sleepBangDreamRuntime(ms: number) {
|
|||||||
await new Promise((resolve) => globalThis[`set${'Timeout'}`](resolve, ms));
|
await new Promise((resolve) => globalThis[`set${'Timeout'}`](resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bangDreamFallbackImageBuffer = defaultPng;
|
export const bangdreamFallbackImageBuffer = defaultPng;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {
|
|||||||
getCacheDirectory,
|
getCacheDirectory,
|
||||||
getFileNameFromUrl,
|
getFileNameFromUrl,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/cache-path';
|
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/cache-path';
|
||||||
import { download } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/file-cache.client';
|
import { fetchRemoteResourceBuffer } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/remote-resource.client';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { assetErrorImageBuffer } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-image';
|
import { assetErrorImageBuffer } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-image';
|
||||||
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
import { logger } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger';
|
||||||
@ -53,7 +53,12 @@ async function downloadFile(
|
|||||||
return await runWithCacheClientRetry({
|
return await runWithCacheClientRetry({
|
||||||
action: async () => {
|
action: async () => {
|
||||||
assetNotExists = false;
|
assetNotExists = false;
|
||||||
const data = await download(url, cacheDir, fileName, cacheTime);
|
const data = await fetchRemoteResourceBuffer(
|
||||||
|
url,
|
||||||
|
cacheDir,
|
||||||
|
fileName,
|
||||||
|
cacheTime,
|
||||||
|
);
|
||||||
const htmlSig = Buffer.from('<!DOCTYPE html>');
|
const htmlSig = Buffer.from('<!DOCTYPE html>');
|
||||||
const slice = Buffer.from(data.subarray(0, htmlSig.length));
|
const slice = Buffer.from(data.subarray(0, htmlSig.length));
|
||||||
if (slice.equals(htmlSig)) {
|
if (slice.equals(htmlSig)) {
|
||||||
|
|||||||
@ -36,7 +36,7 @@ function rememberNotFound(url: string, statusCode?: number) {
|
|||||||
if (statusCode === 404) errorUrlCache[url] = Date.now();
|
if (statusCode === 404) errorUrlCache[url] = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function download(
|
export async function fetchRemoteResourceBuffer(
|
||||||
url: string,
|
url: string,
|
||||||
_directory?: string,
|
_directory?: string,
|
||||||
_fileName?: string,
|
_fileName?: string,
|
||||||
@ -66,7 +66,7 @@ export async function download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getJsonAndSave<T = object>(
|
export async function fetchRemoteResourceJson<T = object>(
|
||||||
url: string,
|
url: string,
|
||||||
_directory?: string,
|
_directory?: string,
|
||||||
_fileName?: string,
|
_fileName?: string,
|
||||||
@ -29,4 +29,4 @@ export class BangDreamStaticPatchProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bangDreamStaticPatchProvider = new BangDreamStaticPatchProvider();
|
export const bangdreamStaticPatchProvider = new BangDreamStaticPatchProvider();
|
||||||
|
|||||||
@ -3,15 +3,38 @@ import { drawCutoffEventTop } from '@/modules/qqbot/plugins/bangdream/src/domain
|
|||||||
import { getPresentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
|
import { getPresentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
|
||||||
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
|
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
|
||||||
|
|
||||||
|
const CUTOFF_DETAIL_COMMAND_ALIASES = new Set([
|
||||||
|
'ycx',
|
||||||
|
'预测线',
|
||||||
|
'查档线',
|
||||||
|
'bd档线',
|
||||||
|
]);
|
||||||
|
const DEFAULT_CUTOFF_DETAIL_TIER = 1000;
|
||||||
|
|
||||||
export const cutoffDetailOperation: BangDreamOperationModule = {
|
export const cutoffDetailOperation: BangDreamOperationModule = {
|
||||||
handlerName: 'getCutoffDetail',
|
handlerName: 'getCutoffDetail',
|
||||||
execute: async (input, context) => {
|
execute: async (input, context) => {
|
||||||
const tokens = context.getTokens(input);
|
const tokens = context.getTokens(input);
|
||||||
const tier = context.requireNumber(input.tier, tokens[0], '请提供档位');
|
const hasCommandAlias = CUTOFF_DETAIL_COMMAND_ALIASES.has(tokens[0]);
|
||||||
const mainServer = context.pickMainServer(input, tokens.slice(1));
|
const argumentTokens = hasCommandAlias ? tokens.slice(1) : tokens;
|
||||||
|
const explicitTier = context.optionalNumber(input.tier);
|
||||||
|
const tierFromText = context.optionalNumber(argumentTokens[0]);
|
||||||
|
const tier =
|
||||||
|
explicitTier ??
|
||||||
|
tierFromText ??
|
||||||
|
(hasCommandAlias || argumentTokens.length === 0
|
||||||
|
? DEFAULT_CUTOFF_DETAIL_TIER
|
||||||
|
: undefined);
|
||||||
|
if (tier === undefined) throw new Error('请提供档位');
|
||||||
|
|
||||||
|
const remainingTokens =
|
||||||
|
explicitTier === undefined && tierFromText !== undefined
|
||||||
|
? argumentTokens.slice(1)
|
||||||
|
: argumentTokens;
|
||||||
|
const mainServer = context.pickMainServer(input, remainingTokens);
|
||||||
const eventId =
|
const eventId =
|
||||||
context.optionalNumber(input.eventId) ??
|
context.optionalNumber(input.eventId) ??
|
||||||
context.firstNumber(tokens.slice(1)) ??
|
context.firstNumber(remainingTokens) ??
|
||||||
getPresentEvent(mainServer).eventId;
|
getPresentEvent(mainServer).eventId;
|
||||||
const options = context.getRenderOptions({ ...input, mainServer });
|
const options = context.getRenderOptions({ ...input, mainServer });
|
||||||
const images =
|
const images =
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import type { BangDreamCommandContext } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context';
|
import type { BangDreamCommandContext } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context';
|
||||||
import type {
|
import type {
|
||||||
QqbotBangDreamCommandInput,
|
BangDreamCommandInput,
|
||||||
QqbotBangDreamCommandOutput,
|
BangDreamCommandOutput,
|
||||||
QqbotBangDreamOperationHandlerName,
|
BangDreamOperationHandlerName,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/qqbot-bangdream.types';
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream.types';
|
||||||
|
|
||||||
export type BangDreamOperationExecute = (
|
export type BangDreamOperationExecute = (
|
||||||
input: QqbotBangDreamCommandInput,
|
input: BangDreamCommandInput,
|
||||||
context: BangDreamCommandContext,
|
context: BangDreamCommandContext,
|
||||||
) => Promise<QqbotBangDreamCommandOutput>;
|
) => Promise<BangDreamCommandOutput>;
|
||||||
|
|
||||||
export type BangDreamOperationModule = {
|
export type BangDreamOperationModule = {
|
||||||
execute: BangDreamOperationExecute;
|
execute: BangDreamOperationExecute;
|
||||||
expectedImageCount?: number;
|
expectedImageCount?: number;
|
||||||
handlerName: QqbotBangDreamOperationHandlerName;
|
handlerName: BangDreamOperationHandlerName;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Canvas, loadImage, Image } from 'skia-canvas';
|
import { Canvas, loadImage, Image } from 'skia-canvas';
|
||||||
import * as svg2img from 'svg2img';
|
import * as svg2img from 'svg2img';
|
||||||
import {
|
import {
|
||||||
bangDreamFallbackImageBuffer,
|
bangdreamFallbackImageBuffer,
|
||||||
readBangDreamAsset,
|
readBangDreamAsset,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ const convertSvg = svg2img as unknown as (
|
|||||||
callback: (error: Error | null, buffer: Buffer) => void,
|
callback: (error: Error | null, buffer: Buffer) => void,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export const assetErrorImageBuffer = bangDreamFallbackImageBuffer;
|
export const assetErrorImageBuffer = bangdreamFallbackImageBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在底层绘图工具层中加载图片FromPath。
|
* 在底层绘图工具层中加载图片FromPath。
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Ff14MarketClient } from '../infrastructure/integration/ff14-market-client';
|
import { Ff14MarketClient } from '../infrastructure/integration/ff14-market-client';
|
||||||
|
import { parseFf14MarketPriceInput } from './ff14-market-input-parser';
|
||||||
|
|
||||||
export class Ff14MarketApplication {
|
export class Ff14MarketApplication {
|
||||||
constructor(private readonly client: Ff14MarketClient) {}
|
constructor(private readonly client: Ff14MarketClient) {}
|
||||||
@ -7,6 +8,13 @@ export class Ff14MarketApplication {
|
|||||||
return this.client.getPrice(input);
|
return this.client.getPrice(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async parsePriceInput(rawArgs: string) {
|
||||||
|
return parseFf14MarketPriceInput(
|
||||||
|
rawArgs,
|
||||||
|
await this.client.getMarketCatalog(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async resolveItem(input: Record<string, any>) {
|
async resolveItem(input: Record<string, any>) {
|
||||||
return this.client.resolveItem(input);
|
return this.client.resolveItem(input);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import type { QqbotFf14MarketCatalog } from '../domain/ff14-market.types';
|
import type { Ff14MarketCatalog } from '../domain/ff14-market.types';
|
||||||
import {
|
import {
|
||||||
isQqbotFf14DataCenterName,
|
isFf14DataCenterName,
|
||||||
isQqbotFf14LocationName,
|
isFf14LocationName,
|
||||||
isQqbotFf14RegionName,
|
isFf14RegionName,
|
||||||
isQqbotFf14WorldName,
|
isFf14WorldName,
|
||||||
splitQqbotFf14WorldPath,
|
splitFf14WorldPath,
|
||||||
} from '../domain/ff14-worlds';
|
} from '../domain/ff14-worlds';
|
||||||
|
|
||||||
export type QqbotFf14MarketPriceInput = {
|
export type Ff14MarketPriceInput = {
|
||||||
dataCenter?: string;
|
dataCenter?: string;
|
||||||
hq?: boolean;
|
hq?: boolean;
|
||||||
item?: string;
|
item?: string;
|
||||||
@ -17,10 +17,10 @@ export type QqbotFf14MarketPriceInput = {
|
|||||||
world?: string;
|
world?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseQqbotFf14MarketPriceInput(
|
export function parseFf14MarketPriceInput(
|
||||||
rawArgs: string,
|
rawArgs: string,
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
): QqbotFf14MarketPriceInput {
|
): Ff14MarketPriceInput {
|
||||||
const tokens = rawArgs.split(/\s+/).filter(Boolean);
|
const tokens = rawArgs.split(/\s+/).filter(Boolean);
|
||||||
const flags = new Map<string, string | true>();
|
const flags = new Map<string, string | true>();
|
||||||
const positional: string[] = [];
|
const positional: string[] = [];
|
||||||
@ -53,7 +53,7 @@ export function parseQqbotFf14MarketPriceInput(
|
|||||||
);
|
);
|
||||||
let item = positional.join(' ');
|
let item = positional.join(' ');
|
||||||
|
|
||||||
const worldPath = splitQqbotFf14WorldPath(world);
|
const worldPath = splitFf14WorldPath(world);
|
||||||
if (worldPath.dataCenter && worldPath.world) {
|
if (worldPath.dataCenter && worldPath.world) {
|
||||||
dataCenter = dataCenter || worldPath.dataCenter;
|
dataCenter = dataCenter || worldPath.dataCenter;
|
||||||
region = region || worldPath.region || '';
|
region = region || worldPath.region || '';
|
||||||
@ -71,7 +71,7 @@ export function parseQqbotFf14MarketPriceInput(
|
|||||||
}
|
}
|
||||||
if (item.includes('@')) {
|
if (item.includes('@')) {
|
||||||
const [itemName, worldName] = item.split('@');
|
const [itemName, worldName] = item.split('@');
|
||||||
const itemWorldPath = splitQqbotFf14WorldPath(worldName);
|
const itemWorldPath = splitFf14WorldPath(worldName);
|
||||||
item = itemName.trim();
|
item = itemName.trim();
|
||||||
dataCenter = dataCenter || itemWorldPath.dataCenter || '';
|
dataCenter = dataCenter || itemWorldPath.dataCenter || '';
|
||||||
region = region || itemWorldPath.region || '';
|
region = region || itemWorldPath.region || '';
|
||||||
@ -90,13 +90,13 @@ export function parseQqbotFf14MarketPriceInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pickTrailingFf14Location(
|
function pickTrailingFf14Location(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
positional: string[],
|
positional: string[],
|
||||||
) {
|
) {
|
||||||
const last = positional[positional.length - 1];
|
const last = positional[positional.length - 1];
|
||||||
if (!isQqbotFf14LocationName(catalog, last)) return null;
|
if (!isFf14LocationName(catalog, last)) return null;
|
||||||
|
|
||||||
const path = splitQqbotFf14WorldPath(last);
|
const path = splitFf14WorldPath(last);
|
||||||
if (path.dataCenter && path.world) {
|
if (path.dataCenter && path.world) {
|
||||||
return {
|
return {
|
||||||
dataCenter: path.dataCenter,
|
dataCenter: path.dataCenter,
|
||||||
@ -110,11 +110,11 @@ function pickTrailingFf14Location(
|
|||||||
const beforePrevious = positional[positional.length - 3];
|
const beforePrevious = positional[positional.length - 3];
|
||||||
if (
|
if (
|
||||||
previous &&
|
previous &&
|
||||||
isQqbotFf14DataCenterName(catalog, previous) &&
|
isFf14DataCenterName(catalog, previous) &&
|
||||||
isQqbotFf14WorldName(catalog, last)
|
isFf14WorldName(catalog, last)
|
||||||
) {
|
) {
|
||||||
const hasRegion =
|
const hasRegion =
|
||||||
beforePrevious && isQqbotFf14RegionName(catalog, beforePrevious);
|
beforePrevious && isFf14RegionName(catalog, beforePrevious);
|
||||||
return {
|
return {
|
||||||
dataCenter: previous,
|
dataCenter: previous,
|
||||||
item: positional.slice(0, hasRegion ? -3 : -2).join(' '),
|
item: positional.slice(0, hasRegion ? -3 : -2).join(' '),
|
||||||
@ -125,8 +125,8 @@ function pickTrailingFf14Location(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
previous &&
|
previous &&
|
||||||
isQqbotFf14RegionName(catalog, previous) &&
|
isFf14RegionName(catalog, previous) &&
|
||||||
isQqbotFf14DataCenterName(catalog, last)
|
isFf14DataCenterName(catalog, last)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
dataCenter: last,
|
dataCenter: last,
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
export type Ff14HttpMethod = 'GET';
|
export type Ff14HttpMethod = 'GET';
|
||||||
|
|
||||||
export type QqbotFf14DataCenter = {
|
export type Ff14DataCenter = {
|
||||||
name: string;
|
name: string;
|
||||||
region: string;
|
region: string;
|
||||||
worlds: string[];
|
worlds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotFf14MarketCatalog = {
|
export type Ff14MarketCatalog = {
|
||||||
dataCenters: QqbotFf14DataCenter[];
|
dataCenters: Ff14DataCenter[];
|
||||||
defaultRegion?: string;
|
defaultRegion?: string;
|
||||||
regions: string[];
|
regions: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotFf14MarketTarget = {
|
export type Ff14MarketTarget = {
|
||||||
dataCenter?: string;
|
dataCenter?: string;
|
||||||
label: string;
|
label: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
@ -20,7 +20,7 @@ export type QqbotFf14MarketTarget = {
|
|||||||
world?: string;
|
world?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotFf14ResolvedItem = {
|
export type Ff14ResolvedItem = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
isUntradable?: boolean;
|
isUntradable?: boolean;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
@ -38,10 +38,10 @@ export type UniversalisListing = {
|
|||||||
worldName?: string;
|
worldName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QqbotFf14PriceResult = {
|
export type Ff14PriceResult = {
|
||||||
averagePrice?: number;
|
averagePrice?: number;
|
||||||
hq?: boolean;
|
hq?: boolean;
|
||||||
item: QqbotFf14ResolvedItem;
|
item: Ff14ResolvedItem;
|
||||||
listings: UniversalisListing[];
|
listings: UniversalisListing[];
|
||||||
minPrice?: number;
|
minPrice?: number;
|
||||||
replyText: string;
|
replyText: string;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
QqbotFf14DataCenter,
|
Ff14DataCenter,
|
||||||
QqbotFf14MarketCatalog,
|
Ff14MarketCatalog,
|
||||||
QqbotFf14MarketTarget,
|
Ff14MarketTarget,
|
||||||
} from './ff14-market.types';
|
} from './ff14-market.types';
|
||||||
|
|
||||||
export type Ff14DictItem = {
|
export type Ff14DictItem = {
|
||||||
@ -22,11 +22,11 @@ export const QQBOT_FF14_MARKET_DICT_CODES = {
|
|||||||
world: 'FF14_MARKET_WORLD',
|
world: 'FF14_MARKET_WORLD',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildQqbotFf14MarketCatalog(input: {
|
export function buildFf14MarketCatalog(input: {
|
||||||
dataCenters: Ff14DictItem[];
|
dataCenters: Ff14DictItem[];
|
||||||
regions: Ff14DictItem[];
|
regions: Ff14DictItem[];
|
||||||
worlds: Ff14DictItem[];
|
worlds: Ff14DictItem[];
|
||||||
}): QqbotFf14MarketCatalog {
|
}): Ff14MarketCatalog {
|
||||||
const regions = input.regions.map(getDictDisplayValue).filter(Boolean);
|
const regions = input.regions.map(getDictDisplayValue).filter(Boolean);
|
||||||
const defaultRegion = regions[0];
|
const defaultRegion = regions[0];
|
||||||
const dataCenters = input.dataCenters
|
const dataCenters = input.dataCenters
|
||||||
@ -36,14 +36,14 @@ export function buildQqbotFf14MarketCatalog(input: {
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
region:
|
region:
|
||||||
normalizeQqbotFf14WorldValue(item.childrenCode) || defaultRegion,
|
normalizeFf14WorldValue(item.childrenCode) || defaultRegion,
|
||||||
worlds: input.worlds
|
worlds: input.worlds
|
||||||
.filter(({ childrenCode }) => childrenCode === getDictRawValue(item))
|
.filter(({ childrenCode }) => childrenCode === getDictRawValue(item))
|
||||||
.map(getDictDisplayValue)
|
.map(getDictDisplayValue)
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item): item is QqbotFf14DataCenter => !!item);
|
.filter((item): item is Ff14DataCenter => !!item);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataCenters,
|
dataCenters,
|
||||||
@ -52,9 +52,9 @@ export function buildQqbotFf14MarketCatalog(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildQqbotFf14MarketCatalogFromTree(
|
export function buildFf14MarketCatalogFromTree(
|
||||||
roots: Ff14DictItem[],
|
roots: Ff14DictItem[],
|
||||||
): QqbotFf14MarketCatalog {
|
): Ff14MarketCatalog {
|
||||||
const regionNodes = roots.filter(
|
const regionNodes = roots.filter(
|
||||||
(item) => item.dictCode === QQBOT_FF14_MARKET_DICT_CODES.region,
|
(item) => item.dictCode === QQBOT_FF14_MARKET_DICT_CODES.region,
|
||||||
);
|
);
|
||||||
@ -76,7 +76,7 @@ export function buildQqbotFf14MarketCatalogFromTree(
|
|||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item): item is QqbotFf14DataCenter => !!item);
|
.filter((item): item is Ff14DataCenter => !!item);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -86,46 +86,46 @@ export function buildQqbotFf14MarketCatalogFromTree(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isQqbotFf14DataCenterName(
|
export function isFf14DataCenterName(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
value?: string,
|
value?: string,
|
||||||
) {
|
) {
|
||||||
const name = normalizeQqbotFf14WorldValue(value);
|
const name = normalizeFf14WorldValue(value);
|
||||||
return catalog.dataCenters.some((item) => item.name === name);
|
return catalog.dataCenters.some((item) => item.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isQqbotFf14RegionName(
|
export function isFf14RegionName(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
value?: string,
|
value?: string,
|
||||||
) {
|
) {
|
||||||
const name = normalizeQqbotFf14WorldValue(value);
|
const name = normalizeFf14WorldValue(value);
|
||||||
return catalog.regions.includes(name);
|
return catalog.regions.includes(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isQqbotFf14WorldName(
|
export function isFf14WorldName(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
value?: string,
|
value?: string,
|
||||||
) {
|
) {
|
||||||
const name = normalizeQqbotFf14WorldValue(value);
|
const name = normalizeFf14WorldValue(value);
|
||||||
return catalog.dataCenters.some((item) => item.worlds.includes(name));
|
return catalog.dataCenters.some((item) => item.worlds.includes(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isQqbotFf14LocationName(
|
export function isFf14LocationName(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
value?: string,
|
value?: string,
|
||||||
) {
|
) {
|
||||||
const name = normalizeQqbotFf14WorldValue(value);
|
const name = normalizeFf14WorldValue(value);
|
||||||
const path = splitQqbotFf14WorldPath(name);
|
const path = splitFf14WorldPath(name);
|
||||||
return (
|
return (
|
||||||
isQqbotFf14RegionName(catalog, name) ||
|
isFf14RegionName(catalog, name) ||
|
||||||
isQqbotFf14DataCenterName(catalog, name) ||
|
isFf14DataCenterName(catalog, name) ||
|
||||||
isQqbotFf14WorldName(catalog, name) ||
|
isFf14WorldName(catalog, name) ||
|
||||||
(!!path.dataCenter && !!path.world)
|
(!!path.dataCenter && !!path.world)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitQqbotFf14WorldPath(value?: string) {
|
export function splitFf14WorldPath(value?: string) {
|
||||||
const raw = normalizeQqbotFf14WorldValue(value);
|
const raw = normalizeFf14WorldValue(value);
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
|
|
||||||
const parts = raw
|
const parts = raw
|
||||||
@ -148,31 +148,31 @@ export function splitQqbotFf14WorldPath(value?: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findQqbotFf14DataCenterByWorld(
|
export function findFf14DataCenterByWorld(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
world?: string,
|
world?: string,
|
||||||
) {
|
) {
|
||||||
const worldName = normalizeQqbotFf14WorldValue(world);
|
const worldName = normalizeFf14WorldValue(world);
|
||||||
return catalog.dataCenters.find((item) => item.worlds.includes(worldName));
|
return catalog.dataCenters.find((item) => item.worlds.includes(worldName));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveQqbotFf14MarketTarget(
|
export function resolveFf14MarketTarget(
|
||||||
catalog: QqbotFf14MarketCatalog,
|
catalog: Ff14MarketCatalog,
|
||||||
params: {
|
params: {
|
||||||
dataCenter?: string;
|
dataCenter?: string;
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
world?: string;
|
world?: string;
|
||||||
},
|
},
|
||||||
): QqbotFf14MarketTarget {
|
): Ff14MarketTarget {
|
||||||
const defaultRegion = catalog.defaultRegion || '';
|
const defaultRegion = catalog.defaultRegion || '';
|
||||||
const fallback = normalizeQqbotFf14WorldValue(params.fallback);
|
const fallback = normalizeFf14WorldValue(params.fallback);
|
||||||
const path = splitQqbotFf14WorldPath(params.world);
|
const path = splitFf14WorldPath(params.world);
|
||||||
const region = normalizeQqbotFf14WorldValue(params.region || path.region);
|
const region = normalizeFf14WorldValue(params.region || path.region);
|
||||||
const dataCenter = normalizeQqbotFf14WorldValue(
|
const dataCenter = normalizeFf14WorldValue(
|
||||||
params.dataCenter || path.dataCenter,
|
params.dataCenter || path.dataCenter,
|
||||||
);
|
);
|
||||||
const rawWorld = normalizeQqbotFf14WorldValue(path.world || params.world);
|
const rawWorld = normalizeFf14WorldValue(path.world || params.world);
|
||||||
const world = dataCenter && rawWorld === defaultRegion ? '' : rawWorld;
|
const world = dataCenter && rawWorld === defaultRegion ? '' : rawWorld;
|
||||||
const raw = world || dataCenter || region || fallback || defaultRegion;
|
const raw = world || dataCenter || region || fallback || defaultRegion;
|
||||||
|
|
||||||
@ -211,7 +211,7 @@ export function resolveQqbotFf14MarketTarget(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedWorldDataCenter = findQqbotFf14DataCenterByWorld(catalog, raw);
|
const matchedWorldDataCenter = findFf14DataCenterByWorld(catalog, raw);
|
||||||
if (matchedWorldDataCenter) {
|
if (matchedWorldDataCenter) {
|
||||||
return {
|
return {
|
||||||
dataCenter: matchedWorldDataCenter.name,
|
dataCenter: matchedWorldDataCenter.name,
|
||||||
@ -222,7 +222,7 @@ export function resolveQqbotFf14MarketTarget(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQqbotFf14DataCenterName(catalog, raw)) {
|
if (isFf14DataCenterName(catalog, raw)) {
|
||||||
return {
|
return {
|
||||||
dataCenter: raw,
|
dataCenter: raw,
|
||||||
label: defaultRegion ? `${defaultRegion} / ${raw}` : raw,
|
label: defaultRegion ? `${defaultRegion} / ${raw}` : raw,
|
||||||
@ -238,13 +238,13 @@ export function resolveQqbotFf14MarketTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDictDisplayValue(item: Ff14DictItem) {
|
function getDictDisplayValue(item: Ff14DictItem) {
|
||||||
return normalizeQqbotFf14WorldValue(item.label || item.value);
|
return normalizeFf14WorldValue(item.label || item.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDictRawValue(item: Ff14DictItem) {
|
function getDictRawValue(item: Ff14DictItem) {
|
||||||
return normalizeQqbotFf14WorldValue(item.value || item.label);
|
return normalizeFf14WorldValue(item.value || item.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeQqbotFf14WorldValue(value?: string | null) {
|
function normalizeFf14WorldValue(value?: string | null) {
|
||||||
return `${value || ''}`.trim();
|
return `${value || ''}`.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user