From a364e1de8181be484baba868a273fa51e18b817f Mon Sep 17 00:00:00 2001 From: panda <1415243231@qq.com> Date: Thu, 21 May 2026 23:43:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=205-issue=20fix=20=E2=80=94=20OCR=20image?= =?UTF-8?q?=20parse=20bug=20+=20Vue=20frontend=20feature=20parity=20+=20st?= =?UTF-8?q?reaming=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1 (CRITICAL): file_parser.py suffix normalization ".jpg", api_server.py Path.suffix Fix 2: Sidebar version history download, ProcessSection replaces old components Fix 3: OCR content/position layer structured logging in agent/nodes.py Fix 4: collapsible process sections with per-section stream routing + auto-fold Fix 5: agent_complete total_duration_ms, SummaryCard duration display - backend/file_parser.py: normalize suffix to always include leading dot - api_server.py: step_index in node_start, total_duration_ms in agent_complete - agent/nodes.py: _log_ocr_layers() for [内容层]/[位置层]/[合并] logging - frontend: ProcessSection.vue (NEW), chat.ts sections model, Sidebar versions - CLAUDE.md: updated component list and v6 changelog --- CLAUDE.md | 29 ++- agent/nodes.py | 73 +++++++ api_server.py | 10 +- frontend/src/App.vue | 9 +- frontend/src/api/client.ts | 3 +- frontend/src/components/ProcessSection.vue | 209 +++++++++++++++++++++ frontend/src/components/Sidebar.vue | 55 +++++- frontend/src/components/SummaryCard.vue | 15 +- frontend/src/stores/chat.ts | 110 ++++++++++- 9 files changed, 492 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/ProcessSection.vue diff --git a/CLAUDE.md b/CLAUDE.md index bc6c48c..5cae8e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,12 +45,11 @@ cd frontend && npm run dev │ ├── stores/chat.ts Pinia: 消息/流式/节点进度 │ ├── stores/session.ts Pinia: 会话管理 │ ├── components/ - │ │ ├── Sidebar.vue 会话列表 + 下载 + │ │ ├── Sidebar.vue 会话列表 + 下载 + 历史版本 │ │ ├── ChatMessages.vue 消息列表渲染 - │ │ ├── StreamingMessage.vue 流式文本展示 - │ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴) - │ │ ├── NodeProgress.vue 节点进度指示 - │ │ └── SummaryCard.vue 结果摘要卡片 + │ │ ├── ProcessSection.vue 过程折叠区(替代 StreamingMessage + NodeProgress) + │ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴/芯片) + │ │ └── SummaryCard.vue 结果摘要卡片(含耗时) │ └── utils/format.ts 工具函数 │ ▼ HTTP + SSE (Server-Sent Events) @@ -243,3 +242,23 @@ validation_service/ (FastAPI, 端口 8001) — 不变 - **st-multimodal-chatinput**: Streamlit 聊天输入增强组件,替代 `st.chat_input`,支持粘贴/拖拽文件。返回 base64 编码文件内容。 - **xlwt**: 仅在测试中使用(生成 .xls 测试文件)。 - **分层精确生成**: 3 阶段管线仅在 `layout_schema.total_rows > 0` 时触发。文本请求和 `modify_report` 等意图不受影响,走原有 `generate` 节点。中间阶段(骨架/精调)跳过验证,只有最终 mapped 结果进入 `validate`。 + +## 新增功能 (v6) + +### 5-Issue Fix — 图片解析 Bug + 前端功能补全 + +**Fix 1 — 图片后缀 dot 缺失**: `file_parser.py` 后缀规范化(`"jpg"` → `".jpg"`),`api_server.py` 使用 `Path.suffix` 替代 `rsplit`。所有图片上传之前均因后缀不匹配回退到文本解析器,OCR/布局分析从未实际触发。 + +**Fix 2 — Vue 前端功能补全**: +- `ProcessSection.vue` 替代 `StreamingMessage.vue` + `NodeProgress.vue`,使用 `
`/`` 原生可折叠区域 +- `Sidebar.vue` 新增历史版本下载列表(`jrxml_versions` 索引下载) +- `UnifiedInput.vue` 已集成文件拖拽/粘贴/芯片/移除(v5 已完成) + +**Fix 3 — OCR 两层日志**: `agent/nodes.py` 新增 `_log_ocr_layers()` — `[内容层]` OCR 文本+字段提取,`[位置层]` 布局 schema 列×行+区域分类,`[合并]` 管线选择(3阶段 vs 单阶段) + +**Fix 4 — 全过程流式输出+自动折叠**: +- `api_server.py` `node_start` 事件携带 `step_index` +- `chat.ts` 新增 `ProcessSection[]` 模型:per-section stream routing、完成自动折叠、运行中自动展开 +- `ProcessSection.vue` 渲染步骤编号/标签/耗时/内容(XML 代码高亮) + +**Fix 5 — 消息耗时显示**: `api_server.py` `agent_complete` 事件新增 `total_duration_ms`,`SummaryCard.vue` 显示总耗时,`chat.ts` 暴露 `lastDurationMs` + `formatDuration()` diff --git a/agent/nodes.py b/agent/nodes.py index 791f938..12b2f14 100644 --- a/agent/nodes.py +++ b/agent/nodes.py @@ -176,6 +176,9 @@ def process_input(state: AgentState) -> Dict: state["ocr_extraction_result"] = {"error": str(e)} state["uploaded_file_path"] = "" + # ── OCR 两层日志:内容层 + 位置层 ── + _log_ocr_layers(state) + # 重置本轮请求字段 state["retry_count"] = 0 state["user_modification_request"] = user_input @@ -532,6 +535,76 @@ def _format_ocr_context(state: AgentState) -> str: return "\n".join(parts) +def _log_ocr_layers(state: AgentState) -> None: + """记录 OCR 两层分离日志:内容层(文本/字段)+ 位置层(布局/坐标)。""" + # ── 内容层:OCR 文本元素 + 提取的字段 ── + ocr_result = state.get("ocr_extraction_result") + ocr_elements = state.get("ocr_elements", []) + + content_parts = [] + if isinstance(ocr_result, dict) and not ocr_result.get("error"): + total = ocr_result.get("total_elements", 0) + fields = ocr_result.get("fields", []) + non_empty = [f for f in fields if f.get("field_value")] + if total or non_empty: + content_parts.append( + f"OCR 提取: {total} 个文本元素, {len(non_empty)} 个有效字段" + ) + if isinstance(ocr_elements, list) and ocr_elements: + elem_count = sum(len(row.get("elements", [])) for row in ocr_elements) + content_parts.append( + f"API 注入 OCR 元素: {len(ocr_elements)} 行, {elem_count} 个文本" + ) + + if content_parts: + _node_log.info( + "[内容层] " + " | ".join(content_parts), + extra={"layer": "content", "phase": "ocr_extraction"}, + ) + + # ── 位置层:布局 schema(行/列/区域)── + layout = state.get("layout_schema") + if isinstance(layout, dict) and layout.get("total_rows", 0) > 0: + regions = layout.get("regions", {}) + region_names = list(regions.keys()) if regions else [] + cols = layout.get("total_columns", 0) + rows = layout.get("total_rows", 0) + regions_label = ", ".join(region_names) if region_names else "标题/表头/数据/表尾" + _node_log.info( + f"[位置层] 布局 schema: {cols} 列 × {rows} 行, 区域: {regions_label}", + extra={ + "layer": "position", + "phase": "layout_analysis", + "columns": cols, + "rows": rows, + "regions": region_names, + "a4_confidence": layout.get("a4_confidence", ""), + }, + ) + + # ── 合并:两阶段处理总结 ── + has_content = (isinstance(ocr_result, dict) and not ocr_result.get("error")) or \ + (isinstance(ocr_elements, list) and ocr_elements) + has_layout = isinstance(layout, dict) and layout.get("total_rows", 0) > 0 + + if has_content and has_layout: + _node_log.info( + "[合并] 内容层 + 位置层均已就绪 — " + "注入 prompt: 骨架生成 → 精调布局 → 字段映射", + extra={"layer": "merge", "pipeline": "skeleton→refine→map_fields"}, + ) + elif has_content and not has_layout: + _node_log.info( + "[合并] 仅有内容层 — 使用单阶段 generate(无布局 schema)", + extra={"layer": "merge", "pipeline": "generate_only"}, + ) + elif has_layout and not has_content: + _node_log.info( + "[合并] 仅有位置层 — 使用布局 schema 指导生成", + extra={"layer": "merge", "pipeline": "layout_only"}, + ) + + @log_node("retrieve") def retrieve(state: AgentState) -> Dict: """在 ChromaDB + 错误知识库中搜索相关的 JRXML 模板和组件。""" diff --git a/api_server.py b/api_server.py index 86c143e..fe95cf7 100644 --- a/api_server.py +++ b/api_server.py @@ -103,15 +103,19 @@ UPLOADS_DIR = Path(os.getenv("UPLOADS_DIR", "./uploads")) # 当前请求的事件队列(单个用户桌面应用,无并发问题) _current_event_queue: Optional[queue.Queue] = None +_step_counter: int = 0 def _on_node_start(node_name: str): """全局 node_start 回调 — 将事件推入当前请求的事件队列。""" + global _step_counter q = _current_event_queue if q is not None: + _step_counter += 1 q.put(("node_start", { "node": node_name, "label": NODE_LABELS.get(node_name, node_name), + "step_index": _step_counter, })) @@ -176,8 +180,10 @@ def _run_graph_sync(agent_state: AgentState, event_q: queue.Queue): async def _sse_generator(agent_state: AgentState) -> str: """SSE 事件生成器 —— 在后台线程运行图,异步产出 SSE 字符串。""" - global _current_event_queue + global _current_event_queue, _step_counter + _step_counter = 0 + t_start = time.time() event_q: queue.Queue = queue.Queue() _current_event_queue = event_q @@ -198,6 +204,7 @@ async def _sse_generator(agent_state: AgentState) -> str: kind = item[0] if kind == "done": _current_event_queue = None + total_ms = round((time.time() - t_start) * 1000) yield _sse_line("agent_complete", { "reason": "done", "intent": agent_state.get("intent", ""), @@ -206,6 +213,7 @@ async def _sse_generator(agent_state: AgentState) -> str: "error_msg": agent_state.get("error_msg", ""), "natural_explanation": agent_state.get("natural_explanation", ""), "retry_count": agent_state.get("retry_count", 0), + "total_duration_ms": total_ms, "ocr_extraction_result": agent_state.get("ocr_extraction_result", {}), }) await future diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a150dcd..454436d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,8 +5,7 @@ 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 ProcessSection from './components/ProcessSection.vue' import SummaryCard from './components/SummaryCard.vue' import UnifiedInput from './components/UnifiedInput.vue' @@ -55,7 +54,7 @@ async function handleSend(text: string, files: File[]) { try { await api.chat(session.currentId, text, remoteIds, { onNodeStart(data) { - chat.addNode(data) + chat.addNode({ node: data.node, label: data.label, step_index: data.step_index }) }, onNodeComplete(data) { chat.completeNode(data) @@ -72,6 +71,7 @@ async function handleSend(text: string, files: File[]) { error_msg: data.error_msg, natural_explanation: data.natural_explanation, retry_count: data.retry_count, + total_duration_ms: data.total_duration_ms, ocr_extraction_result: data.ocr_extraction_result, }) @@ -119,8 +119,7 @@ async function handleSend(text: string, files: File[]) {
- - +
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 9218180..f124cda 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -28,11 +28,12 @@ export interface AgentCompleteData { error_msg: string natural_explanation: string retry_count: number + total_duration_ms: number ocr_extraction_result: any } export interface SSECallbacks { - onNodeStart?: (data: { node: string; label: string }) => void + 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 diff --git a/frontend/src/components/ProcessSection.vue b/frontend/src/components/ProcessSection.vue new file mode 100644 index 0000000..35aa7f0 --- /dev/null +++ b/frontend/src/components/ProcessSection.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 48c70e7..4a666b1 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -63,15 +63,29 @@ async function handleDelete() { -