前端
用户输入发送
js
<button onClick={
sendMessage(input)
}>
send
</button>
sendMessage函数签名
js
//async await后段响应等待通信
const sendMessage = async (input) => {
//message是用户输入的文本woshuodeshi
//1. 确认session
const sessionId = currentSessionId
if(isNewSession || !sessionId){
tempSessionId = `${uuidv4()}`
const Session = {
... // session创建
temp:true
}
// 乐观更新 立刻给前端展示
setSessions(prev=>[tempSession, ...prev])
navgitate(`${tempSessionId}`)
setIsNewSession(true)
//backend creates new session with uuid from frontend
const realSession = await createSession(session)
setSessions(prev=> prev.filter(session => session.session_id !== tempSessionId))
setCurrentSessionId(realSession.session_id)
}
//make sure the session exists
//then add user message to the end of list
const newUserMessage = {..., input}
setMessages(prev => [...messages, newUserMessage]
// 2. add streaming message flag
// optmistic update
const streamId = uuid4()
const streamingMessage = {...}
setMessages(prev => [...messages, streamingMessage]
setStreamingMessageId(streamId)
setStreamingChunks([])
//3. streaming请求
const controller = await sendStreamingMessage(
userMessage,
(chunk) => {
//onChunk回调
const updatedstage = ['md','text','header', 'finish', ...].includes(chunk.type)
// 自己加状态区分要输出啥
if(updatedstage){
// 批量更新
setPendingMessageUpdate({streamId, chunk})
}
setMessages(prev =>
prev.map(msg =>
msg.message_id === streamId
? {...msg, streamingChunks:[...(streamingChunks || []), chunk}] }
: msg
))
},
// onComplete - 结束stream
() => {
setStreamingMessageId(null)
setStreamingController(null)
setStreamingChunks([])
},
// error hanlder
(error) => {
...
}
)
setStreamingController(controller)
return
}
fetch代码
js
export const sendStreamingMessage = async(message, onChunk, onComplete, onError, cancelSignal) => {
try{
const controller = new AbortController()
//1. 判断是否需要 中断sse
if(cancelSignal){
cancelSignal.addEventListener('abort',
() => {
if(controller){
controller.abort()
}
}
)
}
// 2. request
const requestBody = {...}
const header = {...}
const response = await fetch(`url`, {
method: 'post',
header:header,
body:requestBody,
signal:controller.signal // key variable, bind with signal of controller
})
//3. read streaming and check if it is cancelled
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let chunkId = 0
while(true){
//输出前看是否取消
if(controller.signal.aborted){
if(reader){
await reader.cancel('user cancel')
}
}
throw new DOMException('cancelled by user', 'AbortError')
const{done, value} = value reader.read() // 读取body的ReadableStream类
if(done){
if(onComplete) {onComplete()}
break
}
buffer += decoder.decode(value, {stream:true})
const lines = buffer.split('\n')
buffer = lines.pop()
for(const line of lines){
//处理data逻辑略过
chunk.id = chunkId++
if(controller.signal.abort){throw)
if(onChunk){
onChunk(chunk)
}
if(chunk.type === 'error' ){
if(onError){onError()}
return controller
}
if(chunk.type === 'cancelled' ){
if(onError){onError()}
return controller
}
if(chunk.type === 'ok' ){
break
}
}
}
} //忽略catch
finally{
if(reader) await reader.cancel()
if(controller) controller.abort();
return controller
}
}
前端取消sse的话
js
const stop = () => {
if(streamController && !steamController.signal.aborted){
streamController.abort()
}
}
后端
py
@app.post("/api/streaming")
async def streaming(request:Request):
async gen_sse_stream():
try:
service = Service()
async for chunk in service.stream(request.start):
chunkData = chunk.model_dump()
if chunkData.type == 'done':
# save to database
yield f'data: {json.dumps(chunkData}\n\n'
except Exception as e:
yiele f'data: {json.dumps({"type": "error", })}'
return StreamingResponse(
gen_sse_stream(),
media_type='text/plain',
header={'Connection':'keep-alive'}
)
Service
py
class Service:
async def stream(self, start:int) -> AsyncGenerator[StreamChunk,None]:
processor = CountProecessor()
async for chunk in processor.count(start):
yiled chunk
class CountProecessor:
def __init__(self):
self.chunk_id = 0
def _create_chunk(self, chunk_type:str, content: str = '', data:dict=None):
self.chunk_id += 1
return StreamChunk(
type = chunk_type,
content = content,
data = data or {},
chunk_id = chunk_id
timestamp = ...
)
Chunk
py
class StreamChunk():
type:str,
content:str,
data:dict={},
chunk_id:int,
timestamp:str