@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -661
- package/README.md +0 -14
- package/package.json +1 -1
- package/vendor/gugle-chat-bot/LICENSE +661 -661
- package/vendor/gugle-chat-bot/package.json +57 -57
- package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
- package/vendor/gugle-chat-bot/src/api.json +4284 -4284
- package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
- package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
- package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
- package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
- package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
- package/vendor/gugle-chat-bot/src/custom.ts +548 -548
- package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
- package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
- package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
- package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
- package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
- package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
- package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
- package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
- package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
- package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
- package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
- package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
- package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
- package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
- package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
- package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
- package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
- package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
- package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
- package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
- package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
- package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
- package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
- package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
- package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
- package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
- package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
- package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
- package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
- package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
- package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
- package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
- package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
- package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
- package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
- package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
- package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
- package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
- package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
- package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
- package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
- package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
- package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
- package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
- package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
- package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
- package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
- package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
- package/vendor/gugle-chat-bot/src/index.ts +695 -695
- package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
- package/vendor/gugle-chat-bot/src/result.json +359 -359
- package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
- package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
- package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
- package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
- package/vendor/gugle-chat-bot/tsconfig.json +46 -46
- package/scripts/dump-npm-latest.mjs +0 -30
|
@@ -1,355 +1,355 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ApplicationCommandOptionType,
|
|
3
|
-
ChatInputCommandInteraction,
|
|
4
|
-
Client,
|
|
5
|
-
PermissionFlagsBits,
|
|
6
|
-
SlashCommandBuilder,
|
|
7
|
-
SlashCommandSubcommandBuilder
|
|
8
|
-
} from 'discord.js';
|
|
9
|
-
import { Arguments, CommandManager, CommandNode } from 'gugle-command';
|
|
10
|
-
import { BotCommandSource, executeCommandTokens } from '@/command';
|
|
11
|
-
import { isDiscordOperator } from '@/config/features';
|
|
12
|
-
import { QQBot } from '@/index';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Discord 原生斜杠命令:把 gugle-command 已注册的命令树转换为 Discord application
|
|
16
|
-
* commands(guild 级注册,即时生效),交互经 token 精确回放交给同一套命令实现。
|
|
17
|
-
*
|
|
18
|
-
* 映射规则:
|
|
19
|
-
* - 根字面量(help/mcv/server/...)→ 顶命令;其二层字面量(github→bind 等)→ 子命令;
|
|
20
|
-
* - 参数节点 → options,类型按 decode 识别(NUMBER→Number、BOOLEAN→Boolean、其余 String);
|
|
21
|
-
* - 必填规则:参数节点的祖先(到父节点为止)都没有 exec 则必填,否则选填
|
|
22
|
-
* (/server <ip> [port]:ip 必填、port 选填,天然保证必填在选填之前);
|
|
23
|
-
* - 字面量与参数混排、三层以上字面量嵌套不支持(当前命令树无此情况,出现时告警跳过)。
|
|
24
|
-
*
|
|
25
|
-
* 前置条件:bot 邀请链接需包含 applications.commands 权限范围(否则注册报错并告警)。
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/** option 规格(交互时按声明顺序回放 token 用) */
|
|
29
|
-
export interface SlashOptionSpec {
|
|
30
|
-
/** 小写 option 名 */
|
|
31
|
-
name: string;
|
|
32
|
-
type: ApplicationCommandOptionType.String | ApplicationCommandOptionType.Number | ApplicationCommandOptionType.Boolean;
|
|
33
|
-
required: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 命令规格:一个顶命令或一个子命令 */
|
|
37
|
-
export interface SlashCommandSpec {
|
|
38
|
-
name: string;
|
|
39
|
-
subcommand?: string;
|
|
40
|
-
options: SlashOptionSpec[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface BuiltSlashCommands {
|
|
44
|
-
builders: SlashCommandBuilder[];
|
|
45
|
-
specs: SlashCommandSpec[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** 命令/参数描述(Discord 必填项;未收录时回退通用文案) */
|
|
49
|
-
const DESCRIPTIONS: Record<string, string> = {
|
|
50
|
-
help: '帮助',
|
|
51
|
-
mcv: '获取 Minecraft 版本信息',
|
|
52
|
-
server: '获取 Minecraft 服务器状态',
|
|
53
|
-
wiki: '搜索 Minecraft 维基',
|
|
54
|
-
github: 'GitHub 绑定/订阅/授权',
|
|
55
|
-
pardon: '把用户移出黑名单(管理员)',
|
|
56
|
-
reload: '重载配置与数据文件(管理员)',
|
|
57
|
-
reboot: '重启应用(管理员)',
|
|
58
|
-
shutdown: '关闭应用(管理员)',
|
|
59
|
-
pvtime: '查询当前是梁文峰时间还是梁文谷时间',
|
|
60
|
-
bind: '绑定 GitHub 用户名',
|
|
61
|
-
subscribe: '订阅仓库消息推送',
|
|
62
|
-
allow: '授权仓库访问(管理员)',
|
|
63
|
-
ip: '服务器地址',
|
|
64
|
-
port: '端口',
|
|
65
|
-
query: '搜索词',
|
|
66
|
-
username: 'GitHub 用户名',
|
|
67
|
-
repository: 'owner/repo',
|
|
68
|
-
target: 'owner 或 owner/repo',
|
|
69
|
-
userid: 'QQ 号'
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
function describe(name: string): string {
|
|
73
|
-
return DESCRIPTIONS[name] ?? `参数 ${name}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Discord chat-input 名称约束:小写字母数字 _ -,1-32 字符 */
|
|
77
|
-
const DISCORD_NAME_RE = /^[a-z0-9_-]{1,32}$/;
|
|
78
|
-
|
|
79
|
-
/** 参数节点 → option 名(`<userId>` → `userid`) */
|
|
80
|
-
function optionName(node: CommandNode): string {
|
|
81
|
-
const raw = node.toString().replace(/^<(.+)>$/, '$1');
|
|
82
|
-
return raw.toLowerCase();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** 参数节点 → option 类型(按构造时存入的 decorator 身份识别) */
|
|
86
|
-
function optionType(node: CommandNode): SlashOptionSpec['type'] {
|
|
87
|
-
const decorator = (node as unknown as { decorator?: unknown }).decorator;
|
|
88
|
-
if (decorator === Arguments.NUMBER) return ApplicationCommandOptionType.Number;
|
|
89
|
-
if (decorator === Arguments.BOOLEAN) return ApplicationCommandOptionType.Boolean;
|
|
90
|
-
return ApplicationCommandOptionType.String;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 收集节点的参数链(沿第一个参数子节点下行)。
|
|
95
|
-
* 必填规则:祖先(rootLiteral 到父节点)都没有 exec 则必填。
|
|
96
|
-
*/
|
|
97
|
-
function collectOptions(rootLiteral: CommandNode, warn: (msg: string) => void): SlashOptionSpec[] {
|
|
98
|
-
const options: SlashOptionSpec[] = [];
|
|
99
|
-
let ancestorHasExec = rootLiteral.exec !== undefined;
|
|
100
|
-
let current: CommandNode = rootLiteral;
|
|
101
|
-
for (;;) {
|
|
102
|
-
const literals = current.children.filter(child => child.isLiteral());
|
|
103
|
-
const args = current.children.filter(child => !child.isLiteral());
|
|
104
|
-
if (args.length === 0) return options;
|
|
105
|
-
if (literals.length > 0) {
|
|
106
|
-
warn(`参数链中出现字面量分支(${rootLiteral.toString()}),斜杠命令不支持,已截断`);
|
|
107
|
-
return options;
|
|
108
|
-
}
|
|
109
|
-
if (args.length > 1) {
|
|
110
|
-
warn(`同一层级有多个参数节点(${rootLiteral.toString()}),只取第一个`);
|
|
111
|
-
}
|
|
112
|
-
const arg = args[0]!;
|
|
113
|
-
const required = !ancestorHasExec;
|
|
114
|
-
options.push({ name: optionName(arg), type: optionType(arg), required });
|
|
115
|
-
ancestorHasExec = ancestorHasExec || arg.exec !== undefined;
|
|
116
|
-
current = arg;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function applyOptions(
|
|
121
|
-
target: SlashCommandBuilder | SlashCommandSubcommandBuilder,
|
|
122
|
-
options: SlashOptionSpec[]
|
|
123
|
-
): void {
|
|
124
|
-
for (const option of options) {
|
|
125
|
-
if (option.type === ApplicationCommandOptionType.Number) {
|
|
126
|
-
target.addNumberOption(builder =>
|
|
127
|
-
builder.setName(option.name).setDescription(describe(option.name)).setRequired(option.required)
|
|
128
|
-
);
|
|
129
|
-
} else if (option.type === ApplicationCommandOptionType.Boolean) {
|
|
130
|
-
target.addBooleanOption(builder =>
|
|
131
|
-
builder.setName(option.name).setDescription(describe(option.name)).setRequired(option.required)
|
|
132
|
-
);
|
|
133
|
-
} else {
|
|
134
|
-
target.addStringOption(builder =>
|
|
135
|
-
builder.setName(option.name).setDescription(describe(option.name)).setRequired(option.required)
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let cachedSpecs: SlashCommandSpec[] = [];
|
|
142
|
-
|
|
143
|
-
/** 把 gugle-command 命令树转换为 Discord 斜杠命令构建器与回放规格 */
|
|
144
|
-
export function buildSlashCommands(manager: CommandManager, warn: (msg: string) => void = () => undefined): BuiltSlashCommands {
|
|
145
|
-
const builders: SlashCommandBuilder[] = [];
|
|
146
|
-
const specs: SlashCommandSpec[] = [];
|
|
147
|
-
|
|
148
|
-
for (const root of manager.roots.values()) {
|
|
149
|
-
for (const literal of root.children) {
|
|
150
|
-
if (!literal.isLiteral()) continue;
|
|
151
|
-
const name = literal.toString();
|
|
152
|
-
if (!DISCORD_NAME_RE.test(name)) {
|
|
153
|
-
warn(`命令名 ${name} 不符合 Discord 命名约束,已跳过`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const literalChildren = literal.children.filter(child => child.isLiteral());
|
|
158
|
-
const argumentChildren = literal.children.filter(child => !child.isLiteral());
|
|
159
|
-
const builder = new SlashCommandBuilder().setName(name).setDescription(describe(name));
|
|
160
|
-
|
|
161
|
-
if (literalChildren.length > 0 && argumentChildren.length > 0) {
|
|
162
|
-
warn(`命令 ${name} 字面量与参数混排,斜杠命令不支持,已跳过`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (literalChildren.length > 0) {
|
|
167
|
-
// 二层字面量 → 子命令
|
|
168
|
-
for (const sub of literalChildren) {
|
|
169
|
-
const subName = sub.toString();
|
|
170
|
-
if (!DISCORD_NAME_RE.test(subName)) {
|
|
171
|
-
warn(`子命令名 ${name} ${subName} 不符合 Discord 命名约束,已跳过`);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (sub.children.some(child => child.isLiteral())) {
|
|
175
|
-
warn(`命令 ${name} ${subName} 存在三层嵌套,斜杠命令不支持,已跳过`);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const options = collectOptions(sub, warn);
|
|
179
|
-
builder.addSubcommand(subBuilder => {
|
|
180
|
-
subBuilder.setName(subName).setDescription(describe(subName));
|
|
181
|
-
applyOptions(subBuilder, options);
|
|
182
|
-
return subBuilder;
|
|
183
|
-
});
|
|
184
|
-
specs.push({ name, subcommand: subName, options });
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
const options = collectOptions(literal, warn);
|
|
188
|
-
applyOptions(builder, options);
|
|
189
|
-
specs.push({ name, options });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
builders.push(builder);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
cachedSpecs = specs;
|
|
197
|
-
return { builders, specs };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* 对 bridges 配置去重后的每个 guild 注册斜杠命令(guild 级,即时生效)。
|
|
202
|
-
* 配置热重载新增的 guild 需重启后才会注册。
|
|
203
|
-
*/
|
|
204
|
-
export async function registerSlashCommands(client: Client<true>, guildIds: string[], bot: QQBot): Promise<void> {
|
|
205
|
-
const { builders } = buildSlashCommands(bot.getCommandManager(), msg => bot.logger?.warn(`[DiscordBridge] ${msg}`));
|
|
206
|
-
if (builders.length === 0) return;
|
|
207
|
-
for (const guildId of guildIds) {
|
|
208
|
-
const guild = client.guilds.cache.get(guildId);
|
|
209
|
-
if (!guild) {
|
|
210
|
-
bot.logger?.warn(`[DiscordBridge] 注册斜杠命令:未找到服务器 ${guildId}`);
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
await guild.commands.set(builders);
|
|
215
|
-
bot.logger?.info(`[DiscordBridge] 已在 ${guild.name} 注册 ${builders.length} 个斜杠命令`);
|
|
216
|
-
} catch (error) {
|
|
217
|
-
bot.logger?.error(
|
|
218
|
-
`[DiscordBridge] 注册斜杠命令失败(${guild.name}): ${error instanceof Error ? error.message : String(error)}` +
|
|
219
|
-
`。请确认 bot 邀请链接包含 applications.commands 权限范围(需在开发者后台重新生成邀请并授权)`
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Discord 交互命令源:回复经 interaction 的 deferReply → editReply/followUp 完成。
|
|
227
|
-
* 身份与权限映射与 DiscordMsgCommandSource 一致(dc: 前缀用户标识、服务器权限即管理员)。
|
|
228
|
-
*/
|
|
229
|
-
export class DiscordInteractionCommandSource implements BotCommandSource {
|
|
230
|
-
private readonly bot: QQBot;
|
|
231
|
-
private readonly interaction: ChatInputCommandInteraction;
|
|
232
|
-
private readonly groupId: number | undefined;
|
|
233
|
-
private responded = false;
|
|
234
|
-
|
|
235
|
-
public constructor(bot: QQBot, interaction: ChatInputCommandInteraction, groupId?: number) {
|
|
236
|
-
this.bot = bot;
|
|
237
|
-
this.interaction = interaction;
|
|
238
|
-
this.groupId = groupId;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
public getUserId(): string {
|
|
242
|
-
return `dc:${this.interaction.user.id}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
public getGroupId(): number | undefined {
|
|
246
|
-
return this.groupId;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
public isAdmin(): boolean {
|
|
250
|
-
if (isDiscordOperator(this.interaction.user.id)) return true;
|
|
251
|
-
if (this.interaction.guild?.ownerId === this.interaction.user.id) return true;
|
|
252
|
-
const permissions = this.interaction.memberPermissions;
|
|
253
|
-
if (!permissions) return false;
|
|
254
|
-
return permissions.has(PermissionFlagsBits.Administrator) || permissions.has(PermissionFlagsBits.ManageGuild);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** 立即 deferReply(交互 3 秒时限),后续 success/fail 用 editReply/followUp */
|
|
258
|
-
public async defer(): Promise<void> {
|
|
259
|
-
await this.interaction.deferReply().catch(error => {
|
|
260
|
-
this.bot.logger?.error(
|
|
261
|
-
`[DiscordBridge] 交互 deferReply 失败: ${error instanceof Error ? error.message : String(error)}`
|
|
262
|
-
);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
public success(message: string): void {
|
|
267
|
-
this.reply(message);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
public fail(message: string): void {
|
|
271
|
-
this.reply(message);
|
|
272
|
-
this.bot.logger?.error(
|
|
273
|
-
`[Discord:${this.interaction.user.username}] ${message}: /${this.interaction.commandName}`
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
public getName(): string {
|
|
278
|
-
return this.interaction.user.displayName ?? this.interaction.user.username;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
public hasPermission(permission: string): boolean {
|
|
282
|
-
if (!permission) return true;
|
|
283
|
-
const getPermissionLevel = (text: string): number => {
|
|
284
|
-
const level = Number.parseInt(text);
|
|
285
|
-
if (Number.isNaN(level)) {
|
|
286
|
-
return text === 'owner' ? 2 : text === 'admin' ? 1 : 0;
|
|
287
|
-
}
|
|
288
|
-
return level;
|
|
289
|
-
};
|
|
290
|
-
const isOwner = this.interaction.guild?.ownerId === this.interaction.user.id;
|
|
291
|
-
const permissionLevel = isOwner ? 2 : this.isAdmin() ? 1 : 0;
|
|
292
|
-
return permissionLevel >= getPermissionLevel(permission);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** 首次回复 editReply,其后 followUp;2000 字截断 */
|
|
296
|
-
private reply(message: string): void {
|
|
297
|
-
const content = message.length <= 2000 ? message : `${message.slice(0, 1984)}\n…(消息过长已截断)`;
|
|
298
|
-
if (!this.responded) {
|
|
299
|
-
this.responded = true;
|
|
300
|
-
this.interaction.editReply({ content }).catch(error => {
|
|
301
|
-
this.bot.logger?.error(
|
|
302
|
-
`[DiscordBridge] 交互回复失败: ${error instanceof Error ? error.message : String(error)}`
|
|
303
|
-
);
|
|
304
|
-
});
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
this.interaction.followUp({ content }).catch(error => {
|
|
308
|
-
this.bot.logger?.error(
|
|
309
|
-
`[DiscordBridge] 交互 followUp 失败: ${error instanceof Error ? error.message : String(error)}`
|
|
310
|
-
);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* 斜杠命令交互分发:把 option 值按声明顺序回放为精确 token(不经字符串切分,
|
|
317
|
-
* 带空格的字符串参数保持完整),交给 gugle-command 执行。
|
|
318
|
-
*
|
|
319
|
-
* @param resolveGroupId 由 DiscordBridge 注入的频道 → 桥接 QQ 群解析
|
|
320
|
-
*/
|
|
321
|
-
export async function handleSlashInteraction(
|
|
322
|
-
bot: QQBot,
|
|
323
|
-
interaction: ChatInputCommandInteraction,
|
|
324
|
-
resolveGroupId: (guildId: string, channelName: string | undefined) => number | undefined
|
|
325
|
-
): Promise<void> {
|
|
326
|
-
if (!interaction.guildId) return;
|
|
327
|
-
const specs = cachedSpecs.length > 0 ? cachedSpecs : buildSlashCommands(bot.getCommandManager()).specs;
|
|
328
|
-
const subcommand = interaction.options.getSubcommand(false) ?? undefined;
|
|
329
|
-
const spec = specs.find(item => item.name === interaction.commandName && item.subcommand === subcommand);
|
|
330
|
-
if (!spec) {
|
|
331
|
-
await interaction.reply({ content: '未知命令(可能刚更新,请稍后重试)', ephemeral: true }).catch(() => undefined);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// 按声明顺序回放 token;可选参数缺失即截断(位置参数必须连续)
|
|
336
|
-
const tokens = [spec.name];
|
|
337
|
-
if (spec.subcommand) tokens.push(spec.subcommand);
|
|
338
|
-
for (const option of spec.options) {
|
|
339
|
-
const value = interaction.options.get(option.name)?.value;
|
|
340
|
-
if (value === undefined || value === null) break;
|
|
341
|
-
tokens.push(String(value));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const channel = interaction.channel;
|
|
345
|
-
const channelName = channel && 'name' in channel ? (channel.name ?? undefined) : undefined;
|
|
346
|
-
const source = new DiscordInteractionCommandSource(bot, interaction, resolveGroupId(interaction.guildId, channelName));
|
|
347
|
-
await source.defer();
|
|
348
|
-
try {
|
|
349
|
-
executeCommandTokens(bot.getCommandManager(), source, tokens);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
bot.logger?.error(
|
|
352
|
-
`[DiscordBridge] 斜杠命令执行失败: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
ApplicationCommandOptionType,
|
|
3
|
+
ChatInputCommandInteraction,
|
|
4
|
+
Client,
|
|
5
|
+
PermissionFlagsBits,
|
|
6
|
+
SlashCommandBuilder,
|
|
7
|
+
SlashCommandSubcommandBuilder
|
|
8
|
+
} from 'discord.js';
|
|
9
|
+
import { Arguments, CommandManager, CommandNode } from 'gugle-command';
|
|
10
|
+
import { BotCommandSource, executeCommandTokens } from '@/command';
|
|
11
|
+
import { isDiscordOperator } from '@/config/features';
|
|
12
|
+
import { QQBot } from '@/index';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Discord 原生斜杠命令:把 gugle-command 已注册的命令树转换为 Discord application
|
|
16
|
+
* commands(guild 级注册,即时生效),交互经 token 精确回放交给同一套命令实现。
|
|
17
|
+
*
|
|
18
|
+
* 映射规则:
|
|
19
|
+
* - 根字面量(help/mcv/server/...)→ 顶命令;其二层字面量(github→bind 等)→ 子命令;
|
|
20
|
+
* - 参数节点 → options,类型按 decode 识别(NUMBER→Number、BOOLEAN→Boolean、其余 String);
|
|
21
|
+
* - 必填规则:参数节点的祖先(到父节点为止)都没有 exec 则必填,否则选填
|
|
22
|
+
* (/server <ip> [port]:ip 必填、port 选填,天然保证必填在选填之前);
|
|
23
|
+
* - 字面量与参数混排、三层以上字面量嵌套不支持(当前命令树无此情况,出现时告警跳过)。
|
|
24
|
+
*
|
|
25
|
+
* 前置条件:bot 邀请链接需包含 applications.commands 权限范围(否则注册报错并告警)。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** option 规格(交互时按声明顺序回放 token 用) */
|
|
29
|
+
export interface SlashOptionSpec {
|
|
30
|
+
/** 小写 option 名 */
|
|
31
|
+
name: string;
|
|
32
|
+
type: ApplicationCommandOptionType.String | ApplicationCommandOptionType.Number | ApplicationCommandOptionType.Boolean;
|
|
33
|
+
required: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 命令规格:一个顶命令或一个子命令 */
|
|
37
|
+
export interface SlashCommandSpec {
|
|
38
|
+
name: string;
|
|
39
|
+
subcommand?: string;
|
|
40
|
+
options: SlashOptionSpec[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuiltSlashCommands {
|
|
44
|
+
builders: SlashCommandBuilder[];
|
|
45
|
+
specs: SlashCommandSpec[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 命令/参数描述(Discord 必填项;未收录时回退通用文案) */
|
|
49
|
+
const DESCRIPTIONS: Record<string, string> = {
|
|
50
|
+
help: '帮助',
|
|
51
|
+
mcv: '获取 Minecraft 版本信息',
|
|
52
|
+
server: '获取 Minecraft 服务器状态',
|
|
53
|
+
wiki: '搜索 Minecraft 维基',
|
|
54
|
+
github: 'GitHub 绑定/订阅/授权',
|
|
55
|
+
pardon: '把用户移出黑名单(管理员)',
|
|
56
|
+
reload: '重载配置与数据文件(管理员)',
|
|
57
|
+
reboot: '重启应用(管理员)',
|
|
58
|
+
shutdown: '关闭应用(管理员)',
|
|
59
|
+
pvtime: '查询当前是梁文峰时间还是梁文谷时间',
|
|
60
|
+
bind: '绑定 GitHub 用户名',
|
|
61
|
+
subscribe: '订阅仓库消息推送',
|
|
62
|
+
allow: '授权仓库访问(管理员)',
|
|
63
|
+
ip: '服务器地址',
|
|
64
|
+
port: '端口',
|
|
65
|
+
query: '搜索词',
|
|
66
|
+
username: 'GitHub 用户名',
|
|
67
|
+
repository: 'owner/repo',
|
|
68
|
+
target: 'owner 或 owner/repo',
|
|
69
|
+
userid: 'QQ 号'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function describe(name: string): string {
|
|
73
|
+
return DESCRIPTIONS[name] ?? `参数 ${name}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Discord chat-input 名称约束:小写字母数字 _ -,1-32 字符 */
|
|
77
|
+
const DISCORD_NAME_RE = /^[a-z0-9_-]{1,32}$/;
|
|
78
|
+
|
|
79
|
+
/** 参数节点 → option 名(`<userId>` → `userid`) */
|
|
80
|
+
function optionName(node: CommandNode): string {
|
|
81
|
+
const raw = node.toString().replace(/^<(.+)>$/, '$1');
|
|
82
|
+
return raw.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 参数节点 → option 类型(按构造时存入的 decorator 身份识别) */
|
|
86
|
+
function optionType(node: CommandNode): SlashOptionSpec['type'] {
|
|
87
|
+
const decorator = (node as unknown as { decorator?: unknown }).decorator;
|
|
88
|
+
if (decorator === Arguments.NUMBER) return ApplicationCommandOptionType.Number;
|
|
89
|
+
if (decorator === Arguments.BOOLEAN) return ApplicationCommandOptionType.Boolean;
|
|
90
|
+
return ApplicationCommandOptionType.String;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 收集节点的参数链(沿第一个参数子节点下行)。
|
|
95
|
+
* 必填规则:祖先(rootLiteral 到父节点)都没有 exec 则必填。
|
|
96
|
+
*/
|
|
97
|
+
function collectOptions(rootLiteral: CommandNode, warn: (msg: string) => void): SlashOptionSpec[] {
|
|
98
|
+
const options: SlashOptionSpec[] = [];
|
|
99
|
+
let ancestorHasExec = rootLiteral.exec !== undefined;
|
|
100
|
+
let current: CommandNode = rootLiteral;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const literals = current.children.filter(child => child.isLiteral());
|
|
103
|
+
const args = current.children.filter(child => !child.isLiteral());
|
|
104
|
+
if (args.length === 0) return options;
|
|
105
|
+
if (literals.length > 0) {
|
|
106
|
+
warn(`参数链中出现字面量分支(${rootLiteral.toString()}),斜杠命令不支持,已截断`);
|
|
107
|
+
return options;
|
|
108
|
+
}
|
|
109
|
+
if (args.length > 1) {
|
|
110
|
+
warn(`同一层级有多个参数节点(${rootLiteral.toString()}),只取第一个`);
|
|
111
|
+
}
|
|
112
|
+
const arg = args[0]!;
|
|
113
|
+
const required = !ancestorHasExec;
|
|
114
|
+
options.push({ name: optionName(arg), type: optionType(arg), required });
|
|
115
|
+
ancestorHasExec = ancestorHasExec || arg.exec !== undefined;
|
|
116
|
+
current = arg;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function applyOptions(
|
|
121
|
+
target: SlashCommandBuilder | SlashCommandSubcommandBuilder,
|
|
122
|
+
options: SlashOptionSpec[]
|
|
123
|
+
): void {
|
|
124
|
+
for (const option of options) {
|
|
125
|
+
if (option.type === ApplicationCommandOptionType.Number) {
|
|
126
|
+
target.addNumberOption(builder =>
|
|
127
|
+
builder.setName(option.name).setDescription(describe(option.name)).setRequired(option.required)
|
|
128
|
+
);
|
|
129
|
+
} else if (option.type === ApplicationCommandOptionType.Boolean) {
|
|
130
|
+
target.addBooleanOption(builder =>
|
|
131
|
+
builder.setName(option.name).setDescription(describe(option.name)).setRequired(option.required)
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
target.addStringOption(builder =>
|
|
135
|
+
builder.setName(option.name).setDescription(describe(option.name)).setRequired(option.required)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let cachedSpecs: SlashCommandSpec[] = [];
|
|
142
|
+
|
|
143
|
+
/** 把 gugle-command 命令树转换为 Discord 斜杠命令构建器与回放规格 */
|
|
144
|
+
export function buildSlashCommands(manager: CommandManager, warn: (msg: string) => void = () => undefined): BuiltSlashCommands {
|
|
145
|
+
const builders: SlashCommandBuilder[] = [];
|
|
146
|
+
const specs: SlashCommandSpec[] = [];
|
|
147
|
+
|
|
148
|
+
for (const root of manager.roots.values()) {
|
|
149
|
+
for (const literal of root.children) {
|
|
150
|
+
if (!literal.isLiteral()) continue;
|
|
151
|
+
const name = literal.toString();
|
|
152
|
+
if (!DISCORD_NAME_RE.test(name)) {
|
|
153
|
+
warn(`命令名 ${name} 不符合 Discord 命名约束,已跳过`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const literalChildren = literal.children.filter(child => child.isLiteral());
|
|
158
|
+
const argumentChildren = literal.children.filter(child => !child.isLiteral());
|
|
159
|
+
const builder = new SlashCommandBuilder().setName(name).setDescription(describe(name));
|
|
160
|
+
|
|
161
|
+
if (literalChildren.length > 0 && argumentChildren.length > 0) {
|
|
162
|
+
warn(`命令 ${name} 字面量与参数混排,斜杠命令不支持,已跳过`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (literalChildren.length > 0) {
|
|
167
|
+
// 二层字面量 → 子命令
|
|
168
|
+
for (const sub of literalChildren) {
|
|
169
|
+
const subName = sub.toString();
|
|
170
|
+
if (!DISCORD_NAME_RE.test(subName)) {
|
|
171
|
+
warn(`子命令名 ${name} ${subName} 不符合 Discord 命名约束,已跳过`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (sub.children.some(child => child.isLiteral())) {
|
|
175
|
+
warn(`命令 ${name} ${subName} 存在三层嵌套,斜杠命令不支持,已跳过`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const options = collectOptions(sub, warn);
|
|
179
|
+
builder.addSubcommand(subBuilder => {
|
|
180
|
+
subBuilder.setName(subName).setDescription(describe(subName));
|
|
181
|
+
applyOptions(subBuilder, options);
|
|
182
|
+
return subBuilder;
|
|
183
|
+
});
|
|
184
|
+
specs.push({ name, subcommand: subName, options });
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
const options = collectOptions(literal, warn);
|
|
188
|
+
applyOptions(builder, options);
|
|
189
|
+
specs.push({ name, options });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
builders.push(builder);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
cachedSpecs = specs;
|
|
197
|
+
return { builders, specs };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 对 bridges 配置去重后的每个 guild 注册斜杠命令(guild 级,即时生效)。
|
|
202
|
+
* 配置热重载新增的 guild 需重启后才会注册。
|
|
203
|
+
*/
|
|
204
|
+
export async function registerSlashCommands(client: Client<true>, guildIds: string[], bot: QQBot): Promise<void> {
|
|
205
|
+
const { builders } = buildSlashCommands(bot.getCommandManager(), msg => bot.logger?.warn(`[DiscordBridge] ${msg}`));
|
|
206
|
+
if (builders.length === 0) return;
|
|
207
|
+
for (const guildId of guildIds) {
|
|
208
|
+
const guild = client.guilds.cache.get(guildId);
|
|
209
|
+
if (!guild) {
|
|
210
|
+
bot.logger?.warn(`[DiscordBridge] 注册斜杠命令:未找到服务器 ${guildId}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await guild.commands.set(builders);
|
|
215
|
+
bot.logger?.info(`[DiscordBridge] 已在 ${guild.name} 注册 ${builders.length} 个斜杠命令`);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
bot.logger?.error(
|
|
218
|
+
`[DiscordBridge] 注册斜杠命令失败(${guild.name}): ${error instanceof Error ? error.message : String(error)}` +
|
|
219
|
+
`。请确认 bot 邀请链接包含 applications.commands 权限范围(需在开发者后台重新生成邀请并授权)`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Discord 交互命令源:回复经 interaction 的 deferReply → editReply/followUp 完成。
|
|
227
|
+
* 身份与权限映射与 DiscordMsgCommandSource 一致(dc: 前缀用户标识、服务器权限即管理员)。
|
|
228
|
+
*/
|
|
229
|
+
export class DiscordInteractionCommandSource implements BotCommandSource {
|
|
230
|
+
private readonly bot: QQBot;
|
|
231
|
+
private readonly interaction: ChatInputCommandInteraction;
|
|
232
|
+
private readonly groupId: number | undefined;
|
|
233
|
+
private responded = false;
|
|
234
|
+
|
|
235
|
+
public constructor(bot: QQBot, interaction: ChatInputCommandInteraction, groupId?: number) {
|
|
236
|
+
this.bot = bot;
|
|
237
|
+
this.interaction = interaction;
|
|
238
|
+
this.groupId = groupId;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public getUserId(): string {
|
|
242
|
+
return `dc:${this.interaction.user.id}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
public getGroupId(): number | undefined {
|
|
246
|
+
return this.groupId;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public isAdmin(): boolean {
|
|
250
|
+
if (isDiscordOperator(this.interaction.user.id)) return true;
|
|
251
|
+
if (this.interaction.guild?.ownerId === this.interaction.user.id) return true;
|
|
252
|
+
const permissions = this.interaction.memberPermissions;
|
|
253
|
+
if (!permissions) return false;
|
|
254
|
+
return permissions.has(PermissionFlagsBits.Administrator) || permissions.has(PermissionFlagsBits.ManageGuild);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 立即 deferReply(交互 3 秒时限),后续 success/fail 用 editReply/followUp */
|
|
258
|
+
public async defer(): Promise<void> {
|
|
259
|
+
await this.interaction.deferReply().catch(error => {
|
|
260
|
+
this.bot.logger?.error(
|
|
261
|
+
`[DiscordBridge] 交互 deferReply 失败: ${error instanceof Error ? error.message : String(error)}`
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public success(message: string): void {
|
|
267
|
+
this.reply(message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public fail(message: string): void {
|
|
271
|
+
this.reply(message);
|
|
272
|
+
this.bot.logger?.error(
|
|
273
|
+
`[Discord:${this.interaction.user.username}] ${message}: /${this.interaction.commandName}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public getName(): string {
|
|
278
|
+
return this.interaction.user.displayName ?? this.interaction.user.username;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public hasPermission(permission: string): boolean {
|
|
282
|
+
if (!permission) return true;
|
|
283
|
+
const getPermissionLevel = (text: string): number => {
|
|
284
|
+
const level = Number.parseInt(text);
|
|
285
|
+
if (Number.isNaN(level)) {
|
|
286
|
+
return text === 'owner' ? 2 : text === 'admin' ? 1 : 0;
|
|
287
|
+
}
|
|
288
|
+
return level;
|
|
289
|
+
};
|
|
290
|
+
const isOwner = this.interaction.guild?.ownerId === this.interaction.user.id;
|
|
291
|
+
const permissionLevel = isOwner ? 2 : this.isAdmin() ? 1 : 0;
|
|
292
|
+
return permissionLevel >= getPermissionLevel(permission);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 首次回复 editReply,其后 followUp;2000 字截断 */
|
|
296
|
+
private reply(message: string): void {
|
|
297
|
+
const content = message.length <= 2000 ? message : `${message.slice(0, 1984)}\n…(消息过长已截断)`;
|
|
298
|
+
if (!this.responded) {
|
|
299
|
+
this.responded = true;
|
|
300
|
+
this.interaction.editReply({ content }).catch(error => {
|
|
301
|
+
this.bot.logger?.error(
|
|
302
|
+
`[DiscordBridge] 交互回复失败: ${error instanceof Error ? error.message : String(error)}`
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.interaction.followUp({ content }).catch(error => {
|
|
308
|
+
this.bot.logger?.error(
|
|
309
|
+
`[DiscordBridge] 交互 followUp 失败: ${error instanceof Error ? error.message : String(error)}`
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 斜杠命令交互分发:把 option 值按声明顺序回放为精确 token(不经字符串切分,
|
|
317
|
+
* 带空格的字符串参数保持完整),交给 gugle-command 执行。
|
|
318
|
+
*
|
|
319
|
+
* @param resolveGroupId 由 DiscordBridge 注入的频道 → 桥接 QQ 群解析
|
|
320
|
+
*/
|
|
321
|
+
export async function handleSlashInteraction(
|
|
322
|
+
bot: QQBot,
|
|
323
|
+
interaction: ChatInputCommandInteraction,
|
|
324
|
+
resolveGroupId: (guildId: string, channelName: string | undefined) => number | undefined
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
if (!interaction.guildId) return;
|
|
327
|
+
const specs = cachedSpecs.length > 0 ? cachedSpecs : buildSlashCommands(bot.getCommandManager()).specs;
|
|
328
|
+
const subcommand = interaction.options.getSubcommand(false) ?? undefined;
|
|
329
|
+
const spec = specs.find(item => item.name === interaction.commandName && item.subcommand === subcommand);
|
|
330
|
+
if (!spec) {
|
|
331
|
+
await interaction.reply({ content: '未知命令(可能刚更新,请稍后重试)', ephemeral: true }).catch(() => undefined);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 按声明顺序回放 token;可选参数缺失即截断(位置参数必须连续)
|
|
336
|
+
const tokens = [spec.name];
|
|
337
|
+
if (spec.subcommand) tokens.push(spec.subcommand);
|
|
338
|
+
for (const option of spec.options) {
|
|
339
|
+
const value = interaction.options.get(option.name)?.value;
|
|
340
|
+
if (value === undefined || value === null) break;
|
|
341
|
+
tokens.push(String(value));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const channel = interaction.channel;
|
|
345
|
+
const channelName = channel && 'name' in channel ? (channel.name ?? undefined) : undefined;
|
|
346
|
+
const source = new DiscordInteractionCommandSource(bot, interaction, resolveGroupId(interaction.guildId, channelName));
|
|
347
|
+
await source.defer();
|
|
348
|
+
try {
|
|
349
|
+
executeCommandTokens(bot.getCommandManager(), source, tokens);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
bot.logger?.error(
|
|
352
|
+
`[DiscordBridge] 斜杠命令执行失败: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|