From 3c6a15ce9811f9f3f06dd173a4aa3e0798486bd2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 18:45:31 +0100 Subject: [PATCH] fix(discord): make opus optional and log fallback --- CHANGELOG.md | 1 + package.json | 4 ++- pnpm-lock.yaml | 51 ++++++++++++++++++++---------------- src/discord/voice/manager.ts | 32 +++++++++++++--------- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c3c6c7b5d..acf8e02626d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai - Memory/Remote HTTP: centralize remote memory HTTP calls behind a shared guarded helper (`withRemoteHttpResponse`) so embeddings and batch flows use one request/release path. - Memory/Embeddings: apply configured remote-base host pinning (`allowedHostnames`) across OpenAI/Voyage/Gemini embedding requests to keep private/self-hosted endpoints working without cross-host drift. (#18198) Thanks @ianpcook. - Memory/Batch: route OpenAI/Voyage/Gemini batch upload/create/status/download requests through the same guarded HTTP path for consistent SSRF policy enforcement. +- Install/Discord Voice: make `@discordjs/opus` an optional dependency so `openclaw` install/update no longer hard-fails when native Opus builds fail, while keeping `opusscript` as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) - Signal/RPC: guard malformed Signal RPC JSON responses with a clear status-scoped error and add regression coverage for invalid JSON responses. (#22995) Thanks @adhitShet. - Gateway/Subagents: guard gateway and subagent session-key/message trim paths against undefined inputs to prevent early `Cannot read properties of undefined (reading 'trim')` crashes during subagent spawn and wait flows. - Agents/Workspace: guard `resolveUserPath` against undefined/null input to prevent `Cannot read properties of undefined (reading 'trim')` crashes when workspace paths are missing in embedded runner flows. diff --git a/package.json b/package.json index 6638617c541..bf133a9a266 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,6 @@ "@aws-sdk/client-bedrock": "^3.995.0", "@buape/carbon": "0.0.0-beta-20260216184201", "@clack/prompts": "^1.0.1", - "@discordjs/opus": "^0.10.0", "@discordjs/voice": "^0.19.0", "@grammyjs/runner": "^2.0.3", "@grammyjs/transformer-throttler": "^1.2.1", @@ -217,6 +216,9 @@ "@napi-rs/canvas": "^0.1.89", "node-llama-cpp": "3.15.1" }, + "optionalDependencies": { + "@discordjs/opus": "^0.10.0" + }, "engines": { "node": ">=22.12.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a25231a1a6..5e9de9be20c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,9 +32,6 @@ importers: '@clack/prompts': specifier: ^1.0.1 version: 1.0.1 - '@discordjs/opus': - specifier: ^0.10.0 - version: 0.10.0 '@discordjs/voice': specifier: ^0.19.0 version: 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.0.8) @@ -243,6 +240,10 @@ importers: vitest: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + '@discordjs/opus': + specifier: ^0.10.0 + version: 0.10.0 extensions/bluebubbles: devDependencies: @@ -6525,6 +6526,7 @@ snapshots: transitivePeerDependencies: - encoding - supports-color + optional: true '@discordjs/opus@0.10.0': dependencies: @@ -6533,6 +6535,7 @@ snapshots: transitivePeerDependencies: - encoding - supports-color + optional: true '@discordjs/voice@0.19.0(@discordjs/opus@0.10.0)(opusscript@0.0.8)': dependencies: @@ -6880,7 +6883,7 @@ snapshots: '@larksuiteoapi/node-sdk@1.59.0': dependencies: - axios: 1.13.5 + axios: 1.13.5(debug@4.4.3) lodash.identity: 3.0.0 lodash.merge: 4.6.2 lodash.pickby: 4.6.0 @@ -6896,7 +6899,7 @@ snapshots: dependencies: '@types/node': 24.10.13 optionalDependencies: - axios: 1.13.5 + axios: 1.13.5(debug@4.4.3) transitivePeerDependencies: - debug @@ -7085,7 +7088,7 @@ snapshots: '@azure/core-auth': 1.10.1 '@azure/msal-node': 5.0.4 '@microsoft/agents-activity': 1.3.1 - axios: 1.13.5 + axios: 1.13.5(debug@4.4.3) jsonwebtoken: 9.0.3 jwks-rsa: 3.2.2 object-path: 0.11.8 @@ -7987,7 +7990,7 @@ snapshots: '@slack/types': 2.20.0 '@slack/web-api': 7.14.1 '@types/express': 5.0.6 - axios: 1.13.5 + axios: 1.13.5(debug@4.4.3) express: 5.2.1 path-to-regexp: 8.3.0 raw-body: 3.0.2 @@ -8033,7 +8036,7 @@ snapshots: '@slack/types': 2.20.0 '@types/node': 25.3.0 '@types/retry': 0.12.0 - axios: 1.13.5 + axios: 1.13.5(debug@4.4.3) eventemitter3: 5.0.4 form-data: 2.5.4 is-electron: 2.2.2 @@ -8771,7 +8774,8 @@ snapshots: curve25519-js: 0.0.4 protobufjs: 6.8.8 - abbrev@1.1.1: {} + abbrev@1.1.1: + optional: true abort-controller@3.0.0: dependencies: @@ -8798,6 +8802,7 @@ snapshots: debug: 4.4.3 transitivePeerDependencies: - supports-color + optional: true agent-base@7.1.4: {} @@ -8848,6 +8853,7 @@ snapshots: dependencies: delegates: 1.0.0 readable-stream: 3.6.2 + optional: true are-we-there-yet@3.0.1: dependencies: @@ -8922,14 +8928,6 @@ snapshots: aws4@1.13.2: {} - axios@1.13.5: - dependencies: - follow-redirects: 1.15.11 - form-data: 2.5.4 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.13.5(debug@4.4.3): dependencies: follow-redirects: 1.15.11(debug@4.4.3) @@ -9499,8 +9497,6 @@ snapshots: flatbuffers@24.12.23: {} - follow-redirects@1.15.11: {} - follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: debug: 4.4.3 @@ -9537,7 +9533,8 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 - fs.realpath@1.0.0: {} + fs.realpath@1.0.0: + optional: true fsevents@2.3.2: optional: true @@ -9558,6 +9555,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 wide-align: 1.1.5 + optional: true gauge@4.0.4: dependencies: @@ -9652,6 +9650,7 @@ snapshots: minimatch: 10.2.1 once: 1.4.0 path-is-absolute: 1.0.1 + optional: true google-auth-library@10.5.0: dependencies: @@ -9781,6 +9780,7 @@ snapshots: debug: 4.4.3 transitivePeerDependencies: - supports-color + optional: true https-proxy-agent@7.0.6: dependencies: @@ -9816,6 +9816,7 @@ snapshots: dependencies: once: 1.4.0 wrappy: 1.0.2 + optional: true inherits@2.0.4: {} @@ -10178,6 +10179,7 @@ snapshots: make-dir@3.1.0: dependencies: semver: 6.3.1 + optional: true make-dir@4.0.0: dependencies: @@ -10386,6 +10388,7 @@ snapshots: nopt@5.0.0: dependencies: abbrev: 1.1.1 + optional: true nostr-tools@2.23.1(typescript@5.9.3): dependencies: @@ -10407,6 +10410,7 @@ snapshots: console-control-strings: 1.1.0 gauge: 3.0.2 set-blocking: 2.0.0 + optional: true npmlog@6.0.2: dependencies: @@ -10624,7 +10628,8 @@ snapshots: partial-json@0.1.7: {} - path-is-absolute@1.0.1: {} + path-is-absolute@1.0.1: + optional: true path-key@3.1.1: {} @@ -10898,6 +10903,7 @@ snapshots: rimraf@3.0.2: dependencies: glob: 7.2.3 + optional: true rimraf@5.0.10: dependencies: @@ -11002,7 +11008,8 @@ snapshots: dependencies: parseley: 0.12.1 - semver@6.3.1: {} + semver@6.3.1: + optional: true semver@7.7.4: {} diff --git a/src/discord/voice/manager.ts b/src/discord/voice/manager.ts index f1e7585d65a..f9da749a74f 100644 --- a/src/discord/voice/manager.ts +++ b/src/discord/voice/manager.ts @@ -146,25 +146,33 @@ type OpusDecoder = { decode: (buffer: Buffer) => Buffer; }; +let warnedOpusFallback = false; + function createOpusDecoder(): { decoder: OpusDecoder; name: string } | null { - try { - const OpusScript = require("opusscript") as { - new (sampleRate: number, channels: number, application: number): OpusDecoder; - Application: { AUDIO: number }; - }; - const decoder = new OpusScript(SAMPLE_RATE, CHANNELS, OpusScript.Application.AUDIO); - return { decoder, name: "opusscript" }; - } catch (err) { - logger.warn(`discord voice: opusscript init failed: ${formatErrorMessage(err)}`); - } try { const { OpusEncoder } = require("@discordjs/opus") as { OpusEncoder: new (sampleRate: number, channels: number) => OpusDecoder; }; const decoder = new OpusEncoder(SAMPLE_RATE, CHANNELS); return { decoder, name: "@discordjs/opus" }; - } catch (err) { - logger.warn(`discord voice: opus decoder init failed: ${formatErrorMessage(err)}`); + } catch (nativeErr) { + try { + const OpusScript = require("opusscript") as { + new (sampleRate: number, channels: number, application: number): OpusDecoder; + Application: { AUDIO: number }; + }; + const decoder = new OpusScript(SAMPLE_RATE, CHANNELS, OpusScript.Application.AUDIO); + if (!warnedOpusFallback) { + warnedOpusFallback = true; + logger.warn( + `discord voice: @discordjs/opus unavailable (${formatErrorMessage(nativeErr)}); using opusscript fallback`, + ); + } + return { decoder, name: "opusscript" }; + } catch (jsErr) { + logger.warn(`discord voice: opus decoder init failed: ${formatErrorMessage(nativeErr)}`); + logger.warn(`discord voice: opusscript init failed: ${formatErrorMessage(jsErr)}`); + } } return null; }