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
+122
View File
@@ -0,0 +1,122 @@
/** Pinia store — chat messages + streaming state. */
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface Message {
id: string
role: 'user' | 'assistant'
content: string
type?: 'text' | 'jrxml' | 'error' | 'success' | 'consult'
timestamp: string
}
export interface NodeProgress {
node: string
label: string
detail?: string
status: 'running' | 'done'
}
export interface AgentSummary {
intent: string
status: string
jrxml_length: number
error_msg: string
natural_explanation: string
retry_count: number
}
export const useChatStore = defineStore('chat', () => {
const messages = ref<Message[]>([])
const streaming = ref(false)
const streamText = ref('')
const nodes = ref<NodeProgress[]>([])
const error = ref<string>('')
const ocrResult = ref<any>(null)
const summary = ref<AgentSummary>({
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
})
function addMessage(msg: Omit<Message, 'id' | 'timestamp'>) {
messages.value.push({
...msg,
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
})
}
function startStreaming() {
streaming.value = true
streamText.value = ''
nodes.value = []
error.value = ''
summary.value = {
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
}
}
function appendStreamToken(text: string) {
streamText.value += text
}
function addNode(node: { node: string; label: string }) {
nodes.value.push({ ...node, status: 'running' })
}
function completeNode(node: { node: string; label: string; detail: string }) {
const existing = nodes.value.find(n => n.node === node.node)
if (existing) {
existing.status = 'done'
existing.detail = node.detail
}
}
function finishStreaming(data?: {
intent?: string; status?: string; jrxml_length?: number
error_msg?: string; natural_explanation?: string; retry_count?: number
ocr_extraction_result?: any
}) {
streaming.value = false
nodes.value.forEach(n => { n.status = 'done' })
if (data) {
summary.value = {
intent: data.intent || '',
status: data.status || '',
jrxml_length: data.jrxml_length || 0,
error_msg: data.error_msg || '',
natural_explanation: data.natural_explanation || '',
retry_count: data.retry_count || 0,
}
if (data.ocr_extraction_result) {
ocrResult.value = data.ocr_extraction_result
}
}
}
function setError(err: string) {
error.value = err
streaming.value = false
}
function reset() {
messages.value = []
streamText.value = ''
nodes.value = []
error.value = ''
streaming.value = false
ocrResult.value = null
summary.value = {
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
}
}
return {
messages, streaming, streamText, nodes, error, ocrResult, summary,
addMessage, startStreaming, appendStreamToken, addNode, completeNode,
finishStreaming, setError, reset,
}
})