Discord VC: voice channels, transcription, and TTS (#18774)

This commit is contained in:
Shadow
2026-02-20 16:06:07 -06:00
committed by GitHub
parent 3100b77f12
commit 4ab946eebf
23 changed files with 1924 additions and 1076 deletions

View File

@@ -6,6 +6,7 @@ import {
type Modal,
} from "@buape/carbon";
import { GatewayCloseCodes, type GatewayPlugin } from "@buape/carbon/gateway";
import { VoicePlugin } from "@buape/carbon/voice";
import { Routes } from "discord-api-types/v10";
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
@@ -38,6 +39,8 @@ import { fetchDiscordApplicationId } from "../probe.js";
import { resolveDiscordChannelAllowlist } from "../resolve-channels.js";
import { resolveDiscordUserAllowlist } from "../resolve-users.js";
import { normalizeDiscordToken } from "../token.js";
import { createDiscordVoiceCommand } from "../voice/command.js";
import { DiscordVoiceManager, DiscordVoiceReadyListener } from "../voice/manager.js";
import {
createAgentComponentButton,
createAgentSelectMenu,
@@ -241,6 +244,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const sessionPrefix = "discord:slash";
const ephemeralDefault = true;
const voiceEnabled = discordCfg.voice?.enabled !== false;
if (token) {
if (guildEntries && Object.keys(guildEntries).length > 0) {
@@ -428,6 +432,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
),
);
}
const voiceManagerRef: { current: DiscordVoiceManager | null } = { current: null };
const commands = commandSpecs.map((spec) =>
createDiscordNativeCommand({
command: spec,
@@ -438,6 +443,19 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
ephemeralDefault,
}),
);
if (nativeEnabled && voiceEnabled) {
commands.push(
createDiscordVoiceCommand({
cfg,
discordConfig: discordCfg,
accountId: account.accountId,
groupPolicy,
useAccessGroups,
getManager: () => voiceManagerRef.current,
ephemeralDefault,
}),
);
}
// Initialize exec approvals handler if enabled
const execApprovalsConfig = discordCfg.execApprovals ?? {};
@@ -506,6 +524,10 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
}
}
const clientPlugins = [createDiscordGatewayPlugin({ discordConfig: discordCfg, runtime })];
if (voiceEnabled) {
clientPlugins.push(new VoicePlugin());
}
const client = new Client(
{
baseUrl: "http://localhost",
@@ -521,7 +543,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
components,
modals,
},
[createDiscordGatewayPlugin({ discordConfig: discordCfg, runtime })],
clientPlugins,
);
await deployDiscordCommands({ client, runtime, enabled: nativeEnabled });
@@ -529,6 +551,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const logger = createSubsystemLogger("discord/monitor");
const guildHistories = new Map<string, HistoryEntry[]>();
let botUserId: string | undefined;
let voiceManager: DiscordVoiceManager | null = null;
if (nativeDisabledExplicit) {
await clearDiscordNativeCommands({
@@ -545,6 +568,19 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
}
if (voiceEnabled) {
voiceManager = new DiscordVoiceManager({
client,
cfg,
discordConfig: discordCfg,
accountId: account.accountId,
runtime,
botUserId,
});
voiceManagerRef.current = voiceManager;
registerDiscordListener(client.listeners, new DiscordVoiceReadyListener(voiceManager));
}
const messageHandler = createDiscordMessageHandler({
cfg,
discordConfig: discordCfg,
@@ -697,6 +733,10 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
}
gatewayEmitter?.removeListener("debug", onGatewayDebug);
abortSignal?.removeEventListener("abort", onAbort);
if (voiceManager) {
await voiceManager.destroy();
voiceManagerRef.current = null;
}
if (execApprovalsHandler) {
await execApprovalsHandler.stop();
}