feat: 接入 QQBot 在线命令与 FF14 插件
This commit is contained in:
parent
c51535eed8
commit
31119bbc93
@ -53,3 +53,8 @@ MQTT_URL=mqtt://127.0.0.1:1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
MQTT_CLIENT_ID=kt-template-online-api-qqbot
|
||||
|
||||
FF14_XIVAPI_BASE_URL=https://v2.xivapi.com/api
|
||||
FF14_UNIVERSALIS_BASE_URL=https://universalis.app/api/v2
|
||||
FF14_DEFAULT_WORLD=中国
|
||||
FF14_MARKET_CACHE_TTL_MS=60000
|
||||
|
||||
@ -94,6 +94,56 @@ CREATE TABLE IF NOT EXISTS `qqbot_rule` (
|
||||
KEY `idx_qqbot_rule_enabled` (`enabled`, `is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_command` (
|
||||
`id` bigint NOT NULL,
|
||||
`code` varchar(80) NOT NULL,
|
||||
`name` varchar(120) NOT NULL DEFAULT '',
|
||||
`aliases` text NOT NULL,
|
||||
`prefixes` varchar(120) NOT NULL DEFAULT '/,!,!',
|
||||
`plugin_key` varchar(80) NOT NULL,
|
||||
`operation_key` varchar(120) NOT NULL,
|
||||
`parser_key` varchar(40) NOT NULL DEFAULT 'plain',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT 'all',
|
||||
`default_params` text DEFAULT NULL,
|
||||
`reply_template` text DEFAULT NULL,
|
||||
`error_template` text DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`priority` int NOT NULL DEFAULT 0,
|
||||
`cooldown_ms` int NOT NULL DEFAULT 1500,
|
||||
`last_hit_at` datetime DEFAULT NULL,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_qqbot_command_code` (`code`, `is_deleted`),
|
||||
KEY `idx_qqbot_command_plugin` (`plugin_key`, `operation_key`),
|
||||
KEY `idx_qqbot_command_enabled` (`enabled`, `is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_command_log` (
|
||||
`id` bigint NOT NULL,
|
||||
`command_id` varchar(64) NOT NULL,
|
||||
`command_code` varchar(80) NOT NULL DEFAULT '',
|
||||
`plugin_key` varchar(80) NOT NULL,
|
||||
`operation_key` varchar(120) NOT NULL,
|
||||
`self_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT 'private',
|
||||
`target_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`user_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`raw_message` text NOT NULL,
|
||||
`input` longtext DEFAULT NULL,
|
||||
`output` longtext DEFAULT NULL,
|
||||
`status` varchar(32) NOT NULL DEFAULT 'success',
|
||||
`error_message` text 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_qqbot_command_log_command` (`command_id`, `status`),
|
||||
KEY `idx_qqbot_command_log_target` (`self_id`, `target_type`, `target_id`),
|
||||
KEY `idx_qqbot_command_log_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `qqbot_conversation` (
|
||||
`id` bigint NOT NULL,
|
||||
`self_id` varchar(64) NOT NULL,
|
||||
@ -263,6 +313,18 @@ VALUES
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`config_key` = VALUES(`config_key`);
|
||||
|
||||
INSERT INTO `qqbot_command` (`id`, `code`, `name`, `aliases`, `prefixes`, `plugin_key`, `operation_key`, `parser_key`, `target_type`, `default_params`, `reply_template`, `error_template`, `enabled`, `priority`, `cooldown_ms`, `remark`)
|
||||
VALUES
|
||||
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!","!"]', 'ff14Market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"zh","world":"中国"}', '', 'FF14 查价失败:{{error}}', 0, 0, 1500, '默认示例命令;默认查询范围为中国,可按需改为具体服务器')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`plugin_key` = VALUES(`plugin_key`),
|
||||
`operation_key` = VALUES(`operation_key`),
|
||||
`parser_key` = VALUES(`parser_key`),
|
||||
`target_type` = VALUES(`target_type`),
|
||||
`remark` = VALUES(`remark`),
|
||||
`is_deleted` = 0;
|
||||
|
||||
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`)
|
||||
VALUES
|
||||
(2041700000000100400, 0, 'QqBot', '/qqbot', NULL, '/qqbot/dashboard', NULL, 'catalog', '{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}', 1, 110),
|
||||
@ -278,12 +340,19 @@ VALUES
|
||||
(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),
|
||||
(2041700000000100404, 2041700000000100400, 'QqBotConversation', '/qqbot/conversation', '/qqbot/conversation/list', NULL, 'QqBot:Conversation:List', 'menu', '{"icon":"lucide:messages-square","title":"会话管理"}', 1, 3),
|
||||
(2041700000000100405, 2041700000000100400, 'QqBotMessage', '/qqbot/message', '/qqbot/message/list', NULL, 'QqBot:Message:List', 'menu', '{"icon":"lucide:message-square-text","title":"消息日志"}', 1, 4),
|
||||
(2041700000000100406, 2041700000000100400, 'QqBotSendLog', '/qqbot/sendLog', '/qqbot/sendLog/list', NULL, 'QqBot:SendLog:List', 'menu', '{"icon":"lucide:send","title":"发送日志"}', 1, 5),
|
||||
(2041700000000100408, 2041700000000100400, 'QqBotCommand', '/qqbot/command', '/qqbot/command/list', NULL, 'QqBot:Command:List', 'menu', '{"icon":"lucide:square-terminal","title":"在线命令"}', 1, 3),
|
||||
(2041700000000120441, 2041700000000100408, 'QqBotCommandCreate', NULL, NULL, NULL, 'QqBot:Command:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120442, 2041700000000100408, 'QqBotCommandEdit', NULL, NULL, NULL, 'QqBot:Command:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
(2041700000000120443, 2041700000000100408, 'QqBotCommandDelete', NULL, NULL, NULL, 'QqBot:Command:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||
(2041700000000120444, 2041700000000100408, 'QqBotCommandToggle', NULL, NULL, NULL, 'QqBot:Command:Toggle', 'button', '{"title":"启停"}', 1, 0),
|
||||
(2041700000000120445, 2041700000000100408, 'QqBotCommandTest', NULL, NULL, NULL, 'QqBot:Command:Test', 'button', '{"title":"测试命令"}', 1, 0),
|
||||
(2041700000000100409, 2041700000000100400, 'QqBotPlugin', '/qqbot/plugin', '/qqbot/plugin/list', NULL, 'QqBot:Plugin:List', 'menu', '{"icon":"lucide:plug","title":"插件能力"}', 1, 4),
|
||||
(2041700000000100404, 2041700000000100400, 'QqBotConversation', '/qqbot/conversation', '/qqbot/conversation/list', NULL, 'QqBot:Conversation:List', 'menu', '{"icon":"lucide:messages-square","title":"会话管理"}', 1, 5),
|
||||
(2041700000000100405, 2041700000000100400, 'QqBotMessage', '/qqbot/message', '/qqbot/message/list', NULL, 'QqBot:Message:List', 'menu', '{"icon":"lucide:message-square-text","title":"消息日志"}', 1, 6),
|
||||
(2041700000000100406, 2041700000000100400, 'QqBotSendLog', '/qqbot/sendLog', '/qqbot/sendLog/list', NULL, 'QqBot:SendLog:List', 'menu', '{"icon":"lucide:send","title":"发送日志"}', 1, 7),
|
||||
(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, 6),
|
||||
(2041700000000100407, 2041700000000100400, 'QqBotPermission', '/qqbot/permission', '/qqbot/permission/list', NULL, 'QqBot:Permission:List', 'menu', '{"icon":"lucide:shield-check","title":"权限名单"}', 1, 8),
|
||||
(2041700000000120431, 2041700000000100407, 'QqBotPermissionCreate', NULL, NULL, NULL, 'QqBot:Permission:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120432, 2041700000000100407, 'QqBotPermissionEdit', NULL, NULL, NULL, 'QqBot:Permission:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
(2041700000000120433, 2041700000000100407, 'QqBotPermissionDelete', NULL, NULL, NULL, 'QqBot:Permission:Delete', 'button', '{"title":"common.delete"}', 1, 0)
|
||||
|
||||
199
src/qqbot/command/qqbot-command-engine.service.ts
Normal file
199
src/qqbot/command/qqbot-command-engine.service.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
|
||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||
import { QqbotSendService } from '../send/qqbot-send.service';
|
||||
import type { QqbotCommandTestDto } from './qqbot-command.dto';
|
||||
import type { QqbotCommand } from './qqbot-command.entity';
|
||||
import { QqbotCommandParserService } from './qqbot-command-parser.service';
|
||||
import { QqbotCommandService } from './qqbot-command.service';
|
||||
import { QqbotReplyTemplateService } from './qqbot-reply-template.service';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotCommandEngineService {
|
||||
private readonly logger = new Logger(QqbotCommandEngineService.name);
|
||||
|
||||
constructor(
|
||||
private readonly commandParser: QqbotCommandParserService,
|
||||
private readonly commandService: QqbotCommandService,
|
||||
private readonly pluginRegistry: QqbotPluginRegistryService,
|
||||
private readonly replyTemplate: QqbotReplyTemplateService,
|
||||
private readonly sendService: QqbotSendService,
|
||||
) {}
|
||||
|
||||
async handleMessage(message: QqbotNormalizedMessage) {
|
||||
const commands = await this.commandService.listEnabledForMessage(message);
|
||||
for (const command of commands) {
|
||||
const matched = this.commandParser.match(command, message);
|
||||
if (!matched) continue;
|
||||
if (this.commandService.isInCooldown(command)) return true;
|
||||
|
||||
await this.commandService.markHit(command);
|
||||
const input = this.mergeInput(command, matched.input);
|
||||
try {
|
||||
const output = await this.pluginRegistry.execute(
|
||||
command.pluginKey,
|
||||
command.operationKey,
|
||||
input,
|
||||
{
|
||||
args: matched.input,
|
||||
command,
|
||||
message,
|
||||
},
|
||||
);
|
||||
const replyText = this.buildReplyText(command, input, output);
|
||||
if (replyText) {
|
||||
await this.sendService.sendText({
|
||||
channelId: message.channelId,
|
||||
guildId: message.rawEvent.guild_id
|
||||
? `${message.rawEvent.guild_id}`
|
||||
: undefined,
|
||||
message: replyText,
|
||||
selfId: message.selfId,
|
||||
targetId: message.targetId,
|
||||
targetType: message.messageType,
|
||||
});
|
||||
}
|
||||
await this.commandService.logExecution({
|
||||
command,
|
||||
input,
|
||||
message,
|
||||
output,
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '命令执行失败';
|
||||
await this.commandService.logExecution({
|
||||
command,
|
||||
errorMessage,
|
||||
input,
|
||||
message,
|
||||
status: 'failed',
|
||||
});
|
||||
await this.sendErrorReply(command, input, message, errorMessage);
|
||||
this.logger.warn(`QQBot 命令执行失败: ${errorMessage}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async preview(body: QqbotCommandTestDto) {
|
||||
const message = this.buildPreviewMessage(body);
|
||||
const command = body.commandId
|
||||
? await this.commandService.findById(body.commandId)
|
||||
: await this.findMatchedCommand(message);
|
||||
const matched = this.commandParser.match(command, message);
|
||||
if (!matched) {
|
||||
return {
|
||||
matched: false,
|
||||
message: '未匹配到命令',
|
||||
};
|
||||
}
|
||||
|
||||
const input = this.mergeInput(command, matched.input);
|
||||
const output = await this.pluginRegistry.execute(
|
||||
command.pluginKey,
|
||||
command.operationKey,
|
||||
input,
|
||||
{
|
||||
args: matched.input,
|
||||
command,
|
||||
message,
|
||||
},
|
||||
);
|
||||
const replyText = this.buildReplyText(command, input, output);
|
||||
return {
|
||||
command: this.commandService.toResponse(command),
|
||||
input,
|
||||
matched: true,
|
||||
output,
|
||||
replyText,
|
||||
};
|
||||
}
|
||||
|
||||
private async findMatchedCommand(message: QqbotNormalizedMessage) {
|
||||
const commands = await this.commandService.listEnabledForMessage(message);
|
||||
const command = commands.find((item) =>
|
||||
this.commandParser.match(item, message),
|
||||
);
|
||||
if (!command) {
|
||||
throw new Error('未匹配到命令');
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
private buildReplyText(
|
||||
command: QqbotCommand,
|
||||
input: Record<string, any>,
|
||||
output: any,
|
||||
) {
|
||||
const data = { input, output, ...output };
|
||||
return (
|
||||
this.replyTemplate.render(command.replyTemplate, data) ||
|
||||
this.replyTemplate.stringifyOutput(output)
|
||||
);
|
||||
}
|
||||
|
||||
private async sendErrorReply(
|
||||
command: QqbotCommand,
|
||||
input: Record<string, any>,
|
||||
message: QqbotNormalizedMessage,
|
||||
errorMessage: string,
|
||||
) {
|
||||
const reply =
|
||||
this.replyTemplate.render(command.errorTemplate, {
|
||||
error: errorMessage,
|
||||
input,
|
||||
}) || `命令执行失败:${errorMessage}`;
|
||||
try {
|
||||
await this.sendService.sendText({
|
||||
channelId: message.channelId,
|
||||
guildId: message.rawEvent.guild_id
|
||||
? `${message.rawEvent.guild_id}`
|
||||
: undefined,
|
||||
message: reply,
|
||||
selfId: message.selfId,
|
||||
targetId: message.targetId,
|
||||
targetType: message.messageType,
|
||||
});
|
||||
} catch (err) {
|
||||
const sendErr = err instanceof Error ? err.message : '错误回复发送失败';
|
||||
this.logger.warn(`QQBot 命令错误回复发送失败: ${sendErr}`);
|
||||
}
|
||||
}
|
||||
|
||||
private mergeInput(command: QqbotCommand, input: Record<string, any>) {
|
||||
return {
|
||||
...this.commandService.parseDefaultParams(command),
|
||||
...this.removeUndefined(input),
|
||||
};
|
||||
}
|
||||
|
||||
private removeUndefined(input: Record<string, any>) {
|
||||
return Object.entries(input).reduce<Record<string, any>>(
|
||||
(result, [key, value]) => {
|
||||
if (value !== undefined && value !== '') result[key] = value;
|
||||
return result;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
private buildPreviewMessage(body: QqbotCommandTestDto): QqbotNormalizedMessage {
|
||||
const targetType = body.targetType || 'private';
|
||||
const targetId = body.targetId || body.userId || '10000';
|
||||
const userId = body.userId || targetId;
|
||||
return {
|
||||
eventTime: new Date(),
|
||||
groupId: targetType === 'group' ? targetId : undefined,
|
||||
messageId: `preview-${Date.now()}`,
|
||||
messageText: body.text,
|
||||
messageType: targetType,
|
||||
rawEvent: {},
|
||||
rawMessage: body.text,
|
||||
selfId: body.selfId || 'preview',
|
||||
targetId,
|
||||
userId,
|
||||
};
|
||||
}
|
||||
}
|
||||
68
src/qqbot/command/qqbot-command-log.entity.ts
Normal file
68
src/qqbot/command/qqbot-command-log.entity.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId } from '@/common';
|
||||
import type { QqbotMessageType } from '../qqbot.types';
|
||||
|
||||
export type QqbotCommandLogStatus = 'failed' | 'success';
|
||||
|
||||
@Entity('qqbot_command_log')
|
||||
export class QqbotCommandLog {
|
||||
@PrimaryColumn({ type: 'bigint' })
|
||||
id: string;
|
||||
|
||||
@Column({ length: 64, name: 'command_id' })
|
||||
commandId: string;
|
||||
|
||||
@Column({ default: '', length: 80, name: 'command_code' })
|
||||
commandCode: string;
|
||||
|
||||
@Column({ length: 80, name: 'plugin_key' })
|
||||
pluginKey: string;
|
||||
|
||||
@Column({ length: 120, name: 'operation_key' })
|
||||
operationKey: string;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'self_id' })
|
||||
selfId: string;
|
||||
|
||||
@Column({ default: 'private', length: 32, name: 'target_type' })
|
||||
targetType: QqbotMessageType;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'target_id' })
|
||||
targetId: string;
|
||||
|
||||
@Column({ default: '', length: 64, name: 'user_id' })
|
||||
userId: string;
|
||||
|
||||
@Column({ name: 'raw_message', type: 'text' })
|
||||
rawMessage: string;
|
||||
|
||||
@Column({ default: null, nullable: true, type: 'text' })
|
||||
input: string | null;
|
||||
|
||||
@Column({ default: null, nullable: true, type: 'text' })
|
||||
output: string | null;
|
||||
|
||||
@Column({ default: 'success', length: 32 })
|
||||
status: QqbotCommandLogStatus;
|
||||
|
||||
@Column({ default: null, name: 'error_message', nullable: true, type: 'text' })
|
||||
errorMessage: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
createTime: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: Date;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
ensureSnowflakeId(this);
|
||||
}
|
||||
}
|
||||
135
src/qqbot/command/qqbot-command-parser.service.ts
Normal file
135
src/qqbot/command/qqbot-command-parser.service.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { QqbotCommand } from './qqbot-command.entity';
|
||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||
|
||||
export type QqbotCommandMatchResult = {
|
||||
alias: string;
|
||||
input: Record<string, any>;
|
||||
matched: true;
|
||||
rawArgs: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QqbotCommandParserService {
|
||||
match(command: QqbotCommand, message: QqbotNormalizedMessage) {
|
||||
const source = `${message.messageText || ''}`.trim();
|
||||
if (!source) return null;
|
||||
|
||||
const aliases = this.getAliases(command);
|
||||
const prefixes = this.getPrefixes(command);
|
||||
for (const alias of aliases) {
|
||||
for (const prefix of prefixes) {
|
||||
const commandText = `${prefix}${alias}`.trim();
|
||||
const rawArgs = this.pickArgs(source, commandText);
|
||||
if (rawArgs === null) continue;
|
||||
return {
|
||||
alias,
|
||||
input: this.parseInput(command, rawArgs),
|
||||
matched: true,
|
||||
rawArgs,
|
||||
} satisfies QqbotCommandMatchResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getAliases(command: QqbotCommand) {
|
||||
return this.normalizeList(command.aliases, [command.code, command.name]);
|
||||
}
|
||||
|
||||
getPrefixes(command: QqbotCommand) {
|
||||
return this.normalizeList(command.prefixes, ['/', '!', '!']);
|
||||
}
|
||||
|
||||
private pickArgs(source: string, commandText: string) {
|
||||
if (!commandText) return null;
|
||||
if (source === commandText) return '';
|
||||
if (source.startsWith(`${commandText} `)) {
|
||||
return source.slice(commandText.length).trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private parseInput(command: QqbotCommand, rawArgs: string) {
|
||||
if (command.parserKey === 'ff14Price') {
|
||||
return this.parseFf14PriceInput(rawArgs);
|
||||
}
|
||||
const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
|
||||
return {
|
||||
args,
|
||||
raw: rawArgs,
|
||||
text: rawArgs,
|
||||
};
|
||||
}
|
||||
|
||||
private parseFf14PriceInput(rawArgs: string) {
|
||||
const tokens = rawArgs.split(/\s+/).filter(Boolean);
|
||||
const flags = new Map<string, string | true>();
|
||||
const positional: string[] = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
if (/^(hq|HQ)$/.test(token)) {
|
||||
flags.set('hq', true);
|
||||
} else if (/^(nq|NQ)$/.test(token)) {
|
||||
flags.set('hq', 'false');
|
||||
} else if (token.includes('=')) {
|
||||
const [key, ...rest] = token.split('=');
|
||||
flags.set(key, rest.join('='));
|
||||
} else {
|
||||
positional.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
let world = this.normalizeString(flags.get('world') || flags.get('server'));
|
||||
let item = positional.join(' ');
|
||||
if (!world && positional.length > 1) {
|
||||
world = positional[positional.length - 1];
|
||||
item = positional.slice(0, -1).join(' ');
|
||||
}
|
||||
if (item.includes('@')) {
|
||||
const [itemName, worldName] = item.split('@');
|
||||
item = itemName.trim();
|
||||
world = world || worldName?.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
hq: this.normalizeHq(flags.get('hq')),
|
||||
item,
|
||||
language: this.normalizeString(flags.get('lang')) || 'zh',
|
||||
raw: rawArgs,
|
||||
world,
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeHq(value?: string | true) {
|
||||
if (value === undefined) return undefined;
|
||||
if (value === true) return true;
|
||||
if (value === 'false') return false;
|
||||
return ['1', 'true', 'yes', 'hq'].includes(`${value}`.toLowerCase());
|
||||
}
|
||||
|
||||
private normalizeList(value: string | undefined, fallback: string[]) {
|
||||
const raw = `${value || ''}`.trim();
|
||||
const parsed = this.tryParseJsonArray(raw);
|
||||
const source = parsed.length > 0 ? parsed : raw.split(',');
|
||||
const list = [...source, ...fallback]
|
||||
.map((item) => `${item || ''}`.trim())
|
||||
.filter(Boolean);
|
||||
return [...new Set(list)];
|
||||
}
|
||||
|
||||
private tryParseJsonArray(value: string) {
|
||||
if (!value.startsWith('[')) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeString(value?: string | true) {
|
||||
if (value === true) return '';
|
||||
return `${value || ''}`.trim();
|
||||
}
|
||||
}
|
||||
78
src/qqbot/command/qqbot-command.controller.ts
Normal file
78
src/qqbot/command/qqbot-command.controller.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||
import { vbenSuccess } from '@/common';
|
||||
import {
|
||||
QqbotCommandBodyDto,
|
||||
QqbotCommandQueryDto,
|
||||
QqbotCommandTestDto,
|
||||
QqbotCommandUpdateDto,
|
||||
} from './qqbot-command.dto';
|
||||
import { QqbotCommandEngineService } from './qqbot-command-engine.service';
|
||||
import { QqbotCommandService } from './qqbot-command.service';
|
||||
import { normalizeBoolean } from '../qqbot.utils';
|
||||
|
||||
@ApiTags('qqbot-command')
|
||||
@Controller('qqbot/command')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class QqbotCommandController {
|
||||
constructor(
|
||||
private readonly commandEngine: QqbotCommandEngineService,
|
||||
private readonly commandService: QqbotCommandService,
|
||||
) {}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: 'QQBot 在线命令分页' })
|
||||
async list(@Query() query: QqbotCommandQueryDto) {
|
||||
return vbenSuccess(await this.commandService.page(query));
|
||||
}
|
||||
|
||||
@Post('save')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '新增 QQBot 在线命令' })
|
||||
async save(@Body() body: QqbotCommandBodyDto) {
|
||||
return vbenSuccess(await this.commandService.save(body));
|
||||
}
|
||||
|
||||
@Post('update')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '编辑 QQBot 在线命令' })
|
||||
async update(@Body() body: QqbotCommandUpdateDto) {
|
||||
return vbenSuccess(await this.commandService.update(body));
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '删除 QQBot 在线命令' })
|
||||
@ApiQuery({ name: 'id', type: String })
|
||||
async delete(@Query('id') id: string) {
|
||||
return vbenSuccess(await this.commandService.remove(id));
|
||||
}
|
||||
|
||||
@Post('toggle')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '启停 QQBot 在线命令' })
|
||||
@ApiQuery({ name: 'id', type: String })
|
||||
@ApiQuery({ name: 'enabled', type: Boolean })
|
||||
async toggle(@Query('id') id: string, @Query('enabled') enabled: string) {
|
||||
return vbenSuccess(
|
||||
await this.commandService.toggle(id, normalizeBoolean(enabled)),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('test')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '测试 QQBot 在线命令' })
|
||||
async test(@Body() body: QqbotCommandTestDto) {
|
||||
return vbenSuccess(await this.commandEngine.preview(body));
|
||||
}
|
||||
}
|
||||
103
src/qqbot/command/qqbot-command.dto.ts
Normal file
103
src/qqbot/command/qqbot-command.dto.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import type {
|
||||
QqbotCommandParserType,
|
||||
QqbotMessageType,
|
||||
QqbotRuleTargetType,
|
||||
} from '../qqbot.types';
|
||||
import type { QqbotPageQuery } from '../qqbot.utils';
|
||||
|
||||
export class QqbotCommandQueryDto implements QqbotPageQuery {
|
||||
@ApiPropertyOptional()
|
||||
pageNo?: number | string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
pageSize?: number | string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
keyword?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
pluginKey?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
operationKey?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
enabled?: boolean | string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
targetType?: QqbotRuleTargetType;
|
||||
}
|
||||
|
||||
export class QqbotCommandBodyDto {
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
aliases?: string[] | string;
|
||||
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
prefixes?: string[] | string;
|
||||
|
||||
@ApiProperty()
|
||||
pluginKey: string;
|
||||
|
||||
@ApiProperty()
|
||||
operationKey: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
parserKey?: QqbotCommandParserType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
targetType?: QqbotRuleTargetType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
defaultParams?: any;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
replyTemplate?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
errorTemplate?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
enabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
priority?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
cooldownMs?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export class QqbotCommandUpdateDto extends PartialType(QqbotCommandBodyDto) {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class QqbotCommandTestDto {
|
||||
@ApiPropertyOptional()
|
||||
commandId?: string;
|
||||
|
||||
@ApiProperty()
|
||||
text: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
selfId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
targetType?: QqbotMessageType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
targetId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
userId?: string;
|
||||
}
|
||||
83
src/qqbot/command/qqbot-command.entity.ts
Normal file
83
src/qqbot/command/qqbot-command.entity.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId } from '@/common';
|
||||
import type { QqbotCommandParserType, QqbotRuleTargetType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_command')
|
||||
export class QqbotCommand {
|
||||
@PrimaryColumn({ type: 'bigint' })
|
||||
id: string;
|
||||
|
||||
@Column({ length: 80 })
|
||||
code: string;
|
||||
|
||||
@Column({ default: '', length: 120 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
aliases: string;
|
||||
|
||||
@Column({ default: '/,!,!', length: 120 })
|
||||
prefixes: string;
|
||||
|
||||
@Column({ length: 80, name: 'plugin_key' })
|
||||
pluginKey: string;
|
||||
|
||||
@Column({ length: 120, name: 'operation_key' })
|
||||
operationKey: string;
|
||||
|
||||
@Column({ default: 'plain', length: 40, name: 'parser_key' })
|
||||
parserKey: QqbotCommandParserType;
|
||||
|
||||
@Column({ default: 'all', length: 32, name: 'target_type' })
|
||||
targetType: QqbotRuleTargetType;
|
||||
|
||||
@Column({ default: null, name: 'default_params', nullable: true, type: 'text' })
|
||||
defaultParams: string | null;
|
||||
|
||||
@Column({ default: null, name: 'reply_template', nullable: true, type: 'text' })
|
||||
replyTemplate: string | null;
|
||||
|
||||
@Column({ default: null, name: 'error_template', nullable: true, type: 'text' })
|
||||
errorTemplate: string | null;
|
||||
|
||||
@Column({ default: true })
|
||||
enabled: boolean;
|
||||
|
||||
@Column({ default: 0 })
|
||||
priority: number;
|
||||
|
||||
@Column({ default: 1500, name: 'cooldown_ms' })
|
||||
cooldownMs: number;
|
||||
|
||||
@Column({
|
||||
default: null,
|
||||
name: 'last_hit_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
lastHitAt: Date | null;
|
||||
|
||||
@Column({ default: '', length: 255 })
|
||||
remark: string;
|
||||
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
createTime: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: Date;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
ensureSnowflakeId(this);
|
||||
}
|
||||
}
|
||||
288
src/qqbot/command/qqbot-command.service.ts
Normal file
288
src/qqbot/command/qqbot-command.service.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { throwVbenError } from '@/common';
|
||||
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
|
||||
import type {
|
||||
QqbotCommandParserType,
|
||||
QqbotNormalizedMessage,
|
||||
QqbotRuleTargetType,
|
||||
} from '../qqbot.types';
|
||||
import { getPageParams, normalizeBoolean } from '../qqbot.utils';
|
||||
import type {
|
||||
QqbotCommandBodyDto,
|
||||
QqbotCommandQueryDto,
|
||||
QqbotCommandUpdateDto,
|
||||
} from './qqbot-command.dto';
|
||||
import { QqbotCommandLog } from './qqbot-command-log.entity';
|
||||
import { QqbotCommand } from './qqbot-command.entity';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotCommandService {
|
||||
constructor(
|
||||
@InjectRepository(QqbotCommand)
|
||||
private readonly commandRepository: Repository<QqbotCommand>,
|
||||
@InjectRepository(QqbotCommandLog)
|
||||
private readonly commandLogRepository: Repository<QqbotCommandLog>,
|
||||
private readonly pluginRegistry: QqbotPluginRegistryService,
|
||||
) {}
|
||||
|
||||
async page(query: QqbotCommandQueryDto) {
|
||||
const { pageNo, pageSize, skip } = getPageParams(query);
|
||||
const builder = this.commandRepository
|
||||
.createQueryBuilder('command')
|
||||
.where('command.isDeleted = :isDeleted', { isDeleted: false });
|
||||
|
||||
if (query.keyword) {
|
||||
builder.andWhere(
|
||||
'(command.code LIKE :keyword OR command.name LIKE :keyword OR command.aliases LIKE :keyword)',
|
||||
{ keyword: `%${query.keyword}%` },
|
||||
);
|
||||
}
|
||||
if (query.pluginKey) {
|
||||
builder.andWhere('command.pluginKey = :pluginKey', {
|
||||
pluginKey: query.pluginKey,
|
||||
});
|
||||
}
|
||||
if (query.operationKey) {
|
||||
builder.andWhere('command.operationKey = :operationKey', {
|
||||
operationKey: query.operationKey,
|
||||
});
|
||||
}
|
||||
if (query.targetType) {
|
||||
builder.andWhere('command.targetType = :targetType', {
|
||||
targetType: query.targetType,
|
||||
});
|
||||
}
|
||||
if (query.enabled !== undefined && `${query.enabled}` !== '') {
|
||||
builder.andWhere('command.enabled = :enabled', {
|
||||
enabled: normalizeBoolean(query.enabled),
|
||||
});
|
||||
}
|
||||
|
||||
const [list, total] = await builder
|
||||
.orderBy('command.priority', 'DESC')
|
||||
.addOrderBy('command.createTime', 'DESC')
|
||||
.skip(skip)
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
return {
|
||||
list: list.map((item) => this.toResponse(item)),
|
||||
pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
async listEnabledForMessage(message: QqbotNormalizedMessage) {
|
||||
return this.commandRepository
|
||||
.createQueryBuilder('command')
|
||||
.where('command.isDeleted = :isDeleted', { isDeleted: false })
|
||||
.andWhere('command.enabled = :enabled', { enabled: true })
|
||||
.andWhere('command.targetType IN (:...targetTypes)', {
|
||||
targetTypes: ['all', message.messageType],
|
||||
})
|
||||
.orderBy('command.priority', 'DESC')
|
||||
.addOrderBy('command.createTime', 'ASC')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const command = await this.commandRepository.findOne({
|
||||
where: { id, isDeleted: false },
|
||||
});
|
||||
if (!command) throwVbenError('命令不存在');
|
||||
return command;
|
||||
}
|
||||
|
||||
async save(body: QqbotCommandBodyDto) {
|
||||
const payload = await this.normalizeBody(body);
|
||||
await this.assertCodeAvailable(payload.code);
|
||||
const saved = await this.commandRepository.save(
|
||||
this.commandRepository.create(payload),
|
||||
);
|
||||
return saved.id;
|
||||
}
|
||||
|
||||
async update(body: QqbotCommandUpdateDto) {
|
||||
const current = await this.findById(body.id);
|
||||
const payload = await this.normalizeBody({
|
||||
...this.toRawBody(current),
|
||||
...body,
|
||||
});
|
||||
await this.assertCodeAvailable(payload.code, body.id);
|
||||
await this.commandRepository.update({ id: body.id }, payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
await this.commandRepository.update({ id }, { isDeleted: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
async toggle(id: string, enabled: boolean) {
|
||||
await this.commandRepository.update({ id }, { enabled });
|
||||
return true;
|
||||
}
|
||||
|
||||
async markHit(command: QqbotCommand) {
|
||||
await this.commandRepository.update(
|
||||
{ id: command.id },
|
||||
{ lastHitAt: new Date() },
|
||||
);
|
||||
}
|
||||
|
||||
isInCooldown(command: QqbotCommand) {
|
||||
if (!command.lastHitAt || !command.cooldownMs) return false;
|
||||
return (
|
||||
Date.now() - new Date(command.lastHitAt).getTime() < command.cooldownMs
|
||||
);
|
||||
}
|
||||
|
||||
async logExecution(params: {
|
||||
command: QqbotCommand;
|
||||
errorMessage?: string;
|
||||
input: Record<string, any>;
|
||||
message: QqbotNormalizedMessage;
|
||||
output?: any;
|
||||
status: 'failed' | 'success';
|
||||
}) {
|
||||
await this.commandLogRepository.save(
|
||||
this.commandLogRepository.create({
|
||||
commandCode: params.command.code,
|
||||
commandId: params.command.id,
|
||||
errorMessage: params.errorMessage || null,
|
||||
input: JSON.stringify(params.input || {}),
|
||||
operationKey: params.command.operationKey,
|
||||
output:
|
||||
params.output === undefined ? null : JSON.stringify(params.output),
|
||||
pluginKey: params.command.pluginKey,
|
||||
rawMessage: params.message.messageText,
|
||||
selfId: params.message.selfId,
|
||||
status: params.status,
|
||||
targetId: params.message.targetId,
|
||||
targetType: params.message.messageType,
|
||||
userId: params.message.userId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
parseDefaultParams(command: QqbotCommand) {
|
||||
return this.parseJson(command.defaultParams);
|
||||
}
|
||||
|
||||
toResponse(command: QqbotCommand) {
|
||||
return {
|
||||
...command,
|
||||
aliases: this.parseList(command.aliases),
|
||||
defaultParams: this.parseDefaultParams(command),
|
||||
prefixes: this.parseList(command.prefixes),
|
||||
};
|
||||
}
|
||||
|
||||
private async normalizeBody(body: QqbotCommandBodyDto) {
|
||||
const code = `${body.code || ''}`.trim();
|
||||
const pluginKey = `${body.pluginKey || ''}`.trim();
|
||||
const operationKey = `${body.operationKey || ''}`.trim();
|
||||
if (!code) throwVbenError('命令编码不能为空');
|
||||
if (!body.name?.trim()) throwVbenError('命令名称不能为空');
|
||||
this.pluginRegistry.assertOperation(pluginKey, operationKey);
|
||||
|
||||
return {
|
||||
aliases: this.stringifyList(body.aliases),
|
||||
code,
|
||||
cooldownMs: Number(body.cooldownMs ?? 1500),
|
||||
defaultParams: this.stringifyParams(body.defaultParams),
|
||||
enabled: body.enabled ?? true,
|
||||
errorTemplate: body.errorTemplate || null,
|
||||
name: body.name.trim(),
|
||||
operationKey,
|
||||
parserKey: (body.parserKey || 'plain') as QqbotCommandParserType,
|
||||
pluginKey,
|
||||
prefixes: this.stringifyList(body.prefixes, ['/', '!', '!']),
|
||||
priority: Number(body.priority || 0),
|
||||
remark: body.remark || '',
|
||||
replyTemplate: body.replyTemplate || null,
|
||||
targetType: (body.targetType || 'all') as QqbotRuleTargetType,
|
||||
} as Partial<QqbotCommand>;
|
||||
}
|
||||
|
||||
private async assertCodeAvailable(code: string, currentId?: string) {
|
||||
const where = currentId
|
||||
? { code, id: Not(currentId), isDeleted: false }
|
||||
: { code, isDeleted: false };
|
||||
const existed = await this.commandRepository.findOne({ where });
|
||||
if (existed) throwVbenError(`命令编码已存在:${code}`);
|
||||
}
|
||||
|
||||
private stringifyList(value: string[] | string | undefined, fallback = []) {
|
||||
const list = Array.isArray(value)
|
||||
? value
|
||||
: `${value || ''}`
|
||||
.split(',')
|
||||
.map((item) => item.trim());
|
||||
const normalized = list
|
||||
.map((item) => `${item || ''}`.trim())
|
||||
.filter(Boolean);
|
||||
return JSON.stringify([
|
||||
...new Set(normalized.length > 0 ? normalized : fallback),
|
||||
]);
|
||||
}
|
||||
|
||||
private parseList(value: string | null | undefined) {
|
||||
const source = `${value || ''}`.trim();
|
||||
if (!source) return [];
|
||||
if (source.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(source);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return source
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
private stringifyParams(value: any) {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
if (typeof value === 'string') {
|
||||
const source = value.trim();
|
||||
if (!source) return null;
|
||||
this.parseJson(source);
|
||||
return source;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
private parseJson(value: string | null | undefined) {
|
||||
if (!value) return {};
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
throwVbenError('默认参数必须是合法 JSON');
|
||||
}
|
||||
}
|
||||
|
||||
private toRawBody(command: QqbotCommand): QqbotCommandBodyDto {
|
||||
return {
|
||||
aliases: this.parseList(command.aliases),
|
||||
code: command.code,
|
||||
cooldownMs: command.cooldownMs,
|
||||
defaultParams: this.parseDefaultParams(command),
|
||||
enabled: command.enabled,
|
||||
errorTemplate: command.errorTemplate || '',
|
||||
name: command.name,
|
||||
operationKey: command.operationKey,
|
||||
parserKey: command.parserKey,
|
||||
pluginKey: command.pluginKey,
|
||||
prefixes: this.parseList(command.prefixes),
|
||||
priority: command.priority,
|
||||
remark: command.remark,
|
||||
replyTemplate: command.replyTemplate || '',
|
||||
targetType: command.targetType,
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/qqbot/command/qqbot-reply-template.service.ts
Normal file
26
src/qqbot/command/qqbot-reply-template.service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotReplyTemplateService {
|
||||
render(template: string | undefined | null, data: Record<string, any>) {
|
||||
const source = `${template || ''}`.trim();
|
||||
if (!source) return '';
|
||||
return source.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_match, path) => {
|
||||
const value = this.pickValue(data, path);
|
||||
return value === undefined || value === null ? '' : `${value}`;
|
||||
});
|
||||
}
|
||||
|
||||
stringifyOutput(output: any) {
|
||||
if (!output) return '';
|
||||
if (typeof output === 'string') return output;
|
||||
if (typeof output.replyText === 'string') return output.replyText;
|
||||
return JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
private pickValue(data: Record<string, any>, path: string) {
|
||||
return `${path}`
|
||||
.split('.')
|
||||
.reduce((current, key) => current?.[key], data);
|
||||
}
|
||||
}
|
||||
104
src/qqbot/plugin/qqbot-plugin-registry.service.ts
Normal file
104
src/qqbot/plugin/qqbot-plugin-registry.service.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { throwVbenError } from '@/common';
|
||||
import { QqbotFf14MarketPluginService } from '../plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||
import type {
|
||||
QqbotIntegrationPlugin,
|
||||
QqbotPluginHealth,
|
||||
QqbotPluginOperationContext,
|
||||
QqbotPluginOperationSummary,
|
||||
QqbotPluginSummary,
|
||||
} from './qqbot-plugin.types';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotPluginRegistryService implements OnModuleInit {
|
||||
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
||||
|
||||
constructor(private readonly ff14MarketPlugin: QqbotFf14MarketPluginService) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.register(this.ff14MarketPlugin.getPlugin());
|
||||
}
|
||||
|
||||
register(plugin: QqbotIntegrationPlugin) {
|
||||
if (!plugin.key || !plugin.operations.length) {
|
||||
throwVbenError('QQBot 插件必须包含 key 和 operation');
|
||||
}
|
||||
this.plugins.set(plugin.key, plugin);
|
||||
}
|
||||
|
||||
listPlugins(): QqbotPluginSummary[] {
|
||||
return [...this.plugins.values()].map((plugin) => ({
|
||||
description: plugin.description,
|
||||
key: plugin.key,
|
||||
name: plugin.name,
|
||||
operationCount: plugin.operations.length,
|
||||
version: plugin.version,
|
||||
}));
|
||||
}
|
||||
|
||||
listOperations(pluginKey?: string): QqbotPluginOperationSummary[] {
|
||||
return this.getPlugins(pluginKey).flatMap((plugin) =>
|
||||
plugin.operations.map((operation) => ({
|
||||
cacheTtlMs: operation.cacheTtlMs,
|
||||
description: operation.description,
|
||||
inputSchema: operation.inputSchema,
|
||||
key: operation.key,
|
||||
name: operation.name,
|
||||
outputSchema: operation.outputSchema,
|
||||
pluginKey: plugin.key,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async health(pluginKey?: string): Promise<QqbotPluginHealth[]> {
|
||||
const plugins = this.getPlugins(pluginKey);
|
||||
return Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
if (!plugin.healthCheck) {
|
||||
return {
|
||||
checkedAt: new Date().toISOString(),
|
||||
message: '插件未提供健康检查',
|
||||
status: 'healthy',
|
||||
};
|
||||
}
|
||||
return plugin.healthCheck();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async execute(
|
||||
pluginKey: string,
|
||||
operationKey: string,
|
||||
input: Record<string, any>,
|
||||
context: QqbotPluginOperationContext = {},
|
||||
) {
|
||||
const operation = this.getOperation(pluginKey, operationKey);
|
||||
return operation.execute(input, context);
|
||||
}
|
||||
|
||||
assertOperation(pluginKey?: string, operationKey?: string) {
|
||||
if (!pluginKey || !operationKey) {
|
||||
throwVbenError('请选择插件和插件能力');
|
||||
}
|
||||
this.getOperation(pluginKey, operationKey);
|
||||
}
|
||||
|
||||
private getOperation(pluginKey: string, operationKey: string) {
|
||||
const plugin = this.plugins.get(pluginKey);
|
||||
if (!plugin) throwVbenError(`QQBot 插件不存在:${pluginKey}`);
|
||||
|
||||
const operation = plugin.operations.find(
|
||||
(item) => item.key === operationKey,
|
||||
);
|
||||
if (!operation) {
|
||||
throwVbenError(`QQBot 插件能力不存在:${pluginKey}.${operationKey}`);
|
||||
}
|
||||
return operation;
|
||||
}
|
||||
|
||||
private getPlugins(pluginKey?: string) {
|
||||
if (!pluginKey) return [...this.plugins.values()];
|
||||
const plugin = this.plugins.get(pluginKey);
|
||||
return plugin ? [plugin] : [];
|
||||
}
|
||||
}
|
||||
32
src/qqbot/plugin/qqbot-plugin.controller.ts
Normal file
32
src/qqbot/plugin/qqbot-plugin.controller.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||
import { vbenSuccess } from '@/common';
|
||||
import { QqbotPluginRegistryService } from './qqbot-plugin-registry.service';
|
||||
|
||||
@ApiTags('qqbot-plugin')
|
||||
@Controller('qqbot/plugin')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class QqbotPluginController {
|
||||
constructor(private readonly pluginRegistry: QqbotPluginRegistryService) {}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: 'QQBot 插件列表' })
|
||||
async list() {
|
||||
return vbenSuccess(this.pluginRegistry.listPlugins());
|
||||
}
|
||||
|
||||
@Get('operation/list')
|
||||
@ApiOperation({ summary: 'QQBot 插件能力列表' })
|
||||
@ApiQuery({ name: 'pluginKey', required: false, type: String })
|
||||
async operationList(@Query('pluginKey') pluginKey?: string) {
|
||||
return vbenSuccess(this.pluginRegistry.listOperations(pluginKey));
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: 'QQBot 插件健康检查' })
|
||||
@ApiQuery({ name: 'pluginKey', required: false, type: String })
|
||||
async health(@Query('pluginKey') pluginKey?: string) {
|
||||
return vbenSuccess(await this.pluginRegistry.health(pluginKey));
|
||||
}
|
||||
}
|
||||
57
src/qqbot/plugin/qqbot-plugin.types.ts
Normal file
57
src/qqbot/plugin/qqbot-plugin.types.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import type {
|
||||
QqbotNormalizedMessage,
|
||||
QqbotPluginHealthStatus,
|
||||
} from '../qqbot.types';
|
||||
import type { QqbotCommand } from '../command/qqbot-command.entity';
|
||||
|
||||
export type QqbotPluginHealth = {
|
||||
checkedAt: string;
|
||||
message?: string;
|
||||
status: QqbotPluginHealthStatus;
|
||||
};
|
||||
|
||||
export type QqbotPluginOperationContext = {
|
||||
args?: Record<string, any>;
|
||||
command?: QqbotCommand;
|
||||
message?: QqbotNormalizedMessage;
|
||||
};
|
||||
|
||||
export type QqbotPluginOperation<Input = any, Output = any> = {
|
||||
cacheTtlMs?: number;
|
||||
description?: string;
|
||||
inputSchema?: Record<string, any>;
|
||||
key: string;
|
||||
name: string;
|
||||
outputSchema?: Record<string, any>;
|
||||
execute: (
|
||||
input: Input,
|
||||
context: QqbotPluginOperationContext,
|
||||
) => Promise<Output>;
|
||||
};
|
||||
|
||||
export type QqbotIntegrationPlugin = {
|
||||
description?: string;
|
||||
healthCheck?: () => Promise<QqbotPluginHealth>;
|
||||
key: string;
|
||||
name: string;
|
||||
operations: QqbotPluginOperation[];
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type QqbotPluginSummary = {
|
||||
description?: string;
|
||||
key: string;
|
||||
name: string;
|
||||
operationCount: number;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type QqbotPluginOperationSummary = {
|
||||
cacheTtlMs?: number;
|
||||
description?: string;
|
||||
inputSchema?: Record<string, any>;
|
||||
key: string;
|
||||
name: string;
|
||||
outputSchema?: Record<string, any>;
|
||||
pluginKey: string;
|
||||
};
|
||||
257
src/qqbot/plugins/ff14Market/qqbot-ff14-client.service.ts
Normal file
257
src/qqbot/plugins/ff14Market/qqbot-ff14-client.service.ts
Normal file
@ -0,0 +1,257 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
|
||||
type HttpMethod = 'GET';
|
||||
|
||||
type XivapiSearchItem = {
|
||||
fields?: {
|
||||
Icon?: string;
|
||||
LevelItem?: number;
|
||||
Name?: string;
|
||||
};
|
||||
id?: number;
|
||||
name?: string;
|
||||
row_id?: number;
|
||||
sheet?: string;
|
||||
};
|
||||
|
||||
type UniversalisListing = {
|
||||
hq?: boolean;
|
||||
lastReviewTime?: number;
|
||||
pricePerUnit?: number;
|
||||
quantity?: number;
|
||||
worldName?: string;
|
||||
};
|
||||
|
||||
type UniversalisMarketResponse = {
|
||||
currentAveragePrice?: number;
|
||||
currentAveragePriceHQ?: number;
|
||||
currentAveragePriceNQ?: number;
|
||||
itemID?: number;
|
||||
lastUploadTime?: number;
|
||||
listings?: UniversalisListing[];
|
||||
minPrice?: number;
|
||||
minPriceHQ?: number;
|
||||
minPriceNQ?: number;
|
||||
worldName?: string;
|
||||
};
|
||||
|
||||
export type QqbotFf14ResolvedItem = {
|
||||
icon?: string;
|
||||
itemId: number;
|
||||
itemLevel?: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type QqbotFf14PriceResult = {
|
||||
averagePrice?: number;
|
||||
hq?: boolean;
|
||||
item: QqbotFf14ResolvedItem;
|
||||
listings: UniversalisListing[];
|
||||
minPrice?: number;
|
||||
replyText: string;
|
||||
updatedAt?: string;
|
||||
world: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QqbotFf14ClientService {
|
||||
private readonly xivapiBaseUrl: string;
|
||||
private readonly universalisBaseUrl: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.xivapiBaseUrl =
|
||||
this.configService.get<string>('FF14_XIVAPI_BASE_URL') ||
|
||||
'https://v2.xivapi.com/api';
|
||||
this.universalisBaseUrl =
|
||||
this.configService.get<string>('FF14_UNIVERSALIS_BASE_URL') ||
|
||||
'https://universalis.app/api/v2';
|
||||
}
|
||||
|
||||
async resolveItem(params: {
|
||||
item?: string;
|
||||
itemId?: number | string;
|
||||
language?: string;
|
||||
}): Promise<QqbotFf14ResolvedItem> {
|
||||
const itemId = Number(params.itemId || params.item);
|
||||
if (Number.isInteger(itemId) && itemId > 0) {
|
||||
return this.getItemById(itemId, params.language);
|
||||
}
|
||||
|
||||
const keyword = `${params.item || ''}`.trim();
|
||||
if (!keyword) throw new Error('请提供 FF14 物品名称或物品 ID');
|
||||
|
||||
const url = new URL(`${this.xivapiBaseUrl}/search`);
|
||||
url.searchParams.set('sheets', 'Item');
|
||||
url.searchParams.set('query', keyword);
|
||||
url.searchParams.set('language', params.language || 'zh');
|
||||
|
||||
const data = await this.requestJson<{ results?: XivapiSearchItem[] }>(
|
||||
url,
|
||||
'GET',
|
||||
);
|
||||
const item = (data.results || []).find(
|
||||
(result) =>
|
||||
result.sheet === 'Item' || result.fields?.Name || result.name,
|
||||
);
|
||||
if (!item) throw new Error(`未找到 FF14 物品:${keyword}`);
|
||||
|
||||
return {
|
||||
icon: item.fields?.Icon,
|
||||
itemId: Number(item.row_id || item.id),
|
||||
itemLevel: item.fields?.LevelItem,
|
||||
name: item.fields?.Name || item.name || keyword,
|
||||
};
|
||||
}
|
||||
|
||||
async getPrice(params: {
|
||||
hq?: boolean;
|
||||
item?: string;
|
||||
itemId?: number | string;
|
||||
language?: string;
|
||||
world?: string;
|
||||
}): Promise<QqbotFf14PriceResult> {
|
||||
const world = this.normalizeWorld(params.world);
|
||||
const item = await this.resolveItem(params);
|
||||
const url = new URL(`${this.universalisBaseUrl}/${world}/${item.itemId}`);
|
||||
url.searchParams.set('entries', '5');
|
||||
url.searchParams.set('listings', '5');
|
||||
if (params.hq !== undefined) url.searchParams.set('hq', `${params.hq}`);
|
||||
|
||||
const data = await this.requestJson<UniversalisMarketResponse>(url, 'GET');
|
||||
const minPrice = this.pickPrice(data, params.hq, 'min');
|
||||
const averagePrice = this.pickPrice(data, params.hq, 'average');
|
||||
const listings = (data.listings || []).slice(0, 5);
|
||||
const updatedAt = data.lastUploadTime
|
||||
? new Date(data.lastUploadTime).toISOString()
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
averagePrice,
|
||||
hq: params.hq,
|
||||
item,
|
||||
listings,
|
||||
minPrice,
|
||||
replyText: this.buildReplyText({
|
||||
averagePrice,
|
||||
hq: params.hq,
|
||||
item,
|
||||
listings,
|
||||
minPrice,
|
||||
updatedAt,
|
||||
world,
|
||||
}),
|
||||
updatedAt,
|
||||
world,
|
||||
};
|
||||
}
|
||||
|
||||
private async getItemById(
|
||||
itemId: number,
|
||||
language = 'zh',
|
||||
): Promise<QqbotFf14ResolvedItem> {
|
||||
const url = new URL(`${this.xivapiBaseUrl}/sheet/Item/${itemId}`);
|
||||
url.searchParams.set('language', language);
|
||||
const data = await this.requestJson<Record<string, any>>(url, 'GET');
|
||||
const fields = data.fields || data;
|
||||
return {
|
||||
icon: fields.Icon,
|
||||
itemId,
|
||||
itemLevel: fields.LevelItem,
|
||||
name: fields.Name || `${itemId}`,
|
||||
};
|
||||
}
|
||||
|
||||
private buildReplyText(result: Omit<QqbotFf14PriceResult, 'replyText'>) {
|
||||
const quality = result.hq === undefined ? '' : result.hq ? ' HQ' : ' NQ';
|
||||
const minPrice =
|
||||
result.minPrice === undefined ? '暂无' : `${result.minPrice}`;
|
||||
const averagePrice =
|
||||
result.averagePrice === undefined ? '暂无' : `${result.averagePrice}`;
|
||||
const listingText = result.listings.length
|
||||
? result.listings
|
||||
.slice(0, 3)
|
||||
.map((item, index) => {
|
||||
const hq = item.hq ? 'HQ' : 'NQ';
|
||||
return `${index + 1}. ${item.pricePerUnit} x${item.quantity || 1} ${hq}`;
|
||||
})
|
||||
.join('\n')
|
||||
: '暂无在售记录';
|
||||
|
||||
return [
|
||||
`FF14 查价:${result.item.name}${quality}`,
|
||||
`服务器:${result.world}`,
|
||||
`最低价:${minPrice}`,
|
||||
`均价:${averagePrice}`,
|
||||
`近期挂单:`,
|
||||
listingText,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private pickPrice(
|
||||
data: UniversalisMarketResponse,
|
||||
hq: boolean | undefined,
|
||||
type: 'average' | 'min',
|
||||
) {
|
||||
if (type === 'min') {
|
||||
if (hq === true) return data.minPriceHQ;
|
||||
if (hq === false) return data.minPriceNQ;
|
||||
return data.minPrice ?? data.minPriceNQ ?? data.minPriceHQ;
|
||||
}
|
||||
if (hq === true) return data.currentAveragePriceHQ;
|
||||
if (hq === false) return data.currentAveragePriceNQ;
|
||||
return (
|
||||
data.currentAveragePrice ??
|
||||
data.currentAveragePriceNQ ??
|
||||
data.currentAveragePriceHQ
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeWorld(world?: string) {
|
||||
const raw =
|
||||
`${world || this.configService.get<string>('FF14_DEFAULT_WORLD') || '中国'}`.trim();
|
||||
return encodeURIComponent(raw);
|
||||
}
|
||||
|
||||
private requestJson<T>(url: URL, method: HttpMethod) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const client = url.protocol === 'http:' ? http : https;
|
||||
const request = client.request(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'kt-template-online-api/qqbot',
|
||||
},
|
||||
method,
|
||||
timeout: 8000,
|
||||
},
|
||||
(response) => {
|
||||
let body = '';
|
||||
response.setEncoding('utf8');
|
||||
response.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
response.on('end', () => {
|
||||
if ((response.statusCode || 500) >= 400) {
|
||||
reject(new Error(`FF14 接口请求失败:${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(body) as T);
|
||||
} catch {
|
||||
reject(new Error('FF14 接口返回不是合法 JSON'));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
request.on('timeout', () => {
|
||||
request.destroy(new Error('FF14 接口请求超时'));
|
||||
});
|
||||
request.on('error', reject);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
89
src/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin.ts
Normal file
89
src/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { QqbotIntegrationPlugin } from '../../plugin/qqbot-plugin.types';
|
||||
import { QqbotFf14ClientService } from './qqbot-ff14-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotFf14MarketPluginService {
|
||||
constructor(private readonly ff14ClientService: QqbotFf14ClientService) {}
|
||||
|
||||
getPlugin(): QqbotIntegrationPlugin {
|
||||
return {
|
||||
description: '对接 XIVAPI v2 与 Universalis,提供 FF14 物品解析和市场价格查询能力。',
|
||||
healthCheck: async () => {
|
||||
const checkedAt = new Date().toISOString();
|
||||
try {
|
||||
await this.ff14ClientService.resolveItem({
|
||||
itemId: 2,
|
||||
language: 'en',
|
||||
});
|
||||
return {
|
||||
checkedAt,
|
||||
message: 'FF14 插件可用',
|
||||
status: 'healthy',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
checkedAt,
|
||||
message: err instanceof Error ? err.message : 'FF14 插件不可用',
|
||||
status: 'degraded',
|
||||
};
|
||||
}
|
||||
},
|
||||
key: 'ff14Market',
|
||||
name: 'FF14 市场价格',
|
||||
operations: [
|
||||
{
|
||||
description: '按物品名称或 ID 解析 XIVAPI 物品信息。',
|
||||
inputSchema: {
|
||||
properties: {
|
||||
item: { description: '物品名称或 ID', type: 'string' },
|
||||
itemId: { description: '物品 ID', type: 'number' },
|
||||
language: { default: 'zh', type: 'string' },
|
||||
},
|
||||
required: ['item'],
|
||||
type: 'object',
|
||||
},
|
||||
key: 'ff14.item.resolve',
|
||||
name: '解析物品',
|
||||
outputSchema: {
|
||||
properties: {
|
||||
itemId: { type: 'number' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
execute: async (input) =>
|
||||
await this.ff14ClientService.resolveItem(input),
|
||||
},
|
||||
{
|
||||
cacheTtlMs: 60_000,
|
||||
description: '查询指定服务器的 FF14 市场最低价、均价与近期挂单。',
|
||||
inputSchema: {
|
||||
properties: {
|
||||
hq: { description: '是否只查 HQ', type: 'boolean' },
|
||||
item: { description: '物品名称或 ID', type: 'string' },
|
||||
itemId: { description: '物品 ID', type: 'number' },
|
||||
language: { default: 'zh', type: 'string' },
|
||||
world: { description: '服务器名或大区名', type: 'string' },
|
||||
},
|
||||
required: ['item', 'world'],
|
||||
type: 'object',
|
||||
},
|
||||
key: 'ff14.market.price',
|
||||
name: '市场查价',
|
||||
outputSchema: {
|
||||
properties: {
|
||||
averagePrice: { type: 'number' },
|
||||
minPrice: { type: 'number' },
|
||||
replyText: { type: 'string' },
|
||||
world: { type: 'string' },
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
execute: async (input) => await this.ff14ClientService.getPrice(input),
|
||||
},
|
||||
],
|
||||
version: '1.0.0',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,13 @@ import { QqbotAccountController } from './account/qqbot-account.controller';
|
||||
import { QqbotAccount } from './account/qqbot-account.entity';
|
||||
import { QqbotAccountService } from './account/qqbot-account.service';
|
||||
import { QqbotNapcatLoginService } from './account/qqbot-napcat-login.service';
|
||||
import { QqbotCommandController } from './command/qqbot-command.controller';
|
||||
import { QqbotCommand } from './command/qqbot-command.entity';
|
||||
import { QqbotCommandEngineService } from './command/qqbot-command-engine.service';
|
||||
import { QqbotCommandLog } from './command/qqbot-command-log.entity';
|
||||
import { QqbotCommandParserService } from './command/qqbot-command-parser.service';
|
||||
import { QqbotCommandService } from './command/qqbot-command.service';
|
||||
import { QqbotReplyTemplateService } from './command/qqbot-reply-template.service';
|
||||
import { QqbotReverseWsService } from './connection/qqbot-reverse-ws.service';
|
||||
import { QqbotConfig } from './config/qqbot-config.entity';
|
||||
import { QqbotConfigService } from './config/qqbot-config.service';
|
||||
@ -26,6 +33,10 @@ import { QqbotAllowlist } from './permission/qqbot-allowlist.entity';
|
||||
import { QqbotBlocklist } from './permission/qqbot-blocklist.entity';
|
||||
import { QqbotPermissionController } from './permission/qqbot-permission.controller';
|
||||
import { QqbotPermissionService } from './permission/qqbot-permission.service';
|
||||
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
|
||||
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
|
||||
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
||||
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||
import { QqbotRuleController } from './rule/qqbot-rule.controller';
|
||||
import { QqbotRule } from './rule/qqbot-rule.entity';
|
||||
import { QqbotRuleEngineService } from './rule/qqbot-rule-engine.service';
|
||||
@ -43,6 +54,8 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
QqbotAccount,
|
||||
QqbotAllowlist,
|
||||
QqbotBlocklist,
|
||||
QqbotCommand,
|
||||
QqbotCommandLog,
|
||||
QqbotConfig,
|
||||
QqbotConversation,
|
||||
QqbotDedupe,
|
||||
@ -55,24 +68,33 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
],
|
||||
controllers: [
|
||||
QqbotAccountController,
|
||||
QqbotCommandController,
|
||||
QqbotDashboardController,
|
||||
QqbotMessageController,
|
||||
QqbotPermissionController,
|
||||
QqbotPluginController,
|
||||
QqbotRuleController,
|
||||
QqbotSendController,
|
||||
],
|
||||
providers: [
|
||||
QqbotAccountService,
|
||||
QqbotBusService,
|
||||
QqbotCommandEngineService,
|
||||
QqbotCommandParserService,
|
||||
QqbotCommandService,
|
||||
QqbotConfigService,
|
||||
QqbotDashboardService,
|
||||
QqbotDedupeService,
|
||||
QqbotEventService,
|
||||
QqbotFf14ClientService,
|
||||
QqbotFf14MarketPluginService,
|
||||
QqbotMessageService,
|
||||
QqbotNapcatLoginService,
|
||||
QqbotNapcatContainerService,
|
||||
QqbotPermissionService,
|
||||
QqbotPluginRegistryService,
|
||||
QqbotRateLimitService,
|
||||
QqbotReplyTemplateService,
|
||||
QqbotReverseWsService,
|
||||
QqbotRuleEngineService,
|
||||
QqbotRuleService,
|
||||
|
||||
@ -24,6 +24,10 @@ export type QqbotRuleMatchType = 'equals' | 'keyword' | 'regex';
|
||||
|
||||
export type QqbotRuleTargetType = 'all' | 'channel' | 'group' | 'private';
|
||||
|
||||
export type QqbotCommandParserType = 'ff14Price' | 'plain';
|
||||
|
||||
export type QqbotPluginHealthStatus = 'degraded' | 'healthy' | 'offline';
|
||||
|
||||
export type QqbotSendStatus = 'failed' | 'pending' | 'success';
|
||||
|
||||
export type QqbotPermissionTargetType = 'channel' | 'group' | 'private' | 'qq';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||
import { QqbotCommandEngineService } from '../command/qqbot-command-engine.service';
|
||||
import { QqbotPermissionService } from '../permission/qqbot-permission.service';
|
||||
import { QqbotSendService } from '../send/qqbot-send.service';
|
||||
import { QqbotRuleService } from './qqbot-rule.service';
|
||||
@ -9,6 +10,7 @@ export class QqbotRuleEngineService {
|
||||
private readonly logger = new Logger(QqbotRuleEngineService.name);
|
||||
|
||||
constructor(
|
||||
private readonly commandEngineService: QqbotCommandEngineService,
|
||||
private readonly permissionService: QqbotPermissionService,
|
||||
private readonly ruleService: QqbotRuleService,
|
||||
private readonly sendService: QqbotSendService,
|
||||
@ -17,6 +19,7 @@ export class QqbotRuleEngineService {
|
||||
async handleMessage(message: QqbotNormalizedMessage) {
|
||||
if (await this.permissionService.isBlocked(message)) return;
|
||||
if (!(await this.permissionService.isAllowed(message))) return;
|
||||
if (await this.commandEngineService.handleMessage(message)) return;
|
||||
|
||||
const rules = await this.ruleService.listEnabledForMessage(message);
|
||||
for (const rule of rules) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user