@nextclaw/channel-runtime 0.1.16 → 0.1.17

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/dist/index.d.ts CHANGED
@@ -56,15 +56,23 @@ declare class DingTalkChannel extends BaseChannel<Config["channels"]["dingtalk"]
56
56
  }
57
57
 
58
58
  declare class DiscordChannel extends BaseChannel<Config["channels"]["discord"]> {
59
+ private sessionManager?;
60
+ private coreConfig?;
59
61
  name: string;
60
62
  private client;
61
63
  private readonly typingController;
62
- constructor(config: Config["channels"]["discord"], bus: MessageBus);
64
+ private readonly commandRegistry;
65
+ constructor(config: Config["channels"]["discord"], bus: MessageBus, sessionManager?: SessionManager | undefined, coreConfig?: Config | undefined);
63
66
  start(): Promise<void>;
64
67
  stop(): Promise<void>;
65
68
  handleControlMessage(msg: OutboundMessage): Promise<boolean>;
66
69
  send(msg: OutboundMessage): Promise<void>;
67
70
  private handleIncoming;
71
+ private handleInteraction;
72
+ private handleSlashCommand;
73
+ private replyInteraction;
74
+ private registerSlashCommands;
75
+ private buildSlashCommandPayloads;
68
76
  private resolveProxyAgent;
69
77
  private resolveAccountId;
70
78
  private isAllowedByPolicy;
package/dist/index.js CHANGED
@@ -176,7 +176,10 @@ import {
176
176
  Client,
177
177
  GatewayIntentBits,
178
178
  Partials,
179
- MessageFlags
179
+ MessageFlags,
180
+ REST,
181
+ Routes,
182
+ ApplicationCommandOptionType
180
183
  } from "discord.js";
181
184
  import { ProxyAgent, fetch as fetch2 } from "undici";
182
185
  import { join } from "path";
@@ -227,7 +230,7 @@ var ChannelTypingController = class {
227
230
  };
228
231
 
229
232
  // src/channels/discord.ts
230
- import { isTypingStopControlMessage } from "@nextclaw/core";
233
+ import { CommandRegistry, isTypingStopControlMessage } from "@nextclaw/core";
231
234
  var DEFAULT_MEDIA_MAX_MB = 8;
232
235
  var MEDIA_FETCH_TIMEOUT_MS = 15e3;
233
236
  var TYPING_HEARTBEAT_MS = 6e3;
@@ -237,12 +240,13 @@ var DISCORD_MAX_LINES_PER_MESSAGE = 17;
237
240
  var STREAM_EDIT_MIN_INTERVAL_MS = 600;
238
241
  var STREAM_MAX_UPDATES_PER_MESSAGE = 40;
239
242
  var FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
243
+ var SLASH_GUILD_THRESHOLD = 10;
240
244
  var DiscordChannel = class extends BaseChannel {
241
- name = "discord";
242
- client = null;
243
- typingController;
244
- constructor(config, bus) {
245
+ constructor(config, bus, sessionManager, coreConfig) {
245
246
  super(config, bus);
247
+ this.sessionManager = sessionManager;
248
+ this.coreConfig = coreConfig;
249
+ this.commandRegistry = this.coreConfig ? new CommandRegistry(this.coreConfig, this.sessionManager) : null;
246
250
  this.typingController = new ChannelTypingController({
247
251
  heartbeatMs: TYPING_HEARTBEAT_MS,
248
252
  autoStopMs: TYPING_AUTO_STOP_MS,
@@ -259,6 +263,10 @@ var DiscordChannel = class extends BaseChannel {
259
263
  }
260
264
  });
261
265
  }
266
+ name = "discord";
267
+ client = null;
268
+ typingController;
269
+ commandRegistry;
262
270
  async start() {
263
271
  if (!this.config.token) {
264
272
  throw new Error("Discord token not configured");
@@ -270,10 +278,14 @@ var DiscordChannel = class extends BaseChannel {
270
278
  });
271
279
  this.client.on("ready", () => {
272
280
  console.log("Discord bot connected");
281
+ void this.registerSlashCommands();
273
282
  });
274
283
  this.client.on("messageCreate", async (message) => {
275
284
  await this.handleIncoming(message);
276
285
  });
286
+ this.client.on("interactionCreate", async (interaction) => {
287
+ await this.handleInteraction(interaction);
288
+ });
277
289
  await this.client.login(this.config.token);
278
290
  }
279
291
  async stop() {
@@ -409,6 +421,97 @@ var DiscordChannel = class extends BaseChannel {
409
421
  throw error;
410
422
  }
411
423
  }
424
+ async handleInteraction(interaction) {
425
+ if (!interaction.isChatInputCommand()) {
426
+ return;
427
+ }
428
+ await this.handleSlashCommand(interaction);
429
+ }
430
+ async handleSlashCommand(interaction) {
431
+ if (!this.commandRegistry) {
432
+ await this.replyInteraction(interaction, "Slash commands are not available.", true);
433
+ return;
434
+ }
435
+ const channelId = interaction.channelId;
436
+ if (!channelId) {
437
+ await this.replyInteraction(interaction, "Slash commands are not available in this channel.", true);
438
+ return;
439
+ }
440
+ const senderId = interaction.user.id;
441
+ const isGroup = Boolean(interaction.guildId);
442
+ if (!this.isAllowedByPolicy({ senderId, channelId, isGroup })) {
443
+ await this.replyInteraction(interaction, "You are not authorized to use commands here.", true);
444
+ return;
445
+ }
446
+ const args = {};
447
+ for (const option of interaction.options.data) {
448
+ if (typeof option.name === "string" && option.value !== void 0) {
449
+ args[option.name] = option.value;
450
+ }
451
+ }
452
+ try {
453
+ const result = await this.commandRegistry.execute(interaction.commandName, args, {
454
+ channel: this.name,
455
+ chatId: channelId,
456
+ senderId,
457
+ sessionKey: `${this.name}:${channelId}`
458
+ });
459
+ await this.replyInteraction(interaction, result.content, result.ephemeral ?? true);
460
+ } catch (error) {
461
+ await this.replyInteraction(interaction, "Command failed to execute.", true);
462
+ console.error(`Discord slash command error: ${String(error)}`);
463
+ }
464
+ }
465
+ async replyInteraction(interaction, content, ephemeral) {
466
+ const payload = { content, ephemeral };
467
+ if (interaction.deferred || interaction.replied) {
468
+ await interaction.followUp(payload);
469
+ return;
470
+ }
471
+ await interaction.reply(payload);
472
+ }
473
+ async registerSlashCommands() {
474
+ if (!this.client || !this.commandRegistry) {
475
+ return;
476
+ }
477
+ const appId = this.client.application?.id ?? this.client.user?.id;
478
+ if (!appId) {
479
+ return;
480
+ }
481
+ const commands = this.buildSlashCommandPayloads();
482
+ if (!commands.length) {
483
+ return;
484
+ }
485
+ const rest = new REST({ version: "10" }).setToken(this.config.token);
486
+ let guildIds = [];
487
+ try {
488
+ const guilds = await this.client.guilds.fetch();
489
+ guildIds = [...guilds.keys()];
490
+ } catch (error) {
491
+ console.error(`Failed to fetch Discord guild list: ${String(error)}`);
492
+ }
493
+ try {
494
+ if (guildIds.length > 0 && guildIds.length <= SLASH_GUILD_THRESHOLD) {
495
+ for (const guildId of guildIds) {
496
+ await rest.put(Routes.applicationGuildCommands(appId, guildId), { body: commands });
497
+ }
498
+ console.log(`Discord slash commands registered for ${guildIds.length} guild(s).`);
499
+ } else {
500
+ await rest.put(Routes.applicationCommands(appId), { body: commands });
501
+ console.log("Discord slash commands registered globally.");
502
+ }
503
+ } catch (error) {
504
+ console.error(`Failed to register Discord slash commands: ${String(error)}`);
505
+ }
506
+ }
507
+ buildSlashCommandPayloads() {
508
+ const specs = this.commandRegistry?.listSlashCommands() ?? [];
509
+ return specs.map((spec) => ({
510
+ name: spec.name,
511
+ description: spec.description,
512
+ options: mapCommandOptions(spec.options)
513
+ }));
514
+ }
412
515
  resolveProxyAgent() {
413
516
  const proxy = this.config.proxy?.trim();
414
517
  if (!proxy) {
@@ -610,6 +713,28 @@ var DiscordChannel = class extends BaseChannel {
610
713
  this.typingController.stop(channelId);
611
714
  }
612
715
  };
716
+ function mapCommandOptions(options) {
717
+ if (!options || options.length === 0) {
718
+ return void 0;
719
+ }
720
+ return options.map((option) => ({
721
+ name: option.name,
722
+ description: option.description,
723
+ type: mapCommandOptionType(option.type),
724
+ required: option.required ?? false
725
+ }));
726
+ }
727
+ function mapCommandOptionType(type) {
728
+ switch (type) {
729
+ case "boolean":
730
+ return ApplicationCommandOptionType.Boolean;
731
+ case "number":
732
+ return ApplicationCommandOptionType.Number;
733
+ case "string":
734
+ default:
735
+ return ApplicationCommandOptionType.String;
736
+ }
737
+ }
613
738
  function sanitizeAttachmentName(name) {
614
739
  return name.replace(/[\\/:*?"<>|]/g, "_");
615
740
  }
@@ -3453,7 +3578,7 @@ var BUILTIN_CHANNEL_RUNTIMES = {
3453
3578
  discord: {
3454
3579
  id: "discord",
3455
3580
  isEnabled: (config) => config.channels.discord.enabled,
3456
- createChannel: (context) => new DiscordChannel(context.config.channels.discord, context.bus)
3581
+ createChannel: (context) => new DiscordChannel(context.config.channels.discord, context.bus, context.sessionManager, context.config)
3457
3582
  },
3458
3583
  feishu: {
3459
3584
  id: "feishu",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@larksuiteoapi/node-sdk": "^1.58.0",
18
- "@nextclaw/core": "^0.6.31",
18
+ "@nextclaw/core": "^0.6.32",
19
19
  "@slack/socket-mode": "^1.3.3",
20
20
  "@slack/web-api": "^7.6.0",
21
21
  "dingtalk-stream": "^2.1.4",