mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(qqbot): bound speech transcription requests (#102028)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
0535679083
commit
64e80888d4
3 changed files with 46 additions and 3 deletions
|
|
@ -259,6 +259,11 @@ STT and TTS support two-level configuration with priority fallback:
|
|||
Set `enabled: false` on either to disable. Account-level TTS overrides use the
|
||||
same shape as `messages.tts` and deep-merge over channel/global TTS config.
|
||||
|
||||
STT requests time out after 60 seconds by default. Plugin-specific STT uses the
|
||||
selected `models.providers.<id>.timeoutSeconds` override. Framework audio STT
|
||||
uses `tools.media.audio.models[0].timeoutSeconds`, then
|
||||
`tools.media.audio.timeoutSeconds`, then the selected provider override.
|
||||
|
||||
Inbound QQ voice attachments are exposed to agents as audio media metadata
|
||||
while keeping raw voice files out of generic `MediaPaths`. `[[audio_as_voice]]`
|
||||
in a plain-text reply synthesizes TTS and sends a native QQ voice message when
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ function requireFirstSsrfRequest(): {
|
|||
url?: unknown;
|
||||
auditContext?: unknown;
|
||||
init?: RequestInit;
|
||||
timeoutMs?: unknown;
|
||||
} {
|
||||
const [call] = ssrfRuntimeMocks.fetchWithSsrFGuard.mock.calls;
|
||||
if (!call) {
|
||||
|
|
@ -84,6 +85,7 @@ function requireFirstSsrfRequest(): {
|
|||
url?: unknown;
|
||||
auditContext?: unknown;
|
||||
init?: RequestInit;
|
||||
timeoutMs?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +120,7 @@ describe("engine/utils/stt", () => {
|
|||
providers: {
|
||||
openai: {
|
||||
apiKey: "provider-key",
|
||||
timeoutSeconds: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -127,6 +130,7 @@ describe("engine/utils/stt", () => {
|
|||
baseUrl: "https://api.example.test/v1",
|
||||
apiKey: "provider-key",
|
||||
model: "whisper-large",
|
||||
timeoutMs: 45_000,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -136,13 +140,14 @@ describe("engine/utils/stt", () => {
|
|||
tools: {
|
||||
media: {
|
||||
audio: {
|
||||
timeoutSeconds: 90,
|
||||
models: [{ provider: "local", baseUrl: "https://stt.example.test/", model: "sense" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
local: { apiKey: "local-key" },
|
||||
local: { apiKey: "local-key", timeoutSeconds: 120 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -151,7 +156,11 @@ describe("engine/utils/stt", () => {
|
|||
baseUrl: "https://stt.example.test",
|
||||
apiKey: "local-key",
|
||||
model: "sense",
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
|
||||
Object.assign(cfg.tools.media.audio.models[0], { timeoutSeconds: 75 });
|
||||
expect(resolveSTTConfig(cfg)?.timeoutMs).toBe(75_000);
|
||||
});
|
||||
|
||||
it("returns null when no usable STT credentials are configured", () => {
|
||||
|
|
@ -191,6 +200,7 @@ describe("engine/utils/stt", () => {
|
|||
const request = requireFirstSsrfRequest();
|
||||
expect(request.url).toBe("https://api.example.test/v1/audio/transcriptions");
|
||||
expect(request.auditContext).toBe("qqbot-stt");
|
||||
expect(request.timeoutMs).toBe(60_000);
|
||||
expect(request.init?.method).toBe("POST");
|
||||
expect(request.init?.headers).toEqual({ Authorization: "Bearer secret" });
|
||||
expect(request.init?.body).toBeInstanceOf(FormData);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
|
||||
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
|
|
@ -22,11 +23,23 @@ import {
|
|||
} from "./string-normalize.js";
|
||||
|
||||
const STT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
const DEFAULT_STT_TIMEOUT_MS = 60_000;
|
||||
|
||||
interface STTConfig {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
function resolveSTTTimeoutMs(...timeoutSeconds: unknown[]): number {
|
||||
for (const value of timeoutSeconds) {
|
||||
const timeoutMs = finiteSecondsToTimerSafeMilliseconds(value);
|
||||
if (timeoutMs !== undefined) {
|
||||
return timeoutMs;
|
||||
}
|
||||
}
|
||||
return DEFAULT_STT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/** Resolve the STT configuration from the nested config object. */
|
||||
|
|
@ -45,7 +58,12 @@ export function resolveSTTConfig(cfg: Record<string, unknown>): STTConfig | null
|
|||
const apiKey = readString(channelStt, "apiKey") ?? readString(providerCfg, "apiKey");
|
||||
const model = readString(channelStt, "model") ?? "whisper-1";
|
||||
if (baseUrl && apiKey) {
|
||||
return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey, model };
|
||||
return {
|
||||
baseUrl: baseUrl.replace(/\/+$/, ""),
|
||||
apiKey,
|
||||
model,
|
||||
timeoutMs: resolveSTTTimeoutMs(providerCfg?.timeoutSeconds),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +80,16 @@ export function resolveSTTConfig(cfg: Record<string, unknown>): STTConfig | null
|
|||
const apiKey = readString(audioModelEntry, "apiKey") ?? readString(providerCfg, "apiKey");
|
||||
const model = readString(audioModelEntry, "model") ?? "whisper-1";
|
||||
if (baseUrl && apiKey) {
|
||||
return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey, model };
|
||||
return {
|
||||
baseUrl: baseUrl.replace(/\/+$/, ""),
|
||||
apiKey,
|
||||
model,
|
||||
timeoutMs: resolveSTTTimeoutMs(
|
||||
audioModelEntry.timeoutSeconds,
|
||||
audio?.timeoutSeconds,
|
||||
providerCfg?.timeoutSeconds,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +117,7 @@ export async function transcribeAudio(
|
|||
const { response: resp, release } = await fetchWithSsrFGuard({
|
||||
url: `${sttCfg.baseUrl}/audio/transcriptions`,
|
||||
auditContext: "qqbot-stt",
|
||||
timeoutMs: sttCfg.timeoutMs,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue