229 lines
7.0 KiB
JavaScript
229 lines
7.0 KiB
JavaScript
import { message } from 'ant-design-vue'
|
|
import { handleChatError } from '@/utils/errorHandler'
|
|
import { unref } from 'vue'
|
|
|
|
/**
|
|
* Process a streaming response from the server
|
|
* @param {Response} response - The fetch response object
|
|
* @param {Function} onChunk - Callback function for each parsed JSON chunk. Return true to stop processing.
|
|
*/
|
|
const processStreamResponse = async (response, onChunk) => {
|
|
if (!response || !response.body) {
|
|
console.warn('Invalid response or missing body for stream processing')
|
|
return
|
|
}
|
|
|
|
const reader = response.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let buffer = ''
|
|
let stopProcessing = false
|
|
|
|
try {
|
|
while (!stopProcessing) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
const lines = buffer.split('\n')
|
|
buffer = lines.pop() || ''
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim()
|
|
if (trimmedLine) {
|
|
try {
|
|
const chunk = JSON.parse(trimmedLine)
|
|
if (onChunk && onChunk(chunk)) {
|
|
stopProcessing = true
|
|
break
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to parse stream chunk JSON:', e, 'Line:', trimmedLine)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!stopProcessing && buffer.trim()) {
|
|
try {
|
|
const chunk = JSON.parse(buffer.trim())
|
|
if (onChunk) {
|
|
onChunk(chunk)
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to parse final stream chunk JSON:', e)
|
|
}
|
|
}
|
|
} finally {
|
|
try {
|
|
reader.releaseLock()
|
|
} catch {
|
|
// Ignore errors on releasing lock
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useAgentStreamHandler({
|
|
getThreadState,
|
|
processApprovalInStream,
|
|
currentAgentId,
|
|
supportsTodo,
|
|
supportsFiles
|
|
}) {
|
|
const debugPrefix = '[AgentStateDebug]'
|
|
/**
|
|
* Process a single stream chunk based on its status
|
|
* @param {Object} chunk - The parsed JSON chunk
|
|
* @param {String} threadId - The current thread ID
|
|
* @returns {Boolean} - Returns true if processing should stop (e.g. error, finished, interrupted)
|
|
*/
|
|
const handleStreamChunk = (chunk, threadId) => {
|
|
const { status, msg, request_id, message: chunkMessage } = chunk
|
|
const threadState = getThreadState(threadId)
|
|
|
|
if (!threadState) return false
|
|
|
|
switch (status) {
|
|
case 'init':
|
|
threadState.onGoingConv.msgChunks[request_id] = [msg]
|
|
return false
|
|
|
|
case 'loading':
|
|
if (msg.id) {
|
|
if (!threadState.onGoingConv.msgChunks[msg.id]) {
|
|
threadState.onGoingConv.msgChunks[msg.id] = []
|
|
}
|
|
threadState.onGoingConv.msgChunks[msg.id].push(msg)
|
|
}
|
|
return false
|
|
|
|
case 'error':
|
|
handleChatError({ message: chunkMessage }, 'stream')
|
|
// Stop the loading indicator
|
|
if (threadState) {
|
|
threadState.isStreaming = false
|
|
|
|
// Abort the stream controller to stop processing further events
|
|
if (threadState.streamAbortController) {
|
|
threadState.streamAbortController.abort()
|
|
threadState.streamAbortController = null
|
|
}
|
|
}
|
|
return true
|
|
|
|
case 'human_approval_required':
|
|
console.log(`${debugPrefix}[approval_required]`, {
|
|
threadId,
|
|
currentAgentId: unref(currentAgentId)
|
|
})
|
|
// 使用审批 composable 处理审批请求
|
|
return processApprovalInStream(chunk, threadId, unref(currentAgentId))
|
|
|
|
case 'agent_state':
|
|
console.log(`${debugPrefix}[agent_state_chunk]`, {
|
|
threadId,
|
|
supportsTodo: unref(supportsTodo),
|
|
supportsFiles: unref(supportsFiles),
|
|
currentAgentId: unref(currentAgentId),
|
|
hasAgentState: !!chunk.agent_state,
|
|
todoCount: Array.isArray(chunk.agent_state?.todos) ? chunk.agent_state.todos.length : 0,
|
|
fileKeys: chunk.agent_state?.files ? Object.keys(chunk.agent_state.files) : []
|
|
})
|
|
if (chunk.agent_state) {
|
|
console.log(`${debugPrefix}[agent_state_apply]`, {
|
|
threadId,
|
|
todos: chunk.agent_state?.todos || [],
|
|
files: chunk.agent_state?.files || []
|
|
})
|
|
threadState.agentState = chunk.agent_state
|
|
} else {
|
|
console.warn(`${debugPrefix}[agent_state_skip]`, {
|
|
reason: 'empty_state',
|
|
supportsTodo: unref(supportsTodo),
|
|
supportsFiles: unref(supportsFiles),
|
|
hasAgentState: !!chunk.agent_state,
|
|
currentAgentId: unref(currentAgentId),
|
|
threadId
|
|
})
|
|
}
|
|
return false
|
|
|
|
case 'finished':
|
|
// 先标记流式结束,但保持消息显示直到历史记录加载完成
|
|
if (threadState) {
|
|
threadState.isStreaming = false
|
|
console.log(`${debugPrefix}[finished]`, {
|
|
threadId,
|
|
currentAgentId: unref(currentAgentId),
|
|
hasThreadAgentState: !!threadState.agentState,
|
|
supportsTodo: unref(supportsTodo),
|
|
supportsFiles: unref(supportsFiles)
|
|
})
|
|
if ((unref(supportsTodo) || unref(supportsFiles)) && threadState.agentState) {
|
|
console.log(
|
|
`[AgentState|Final] ${new Date().toLocaleTimeString()}.${new Date().getMilliseconds()}`,
|
|
{
|
|
threadId,
|
|
todos: threadState.agentState?.todos || [],
|
|
files: threadState.agentState?.files || []
|
|
}
|
|
)
|
|
}
|
|
}
|
|
return true
|
|
|
|
case 'interrupted':
|
|
// 中断状态,刷新消息历史
|
|
console.warn(`${debugPrefix}[interrupted]`, {
|
|
threadId,
|
|
message: chunkMessage,
|
|
currentAgentId: unref(currentAgentId)
|
|
})
|
|
if (threadState) {
|
|
threadState.isStreaming = false
|
|
}
|
|
// 如果有 message 字段,显示提示(例如:敏感内容检测)
|
|
if (chunkMessage) {
|
|
message.info(chunkMessage)
|
|
}
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Process the full agent stream response
|
|
* @param {Response} response - The fetch response
|
|
* @param {String} threadId - The thread ID
|
|
* @param {Function} [onChunk] - Optional callback for each chunk (e.g. for logging)
|
|
*/
|
|
const handleAgentResponse = async (response, threadId, onChunk = null) => {
|
|
console.log(`${debugPrefix}[stream_start]`, {
|
|
threadId,
|
|
currentAgentId: unref(currentAgentId),
|
|
supportsTodo: unref(supportsTodo),
|
|
supportsFiles: unref(supportsFiles)
|
|
})
|
|
await processStreamResponse(response, (chunk) => {
|
|
if (chunk?.status && chunk.status !== 'loading') {
|
|
console.log(`${debugPrefix}[chunk_status]`, {
|
|
threadId,
|
|
status: chunk.status,
|
|
requestId: chunk.request_id
|
|
})
|
|
}
|
|
if (onChunk) onChunk(chunk)
|
|
return handleStreamChunk(chunk, threadId)
|
|
})
|
|
console.log(`${debugPrefix}[stream_end]`, {
|
|
threadId,
|
|
currentAgentId: unref(currentAgentId)
|
|
})
|
|
}
|
|
|
|
return {
|
|
handleStreamChunk,
|
|
handleAgentResponse
|
|
}
|
|
}
|