5 changed files with 321 additions and 1 deletions
Binary file not shown.
@ -0,0 +1,89 @@ |
|||
import { defineEventHandler, getQuery } from "h3"; |
|||
import { R } from "#server/utils/response"; |
|||
import { getCurrentUser } from "#server/utils/context"; |
|||
import { getTempTokenFromCookie } from "#server/service/agent/temp-token"; |
|||
import { getSessionByIdAndUser } from "#server/service/agent/session"; |
|||
import { getStreamBuffer, subscribeToBuffer } from "#server/service/agent/stream-buffer"; |
|||
import log4js from "logger"; |
|||
|
|||
const logger = log4js.getLogger("APP"); |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const user = await getCurrentUser(event); |
|||
const tempToken = getTempTokenFromCookie(event); |
|||
|
|||
if (!user && !tempToken) { |
|||
throw createError({ statusCode: 401, statusMessage: "请先登录或创建临时会话" }); |
|||
} |
|||
|
|||
const query = getQuery(event); |
|||
const sessionId = query.sessionId as string | undefined; |
|||
|
|||
if (!sessionId) { |
|||
throw createError({ statusCode: 400, statusMessage: "缺少 sessionId 参数" }); |
|||
} |
|||
|
|||
const session = await getSessionByIdAndUser(sessionId, user?.id ?? null, tempToken); |
|||
if (!session) { |
|||
throw createError({ statusCode: 404, statusMessage: "会话不存在" }); |
|||
} |
|||
|
|||
const buf = getStreamBuffer(sessionId); |
|||
|
|||
if (!buf) { |
|||
return R.success({ active: false, reason: "no-buffer" }); |
|||
} |
|||
|
|||
if (buf.done) { |
|||
return R.success({ active: false, reason: "already-done", modelId: buf.modelId }); |
|||
} |
|||
|
|||
const stream = new ReadableStream<Uint8Array>({ |
|||
start(controller) { |
|||
for (const chunk of buf.chunks) { |
|||
controller.enqueue(chunk); |
|||
} |
|||
|
|||
if (buf.done) { |
|||
controller.close(); |
|||
return; |
|||
} |
|||
|
|||
const unsubscribe = subscribeToBuffer( |
|||
sessionId, |
|||
(chunk) => { |
|||
try { |
|||
controller.enqueue(chunk); |
|||
} catch (e) { |
|||
logger.error("[STREAM-RESUME] enqueue error: %s", e instanceof Error ? e.message : String(e)); |
|||
} |
|||
}, |
|||
() => { |
|||
try { |
|||
controller.close(); |
|||
} catch { |
|||
} |
|||
}, |
|||
); |
|||
|
|||
event.node.req.on("close", () => { |
|||
unsubscribe(); |
|||
try { |
|||
controller.close(); |
|||
} catch { |
|||
} |
|||
}); |
|||
}, |
|||
}); |
|||
|
|||
return new Response(stream, { |
|||
headers: { |
|||
"Content-Type": "text/event-stream; charset=utf-8", |
|||
"Cache-Control": "no-cache", |
|||
"Connection": "keep-alive", |
|||
"X-Stream-Resume": "true", |
|||
"X-Stream-Model-Id": String(buf.modelId ?? ""), |
|||
"X-Stream-User-Message-Id": buf.userMessageId ?? "", |
|||
}, |
|||
}); |
|||
}); |
|||
@ -0,0 +1,129 @@ |
|||
import log4js from "logger"; |
|||
|
|||
const logger = log4js.getLogger("APP"); |
|||
|
|||
const BUFFER_TTL_MS = 5 * 60 * 1000; |
|||
const CLEANUP_INTERVAL_MS = 60 * 1000; |
|||
|
|||
export interface StreamBuffer { |
|||
chunks: Uint8Array[]; |
|||
done: boolean; |
|||
createdAt: number; |
|||
doneAt: number | null; |
|||
modelId: number | null; |
|||
userMessageId: string | null; |
|||
subscribers: Array<(chunk: Uint8Array) => void>; |
|||
doneSubscribers: Array<() => void>; |
|||
} |
|||
|
|||
const buffers = new Map<string, StreamBuffer>(); |
|||
|
|||
let cleanupTimer: ReturnType<typeof setInterval> | null = null; |
|||
|
|||
function ensureCleanupTimer() { |
|||
if (cleanupTimer) return; |
|||
cleanupTimer = setInterval(() => { |
|||
const now = Date.now(); |
|||
for (const [sid, buf] of buffers) { |
|||
const expireAt = buf.doneAt ?? buf.createdAt + BUFFER_TTL_MS; |
|||
if (now > expireAt) { |
|||
buffers.delete(sid); |
|||
logger.info("[STREAM-BUFFER] expired and removed sessionId=%s", sid); |
|||
} |
|||
} |
|||
if (buffers.size === 0 && cleanupTimer) { |
|||
clearInterval(cleanupTimer); |
|||
cleanupTimer = null; |
|||
} |
|||
}, CLEANUP_INTERVAL_MS); |
|||
cleanupTimer.unref?.(); |
|||
} |
|||
|
|||
export function createStreamBuffer( |
|||
sessionId: string, |
|||
meta: { modelId?: number | null; userMessageId?: string | null }, |
|||
): StreamBuffer { |
|||
const buf: StreamBuffer = { |
|||
chunks: [], |
|||
done: false, |
|||
createdAt: Date.now(), |
|||
doneAt: null, |
|||
modelId: meta.modelId ?? null, |
|||
userMessageId: meta.userMessageId ?? null, |
|||
subscribers: [], |
|||
doneSubscribers: [], |
|||
}; |
|||
buffers.set(sessionId, buf); |
|||
ensureCleanupTimer(); |
|||
logger.info("[STREAM-BUFFER] created sessionId=%s", sessionId); |
|||
return buf; |
|||
} |
|||
|
|||
export function appendChunk(sessionId: string, chunk: Uint8Array): void { |
|||
const buf = buffers.get(sessionId); |
|||
if (!buf || buf.done) return; |
|||
buf.chunks.push(chunk); |
|||
for (const sub of buf.subscribers) { |
|||
try { |
|||
sub(chunk); |
|||
} catch (e) { |
|||
logger.error("[STREAM-BUFFER] subscriber error: %s", e instanceof Error ? e.message : String(e)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export function markBufferDone(sessionId: string): void { |
|||
const buf = buffers.get(sessionId); |
|||
if (!buf) return; |
|||
buf.done = true; |
|||
buf.doneAt = Date.now(); |
|||
const doneSubs = buf.doneSubscribers; |
|||
buf.doneSubscribers = []; |
|||
buf.subscribers = []; |
|||
for (const sub of doneSubs) { |
|||
try { |
|||
sub(); |
|||
} catch (e) { |
|||
logger.error("[STREAM-BUFFER] done subscriber error: %s", e instanceof Error ? e.message : String(e)); |
|||
} |
|||
} |
|||
logger.info("[STREAM-BUFFER] done sessionId=%s chunks=%d", sessionId, buf.chunks.length); |
|||
} |
|||
|
|||
export function getStreamBuffer(sessionId: string): StreamBuffer | undefined { |
|||
return buffers.get(sessionId); |
|||
} |
|||
|
|||
export function removeStreamBuffer(sessionId: string): void { |
|||
buffers.delete(sessionId); |
|||
} |
|||
|
|||
export function hasActiveStream(sessionId: string): boolean { |
|||
const buf = buffers.get(sessionId); |
|||
return !!buf && !buf.done; |
|||
} |
|||
|
|||
export function subscribeToBuffer( |
|||
sessionId: string, |
|||
onChunk: (chunk: Uint8Array) => void, |
|||
onDone: () => void, |
|||
): () => void { |
|||
const buf = buffers.get(sessionId); |
|||
if (!buf) { |
|||
onDone(); |
|||
return () => {}; |
|||
} |
|||
|
|||
if (buf.done) { |
|||
onDone(); |
|||
return () => {}; |
|||
} |
|||
|
|||
buf.subscribers.push(onChunk); |
|||
buf.doneSubscribers.push(onDone); |
|||
|
|||
return () => { |
|||
buf.subscribers = buf.subscribers.filter((s) => s !== onChunk); |
|||
buf.doneSubscribers = buf.doneSubscribers.filter((s) => s !== onDone); |
|||
}; |
|||
} |
|||
Loading…
Reference in new issue