前后端页面同步策略,支持分析模板热编辑以及yaml配置,修改提示词编码,占用符等问题,优化文件扫描

This commit is contained in:
2026-04-20 09:50:35 +08:00
parent 00bd48e7e7
commit 3e1ecf2549
14 changed files with 539 additions and 287 deletions

View File

@@ -85,9 +85,28 @@ class SessionManager:
return self.sessions[session_id]
# Fallback: Try to reconstruct from disk for history sessions
# First try the old convention: outputs/session_{uuid}
output_dir = os.path.join("outputs", f"session_{session_id}")
if os.path.exists(output_dir) and os.path.isdir(output_dir):
return self._reconstruct_session(session_id, output_dir)
# Scan all session directories for session_meta.json matching this session_id
# This handles the case where output_dir uses a timestamp name, not the UUID
outputs_root = "outputs"
if os.path.exists(outputs_root):
for dirname in os.listdir(outputs_root):
dir_path = os.path.join(outputs_root, dirname)
if not os.path.isdir(dir_path) or not dirname.startswith("session_"):
continue
meta_path = os.path.join(dir_path, "session_meta.json")
if os.path.exists(meta_path):
try:
with open(meta_path, "r", encoding="utf-8") as f:
meta = json.load(f)
if meta.get("session_id") == session_id:
return self._reconstruct_session(session_id, dir_path)
except Exception:
continue
return None
@@ -99,33 +118,52 @@ class SessionManager:
session.current_round = session.max_rounds
session.progress_percentage = 100.0
session.status_message = "已完成 (历史记录)"
# Read session_meta.json if available
meta = {}
meta_path = os.path.join(output_dir, "session_meta.json")
if os.path.exists(meta_path):
try:
with open(meta_path, "r", encoding="utf-8") as f:
meta = json.load(f)
except Exception:
pass
# Recover Log
log_path = os.path.join(output_dir, "process.log")
if os.path.exists(log_path):
session.log_file = log_path
# Recover Report
# 宽容查找:扫描所有 .md 文件,优先取包含 "report" 或 "报告" 的文件
md_files = glob.glob(os.path.join(output_dir, "*.md"))
if md_files:
# 默认取第一个
chosen = md_files[0]
# 尝试找更好的匹配
for md in md_files:
fname = os.path.basename(md).lower()
if "report" in fname or "报告" in fname:
chosen = md
break
session.generated_report = chosen
# Recover Report — prefer meta, then scan .md files
report_path = meta.get("report_path")
if report_path and os.path.exists(report_path):
session.generated_report = report_path
else:
md_files = glob.glob(os.path.join(output_dir, "*.md"))
if md_files:
chosen = md_files[0]
for md in md_files:
fname = os.path.basename(md).lower()
if "report" in fname or "报告" in fname:
chosen = md
break
session.generated_report = chosen
# Recover Script (查找可能的脚本文件)
possible_scripts = ["data_analysis_script.py", "script.py", "analysis_script.py"]
for s in possible_scripts:
p = os.path.join(output_dir, s)
if os.path.exists(p):
session.reusable_script = p
break
# Recover Script — prefer meta, then scan for 分析脚本_*.py or other patterns
script_path = meta.get("script_path")
if script_path and os.path.exists(script_path):
session.reusable_script = script_path
else:
# Try Chinese-named scripts first (generated by this system)
script_files = glob.glob(os.path.join(output_dir, "分析脚本_*.py"))
if not script_files:
for s in ["data_analysis_script.py", "script.py", "analysis_script.py"]:
p = os.path.join(output_dir, s)
if os.path.exists(p):
script_files = [p]
break
if script_files:
session.reusable_script = script_files[0]
# Recover Results (images etc)
results_json = os.path.join(output_dir, "results.json")
@@ -219,6 +257,14 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str, is_fo
session_output_dir = session.output_dir
session.log_file = os.path.join(session_output_dir, "process.log")
# Persist session-to-directory mapping immediately so recovery works
# even if the server restarts mid-analysis
try:
with open(os.path.join(session_output_dir, "session_meta.json"), "w") as f:
json.dump({"session_id": session_id, "user_requirement": user_requirement}, f, default=str)
except Exception:
pass
# 使用 PrintCapture 替代全局 FileLogger退出 with 块后自动恢复 stdout
with PrintCapture(session.log_file):
if is_followup:
@@ -285,6 +331,18 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str, is_fo
"data_files": session.data_files,
}, f, default=str)
# Persist session-to-directory mapping for recovery after server restart
try:
with open(os.path.join(session_output_dir, "session_meta.json"), "w") as f:
json.dump({
"session_id": session_id,
"user_requirement": user_requirement,
"report_path": session.generated_report,
"script_path": session.reusable_script,
}, f, default=str)
except Exception:
pass
except Exception as e:
print(f"Error during analysis: {e}")
@@ -350,14 +408,36 @@ async def chat_analysis(request: ChatRequest, background_tasks: BackgroundTasks)
import math as _math
def _sanitize_value(v):
"""Replace NaN/inf with None for JSON safety."""
if isinstance(v, float) and (_math.isnan(v) or _math.isinf(v)):
"""Make any value JSON-serializable.
Handles: NaN/inf floats → None, pandas Timestamp/Timedelta → str,
numpy integers/floats → Python int/float, dicts and lists recursively.
"""
if v is None:
return None
if isinstance(v, float):
if _math.isnan(v) or _math.isinf(v):
return None
return v
if isinstance(v, (int, bool, str)):
return v
if isinstance(v, dict):
return {k: _sanitize_value(val) for k, val in v.items()}
if isinstance(v, list):
return [_sanitize_value(item) for item in v]
return v
# pandas Timestamp, Timedelta, NaT
try:
if pd.isna(v):
return None
except (TypeError, ValueError):
pass
if hasattr(v, 'isoformat'): # datetime, Timestamp
return v.isoformat()
# numpy scalar types
if hasattr(v, 'item'):
return v.item()
# Fallback: convert to string
return str(v)
@app.get("/api/status")
@@ -533,6 +613,37 @@ async def list_available_templates():
return {"templates": list_templates()}
@app.get("/api/templates/{template_name}")
async def get_template_detail(template_name: str):
"""获取单个模板的完整内容(含步骤)"""
from utils.analysis_templates import get_template
try:
tpl = get_template(template_name)
return tpl.to_dict()
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.put("/api/templates/{template_name}")
async def update_template(template_name: str, body: dict):
"""创建或更新模板"""
from utils.analysis_templates import save_template
try:
filepath = save_template(template_name, body)
return {"status": "saved", "filepath": filepath}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/templates/{template_name}")
async def remove_template(template_name: str):
"""删除模板"""
from utils.analysis_templates import delete_template
if delete_template(template_name):
return {"status": "deleted"}
raise HTTPException(status_code=404, detail=f"Template not found: {template_name}")
# --- Data Files API ---
@app.get("/api/data-files")

View File

@@ -209,12 +209,16 @@ function startPolling() {
loadDataFiles();
// Update progress bar during analysis
// Use rounds.length (actual completed analysis rounds) for display
// instead of current_round (which includes non-code rounds like collect_figures)
if (data.is_running && data.progress_percentage !== undefined) {
updateProgressBar(data.progress_percentage, data.status_message, data.current_round, data.max_rounds);
const displayRound = rounds.length || data.current_round || 0;
updateProgressBar(data.progress_percentage, data.status_message, displayRound, data.max_rounds);
}
if (!data.is_running && isRunning) {
updateProgressBar(100, 'Analysis complete', data.current_round || data.max_rounds, data.max_rounds);
const displayRound = rounds.length || data.current_round || data.max_rounds;
updateProgressBar(100, 'Analysis complete', displayRound, data.max_rounds);
setTimeout(hideProgressBar, 3000);
setRunningState(false);