chat.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import api from './index'
  2. const API_BASE = import.meta.env.VITE_API_BASE || ''
  3. export interface ChatMessage {
  4. role: 'user' | 'assistant'
  5. content: string
  6. }
  7. export interface ChatResponse {
  8. content: string
  9. session_id: string | null
  10. }
  11. export interface StreamEvent {
  12. type: 'session' | 'step_start' | 'chunk' | 'tool_start' | 'tool_finish' | 'step_finish' | 'done' | 'error'
  13. content?: string
  14. tool?: string
  15. args?: Record<string, unknown>
  16. result?: string
  17. error?: string
  18. session_id?: string | null
  19. step?: number
  20. max_steps?: number
  21. }
  22. export type StreamCallback = (event: StreamEvent) => void
  23. export const chatApi = {
  24. // 流式发送消息 (SSE)
  25. sendMessage: async (message: string, sessionId?: string) => {
  26. return api.post('/chat/send', { message, session_id: sessionId })
  27. },
  28. // 同步发送消息(支持取消,超时时间 5 分钟)
  29. sendMessageSync: async (
  30. message: string,
  31. sessionId?: string,
  32. signal?: AbortSignal
  33. ): Promise<ChatResponse> => {
  34. return api.post('/chat/send/sync', { message, session_id: sessionId }, {
  35. signal,
  36. timeout: 300000, // 5 分钟超时
  37. })
  38. },
  39. // 流式发送消息 (SSE) - 返回完整响应
  40. sendMessageStream: async (
  41. message: string,
  42. sessionId: string | null | undefined,
  43. onChunk: StreamCallback,
  44. signal?: AbortSignal
  45. ): Promise<ChatResponse> => {
  46. const response = await fetch(`${API_BASE}/api/chat/send/stream`, {
  47. method: 'POST',
  48. headers: {
  49. 'Content-Type': 'application/json',
  50. },
  51. body: JSON.stringify({ message, session_id: sessionId }),
  52. signal,
  53. })
  54. if (!response.ok) {
  55. throw new Error(`HTTP error! status: ${response.status}`)
  56. }
  57. const reader = response.body?.getReader()
  58. if (!reader) {
  59. throw new Error('No response body')
  60. }
  61. const decoder = new TextDecoder()
  62. let buffer = ''
  63. let fullContent = ''
  64. let finalSessionId = sessionId
  65. try {
  66. while (true) {
  67. const { done, value } = await reader.read()
  68. if (done) break
  69. buffer += decoder.decode(value, { stream: true })
  70. // 解析 SSE 事件
  71. const lines = buffer.split('\n')
  72. buffer = lines.pop() || ''
  73. let currentEvent = ''
  74. for (const line of lines) {
  75. // 跳过 ping 事件和空行
  76. if (line.startsWith('ping') || line.trim() === '') {
  77. continue
  78. }
  79. if (line.startsWith('event:')) {
  80. currentEvent = line.substring(6).trim()
  81. } else if (line.startsWith('data:')) {
  82. const data = line.substring(5).trim()
  83. if (data && currentEvent) {
  84. try {
  85. const parsed = JSON.parse(data)
  86. if (currentEvent === 'session') {
  87. finalSessionId = parsed.session_id
  88. onChunk({ type: 'session', session_id: parsed.session_id })
  89. } else if (currentEvent === 'step_start') {
  90. onChunk({ type: 'step_start', step: parsed.step, max_steps: parsed.max_steps })
  91. } else if (currentEvent === 'chunk') {
  92. fullContent += parsed.content || ''
  93. onChunk({ type: 'chunk', content: parsed.content })
  94. } else if (currentEvent === 'tool_start') {
  95. onChunk({ type: 'tool_start', tool: parsed.tool, args: parsed.args })
  96. } else if (currentEvent === 'tool_finish') {
  97. onChunk({ type: 'tool_finish', tool: parsed.tool, result: parsed.result })
  98. } else if (currentEvent === 'step_finish') {
  99. onChunk({ type: 'step_finish', step: parsed.step })
  100. } else if (currentEvent === 'done') {
  101. finalSessionId = parsed.session_id
  102. onChunk({ type: 'done', content: parsed.content, session_id: parsed.session_id })
  103. } else if (currentEvent === 'error') {
  104. onChunk({ type: 'error', error: parsed.error })
  105. }
  106. } catch {
  107. // 忽略解析错误
  108. }
  109. currentEvent = ''
  110. }
  111. }
  112. }
  113. }
  114. } finally {
  115. reader.releaseLock()
  116. }
  117. return { content: fullContent, session_id: finalSessionId ?? null }
  118. },
  119. }