@ -12,6 +12,7 @@ import { getModelWithProviderById, getModelWithProviderByIdAny } from "#server/s
import { generateSessionTitle } from "#server/service/agent/title" ;
import { type StoredPart } from "./types" ;
import { createStreamBuffer , appendChunk , markBufferDone , removeStreamBuffer } from "#server/service/agent/stream-buffer" ;
import { registerAbortController , unregisterAbortController } from "./stop.post" ;
import log4js from "logger" ;
const logger = log4js . getLogger ( "APP" ) ;
@ -414,6 +415,24 @@ export default defineEventHandler(async (event) => {
userMessageId : userMessage?.id ? ? null ,
} ) ;
const serverAbortController = new AbortController ( ) ;
registerAbortController ( sessionId , serverAbortController ) ;
let abortedPartialContent = "" ;
let abortedPartialParts : Array < Record < string , unknown > > = [ ] ;
let streamedText = "" ;
let streamedReasoning = "" ;
let streamedToolCalls : Array < Record < string , unknown > > = [ ] ;
let chunkPartId = 0 ;
event . node . req . on ( "close" , ( ) = > {
if ( ! serverAbortController . signal . aborted ) {
logger . info ( "[%s] [AGENT-CHAT] client disconnected, aborting streamText" , event . context . requestId ? ? "-" ) ;
serverAbortController . abort ( ) ;
}
} ) ;
let approvalPrefixChunk : Uint8Array | null = null ;
if ( isApprovalContinue && Object . keys ( approvalToolResults ) . length > 0 ) {
const prefixChunks = Object . entries ( approvalToolResults ) . map (
@ -433,6 +452,7 @@ export default defineEventHandler(async (event) => {
model : languageModel ,
system : systemPrompt || undefined ,
messages : modelMessages ,
abortSignal : serverAbortController.signal ,
maxOutputTokens : model.maxTokens && model . maxTokens >= 1 && model . maxTokens <= 393216 ? model.maxTokens : undefined ,
. . . ( Object . keys ( effectiveTools ) . length > 0
? { tools : effectiveTools , stopWhen : stepCountIs ( 8 ) }
@ -450,7 +470,34 @@ export default defineEventHandler(async (event) => {
: String ( errorData ? . error ? ? "未知错误" ) ;
logger . error ( "[%s] [AGENT-CHAT] streamText error: %s" , event . context . requestId ? ? "-" , errMsg ) ;
} ,
onChunk : ( { chunk } ) = > {
if ( chunk . type === "text-delta" ) {
streamedText += chunk . text ;
} else if ( chunk . type === "reasoning-delta" ) {
streamedReasoning += chunk . text ;
} else if ( chunk . type === "tool-call" ) {
streamedToolCalls . push ( {
id : ` p_ ${ chunkPartId ++ } ` ,
type : "tool-call" ,
toolCallId : chunk.toolCallId ,
toolName : chunk.toolName ,
args : chunk.input ,
state : "call" ,
} ) ;
} else if ( chunk . type === "tool-result" ) {
const tcIdx = streamedToolCalls . findIndex ( ( tc ) = > tc . toolCallId === chunk . toolCallId ) ;
if ( tcIdx >= 0 ) {
streamedToolCalls [ tcIdx ] . result = chunk . output ;
streamedToolCalls [ tcIdx ] . state = "result" ;
}
}
} ,
onFinish : async ( { finishReason , usage , steps , text : assistantText } ) = > {
if ( serverAbortController . signal . aborted ) {
logger . info ( "[%s] [AGENT-CHAT] onFinish skipped due to abort" , event . context . requestId ? ? "-" ) ;
unregisterAbortController ( sessionId ) ;
return ;
}
logger . info (
"[%s] [AGENT-CHAT] finished: reason=%s steps=%d inputTokens=%d outputTokens=%d" ,
event . context . requestId ? ? "-" ,
@ -578,6 +625,96 @@ export default defineEventHandler(async (event) => {
if ( shouldGenerateTitle && titleModelId && assistantText . trim ( ) . length > 0 ) {
generateSessionTitle ( sessionId , content . trim ( ) , assistantText , titleModelId , user ? . id ? ? null ) . catch ( ( e ) = > { logger . error ( "[AGENT-CHAT] title gen failed: %s" , e ? . message ? ? e ) ; } ) ;
}
unregisterAbortController ( sessionId ) ;
} ,
onAbort : async ( ) = > {
try {
const partialText = streamedText ;
logger . info ( "[%s] [AGENT-CHAT] aborted: streamedTextLen=%d streamedToolCalls=%d" , event . context . requestId ? ? "-" , partialText . length , streamedToolCalls . length ) ;
const parts : Array < Record < string , unknown > > = [ ] ;
if ( streamedReasoning . trim ( ) ) {
parts . push ( { id : ` p_ ${ parts . length } ` , type : "reasoning" , text : streamedReasoning } ) ;
}
if ( partialText . trim ( ) ) {
parts . push ( { id : ` p_ ${ parts . length } ` , type : "text" , text : partialText } ) ;
}
for ( const tc of streamedToolCalls ) {
parts . push ( {
id : ` p_ ${ parts . length } ` ,
type : "tool-call" ,
toolCallId : tc.toolCallId ,
toolName : tc.toolName ,
args : tc.args ,
result : tc.result ,
state : tc.result !== undefined ? "result" : "call" ,
} ) ;
}
abortedPartialContent = partialText ;
abortedPartialParts = parts ;
if ( isApprovalContinue ) {
const lastAssistant = historyMessages . filter ( ( m ) = > m . role === "assistant" ) . pop ( ) ;
if ( lastAssistant ) {
let existingParts : Array < Record < string , unknown > > = [ ] ;
try {
existingParts = lastAssistant . parts ? JSON . parse ( lastAssistant . parts ) : [ ] ;
} catch {
existingParts = [ ] ;
}
for ( const p of existingParts ) {
if ( p . type === "tool-call" && p . state === "approval-responded" ) {
if ( p . approved === true && approvalToolResults [ p . toolCallId ] !== undefined ) {
p . result = approvalToolResults [ p . toolCallId ] ;
p . state = "result" ;
} else if ( p . approved === false ) {
p . result = "工具执行被拒绝" ;
p . state = "result" ;
}
}
}
const newParts = parts . filter (
( np ) = > np . type !== "tool-call" || ! existingParts . some ( ( ep ) = > ep . toolCallId === np . toolCallId ) ,
) ;
const idOffset = existingParts . length ;
for ( let i = 0 ; i < newParts . length ; i ++ ) {
newParts [ i ] . id = ` p_ ${ idOffset + i } ` ;
}
const mergedParts = [ . . . existingParts , . . . newParts ] ;
const mergedContent = ( lastAssistant . content || "" ) + partialText ;
await updateMessageContent ( lastAssistant . id , mergedContent ) ;
await updateMessageParts ( lastAssistant . id , JSON . stringify ( mergedParts ) ) ;
}
} else {
const hasContent = partialText . trim ( ) . length > 0 || parts . length > 0 ;
if ( hasContent ) {
const assistantSortOrder = ( await getMaxSortOrder ( sessionId ) ) + 1 ;
await saveMessage ( {
sessionId ,
role : "assistant" ,
content : partialText ,
parts : parts.length > 0 ? JSON . stringify ( parts ) : null ,
modelId : model.id ,
sortOrder : assistantSortOrder ,
} ) ;
}
}
await touchSession ( sessionId ) ;
if ( ! user ) {
incrementRateLimit ( sessionId , ip ) ;
}
markBufferDone ( sessionId ) ;
unregisterAbortController ( sessionId ) ;
logger . info ( "[%s] [AGENT-CHAT] abort save complete: contentLen=%d partsCount=%d" , event . context . requestId ? ? "-" , partialText . length , parts . length ) ;
} catch ( abortErr ) {
logger . error ( "[%s] [AGENT-CHAT] onAbort error: %s" , event . context . requestId ? ? "-" , abortErr instanceof Error ? abortErr.message : String ( abortErr ) ) ;
markBufferDone ( sessionId ) ;
unregisterAbortController ( sessionId ) ;
}
} ,
} ) ;
@ -594,22 +731,37 @@ export default defineEventHandler(async (event) => {
return new ReadableStream ( {
start ( controller ) {
const reader = originalBody . getReader ( ) ;
let clientDisconnected = false ;
function pump() {
reader . read ( ) . then ( ( { done , value } ) = > {
if ( done ) {
markBufferDone ( sessionId ) ;
controller . close ( ) ;
if ( ! clientDisconnected ) {
try { controller . close ( ) ; } catch { /* already closed */ }
}
return ;
}
if ( value ) {
appendChunk ( sessionId , value ) ;
controller . enqueue ( value ) ;
if ( ! clientDisconnected ) {
try {
controller . enqueue ( value ) ;
} catch {
clientDisconnected = true ;
}
}
}
pump ( ) ;
} ) . catch ( ( err ) = > {
logger . error ( "[%s] [AGENT-CHAT] buffer stream read error: %s" , event . context . requestId ? ? "-" , err instanceof Error ? err.message : String ( err ) ) ;
if ( serverAbortController . signal . aborted ) {
logger . info ( "[%s] [AGENT-CHAT] stream reader aborted by server abort" , event . context . requestId ? ? "-" ) ;
} else {
logger . error ( "[%s] [AGENT-CHAT] buffer stream read error: %s" , event . context . requestId ? ? "-" , err instanceof Error ? err.message : String ( err ) ) ;
}
markBufferDone ( sessionId ) ;
controller . close ( ) ;
if ( ! clientDisconnected ) {
try { controller . close ( ) ; } catch { /* already closed */ }
}
} ) ;
}
pump ( ) ;
@ -626,13 +778,20 @@ export default defineEventHandler(async (event) => {
async start ( controller ) {
controller . enqueue ( prefix ) ;
const reader = originalBody . getReader ( ) ;
let clientDisconnected = false ;
try {
while ( true ) {
const { done , value } = await reader . read ( ) ;
if ( done ) break ;
if ( value ) {
appendChunk ( sessionId , value ) ;
controller . enqueue ( value ) ;
if ( ! clientDisconnected ) {
try {
controller . enqueue ( value ) ;
} catch {
clientDisconnected = true ;
}
}
}
}
} catch ( err ) {
@ -641,10 +800,14 @@ export default defineEventHandler(async (event) => {
` data: ${ JSON . stringify ( { type : "error" , errorText : "流式响应中断" } )} \ n \ n ` ,
) ;
appendChunk ( sessionId , errorChunk ) ;
controller . enqueue ( errorChunk ) ;
if ( ! clientDisconnected ) {
try { controller . enqueue ( errorChunk ) ; } catch { /* client gone */ }
}
} finally {
markBufferDone ( sessionId ) ;
controller . close ( ) ;
if ( ! clientDisconnected ) {
try { controller . close ( ) ; } catch { /* already closed */ }
}
}
} ,
} ) ;