mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:08:25 +00:00
fix: address code review feedback
- Remove unused ffmpeg astats command from generateWaveform() - Use crypto.randomUUID() for temp file names to prevent collision - Wrap upload URL request in retry runner for consistency - Add validation: reject content with asVoice, require local file path - Add clarifying comments for CDN upload behavior
This commit is contained in:
@@ -240,9 +240,20 @@ export async function handleDiscordMessagingAction(
|
|||||||
Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined;
|
Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined;
|
||||||
|
|
||||||
// Handle voice message sending
|
// Handle voice message sending
|
||||||
if (asVoice && mediaUrl) {
|
if (asVoice) {
|
||||||
// Voice messages require a local file path or downloadable URL
|
if (!mediaUrl) {
|
||||||
// They cannot include text content (Discord limitation)
|
throw new Error("Voice messages require a media file path (mediaUrl).");
|
||||||
|
}
|
||||||
|
if (content && content.trim()) {
|
||||||
|
throw new Error(
|
||||||
|
"Voice messages cannot include text content (Discord limitation). Remove the content parameter.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
||||||
|
throw new Error(
|
||||||
|
"Voice messages require a local file path, not a URL. Download the file first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const result = await sendVoiceMessageDiscord(to, mediaUrl, {
|
const result = await sendVoiceMessageDiscord(to, mediaUrl, {
|
||||||
...(accountId ? { accountId } : {}),
|
...(accountId ? { accountId } : {}),
|
||||||
replyTo,
|
replyTo,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import type { RequestClient } from "@buape/carbon";
|
import type { RequestClient } from "@buape/carbon";
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@@ -58,28 +59,10 @@ export async function getAudioDuration(filePath: string): Promise<number> {
|
|||||||
*/
|
*/
|
||||||
export async function generateWaveform(filePath: string): Promise<string> {
|
export async function generateWaveform(filePath: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// Use ffmpeg to extract raw audio samples and compute amplitudes
|
// Extract raw PCM and sample amplitude values
|
||||||
// We'll get the peak amplitude for each segment of the audio
|
return await generateWaveformFromPcm(filePath);
|
||||||
const { stdout } = await execFileAsync(
|
|
||||||
"ffmpeg",
|
|
||||||
[
|
|
||||||
"-i",
|
|
||||||
filePath,
|
|
||||||
"-af",
|
|
||||||
`aresample=8000,asetnsamples=n=${WAVEFORM_SAMPLES}:p=0,astats=metadata=1:reset=1`,
|
|
||||||
"-f",
|
|
||||||
"null",
|
|
||||||
"-",
|
|
||||||
],
|
|
||||||
{ encoding: "buffer", maxBuffer: 1024 * 1024 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fallback: generate a simple waveform by sampling the audio
|
|
||||||
// This is a simplified approach - extract raw PCM and sample it
|
|
||||||
const waveformData = await generateWaveformFromPcm(filePath);
|
|
||||||
return waveformData;
|
|
||||||
} catch {
|
} catch {
|
||||||
// If ffmpeg approach fails, generate a placeholder waveform
|
// If PCM extraction fails, generate a placeholder waveform
|
||||||
return generatePlaceholderWaveform();
|
return generatePlaceholderWaveform();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +72,7 @@ export async function generateWaveform(filePath: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
async function generateWaveformFromPcm(filePath: string): Promise<string> {
|
async function generateWaveformFromPcm(filePath: string): Promise<string> {
|
||||||
const tempDir = os.tmpdir();
|
const tempDir = os.tmpdir();
|
||||||
const tempPcm = path.join(tempDir, `waveform-${Date.now()}.raw`);
|
const tempPcm = path.join(tempDir, `waveform-${crypto.randomUUID()}.raw`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert to raw 16-bit signed PCM, mono, 8kHz
|
// Convert to raw 16-bit signed PCM, mono, 8kHz
|
||||||
@@ -190,7 +173,7 @@ export async function ensureOggOpus(filePath: string): Promise<{ path: string; c
|
|||||||
|
|
||||||
// Convert to OGG/Opus
|
// Convert to OGG/Opus
|
||||||
const tempDir = os.tmpdir();
|
const tempDir = os.tmpdir();
|
||||||
const outputPath = path.join(tempDir, `voice-${Date.now()}.ogg`);
|
const outputPath = path.join(tempDir, `voice-${crypto.randomUUID()}.ogg`);
|
||||||
|
|
||||||
await execFileAsync("ffmpeg", [
|
await execFileAsync("ffmpeg", [
|
||||||
"-y",
|
"-y",
|
||||||
@@ -246,10 +229,10 @@ export async function sendDiscordVoiceMessage(
|
|||||||
const filename = "voice-message.ogg";
|
const filename = "voice-message.ogg";
|
||||||
const fileSize = audioBuffer.byteLength;
|
const fileSize = audioBuffer.byteLength;
|
||||||
|
|
||||||
// Step 1: Request upload URL (using fetch directly for proper headers)
|
// Step 1: Request upload URL (using fetch directly for proper Content-Type header)
|
||||||
const uploadUrlRes = await fetch(
|
// Wrapped in retry runner for consistency with other Discord API calls
|
||||||
`https://discord.com/api/v10/channels/${channelId}/attachments`,
|
const uploadUrlResponse = await request(async () => {
|
||||||
{
|
const res = await fetch(`https://discord.com/api/v10/channels/${channelId}/attachments`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -264,15 +247,15 @@ export async function sendDiscordVoiceMessage(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!uploadUrlRes.ok) {
|
if (!res.ok) {
|
||||||
const errorBody = await uploadUrlRes.text();
|
const errorBody = await res.text();
|
||||||
throw new Error(`Failed to get upload URL: ${uploadUrlRes.status} ${errorBody}`);
|
throw new Error(`Failed to get upload URL: ${res.status} ${errorBody}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadUrlResponse = (await uploadUrlRes.json()) as UploadUrlResponse;
|
return (await res.json()) as UploadUrlResponse;
|
||||||
|
}, "voice-upload-url");
|
||||||
|
|
||||||
if (!uploadUrlResponse.attachments?.[0]) {
|
if (!uploadUrlResponse.attachments?.[0]) {
|
||||||
throw new Error("Failed to get upload URL for voice message");
|
throw new Error("Failed to get upload URL for voice message");
|
||||||
@@ -281,6 +264,7 @@ export async function sendDiscordVoiceMessage(
|
|||||||
const { upload_url, upload_filename } = uploadUrlResponse.attachments[0];
|
const { upload_url, upload_filename } = uploadUrlResponse.attachments[0];
|
||||||
|
|
||||||
// Step 2: Upload the file to Discord's CDN
|
// Step 2: Upload the file to Discord's CDN
|
||||||
|
// Note: Not wrapped in retry runner - upload URLs are single-use and CDN behavior differs
|
||||||
const uploadResponse = await fetch(upload_url, {
|
const uploadResponse = await fetch(upload_url, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user