fix(qqbot): lazy-load silk-wasm to avoid hard failure when package is missing (#58829)

* fix(qqbot): lazy-load silk-wasm to avoid hard failure when package is missing

Replace the static top-level import with a cached dynamic import helper.
If silk-wasm is unavailable the plugin loads normally; voice encode/decode
degrades gracefully instead of crashing the module at load time.

* fix(qqbot): store in-flight Promise in loadSilkWasm to prevent duplicate imports

Concurrent cold-start calls to loadSilkWasm() before the first import()
resolves would each fire a separate dynamic import. Storing the Promise
instead of the resolved value (matching the detectFfmpeg pattern in
platform.ts) ensures all concurrent callers await the same import,
keeping the codebase consistent and avoiding redundant parallel loads.

* QQBot: add changelog for silk-wasm lazy load

* QQBot: move changelog entry for PR #58829

---------

Co-authored-by: sliverp <870080352@qq.com>
Co-authored-by: Sliverp <38134380+sliverp@users.noreply.github.com>
This commit is contained in:
Mingkuan
2026-04-02 14:46:53 +08:00
committed by GitHub
parent 0809c8d29a
commit c15cfeb21c
2 changed files with 27 additions and 6 deletions

View File

@@ -106,6 +106,7 @@ Docs: https://docs.openclaw.ai
- Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to `auth-profiles.json` before returning them, so rotated Codex refresh tokens survive restart and stop falling into `refresh_token_reused` loops. (#53082)
- Agents/Anthropic: honor explicit `cacheRetention` for custom providers using `anthropic-messages`, so Anthropic-compatible proxy providers can reuse prompt caching when they opt in. (#59049) Thanks @wwerst and @vincentkoc.
- Discord/gateway: hand reconnect ownership back to Carbon, keep runtime status aligned with close/reconnect state, and force-stop sockets that open without reaching READY so Discord monitors recover promptly instead of waiting on stale health timeouts. (#59019) Thanks @obviyus
- QQBot/voice: lazy-load `silk-wasm` in `audio-convert.ts` so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.
- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.
### Fixes

View File

@@ -2,10 +2,23 @@ import { execFile } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { decode, encode, isSilk } from "silk-wasm";
import { debugLog, debugError, debugWarn } from "./debug-log.js";
import { detectFfmpeg, isWindows } from "./platform.js";
type SilkWasm = typeof import("silk-wasm");
let _silkWasmPromise: Promise<SilkWasm | null> | null = null;
function loadSilkWasm(): Promise<SilkWasm | null> {
if (_silkWasmPromise) return _silkWasmPromise;
_silkWasmPromise = import("silk-wasm").catch((err) => {
debugWarn(
`[audio-convert] silk-wasm not available; SILK encode/decode disabled (${err instanceof Error ? err.message : String(err)})`,
);
return null;
});
return _silkWasmPromise;
}
/** Wrap PCM s16le bytes in a WAV container. */
function pcmToWav(
pcmData: Uint8Array,
@@ -72,13 +85,14 @@ export async function convertSilkToWav(
strippedBuf.byteLength,
);
if (!isSilk(rawData)) {
const silk = await loadSilkWasm();
if (!silk || !silk.isSilk(rawData)) {
return null;
}
// QQ voice commonly uses 24 kHz.
const sampleRate = 24000;
const result = await decode(rawData, sampleRate);
const result = await silk.decode(rawData, sampleRate);
const wavBuffer = pcmToWav(result.data, sampleRate);
@@ -393,8 +407,12 @@ export async function pcmToSilk(
pcmBuffer: Buffer,
sampleRate: number,
): Promise<{ silkBuffer: Buffer; duration: number }> {
const silk = await loadSilkWasm();
if (!silk) {
throw new Error("silk-wasm is not available; cannot encode PCM to SILK");
}
const pcmData = new Uint8Array(pcmBuffer.buffer, pcmBuffer.byteOffset, pcmBuffer.byteLength);
const result = await encode(pcmData, sampleRate);
const result = await silk.encode(pcmData, sampleRate);
return {
silkBuffer: Buffer.from(result.data.buffer, result.data.byteOffset, result.data.byteLength),
duration: result.duration,
@@ -450,7 +468,8 @@ export async function audioFileToSilkBase64(
if ([".slk", ".slac"].includes(ext)) {
const stripped = stripAmrHeader(buf);
const raw = new Uint8Array(stripped.buffer, stripped.byteOffset, stripped.byteLength);
if (isSilk(raw)) {
const silk = await loadSilkWasm();
if (silk?.isSilk(raw)) {
debugLog(`[audio-convert] SILK file, direct use: ${filePath} (${buf.length} bytes)`);
return buf.toString("base64");
}
@@ -464,7 +483,8 @@ export async function audioFileToSilkBase64(
strippedCheck.byteOffset,
strippedCheck.byteLength,
);
if (isSilk(rawCheck) || isSilk(strippedRaw)) {
const silkForCheck = await loadSilkWasm();
if (silkForCheck?.isSilk(rawCheck) || silkForCheck?.isSilk(strippedRaw)) {
debugLog(`[audio-convert] SILK detected by header: ${filePath} (${buf.length} bytes)`);
return buf.toString("base64");
}