/** JSON fetch wrapper + SSE streaming helper. */ const BASE = '/api' export interface SessionSummary { session_id: string session_name: string created_at: string updated_at: string } export interface SessionData extends SessionSummary { agent_state: Record } export interface FileInfo { file_id: string filename: string content_type: string size: number } export interface AgentCompleteData { reason: string intent: string status: string jrxml_length: number error_msg: string natural_explanation: string consult_answer: string retry_count: number total_duration_ms: number ocr_extraction_result: any } export interface SSECallbacks { onNodeStart?: (data: { node: string; label: string; step_index: number }) => void onNodeComplete?: (data: { node: string; label: string; detail: string }) => void onStreamToken?: (data: { text: string; type: string }) => void onAgentComplete?: (data: AgentCompleteData) => void onAgentError?: (data: { error: string; traceback?: string }) => void } export const api = { // ── Health ── async health() { const r = await fetch(`${BASE}/health`) return r.json() }, async config() { const r = await fetch(`${BASE}/config`) return r.json() }, // ── Sessions ── async createSession(): Promise { const r = await fetch(`${BASE}/sessions`, { method: 'POST' }) return r.json() }, async listSessions(): Promise { const r = await fetch(`${BASE}/sessions`) const data = await r.json() return data.sessions }, async getSession(sessionId: string): Promise { const r = await fetch(`${BASE}/sessions/${sessionId}`) if (!r.ok) throw new Error('会话不存在') return r.json() }, async deleteSession(sessionId: string): Promise { await fetch(`${BASE}/sessions/${sessionId}`, { method: 'DELETE' }) }, // ── Upload ── async uploadFile(file: File, sessionId: string): Promise { const form = new FormData() form.append('file', file) const r = await fetch(`${BASE}/upload?session_id=${encodeURIComponent(sessionId)}`, { method: 'POST', body: form, }) if (!r.ok) throw new Error('上传失败') return r.json() }, // ── Chat (SSE) ── async chat( sessionId: string, text: string, fileIds: string[], callbacks: SSECallbacks, ): Promise { const r = await fetch(`${BASE}/sessions/${sessionId}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, file_ids: fileIds }), }) if (!r.ok) { const err = await r.json().catch(() => ({ detail: r.statusText })) throw new Error(err.detail || '请求失败') } const reader = r.body!.getReader() const decoder = new TextDecoder() let buffer = '' let currentEvent = '' while (true) { 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) { if (line.startsWith('event: ')) { currentEvent = line.slice(7).trim() } else if (line.startsWith('data: ')) { const payload = JSON.parse(line.slice(6)) switch (currentEvent) { case 'node_start': callbacks.onNodeStart?.(payload) break case 'node_complete': callbacks.onNodeComplete?.(payload) break case 'stream_token': callbacks.onStreamToken?.(payload) break case 'agent_complete': callbacks.onAgentComplete?.(payload) break case 'agent_error': callbacks.onAgentError?.(payload) break } } } } }, }