mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 01:58:12 +00:00
audit: fix 18 defects across gateway SSE streaming, voice-call security, and telephony
Gateway (pipecat compatibility): - openai-http: add finish_reason:"stop" on final SSE chunk, fix ID format (chatcmpl- not chatcmpl_), capture timestamp once, use delta only, add writable checks and flush after writes - http-common: add TCP_NODELAY, X-Accel-Buffering:no, flush after writes, writable checks on writeDone - agent-events: fix seqByRun memory leak in clearAgentRunContext Voice-call security: - manager.ts, twiml.ts, twilio.ts: escape voice/language XML attributes to prevent XML injection - voice-mapping: strip control characters in escapeXml Voice-call bugs: - tts-openai: fix broken resample24kTo8k (interpolation frac always 0) - stt-openai-realtime: close zombie WebSocket on connection timeout - telnyx: extract direction/from/to for inbound calls (were silently dropped) - plivo: clean up 5 internal maps on terminal call states (memory leak) - twilio: clean up callWebhookUrls on terminal call states Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,7 +77,11 @@ export async function readJsonBodyOrError(
|
||||
}
|
||||
|
||||
export function writeDone(res: ServerResponse) {
|
||||
if (res.writableEnded || res.destroyed) {
|
||||
return;
|
||||
}
|
||||
res.write("data: [DONE]\n\n");
|
||||
(res as unknown as { flush?: () => void }).flush?.();
|
||||
}
|
||||
|
||||
export function setSseHeaders(res: ServerResponse) {
|
||||
@@ -85,5 +89,7 @@ export function setSseHeaders(res: ServerResponse) {
|
||||
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.socket?.setNoDelay?.(true);
|
||||
res.flushHeaders?.();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ type OpenAiChatCompletionRequest = {
|
||||
};
|
||||
|
||||
function writeSse(res: ServerResponse, data: unknown) {
|
||||
if (res.writableEnded || res.destroyed) {
|
||||
return;
|
||||
}
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
(res as unknown as { flush?: () => void }).flush?.();
|
||||
}
|
||||
|
||||
function asMessages(val: unknown): OpenAiChatMessage[] {
|
||||
@@ -178,7 +182,7 @@ export async function handleOpenAiHttpRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
const runId = `chatcmpl_${randomUUID()}`;
|
||||
const runId = `chatcmpl-${randomUUID()}`;
|
||||
const deps = createDefaultDeps();
|
||||
|
||||
if (!stream) {
|
||||
@@ -231,10 +235,27 @@ export async function handleOpenAiHttpRequest(
|
||||
|
||||
setSseHeaders(res);
|
||||
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
let wroteRole = false;
|
||||
let sawAssistantDelta = false;
|
||||
let closed = false;
|
||||
|
||||
/** Send a final chunk with finish_reason and then [DONE]. */
|
||||
function finishStream(finishReason: string = "stop") {
|
||||
if (res.writableEnded || res.destroyed) {
|
||||
return;
|
||||
}
|
||||
writeSse(res, {
|
||||
id: runId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
|
||||
});
|
||||
writeDone(res);
|
||||
res.end();
|
||||
}
|
||||
|
||||
const unsubscribe = onAgentEvent((evt) => {
|
||||
if (evt.runId !== runId) {
|
||||
return;
|
||||
@@ -254,7 +275,7 @@ export async function handleOpenAiHttpRequest(
|
||||
writeSse(res, {
|
||||
id: runId,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { role: "assistant" } }],
|
||||
});
|
||||
@@ -264,7 +285,7 @@ export async function handleOpenAiHttpRequest(
|
||||
writeSse(res, {
|
||||
id: runId,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
created,
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
@@ -282,8 +303,7 @@ export async function handleOpenAiHttpRequest(
|
||||
if (phase === "end" || phase === "error") {
|
||||
closed = true;
|
||||
unsubscribe();
|
||||
writeDone(res);
|
||||
res.end();
|
||||
finishStream(phase === "error" ? "stop" : "stop");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -319,7 +339,7 @@ export async function handleOpenAiHttpRequest(
|
||||
writeSse(res, {
|
||||
id: runId,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { role: "assistant" } }],
|
||||
});
|
||||
@@ -338,7 +358,7 @@ export async function handleOpenAiHttpRequest(
|
||||
writeSse(res, {
|
||||
id: runId,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
created,
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
@@ -357,7 +377,7 @@ export async function handleOpenAiHttpRequest(
|
||||
writeSse(res, {
|
||||
id: runId,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
created,
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
@@ -376,8 +396,7 @@ export async function handleOpenAiHttpRequest(
|
||||
if (!closed) {
|
||||
closed = true;
|
||||
unsubscribe();
|
||||
writeDone(res);
|
||||
res.end();
|
||||
finishStream();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -48,6 +48,7 @@ export function getAgentRunContext(runId: string) {
|
||||
|
||||
export function clearAgentRunContext(runId: string) {
|
||||
runContextById.delete(runId);
|
||||
seqByRun.delete(runId);
|
||||
}
|
||||
|
||||
export function resetAgentRunContextForTest() {
|
||||
|
||||
Reference in New Issue
Block a user