fix(agent-service): use https.request for Whisper STT to bypass self-signed cert
Node 18 native fetch (undici) ignores https.Agent, causing fetch failed on the self-signed proxy at 67.223.119.33:8443. Switch to https.request with rejectUnauthorized: false which works reliably. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
72584182df
commit
947a47869e
|
|
@ -2,8 +2,8 @@
|
|||
* OpenAISttService
|
||||
*
|
||||
* Calls the OpenAI Whisper transcriptions API to convert audio buffers to text.
|
||||
* Supports the self-signed proxy at 67.223.119.33:8443 by disabling TLS verification
|
||||
* when OPENAI_BASE_URL points to an https host (same pattern as voice-agent).
|
||||
* Supports the self-signed proxy at 67.223.119.33:8443 by disabling TLS
|
||||
* verification via https.request (Node 18 native fetch ignores https.Agent).
|
||||
*/
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
|
@ -16,12 +16,12 @@ export class OpenAISttService {
|
|||
private readonly baseUrl: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.apiKey = this.configService.get<string>('OPENAI_API_KEY', '');
|
||||
// Strip trailing slash and /v1 suffix — we always append /v1/...
|
||||
this.baseUrl = this.configService.get<string>(
|
||||
'OPENAI_BASE_URL',
|
||||
'https://api.openai.com',
|
||||
).replace(/\/$/, '').replace(/\/v1$/, '');
|
||||
this.baseUrl = this.configService
|
||||
.get<string>('OPENAI_BASE_URL', 'https://api.openai.com')
|
||||
.replace(/\/$/, '')
|
||||
.replace(/\/v1$/, '');
|
||||
this.apiKey = this.configService.get<string>('OPENAI_API_KEY', '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,7 +37,9 @@ export class OpenAISttService {
|
|||
language = 'zh',
|
||||
): Promise<string> {
|
||||
const url = `${this.baseUrl}/v1/audio/transcriptions`;
|
||||
this.logger.log(`STT: transcribing ${filename} (${audioBuffer.length} bytes) → ${url}`);
|
||||
this.logger.log(
|
||||
`STT: transcribing ${filename} (${audioBuffer.length} bytes) → ${url}`,
|
||||
);
|
||||
|
||||
// Build multipart/form-data manually to avoid external dependencies
|
||||
const boundary = `----FormBoundary${Date.now().toString(16)}`;
|
||||
|
|
@ -66,27 +68,51 @@ export class OpenAISttService {
|
|||
|
||||
const body = Buffer.concat(parts);
|
||||
|
||||
// Use a custom https agent to allow self-signed certs (proxy at 67.223.119.33:8443)
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': String(body.length),
|
||||
},
|
||||
body,
|
||||
// @ts-ignore — undici (Node 18+ native fetch) accepts dispatcher via agent option
|
||||
agent,
|
||||
// Use https.request so rejectUnauthorized: false works for the self-signed
|
||||
// proxy. Node 18 native fetch (undici) does NOT honour https.Agent.
|
||||
const parsedUrl = new URL(url);
|
||||
const result = await new Promise<{ text: string }>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: parsedUrl.hostname,
|
||||
port: parsedUrl.port || 443,
|
||||
path: parsedUrl.pathname + parsedUrl.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length,
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
(res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8');
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(
|
||||
new Error(
|
||||
`Whisper STT failed: HTTP ${res.statusCode} — ${text}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
resolve(JSON.parse(text) as { text: string });
|
||||
} catch {
|
||||
reject(
|
||||
new Error(`Whisper STT: invalid JSON response: ${text}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text().catch(() => '');
|
||||
throw new Error(`Whisper STT failed: HTTP ${response.status} — ${errText}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as { text: string };
|
||||
this.logger.log(`STT result: "${result.text?.slice(0, 80)}"`);
|
||||
return result.text ?? '';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue