@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.
Files changed (74) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +0 -14
  3. package/package.json +1 -1
  4. package/vendor/gugle-chat-bot/LICENSE +661 -661
  5. package/vendor/gugle-chat-bot/package.json +57 -57
  6. package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -9345
  7. package/vendor/gugle-chat-bot/src/api.json +4284 -4284
  8. package/vendor/gugle-chat-bot/src/command/index.ts +141 -141
  9. package/vendor/gugle-chat-bot/src/config/features.ts +547 -547
  10. package/vendor/gugle-chat-bot/src/config/index.ts +184 -184
  11. package/vendor/gugle-chat-bot/src/config/manager.ts +356 -356
  12. package/vendor/gugle-chat-bot/src/constants/index.ts +13 -13
  13. package/vendor/gugle-chat-bot/src/custom.ts +548 -548
  14. package/vendor/gugle-chat-bot/src/event/index.ts +118 -118
  15. package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -414
  16. package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -39
  17. package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -37
  18. package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -65
  19. package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -97
  20. package/vendor/gugle-chat-bot/src/features/bili/template/video.html +56 -56
  21. package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -1128
  22. package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -105
  23. package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -355
  24. package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -96
  25. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -14
  26. package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -144
  27. package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -93
  28. package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -161
  29. package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -132
  30. package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -144
  31. package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -160
  32. package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -310
  33. package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -302
  34. package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -118
  35. package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -125
  36. package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -53
  37. package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -468
  38. package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +468 -468
  39. package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -412
  40. package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -184
  41. package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -210
  42. package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -67
  43. package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -86
  44. package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -1533
  45. package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -319
  46. package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -181
  47. package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -118
  48. package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -331
  49. package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -406
  50. package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -30
  51. package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -21
  52. package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -81
  53. package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -120
  54. package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -86
  55. package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -327
  56. package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -156
  57. package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -127
  58. package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -71
  59. package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -117
  60. package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +451 -451
  61. package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -24
  62. package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +77 -77
  63. package/vendor/gugle-chat-bot/src/features/welcome/index.ts +57 -57
  64. package/vendor/gugle-chat-bot/src/github-ip.json +5814 -5814
  65. package/vendor/gugle-chat-bot/src/image/index.ts +278 -278
  66. package/vendor/gugle-chat-bot/src/index.ts +695 -695
  67. package/vendor/gugle-chat-bot/src/logger/index.ts +54 -54
  68. package/vendor/gugle-chat-bot/src/result.json +359 -359
  69. package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -825
  70. package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -146
  71. package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -446
  72. package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -254
  73. package/vendor/gugle-chat-bot/tsconfig.json +46 -46
  74. 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
+ }