diff --git a/README.md b/README.md index 1dbe7bf6..29621f45 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,11 @@ API_KEY=your_strong_api_key `.env.example` for defaults. - Run the production server with a **single worker** (`gunicorn -w 1 --threads N`); simulation run-state is held in-process, so multiple workers break stop/status routing. +- **CSP / API origin:** the frontend ships a Content-Security-Policy whose `connect-src` allows + `'self'` + `http://localhost:5001` (the default API). If you point the UI at a different API + host (set `VITE_API_BASE_URL`), you **must** add that origin to `connect-src` in + `frontend/index.html` too, or the browser will silently block all API calls. Restrict + `ALLOWED_ORIGINS` (backend) to your real frontend origin in production. #### 2. Install Dependencies diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index 842c729c..9a7a598c 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -4,7 +4,7 @@ """ import os -from ..utils.security import safe_traceback, upload_content_ok +from ..utils.security import safe_traceback, safe_error, upload_content_ok import threading from flask import request, jsonify @@ -251,7 +251,7 @@ def generate_ontology(): except Exception as e: return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -499,13 +499,13 @@ def build_graph(): build_logger.debug(safe_traceback()) project.status = ProjectStatus.FAILED - project.error = str(e) + project.error = safe_error(e) ProjectManager.save_project(project) task_manager.update_task( task_id, status=TaskStatus.FAILED, - message=t('progress.buildFailed', error=str(e)), + message=t('progress.buildFailed', error=safe_error(e)), error=safe_traceback() ) @@ -525,7 +525,7 @@ def build_graph(): except Exception as e: return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -590,7 +590,7 @@ def get_graph_data(graph_id: str): except Exception as e: return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -618,6 +618,6 @@ def delete_graph(graph_id: str): except Exception as e: return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 diff --git a/backend/app/api/report.py b/backend/app/api/report.py index b4f2ee07..ccd414dd 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -4,7 +4,7 @@ Report API路由 """ import os -from ..utils.security import safe_traceback +from ..utils.security import safe_traceback, safe_error import threading from flask import request, jsonify, send_file @@ -173,7 +173,7 @@ def generate_report(): except Exception as e: logger.error(f"报告生成失败: {str(e)}") - task_manager.fail_task(task_id, str(e)) + task_manager.fail_task(task_id, safe_error(e)) # 启动后台线程 thread = threading.Thread(target=run_generate, daemon=True) @@ -195,7 +195,7 @@ def generate_report(): logger.error(f"启动报告生成任务失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -268,7 +268,7 @@ def get_generate_status(): logger.error(f"查询任务状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e) + "error": safe_error(e) }), 500 @@ -311,7 +311,7 @@ def get_report(report_id: str): logger.error(f"获取报告失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -350,7 +350,7 @@ def get_report_by_simulation(simulation_id: str): logger.error(f"获取报告失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -390,7 +390,7 @@ def list_reports(): logger.error(f"列出报告失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -436,7 +436,7 @@ def download_report(report_id: str): logger.error(f"下载报告失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -462,7 +462,7 @@ def delete_report(report_id: str): logger.error(f"删除报告失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -559,7 +559,7 @@ def chat_with_report_agent(): logger.error(f"对话失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -602,7 +602,7 @@ def get_report_progress(report_id: str): logger.error(f"获取报告进度失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -653,7 +653,7 @@ def get_report_sections(report_id: str): logger.error(f"获取章节列表失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -697,7 +697,7 @@ def get_single_section(report_id: str, section_index: int): logger.error(f"获取章节内容失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -748,7 +748,7 @@ def check_report_status(simulation_id: str): logger.error(f"检查报告状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -809,7 +809,7 @@ def get_agent_log(report_id: str): logger.error(f"获取Agent日志失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -843,7 +843,7 @@ def stream_agent_log(report_id: str): logger.error(f"获取Agent日志失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -891,7 +891,7 @@ def get_console_log(report_id: str): logger.error(f"获取控制台日志失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -925,7 +925,7 @@ def stream_console_log(report_id: str): logger.error(f"获取控制台日志失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -975,7 +975,7 @@ def search_graph_tool(): logger.error(f"图谱搜索失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1015,6 +1015,6 @@ def get_graph_statistics_tool(): logger.error(f"获取图谱统计失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 7526b8e6..f76b2fc9 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -4,7 +4,7 @@ Step2: Zep实体读取与过滤、OASIS模拟准备与运行(全程自动化 """ import os -from ..utils.security import safe_traceback +from ..utils.security import safe_traceback, safe_error from flask import request, jsonify, send_file from . import simulation_bp @@ -85,7 +85,7 @@ def get_graph_entities(graph_id: str): logger.error(f"获取图谱实体失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -118,7 +118,7 @@ def get_entity_detail(graph_id: str, entity_uuid: str): logger.error(f"获取实体详情失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -155,7 +155,7 @@ def get_entities_by_type(graph_id: str, entity_type: str): logger.error(f"获取实体失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -232,7 +232,7 @@ def create_simulation(): logger.error(f"创建模拟失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -597,14 +597,14 @@ def prepare_simulation(): ) except Exception as e: - logger.error(f"准备模拟失败: {str(e)}") - task_manager.fail_task(task_id, str(e)) + logger.error(f"准备模拟失败: {e}", exc_info=True) + task_manager.fail_task(task_id, safe_error(e)) # 更新模拟状态为失败 state = manager.get_simulation(simulation_id) if state: state.status = SimulationStatus.FAILED - state.error = str(e) + state.error = safe_error(e) manager._save_simulation_state(state) # 启动后台线程 @@ -634,7 +634,7 @@ def prepare_simulation(): logger.error(f"启动准备任务失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -748,7 +748,7 @@ def get_prepare_status(): logger.error(f"查询任务状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e) + "error": safe_error(e) }), 500 @@ -780,7 +780,7 @@ def get_simulation(simulation_id: str): logger.error(f"获取模拟状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -809,7 +809,7 @@ def list_simulations(): logger.error(f"列出模拟失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -982,7 +982,7 @@ def get_simulation_history(): logger.error(f"获取历史模拟失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1020,7 +1020,7 @@ def get_simulation_profiles(simulation_id: str): logger.error(f"获取Profile失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1130,7 +1130,7 @@ def get_simulation_profiles_realtime(simulation_id: str): logger.error(f"实时获取Profile失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1250,7 +1250,7 @@ def get_simulation_config_realtime(simulation_id: str): logger.error(f"实时获取Config失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1286,7 +1286,7 @@ def get_simulation_config(simulation_id: str): logger.error(f"获取配置失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1315,7 +1315,7 @@ def download_simulation_config(simulation_id: str): logger.error(f"下载配置失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1367,7 +1367,7 @@ def download_simulation_script(script_name: str): logger.error(f"下载脚本失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1441,7 +1441,7 @@ def generate_profiles(): logger.error(f"生成Profile失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1636,7 +1636,7 @@ def start_simulation(): logger.error(f"启动模拟失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1695,7 +1695,7 @@ def stop_simulation(): logger.error(f"停止模拟失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1755,7 +1755,7 @@ def get_run_status(simulation_id: str): logger.error(f"获取运行状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1856,7 +1856,7 @@ def get_run_status_detail(simulation_id: str): logger.error(f"获取详细状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1910,7 +1910,7 @@ def get_simulation_actions(simulation_id: str): logger.error(f"获取动作历史失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1950,7 +1950,7 @@ def get_simulation_timeline(simulation_id: str): logger.error(f"获取时间线失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -1977,7 +1977,7 @@ def get_agent_stats(simulation_id: str): logger.error(f"获取Agent统计失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2057,7 +2057,7 @@ def get_simulation_posts(simulation_id: str): logger.error(f"获取帖子失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2132,7 +2132,7 @@ def get_simulation_comments(simulation_id: str): logger.error(f"获取评论失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2263,7 +2263,7 @@ def interview_agent(): logger.error(f"Interview失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2401,7 +2401,7 @@ def interview_agents_batch(): logger.error(f"批量Interview失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2504,7 +2504,7 @@ def interview_all_agents(): logger.error(f"全局Interview失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2576,7 +2576,7 @@ def get_interview_history(): logger.error(f"获取Interview历史失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2641,7 +2641,7 @@ def get_env_status(): logger.error(f"获取环境状态失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 @@ -2711,6 +2711,6 @@ def close_simulation_env(): logger.error(f"关闭环境失败: {str(e)}") return jsonify({ "success": False, - "error": str(e), + "error": safe_error(e), "traceback": safe_traceback() }), 500 diff --git a/backend/app/services/graph_builder.py b/backend/app/services/graph_builder.py index 37c9969c..80f9cc6f 100644 --- a/backend/app/services/graph_builder.py +++ b/backend/app/services/graph_builder.py @@ -18,6 +18,10 @@ from ..models.task import TaskManager, TaskStatus from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges from .text_processor import TextProcessor from ..utils.locale import t, get_locale, set_locale +from ..utils.logger import get_logger +from ..utils.security import safe_error + +logger = get_logger('mirofish.graph_builder') @dataclass @@ -186,9 +190,8 @@ class GraphBuilderService: }) except Exception as e: - import traceback - error_msg = f"{str(e)}\n{traceback.format_exc()}" - self.task_manager.fail_task(task_id, error_msg) + logger.error(f"graph build failed (task {task_id})", exc_info=True) + self.task_manager.fail_task(task_id, safe_error(e)) def create_graph(self, name: str) -> str: """创建Zep图谱(公开方法)""" @@ -339,7 +342,7 @@ class GraphBuilderService: except Exception as e: if progress_callback: - progress_callback(t('progress.batchFailed', batch=batch_num, error=str(e)), 0) + progress_callback(t('progress.batchFailed', batch=batch_num, error=safe_error(e)), 0) raise return episode_uuids diff --git a/backend/app/utils/security.py b/backend/app/utils/security.py index 31133eb3..bf0af2d2 100644 --- a/backend/app/utils/security.py +++ b/backend/app/utils/security.py @@ -28,6 +28,16 @@ def safe_traceback() -> str: return tb if Config.DEBUG else 'Internal server error (see server logs)' +def safe_error(e) -> str: + """ + 客户端可见的错误文案:DEBUG 模式返回异常消息,生产环境返回通用提示, + 避免异常消息本身(如 FileNotFoundError 的路径、ValueError 里的配置值)泄露给客户端。 + 完整异常仍由各调用点的 logger.error / safe_traceback 记到服务端日志。 + """ + from ..config import Config + return str(e) if Config.DEBUG else 'Internal server error (see server logs)' + + def validate_id(value: str, kind: str = 'id') -> str: """ 路径校验:拒绝任何不匹配 _ID_RE 的 id(含 '..'、'/'、空值),