前后端页面同步策略,支持分析模板热编辑以及yaml配置,修改提示词编码,占用符等问题,优化文件扫描
This commit is contained in:
157
web/main.py
157
web/main.py
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user