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:
2026-05-21 20:04:27 +08:00
parent 2befd44430
commit 74f3f03d2c
29 changed files with 3668 additions and 72 deletions
+145
View File
@@ -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
}
}
}
}
},
}