feat: 前后端分离架构 — FastAPI SSE后端 + Vue 3前端
将单体 Streamlit 应用拆分为三层架构: - api_server.py: FastAPI SSE 流式后端 (端口 8000) - frontend/: Vue 3 + Vite + Pinia 聊天前端 (端口 5173) - agent/graph.py: 新增 node_start 回调支持 - 更新启动脚本为三服务模式 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/** 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<string, any>
|
||||
}
|
||||
|
||||
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
|
||||
retry_count: number
|
||||
ocr_extraction_result: any
|
||||
}
|
||||
|
||||
export interface SSECallbacks {
|
||||
onNodeStart?: (data: { node: string; label: string }) => 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<SessionSummary> {
|
||||
const r = await fetch(`${BASE}/sessions`, { method: 'POST' })
|
||||
return r.json()
|
||||
},
|
||||
|
||||
async listSessions(): Promise<SessionSummary[]> {
|
||||
const r = await fetch(`${BASE}/sessions`)
|
||||
const data = await r.json()
|
||||
return data.sessions
|
||||
},
|
||||
|
||||
async getSession(sessionId: string): Promise<SessionData> {
|
||||
const r = await fetch(`${BASE}/sessions/${sessionId}`)
|
||||
if (!r.ok) throw new Error('会话不存在')
|
||||
return r.json()
|
||||
},
|
||||
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
await fetch(`${BASE}/sessions/${sessionId}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
// ── Upload ──
|
||||
async uploadFile(file: File, sessionId: string): Promise<FileInfo> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user