diff --git a/CLAUDE.md b/CLAUDE.md index ca406b3..52d2145 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,24 +2,40 @@ ## 项目概述 -一个**本地桌面应用**,通过自然语言多轮对话帮助非技术用户创建 JasperReports 模板(JRXML 文件)。核心技术栈:Streamlit UI + LangGraph 状态机 + LLM 生成/修改 + 自动验证修正循环。 +一个**本地桌面应用**,通过自然语言多轮对话帮助非技术用户创建 JasperReports 模板(JRXML 文件)。核心技术栈:Vue 3 前端 + FastAPI SSE 后端 + LangGraph 状态机 + LLM 生成/修改 + 自动验证修正循环。 -**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多3次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。 +**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多5次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。 + +## 架构 + +``` +前端 (Vue 3 + Vite, 端口 5173) + │ 聊天界面 + 统一输入框 + 流式显示 + 文件上传/粘贴/拖拽 + ▼ HTTP + SSE (Server-Sent Events) +后端 API (FastAPI, 端口 8000) + │ REST 接口 + SSE 流式推送 + │ 包装 LangGraph Agent 不变 + ▼ HTTP +验证服务 (FastAPI, 端口 8001) — 不变 +``` ## 启动命令 -**方式 1 — 一键启动(Windows)**:双击 `start.bat`,自动打开两个窗口分别运行验证服务和 UI。停止用 `stop.bat`。 +**方式 1 — 一键启动(Windows)**:双击 `start.bat`,自动打开三个窗口分别运行验证服务、后端 API、前端开发服务器。停止用 `stop.bat`。 **方式 2 — 手动启动**: ```bash # 终端 1 — 验证服务(必须先启动) python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0 -# 终端 2 — Streamlit UI -STREAMLIT_SERVER_HEADLESS=true streamlit run app.py --server.port 8501 +# 终端 2 — 后端 API(SSE + REST) +python -m uvicorn api_server:app --port 8000 --host 0.0.0.0 + +# 终端 3 — 前端开发服务器 +cd frontend && npm run dev ``` -浏览器打开 `http://localhost:8501`。 +浏览器打开 `http://localhost:5173`。 ## 当前配置(.env) @@ -31,53 +47,48 @@ STREAMLIT_SERVER_HEADLESS=true streamlit run app.py --server.port 8501 - **向量库**: ChromaDB 持久化在 `./db/chroma` - **验证服务**: FastAPI `localhost:8001` - **日志**: JSON 格式化,`logs/app.log` + `logs/llm.log`,中国时区 (UTC+8) -- **MAX_RETRY**: 3 +- **MAX_RETRY**: 5 ## 架构 ``` -app.py (Streamlit UI) - │ run_agent(user_input) - │ 功能: 流式输出/节点平铺/文件上传/历史下载/预览/Ctrl+C修复 +前端 (Vue 3 + Vite, 端口 5173) + │ src/ + │ ├── api/client.ts SSE 客户端 + fetch 封装 + │ ├── stores/chat.ts Pinia: 消息/流式/节点进度 + │ ├── stores/session.ts Pinia: 会话管理 + │ ├── components/ + │ │ ├── Sidebar.vue 会话列表 + 下载 + │ │ ├── ChatMessages.vue 消息列表渲染 + │ │ ├── StreamingMessage.vue 流式文本展示 + │ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴) + │ │ ├── NodeProgress.vue 节点进度指示 + │ │ └── SummaryCard.vue 结果摘要卡片 + │ └── utils/format.ts 工具函数 + │ + ▼ HTTP + SSE (Server-Sent Events) + │ +api_server.py (FastAPI, 端口 8000) + │ POST /api/sessions/{id}/chat → SSE 流式响应 + │ CRUD /api/sessions/... → 会话管理 + │ POST /api/upload → 文件上传 + │ GET /api/download/... → JRXML 下载 + │ GET /api/health, /api/config + │ + │ 包装 LangGraph Agent(不变)──► agent/ ▼ -agent/graph.py (LangGraph 状态机) - │ 节点流程: - │ load_session → process_input → manage_context → save_state_snapshot - │ → classify_intent (8种意图路由) - │ ├─ retrieve → route_after_retrieve - │ ├─ [有布局schema] generate_skeleton → refine_layout → map_fields - │ └─ [无布局schema] generate - ├─ generate/map_fields → save_session → validate → ... → finalize - │ ├─ modify_jrxml → save_session → validate → ... → finalize - │ ├─ handle_consult / handle_undo / handle_reset → finalize - │ └─ preview/export → save_session → finalize (跳过验证) - │ - │ 验证修正循环: validate ─fail─► explain_error ─► correct_jrxml ─► validate - │ ▲ │ - │ └──────── (retry < MAX_RETRY=3) ───────────────────┘ - │ - ├──► prompts/loader.py Prompt 外部化:10 个 .md 文件热重载 - ├──► backend/llm.py LLM 工厂: Anthropic SDK / OpenAI / Ollama (统一 stream/invoke) - ├──► backend/logger.py 集中日志: JSON + trace_id + llm.log/app.log 分离 - ├──► backend/rag_adapter.py 语义搜索: ChromaDB + SentenceTransformer - ├──► backend/error_kb.py 错误知识库: 指纹去重 + ChromaDB 持久化 - ├──► backend/file_parser.py 文件解析: PDF/DOCX/XLSX/XLS/DOC/图片/文本 - ├──► backend/layout_analyzer.py A4布局分析: OCR + 行分组 + JRXML行匹配 - ├──► backend/ocr_extractor.py OCR字段精确提取: 4策略优先级 + 置信度 - ├──► backend/annotation_detector.py 批注检测: 圈选(HoughCircles) + 箭头(HoughLinesP) + OCR关联 - ├──► backend/validation.py HTTP 客户端: POST /validate - ├──► backend/session.py 会话持久化: JSON 文件 CRUD - └──► validation_service/ 独立 FastAPI: 结构检查 + XSD 校验 +validation_service/ (FastAPI, 端口 8001) — 不变 ``` ## 关键文件映射 | 文件 | 职责 | 修改频率 | |------|------|---------| -| `app.py` | Streamlit UI 入口,聊天界面 + 对话文件上传(粘贴/拖拽) + 侧边栏 + 下载 | **高** | -| `agent/state.py` | AgentState 类型定义(~28 字段,含 layout_schema / annotation_result) | 低 | +| `api_server.py` | FastAPI SSE 后端,REST API + 流式推送 | **高** | +| `frontend/src/` | Vue 3 聊天 UI(替代旧 app.py) | **高** | +| `agent/state.py` | AgentState 类型定义(~28 字段) | 低 | | `agent/nodes.py` | 18 个工作流节点 + 流式生成 + 错误记录 | **高** | -| `agent/graph.py` | 状态图编译 + 路由函数(预览跳过验证) | 中 | +| `agent/graph.py` | 状态图编译 + 路由函数 + node_start 回调 | 中 | | `prompts/loader.py` | Prompt 加载器(从 .md 文件热重载) | 低 | | `prompts/*.md` | 10 个独立 Prompt 模板 | **高** | | `backend/llm.py` | LLM 工厂,统一 `_BaseLLM` 接口(invoke + stream)+ `_LLMLoggingWrapper` | 中 | @@ -93,6 +104,7 @@ agent/graph.py (LangGraph 状态机) | `backend/session.py` | 会话 JSON 文件 CRUD | 低 | | `validation_service/main.py` | FastAPI 验证服务 | 低 | | `scripts/init_kb.py` | 知识库初始化/模型下载 | 低 | +| `app.py` | ~~旧 Streamlit UI~~(已由 api_server.py + frontend/ 替代) | 废弃 | ## 关键约定 @@ -234,7 +246,7 @@ agent/graph.py (LangGraph 状态机) - **OCR 引擎**: 优先 PaddleOCR 2.9.x(精确识别,`pip install paddleocr`),回退 EasyOCR 1.7+。两者均未安装时仅返回图片元信息。PaddlePaddle 3.x 在 Windows 上有 ONEDNN bug,固定在 2.6.x。 - **OCR 字段提取**: `process_input` 自动检测上传图片,调用 `OcrExtractor` 提取常见中文字段(发票代码/号码/金额/日期等),提取结果自动注入 LLM 上下文。 - **会话持久化**: `session_id` 现已包含在 `save_session_node` 的持久化字段中,避免切换会话时因 `session_id` 丢失导致的无限 rerun bug。`create_session` 存盘前强制写入 `agent_state["session_id"] = sid`。`load_session_node` 不从磁盘覆盖 `session_id`。切换会话增加 `_last_switched_to` 哨兵防止重复触发。 -- **MAX_RETRY**: 默认 3 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。 +- **MAX_RETRY**: 默认 5 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。 - **验证最小内容检查**: 验证服务额外检查至少 1 个 `` + 1 个 `` 或 ``,拦截空壳 JRXML。 - **XLSX 支持 (v3)**: 需要 `openpyxl>=3.1.0`(已加入 requirements.txt)。表格按工作表逐行读取,单元格用 `|` 分隔。 - **粘贴功能限制**: 文件以 base64 编码在 sessionStorage 中传递,单文件上限 20MB。大文件建议使用 file_uploader 按钮。 diff --git a/agent/graph.py b/agent/graph.py index 91cac15..76f529b 100644 --- a/agent/graph.py +++ b/agent/graph.py @@ -31,8 +31,8 @@ from agent.nodes import ( ) from backend.logger import get_logger -load_dotenv() -MAX_RETRY = int(os.getenv("MAX_RETRY", "3")) +load_dotenv(override=True) +MAX_RETRY = int(os.getenv("MAX_RETRY", "5")) _graph_log = get_logger("agent") @@ -147,33 +147,51 @@ def route_after_correct(state: AgentState) -> Literal["validate", "finalize"]: # 图构建 # ============================================================ -def build_graph() -> StateGraph: +def build_graph(on_node_start=None) -> StateGraph: + """构建 LangGraph 状态图。 + + Args: + on_node_start: 可选回调,在每个节点开始执行时调用。 + 签名: on_node_start(node_name: str) -> None + 用于 SSE 流式推送 node_start 事件。 + """ workflow = StateGraph(AgentState) + def _wrap(name, fn): + """包装节点函数,在开始执行时触发 on_node_start 回调。""" + if on_node_start is None: + return fn + + @functools.wraps(fn) + def wrapped(state, *args, **kwargs): + on_node_start(name) + return fn(state, *args, **kwargs) + return wrapped + # 现有节点 - workflow.add_node("load_session", load_session_node) - workflow.add_node("process_input", process_input) - workflow.add_node("manage_context", manage_context) - workflow.add_node("save_session", save_session_node) - workflow.add_node("retrieve", retrieve) - workflow.add_node("generate", generate) - workflow.add_node("modify_jrxml", modify_jrxml) - workflow.add_node("validate", validate) - workflow.add_node("explain_error", explain_error) - workflow.add_node("correct_jrxml", correct_jrxml) - workflow.add_node("finalize", finalize) + workflow.add_node("load_session", _wrap("load_session", load_session_node)) + workflow.add_node("process_input", _wrap("process_input", process_input)) + workflow.add_node("manage_context", _wrap("manage_context", manage_context)) + workflow.add_node("save_session", _wrap("save_session", save_session_node)) + workflow.add_node("retrieve", _wrap("retrieve", retrieve)) + workflow.add_node("generate", _wrap("generate", generate)) + workflow.add_node("modify_jrxml", _wrap("modify_jrxml", modify_jrxml)) + workflow.add_node("validate", _wrap("validate", validate)) + workflow.add_node("explain_error", _wrap("explain_error", explain_error)) + workflow.add_node("correct_jrxml", _wrap("correct_jrxml", correct_jrxml)) + workflow.add_node("finalize", _wrap("finalize", finalize)) # 新增节点:意图识别 - workflow.add_node("save_state_snapshot", save_state_snapshot) - workflow.add_node("classify_intent", classify_intent) - workflow.add_node("handle_consult", handle_consult) - workflow.add_node("handle_undo", handle_undo) - workflow.add_node("handle_reset", handle_reset) + workflow.add_node("save_state_snapshot", _wrap("save_state_snapshot", save_state_snapshot)) + workflow.add_node("classify_intent", _wrap("classify_intent", classify_intent)) + workflow.add_node("handle_consult", _wrap("handle_consult", handle_consult)) + workflow.add_node("handle_undo", _wrap("handle_undo", handle_undo)) + workflow.add_node("handle_reset", _wrap("handle_reset", handle_reset)) # 新增节点:分层精确生成(阶段一~三) - workflow.add_node("generate_skeleton", generate_skeleton) - workflow.add_node("refine_layout", refine_layout) - workflow.add_node("map_fields", map_fields) + workflow.add_node("generate_skeleton", _wrap("generate_skeleton", generate_skeleton)) + workflow.add_node("refine_layout", _wrap("refine_layout", refine_layout)) + workflow.add_node("map_fields", _wrap("map_fields", map_fields)) # ---- 入口和前置流程 ---- workflow.set_entry_point("load_session") diff --git a/api_server.py b/api_server.py new file mode 100644 index 0000000..2140449 --- /dev/null +++ b/api_server.py @@ -0,0 +1,606 @@ +"""JRXML Agent API Server — FastAPI + SSE streaming. + +Replaces the Streamlit UI (app.py) with a REST + SSE backend. +The LangGraph agent pipeline is wrapped unchanged. + +SSE Event Types: + node_start — 节点开始执行 + node_complete — 节点执行完成(含详情) + stream_token — LLM 逐字输出 + agent_complete — 全图执行完成 + agent_error — 执行异常 + +Usage: + python -m uvicorn api_server:app --host 0.0.0.0 --port 8000 +""" + +import asyncio +import base64 +import json +import mimetypes +import os +import queue +import tempfile +import time +import traceback +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException, UploadFile, File +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, FileResponse + +load_dotenv(override=True) + +from agent.graph import build_graph +from agent.state import AgentState +from backend.logger import get_logger, generate_trace_id, set_trace_id, get_trace_id +from backend.session import ( + create_session, + load_session, + save_session, + list_all_sessions, + delete_session, + get_session_state, + SESSIONS_DIR, +) +from backend.file_parser import parse_file +from backend.layout_analyzer import analyze_layout, extract_layout_schema + +# ───────────────────────────────────────────── +# 常量(从 app.py 迁移) +# ───────────────────────────────────────────── + +NODE_LABELS = { + "load_session": "加载会话", + "process_input": "记录输入", + "manage_context": "管理上下文", + "save_state_snapshot": "保存快照", + "classify_intent": "识别意图", + "retrieve": "检索模板", + "generate": "生成 JRXML", + "modify_jrxml": "修改 JRXML", + "validate": "验证", + "explain_error": "分析错误", + "correct_jrxml": "自动修正", + "finalize": "完成", + "handle_consult": "咨询回答", + "handle_undo": "撤销操作", + "handle_reset": "重置会话", + "save_session": "保存会话", + "generate_skeleton": "生成骨架", + "refine_layout": "精调布局", + "map_fields": "映射字段", +} + +INTENT_LABELS = { + "initial_generation": "新建报表", + "modify_report": "修改报表", + "preview_report": "预览报表", + "export_pdf": "导出 PDF", + "export_jrxml": "下载 JRXML", + "undo_modification": "撤销修改", + "consult_question": "咨询问题", + "reset_session": "重置会话", +} + +SKIP_NODES = {"load_session", "process_input", "manage_context", + "save_state_snapshot", "save_session"} + +# ───────────────────────────────────────────── +# 日志 & 路径 +# ───────────────────────────────────────────── + +_api_log = get_logger("api") +UPLOADS_DIR = Path(os.getenv("UPLOADS_DIR", "./uploads")) + +# ───────────────────────────────────────────── +# 图编译(全局单例,带 node_start 回调) +# ───────────────────────────────────────────── + +# 当前请求的事件队列(单个用户桌面应用,无并发问题) +_current_event_queue: Optional[queue.Queue] = None + + +def _on_node_start(node_name: str): + """全局 node_start 回调 — 将事件推入当前请求的事件队列。""" + q = _current_event_queue + if q is not None: + q.put(("node_start", { + "node": node_name, + "label": NODE_LABELS.get(node_name, node_name), + })) + + +_graph = build_graph(on_node_start=_on_node_start) + +# ───────────────────────────────────────────── +# 文件注册表(内存中,桌面应用级别可接受) +# ───────────────────────────────────────────── + +_file_registry: dict[str, dict] = {} # file_id → {path, filename, content_type, size} + + +def _ensure_upload_dir(session_id: str = "") -> Path: + d = UPLOADS_DIR / session_id if session_id else UPLOADS_DIR + d.mkdir(parents=True, exist_ok=True) + return d + + +# ───────────────────────────────────────────── +# SSE 辅助 +# ───────────────────────────────────────────── + +def _extract_detail(node_name: str, node_state: dict) -> str: + """从节点状态中提取详情文本(用于 node_complete 事件)。""" + if node_name == "classify_intent": + intent = node_state.get("intent", "") + return f"意图: {INTENT_LABELS.get(intent, intent)}" + elif node_name == "retrieve": + ctx = node_state.get("retrieved_context", "") + return f"找到 {len(ctx)} 字符参考模板" if ctx else "未匹配到模板" + elif node_name in ("generate", "modify_jrxml", "correct_jrxml", + "generate_skeleton", "refine_layout", "map_fields"): + jrxml = node_state.get("current_jrxml", "") + return f"生成 {len(jrxml)} 字符 JRXML" + elif node_name == "validate": + status = node_state.get("status", "") + if status == "pass": + return "验证通过 ✓" + err = node_state.get("error_msg", "") + return f"验证失败: {err[:80]}" + elif node_name == "explain_error": + expl = node_state.get("natural_explanation", "") + return expl[:120] + elif node_name == "handle_consult": + ans = node_state.get("consult_answer", "") + return ans[:150] + return "" + + +def _run_graph_sync(agent_state: AgentState, event_q: queue.Queue): + """在后台线程中运行 graph.stream(),将所有事件推入队列。""" + try: + for event in _graph.stream(agent_state, stream_mode=["updates", "custom"]): + event_q.put(event) + event_q.put(("done", {"reason": "graph_completed"})) + except Exception as exc: + event_q.put(("error", { + "error": str(exc), + "traceback": traceback.format_exc(), + })) + + +async def _sse_generator(agent_state: AgentState) -> str: + """SSE 事件生成器 —— 在后台线程运行图,异步产出 SSE 字符串。""" + global _current_event_queue + + event_q: queue.Queue = queue.Queue() + _current_event_queue = event_q + + loop = asyncio.get_running_loop() + future = loop.run_in_executor(None, _run_graph_sync, agent_state, event_q) + + # 从队列读取事件,写 SSE(用 short sleep 做非阻塞轮询) + while True: + # 先排空队列中的所有事件 + had_events = False + while True: + try: + item = event_q.get_nowait() + had_events = True + except queue.Empty: + break + + kind = item[0] + if kind == "done": + _current_event_queue = None + yield _sse_line("agent_complete", { + "reason": "done", + "intent": agent_state.get("intent", ""), + "status": agent_state.get("status", ""), + "jrxml_length": len(agent_state.get("current_jrxml", "")), + "error_msg": agent_state.get("error_msg", ""), + "natural_explanation": agent_state.get("natural_explanation", ""), + "retry_count": agent_state.get("retry_count", 0), + "ocr_extraction_result": agent_state.get("ocr_extraction_result", {}), + }) + await future + return + + elif kind == "error": + _current_event_queue = None + yield _sse_line("agent_error", item[1]) + await future + return + + elif kind == "node_start": + yield _sse_line("node_start", item[1]) + + else: + # mode=data 来自 graph.stream() + mode, data = item + if mode == "updates": + for node_name, node_state in data.items(): + if node_name not in SKIP_NODES: + detail = _extract_detail(node_name, node_state) + yield _sse_line("node_complete", { + "node": node_name, + "label": NODE_LABELS.get(node_name, node_name), + "detail": detail, + }) + elif mode == "custom": + cd = data + if cd.get("type") == "stream": + yield _sse_line("stream_token", { + "text": cd.get("text", ""), + "type": "stream", + }) + + if not had_events: + await asyncio.sleep(0.05) + yield ": keepalive\n\n" + + +def _sse_line(event_type: str, data: dict) -> str: + """构造单条 SSE 消息。""" + payload = json.dumps(data, ensure_ascii=False) + return f"event: {event_type}\ndata: {payload}\n\n" + + +# ───────────────────────────────────────────── +# FastAPI 应用 +# ───────────────────────────────────────────── + +app = FastAPI( + title="JRXML Agent API", + version="5.0", + description="JRXML 报表生成代理 — 前后端分离 API", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# ───────────────────────────────────────────── +# 健康检查 & 配置 +# ───────────────────────────────────────────── + +@app.get("/api/health") +async def health(): + return { + "status": "ok", + "version": "5.0", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + +@app.get("/api/config") +async def config(): + safe = {} + for key in ("LLM_PROVIDER", "OCR_ENGINE", "EMBEDDING_PROVIDER", + "MAX_RETRY", "CONTEXT_MAX_TOKENS", "CONTEXT_KEEP_RECENT"): + val = os.getenv(key, "") + safe[key] = val + return {"config": safe} + + +# ───────────────────────────────────────────── +# 会话 CRUD +# ───────────────────────────────────────────── + +@app.post("/api/sessions") +async def create_new_session(): + data = create_session() + return { + "session_id": data["session_id"], + "session_name": data["session_name"], + "created_at": data["created_at"], + "updated_at": data["updated_at"], + } + + +@app.get("/api/sessions") +async def list_sessions(): + return {"sessions": list_all_sessions()} + + +@app.get("/api/sessions/{session_id}") +async def get_session(session_id: str): + data = get_session_state(session_id) + if data is None: + raise HTTPException(status_code=404, detail="会话不存在") + return { + "session_id": data.get("session_id"), + "session_name": data.get("session_name"), + "created_at": data.get("created_at"), + "updated_at": data.get("updated_at"), + "agent_state": data.get("agent_state", {}), + } + + +@app.delete("/api/sessions/{session_id}") +async def remove_session(session_id: str): + ok = delete_session(session_id) + if not ok: + raise HTTPException(status_code=404, detail="会话不存在或已删除") + return {"status": "deleted", "session_id": session_id} + + +# ───────────────────────────────────────────── +# 文件上传 +# ───────────────────────────────────────────── + +@app.post("/api/upload") +async def upload_file(file: UploadFile = File(...), session_id: str = ""): + file_id = uuid.uuid4().hex[:12] + _ensure_upload_dir(session_id) + + # 保留原始文件名 + safe_name = Path(file.filename or "upload.bin").name + dest = _ensure_upload_dir(session_id) / f"{file_id}_{safe_name}" + + content = await file.read() + dest.write_bytes(content) + + content_type = file.content_type or mimetypes.guess_type(safe_name)[0] or "application/octet-stream" + + _file_registry[file_id] = { + "path": str(dest), + "filename": safe_name, + "content_type": content_type, + "size": len(content), + } + + _api_log.info("文件上传", extra={ + "file_id": file_id, "filename": safe_name, "size": len(content), + }) + + return { + "file_id": file_id, + "filename": safe_name, + "content_type": content_type, + "size": len(content), + } + + +# ───────────────────────────────────────────── +# 文件处理辅助 +# ───────────────────────────────────────────── + +def _process_files(file_ids: list[str], session_id: str) -> dict: + """处理上传的文件:解析 → 布局分析 → 提取 schema 文本。 + + Returns: + {full_prompt_prefix, uploaded_paths, layout_schema, ocr_text} + """ + if not file_ids: + return {"full_prompt_prefix": "", "uploaded_paths": [], + "layout_schema": {}, "ocr_text": ""} + + parts = [] + uploaded_paths = [] + layout_schema = {} + ocr_text = "" + + for fid in file_ids: + info = _file_registry.get(fid) + if not info: + _api_log.warning("文件ID未注册", extra={"file_id": fid}) + continue + + file_path = info["path"] + uploaded_paths.append(file_path) + + parsed = parse_file(file_path, info["filename"].rsplit(".", 1)[-1] if "." in info["filename"] else "") + if parsed.get("error"): + parts.append(f"[文件: {info['filename']}]\n解析失败: {parsed['error']}") + continue + + parts.append(f"[文件: {info['filename']}]\n{parsed['text']}") + + # 图片文件 → 布局分析 + if info["content_type"] and info["content_type"].startswith("image/"): + layout = analyze_layout(file_path) + if layout.get("is_a4_template"): + parts.append( + f"\n[A4模板布局]\n" + f"表格行数: {layout.get('total_rows', 0)}, " + f"总元素: {layout.get('total_elements', 0)}, " + f"比例: {layout.get('a4_confidence', '')}" + ) + if layout.get("description"): + parts.append(f"\n[布局描述]\n{layout['description']}") + + schema = extract_layout_schema(layout) + if schema and schema.get("total_rows", 0) > 0: + layout_schema = schema + schema_text = schema.get("schema_text", "") + if schema_text: + parts.append(f"\n[布局Schema]\n{schema_text}") + + # OCR 元素文本 + ocr_elements = layout.get("rows", []) + if ocr_elements: + ocr_lines = [] + for row in ocr_elements[:30]: + texts = [e.get("text", "") for e in row.get("elements", [])] + ocr_lines.append(" | ".join(texts)) + ocr_text = "\n".join(ocr_lines) + if ocr_lines: + parts.append(f"\n[OCR 识别文本]\n{ocr_text}") + + return { + "full_prompt_prefix": "\n\n".join(parts) if parts else "", + "uploaded_paths": uploaded_paths, + "layout_schema": layout_schema, + "ocr_text": ocr_text, + } + + +# ───────────────────────────────────────────── +# 核心:SSE 聊天端点 +# ───────────────────────────────────────────── + +@app.post("/api/sessions/{session_id}/chat") +async def chat(session_id: str, payload: dict): + """发送消息并获取 SSE 流式响应。 + + Body: + {text: str, file_ids: [str, ...]} + + Returns: + text/event-stream (SSE) + """ + text = payload.get("text", "").strip() + file_ids = payload.get("file_ids", []) + + if not text and not file_ids: + raise HTTPException(status_code=400, detail="text 和 file_ids 均为空") + + # ── 加载或创建会话 ── + trace_id = generate_trace_id() + set_trace_id(trace_id) + + data = load_session(session_id) + if data is None: + data = create_session(session_id=session_id) + _api_log.info("自动创建会话", extra={"session_id": session_id, "trace_id": trace_id}) + + agent_state: AgentState = data.get("agent_state", {}) + agent_state["session_id"] = session_id + + # ── 处理文件 ── + file_result = _process_files(file_ids, session_id) + full_prompt = text + if file_result["full_prompt_prefix"]: + full_prompt = f"{file_result['full_prompt_prefix']}\n\n用户问题: {text}" if text else file_result["full_prompt_prefix"] + + # ── 注入布局 schema(用于分层精确生成)── + if file_result.get("layout_schema"): + agent_state["layout_schema"] = file_result["layout_schema"] + if file_result.get("ocr_text"): + ocr_rows = [{"elements": [{"text": t} for t in line.split(" | ")]} + for line in file_result["ocr_text"].split("\n") if line.strip()] + if ocr_rows: + agent_state["ocr_elements"] = ocr_rows + + # ── 设置本轮输入 ── + if agent_state.get("current_jrxml") and agent_state.get("status") == "pass": + agent_state["user_modification_request"] = full_prompt + + agent_state["user_input"] = full_prompt + agent_state["retry_count"] = 0 + + _api_log.info("对话请求", extra={ + "session_id": session_id, + "trace_id": trace_id, + "text_length": len(text), + "file_count": len(file_ids), + "prompt_total": len(full_prompt), + }) + + # ── 返回 SSE 流 ── + async def stream_and_save(): + final_state = None + async for sse_chunk in _sse_generator(agent_state): + yield sse_chunk + + # 图执行完成后保存会话状态 + save_session(session_id, agent_state) + + return StreamingResponse( + stream_and_save(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + "X-Trace-Id": trace_id, + }, + ) + + +# ───────────────────────────────────────────── +# 下载 +# ───────────────────────────────────────────── + +@app.get("/api/sessions/{session_id}/download/latest") +async def download_latest(session_id: str): + """下载最新 JRXML 文件。""" + data = load_session(session_id) + if data is None: + raise HTTPException(status_code=404, detail="会话不存在") + + agent_state = data.get("agent_state", {}) + jrxml = agent_state.get("current_jrxml", "") + if not jrxml: + raise HTTPException(status_code=404, detail="该会话暂无 JRXML") + + tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".jrxml", delete=False, + encoding="utf-8") + tmp.write(jrxml) + tmp.close() + + return FileResponse( + tmp.name, + media_type="application/xml", + filename=f"report_{session_id}.jrxml", + ) + + +@app.get("/api/sessions/{session_id}/download/{version}") +async def download_version(session_id: str, version: int): + """下载指定版本的 JRXML 文件。""" + data = load_session(session_id) + if data is None: + raise HTTPException(status_code=404, detail="会话不存在") + + versions = data.get("agent_state", {}).get("jrxml_versions", []) + if version < 0 or version >= len(versions): + raise HTTPException(status_code=404, detail="版本不存在") + + jrxml = versions[version].get("jrxml", "") + if not jrxml: + raise HTTPException(status_code=404, detail="该版本内容为空") + + tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".jrxml", delete=False, + encoding="utf-8") + tmp.write(jrxml) + tmp.close() + + return FileResponse( + tmp.name, + media_type="application/xml", + filename=f"report_{session_id}_v{version}.jrxml", + ) + + +# ───────────────────────────────────────────── +# 下载上传文件 +# ───────────────────────────────────────────── + +@app.get("/api/files/{file_id}") +async def download_file(file_id: str): + info = _file_registry.get(file_id) + if not info: + raise HTTPException(status_code=404, detail="文件未找到") + return FileResponse(info["path"], filename=info["filename"]) + + +# ───────────────────────────────────────────── +# 启动入口 +# ───────────────────────────────────────────── + +if __name__ == "__main__": + import uvicorn + port = int(os.getenv("API_PORT", "8000")) + uvicorn.run("api_server:app", host="0.0.0.0", port=port, reload=True) diff --git a/backend/session.py b/backend/session.py index 15f8748..1c8c8f6 100644 --- a/backend/session.py +++ b/backend/session.py @@ -86,6 +86,15 @@ def save_session(session_id: str, agent_state: dict, session_name: str = ""): json.dump(data, f, ensure_ascii=False, indent=2) +def get_session_state(session_id: str) -> Optional[dict]: + """获取会话的完整 agent_state,用于 REST API。 + + 返回 dict 包含 session_id, session_name, created_at, updated_at, agent_state。 + 未找到则返回 None。 + """ + return load_session(session_id) + + def list_all_sessions() -> list[dict]: """列出所有历史会话(仅摘要,不含完整 agent_state)。""" _ensure_dir() diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..58e9e99 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1357 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "pinia": "^3.0.4", + "vue": "^3.5.34" + }, + "devDependencies": { + "@types/node": "^24.12.3", + "@vitejs/plugin-vue": "^6.0.6", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.0.12", + "vue-tsc": "^3.2.8" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.1.tgz", + "integrity": "sha512-NP8g6V7x81NVOXbLupUvYY6i6LqUkjkVowe2epRedmpgaFCOdjgWHE/rQBvEJ4r7koAYODIjGeBWEdt6n7jYXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.1.tgz", + "integrity": "sha512-webBP3jhlxzhELZ2g+11KJ6pg5OVY1xWhWrj7N/yQMi1CrtxJnW+tUACyRVeDK0cQNLP2Va5HNYK8pe+7c+msw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.1" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..981e7f3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "pinia": "^3.0.4", + "vue": "^3.5.34" + }, + "devDependencies": { + "@types/node": "^24.12.3", + "@vitejs/plugin-vue": "^6.0.6", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.0.12", + "vue-tsc": "^3.2.8" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..a150dcd --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..9218180 --- /dev/null +++ b/frontend/src/api/client.ts @@ -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 +} + +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 { + const r = await fetch(`${BASE}/sessions`, { method: 'POST' }) + return r.json() + }, + + async listSessions(): Promise { + const r = await fetch(`${BASE}/sessions`) + const data = await r.json() + return data.sessions + }, + + async getSession(sessionId: string): Promise { + const r = await fetch(`${BASE}/sessions/${sessionId}`) + if (!r.ok) throw new Error('会话不存在') + return r.json() + }, + + async deleteSession(sessionId: string): Promise { + await fetch(`${BASE}/sessions/${sessionId}`, { method: 'DELETE' }) + }, + + // ── Upload ── + async uploadFile(file: File, sessionId: string): Promise { + 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 { + 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 + } + } + } + } + }, +} diff --git a/frontend/src/components/ChatMessages.vue b/frontend/src/components/ChatMessages.vue new file mode 100644 index 0000000..d1a87af --- /dev/null +++ b/frontend/src/components/ChatMessages.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/frontend/src/components/NodeProgress.vue b/frontend/src/components/NodeProgress.vue new file mode 100644 index 0000000..3cf464c --- /dev/null +++ b/frontend/src/components/NodeProgress.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue new file mode 100644 index 0000000..48c70e7 --- /dev/null +++ b/frontend/src/components/Sidebar.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/frontend/src/components/StreamingMessage.vue b/frontend/src/components/StreamingMessage.vue new file mode 100644 index 0000000..c5ed094 --- /dev/null +++ b/frontend/src/components/StreamingMessage.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/components/SummaryCard.vue b/frontend/src/components/SummaryCard.vue new file mode 100644 index 0000000..4d2147a --- /dev/null +++ b/frontend/src/components/SummaryCard.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/components/UnifiedInput.vue b/frontend/src/components/UnifiedInput.vue new file mode 100644 index 0000000..2d52b63 --- /dev/null +++ b/frontend/src/components/UnifiedInput.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..27b8b78 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' + +const app = createApp(App) +app.use(createPinia()) +app.mount('#app') diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts new file mode 100644 index 0000000..8f245d4 --- /dev/null +++ b/frontend/src/stores/chat.ts @@ -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([]) + const streaming = ref(false) + const streamText = ref('') + const nodes = ref([]) + const error = ref('') + const ocrResult = ref(null) + const summary = ref({ + intent: '', status: '', jrxml_length: 0, + error_msg: '', natural_explanation: '', retry_count: 0, + }) + + function addMessage(msg: Omit) { + 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, + } +}) diff --git a/frontend/src/stores/session.ts b/frontend/src/stores/session.ts new file mode 100644 index 0000000..6452f88 --- /dev/null +++ b/frontend/src/stores/session.ts @@ -0,0 +1,71 @@ +/** Pinia store — session management. */ + +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { api, type SessionSummary } from '../api/client' + +export const useSessionStore = defineStore('session', () => { + const sessions = ref([]) + const currentId = ref('') + const currentName = ref('') + const versions = ref([]) + const currentJrxml = ref('') + + const sortedSessions = computed(() => + [...sessions.value].sort((a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ) + ) + + const currentSession = computed(() => + sessions.value.find(s => s.session_id === currentId.value) + ) + + async function loadSessions() { + try { + sessions.value = await api.listSessions() + } catch (e) { + console.error('加载会话列表失败:', e) + } + } + + async function createSession() { + const s = await api.createSession() + sessions.value.unshift(s) + return s.session_id + } + + async function switchSession(sessionId: string) { + currentId.value = sessionId + try { + const data = await api.getSession(sessionId) + currentName.value = data.session_name + const state = data.agent_state + currentJrxml.value = state.current_jrxml || '' + versions.value = state.jrxml_versions || [] + } catch (e) { + console.error('加载会话失败:', e) + } + } + + async function deleteCurrent() { + if (!currentId.value) return + await api.deleteSession(currentId.value) + sessions.value = sessions.value.filter(s => s.session_id !== currentId.value) + currentId.value = '' + currentName.value = '' + currentJrxml.value = '' + versions.value = [] + } + + function refreshFromState(agentState: Record) { + currentJrxml.value = agentState.current_jrxml || currentJrxml.value + versions.value = agentState.jrxml_versions || versions.value + } + + return { + sessions, currentId, currentName, versions, currentJrxml, + sortedSessions, currentSession, + loadSessions, createSession, switchSession, deleteCurrent, refreshFromState, + } +}) diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts new file mode 100644 index 0000000..65d93cf --- /dev/null +++ b/frontend/src/utils/format.ts @@ -0,0 +1,24 @@ +/** Date formatting and text utilities. */ + +export function formatTime(iso: string): string { + if (!iso) return '' + const d = new Date(iso) + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` +} + +export function truncate(str: string, max: number): string { + if (!str) return '' + return str.length > max ? str.slice(0, max) + '...' : str +} + +export function fileIcon(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase() || '' + const map: Record = { + png: '🖼', jpg: '🖼', jpeg: '🖼', bmp: '🖼', webp: '🖼', + pdf: '📄', docx: '📝', doc: '📝', + xlsx: '📊', xls: '📊', + txt: '📃', csv: '📃', + } + return map[ext] || '📎' +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..5c750c5 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..ff9cefb --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/start.bat b/start.bat index b4adf7b..39af46f 100644 --- a/start.bat +++ b/start.bat @@ -1,18 +1,21 @@ @echo off echo ============================================ -echo JRXML 代理 - 全自动启动 (验证服务 + UI) +echo JRXML 代理 - 全自动启动 (验证 + API + UI) echo ============================================ -set STREAMLIT_SERVER_HEADLESS=true - echo. -echo [1/2] 启动验证服务 (端口 8001)... +echo [1/3] 启动验证服务 (端口 8001)... start "JRXML 验证服务" cmd /c "cd /d %~dp0 && .venv\Scripts\python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0" timeout /t 3 /nobreak >nul -echo [2/2] 启动 Streamlit UI (端口 8501)... -start "JRXML UI" cmd /c "cd /d %~dp0 && .venv\Scripts\streamlit run app.py --server.port 8501" +echo [2/3] 启动后端 API (端口 8000)... +start "JRXML API" cmd /c "cd /d %~dp0 && .venv\Scripts\python -m uvicorn api_server:app --port 8000 --host 0.0.0.0" + +timeout /t 3 /nobreak >nul + +echo [3/3] 启动前端开发服务器 (端口 5173)... +start "JRXML Frontend" cmd /c "cd /d %~dp0\frontend && npm run dev" timeout /t 3 /nobreak >nul @@ -20,7 +23,8 @@ echo. echo ============================================ echo 启动完成 echo 验证服务: http://localhost:8001 -echo UI 界面: http://localhost:8501 +echo 后端 API: http://localhost:8000 +echo 前端界面: http://localhost:5173 echo ============================================ echo. echo 关闭此窗口不会停止服务。关闭服务窗口或运行 stop.bat 停止。 diff --git a/stop.bat b/stop.bat index 6eaab8f..b24b71e 100644 --- a/stop.bat +++ b/stop.bat @@ -2,7 +2,8 @@ echo 正在停止 JRXML 代理服务... taskkill /fi "WINDOWTITLE eq JRXML 验证服务*" /f 2>nul -taskkill /fi "WINDOWTITLE eq JRXML UI*" /f 2>nul +taskkill /fi "WINDOWTITLE eq JRXML API*" /f 2>nul +taskkill /fi "WINDOWTITLE eq JRXML Frontend*" /f 2>nul echo 已停止。 pause