feat: v3 robustness upgrade — EasyOCR, failure recovery, minimum content check

- OCR: EasyOCR (primary, ch_sim+en) with PaddleOCR fallback for Windows compatibility
- Validation: _check_minimum_content() rejects empty-shell JRXML (no band/textField)
- Retry: MAX_RETRY 3→5, exhaustion records pending_failure_context for next-turn auto-injection
- Finalize: only saves jrxml_versions on pass, preserves last good final_jrxml on fail
- Extract JRXML: improved empty markdown block handling and XML fragment fallback
- UI: real-time node progress via placeholder updates, initial "analyzing" feedback
- UI: use agent_state (full) instead of node_state (partial) for summary card routing
- UI: unknown template_type now gives LLM meaningful image context instead of metadata
- Docs: updated CLAUDE.md and CODE_GUIDE.md to reflect all v3 changes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 19:15:30 +08:00
parent 70614dff5e
commit 6467fd4ae5
9 changed files with 1297 additions and 51 deletions
+47 -20
View File
@@ -138,18 +138,30 @@ def run_agent(user_input: str):
agent_state["user_input"] = user_input
agent_state["retry_count"] = 0
# ---- UI 容器 ----
streaming_placeholder = st.empty() # 流式文本
nodes_container = st.container() # 节点进度区
# ---- UI 占位 ----
progress_placeholder = st.empty() # 实时节点进度
streaming_placeholder = st.empty() # 流式文本
summary_placeholder = st.empty() # 总结卡片
# 节点追踪
executed_nodes: list[dict] = [] # {name, label, status, detail}
# 初始状态提示
progress_placeholder.info("⏳ 正在分析您的需求...")
executed_nodes: list[dict] = []
stream_text = ""
stream_active = False
current_stream_node = ""
final_state = None
def _render_progress(nodes: list[dict]):
"""渲染实时节点进度到占位符。"""
if not nodes:
return
lines = []
for i, node in enumerate(nodes):
icon = "" if i == len(nodes) - 1 else ""
detail = f"{node['detail']}" if node.get("detail") else ""
lines.append(f"{icon} {node['label']}{detail}")
progress_placeholder.markdown("\n\n".join(lines))
try:
for event in st.session_state.graph.stream(
agent_state, stream_mode=["updates", "custom"]
@@ -177,7 +189,6 @@ def run_agent(user_input: str):
)
elif node_name in ("generate", "modify_jrxml", "correct_jrxml"):
# 流式文本已在上面的 custom 事件中展示
jrxml = node_state.get("current_jrxml", "")
executed_nodes[-1]["detail"] = f"生成 {len(jrxml)} 字符 JRXML"
@@ -199,31 +210,29 @@ def run_agent(user_input: str):
final_state = node_state
# 每个节点完成后立即更新进度
_render_progress(executed_nodes)
elif mode == "custom":
cd = data
if cd.get("type") == "stream":
stream_text += cd.get("text", "")
stream_active = True
current_stream_node = cd.get("node", "")
streaming_placeholder.code(stream_text, language="xml")
except Exception as e:
progress_placeholder.empty()
st.error(f"工作流异常: {e}")
return
# ---- 渲染节点进度区 ----
with nodes_container:
with st.expander("处理过程", expanded=False):
for i, node in enumerate(executed_nodes):
icon = "" if i < len(executed_nodes) - 1 else ""
detail_str = f"{node['detail']}" if node.get("detail") else ""
st.caption(f"{icon} {node['label']}{detail_str}")
# ---- 清除流式占位 ----
# ---- 清理临时占位 ----
progress_placeholder.empty()
if stream_active:
streaming_placeholder.empty()
# ---- 总结卡片 ----
# 注:node_state 只含变更字段,用 agent_state(被所有节点就地修改)获取完整状态
final_state = agent_state
if final_state:
st.session_state.agent_state = final_state
intent = final_state.get("intent", "")
@@ -239,7 +248,6 @@ def run_agent(user_input: str):
elif intent in ("undo_modification", "reset_session"):
st.success("操作已完成")
# 消息已在节点中添加
elif intent in ("preview_report", "export_pdf", "export_jrxml"):
jrxml = final_state.get("current_jrxml", "")
@@ -279,10 +287,10 @@ def run_agent(user_input: str):
if jrxml:
with st.expander("查看当前 JRXML"):
_render_jrxml(jrxml, max_lines=80)
st.caption("请简化报表结构后重试")
st.caption("💡 下次输入修改需求时,系统会自动加载失败上下文继续修复")
st.session_state.messages.append({
"role": "assistant",
"content": f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML。\n\n**错误:** {error_msg}",
"content": f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML。\n\n**错误:** {error_msg}\n\n💡 请直接描述修改需求,系统会自动加载失败上下文。",
"type": "error_explanation",
})
else:
@@ -431,6 +439,25 @@ with st.sidebar:
else:
# 新建模式:按 A4 模板处理
parsed_text = layout["description"]
else:
# tt == "unknown": OCR 不可用或未检测到文字元素
has_ocr = result.get("method") not in ("metadata_only", None)
img_w, img_h = layout["image_size"]
ratio = layout["aspect_ratio"]
if has_ocr:
parsed_text = (
f"[图片上传] 尺寸 {img_w}x{img_h}px, 比例 {ratio}"
f"未检测到 A4 报表结构,图片将被视为参考样式。\n"
f"请根据用户的文字描述生成报表。"
)
else:
parsed_text = (
f"[图片上传] 尺寸 {img_w}x{img_h}px, 比例 {ratio}\n"
f"⚠ OCR 引擎未安装,无法识别图片中的文字内容。\n"
f"请严格根据用户的文字描述来推断图片中的报表需求。\n"
f"(提示:如需图片文字识别,请运行 pip install paddleocr"
)
parsed_type = "image_reference"
Path(tmp_path).unlink(missing_ok=True)