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
+173
View File
@@ -0,0 +1,173 @@
<script setup lang="ts">
import { watch, nextTick, ref } from 'vue'
import { useChatStore } from './stores/chat'
import { useSessionStore } from './stores/session'
import { api } from './api/client'
import Sidebar from './components/Sidebar.vue'
import ChatMessages from './components/ChatMessages.vue'
import StreamingMessage from './components/StreamingMessage.vue'
import NodeProgress from './components/NodeProgress.vue'
import SummaryCard from './components/SummaryCard.vue'
import UnifiedInput from './components/UnifiedInput.vue'
const chat = useChatStore()
const session = useSessionStore()
const chatContainer = ref<HTMLElement | null>(null)
async function scrollToBottom() {
await nextTick()
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}
}
watch(
() => [chat.messages.length, chat.streamText],
() => scrollToBottom(),
{ flush: 'post' }
)
async function handleSend(text: string, files: File[]) {
if (!session.currentId) {
const sid = await session.createSession()
await session.switchSession(sid)
}
// Upload files first
const remoteIds: string[] = []
for (const f of files) {
try {
const info = await api.uploadFile(f, session.currentId)
remoteIds.push(info.file_id)
} catch (e) {
console.error('文件上传失败:', e)
chat.setError('文件上传失败')
return
}
}
chat.addMessage({ role: 'user', content: text || '[附加文件]' })
scrollToBottom()
chat.startStreaming()
try {
await api.chat(session.currentId, text, remoteIds, {
onNodeStart(data) {
chat.addNode(data)
},
onNodeComplete(data) {
chat.completeNode(data)
},
onStreamToken(data) {
chat.appendStreamToken(data.text)
scrollToBottom()
},
onAgentComplete(data) {
chat.finishStreaming({
intent: data.intent,
status: data.status,
jrxml_length: data.jrxml_length,
error_msg: data.error_msg,
natural_explanation: data.natural_explanation,
retry_count: data.retry_count,
ocr_extraction_result: data.ocr_extraction_result,
})
const streamContent = chat.streamText
if (data.status === 'pass') {
if (streamContent) {
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
}
chat.addMessage({ role: 'assistant', content: 'JRXML 生成成功!可从侧边栏下载。', type: 'success' })
} else if (data.status && data.status !== 'pass') {
chat.addMessage({
role: 'assistant',
content: `经过 ${data.retry_count} 次重试后失败。\n\n错误: ${data.error_msg}${data.natural_explanation ? '\n\n原因: ' + data.natural_explanation : ''}`,
type: 'error',
})
} else if (data.intent === 'consult_question') {
if (streamContent) {
chat.addMessage({ role: 'assistant', content: streamContent, type: 'consult' })
}
} else {
if (streamContent) {
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
}
}
// Refresh session sidebar data after a short delay
setTimeout(() => session.refreshFromState({}), 500)
},
onAgentError(data) {
chat.setError(data.error)
chat.addMessage({ role: 'assistant', content: `执行异常: ${data.error}`, type: 'error' })
},
})
} catch (e: any) {
chat.setError(e.message || '网络请求失败')
chat.addMessage({ role: 'assistant', content: `请求失败: ${e.message}`, type: 'error' })
}
}
</script>
<template>
<div class="app-layout">
<Sidebar />
<main class="main-area">
<div class="chat-container" ref="chatContainer">
<ChatMessages />
<StreamingMessage />
<NodeProgress />
<SummaryCard />
</div>
<UnifiedInput
:disabled="chat.streaming"
@send="handleSend"
/>
</main>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #11111b;
color: #cdd6f4;
}
#app {
width: 100vw;
height: 100vh;
}
.app-layout {
display: flex;
width: 100%;
height: 100%;
}
.main-area {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
min-width: 0;
}
.chat-container {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
</style>