From 4b48156e583bef5c065914d8d108b0edf0355d21 Mon Sep 17 00:00:00 2001 From: 666ghj <670939375@qq.com> Date: Wed, 5 Nov 2025 00:24:35 +0800 Subject: [PATCH] Implement comprehensive front-end settings UI. --- app.py | 386 +++++++++++++++++--- templates/index.html | 830 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 1150 insertions(+), 66 deletions(-) diff --git a/app.py b/app.py index 83fcf9a..ff7006e 100644 --- a/app.py +++ b/app.py @@ -16,6 +16,8 @@ import signal import atexit import requests import logging +import importlib +import re from pathlib import Path # 导入ReportEngine @@ -45,6 +47,217 @@ os.environ['PYTHONUTF8'] = '1' LOG_DIR = Path('logs') LOG_DIR.mkdir(exist_ok=True) +CONFIG_MODULE_NAME = 'config' +CONFIG_FILE_PATH = Path(__file__).resolve().parent / 'config.py' +CONFIG_KEYS = [ + 'DB_HOST', + 'DB_PORT', + 'DB_USER', + 'DB_PASSWORD', + 'DB_NAME', + 'DB_CHARSET', + 'INSIGHT_ENGINE_API_KEY', + 'INSIGHT_ENGINE_BASE_URL', + 'INSIGHT_ENGINE_MODEL_NAME', + 'MEDIA_ENGINE_API_KEY', + 'MEDIA_ENGINE_BASE_URL', + 'MEDIA_ENGINE_MODEL_NAME', + 'QUERY_ENGINE_API_KEY', + 'QUERY_ENGINE_BASE_URL', + 'QUERY_ENGINE_MODEL_NAME', + 'REPORT_ENGINE_API_KEY', + 'REPORT_ENGINE_BASE_URL', + 'REPORT_ENGINE_MODEL_NAME', + 'FORUM_HOST_API_KEY', + 'FORUM_HOST_BASE_URL', + 'FORUM_HOST_MODEL_NAME', + 'KEYWORD_OPTIMIZER_API_KEY', + 'KEYWORD_OPTIMIZER_BASE_URL', + 'KEYWORD_OPTIMIZER_MODEL_NAME', + 'TAVILY_API_KEY', + 'BOCHA_WEB_SEARCH_API_KEY' +] + + +def _load_config_module(): + """Load or reload the config module to ensure latest values are available.""" + importlib.invalidate_caches() + module = sys.modules.get(CONFIG_MODULE_NAME) + try: + if module is None: + module = importlib.import_module(CONFIG_MODULE_NAME) + else: + module = importlib.reload(module) + except ModuleNotFoundError: + return None + return module + + +def read_config_values(): + """Return the current configuration values that are exposed to the frontend.""" + module = _load_config_module() + if not module: + return {} + + values = {} + for key in CONFIG_KEYS: + value = getattr(module, key, '') + # Convert to string for uniform handling on the frontend. + if value is None: + values[key] = '' + else: + values[key] = str(value) + return values + + +def _serialize_config_value(value): + """Serialize Python values back to a config.py assignment-friendly string.""" + if isinstance(value, bool): + return 'True' if value else 'False' + if isinstance(value, (int, float)): + return str(value) + if value is None: + return 'None' + + value_str = str(value) + escaped = value_str.replace('\\', '\\\\').replace('"', '\\"') + return f'"{escaped}"' + + +def write_config_values(updates): + """Persist configuration updates into config.py.""" + if not CONFIG_FILE_PATH.exists(): + raise FileNotFoundError("配置文件 config.py 不存在") + + content = CONFIG_FILE_PATH.read_text(encoding='utf-8') + + for key, raw_value in updates.items(): + formatted_value = _serialize_config_value(raw_value) + pattern = re.compile( + rf'^(\s*{key}\s*=\s*)(["\'].*?["\']|None|True|False|[0-9\.-]+)(.*)$', + re.MULTILINE + ) + + def replace(match): + prefix, _, suffix = match.groups() + return f"{prefix}{formatted_value}{suffix}" + + new_content, count = pattern.subn(replace, content, count=1) + + if count == 0: + # Append the new key if it was not present. + if not new_content.endswith('\n'): + new_content += '\n' + new_content += f'{key} = {formatted_value}\n' + + content = new_content + + CONFIG_FILE_PATH.write_text(content, encoding='utf-8') + # Reload the module so the rest of the app observes the new values when possible. + _load_config_module() + + +system_state_lock = threading.Lock() +system_state = { + 'started': False, + 'starting': False +} + + +def _set_system_state(*, started=None, starting=None): + """Safely update the cached system state flags.""" + with system_state_lock: + if started is not None: + system_state['started'] = started + if starting is not None: + system_state['starting'] = starting + + +def _get_system_state(): + """Return a shallow copy of the system state flags.""" + with system_state_lock: + return system_state.copy() + + +def _prepare_system_start(): + """Mark the system as starting if it is not already running or starting.""" + with system_state_lock: + if system_state['started']: + return False, '系统已启动' + if system_state['starting']: + return False, '系统正在启动' + system_state['starting'] = True + return True, None + + +def initialize_system_components(): + """启动所有依赖组件(Streamlit 子应用、ForumEngine、ReportEngine)。""" + logs = [] + errors = [] + + try: + stop_forum_engine() + logs.append("已停止 ForumEngine 监控器以避免文件冲突") + except Exception as exc: # pragma: no cover - 安全捕获 + message = f"停止 ForumEngine 时发生异常: {exc}" + logs.append(message) + logging.exception(message) + + processes['forum']['status'] = 'stopped' + + for app_name, script_path in STREAMLIT_SCRIPTS.items(): + logs.append(f"检查文件: {script_path}") + if os.path.exists(script_path): + success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port']) + logs.append(f"{app_name}: {message}") + if success: + startup_success, startup_message = wait_for_app_startup(app_name, 30) + logs.append(f"{app_name} 启动检查: {startup_message}") + if not startup_success: + errors.append(f"{app_name} 启动失败: {startup_message}") + else: + errors.append(f"{app_name} 启动失败: {message}") + else: + msg = f"文件不存在: {script_path}" + logs.append(f"错误: {msg}") + errors.append(f"{app_name}: {msg}") + + forum_started = False + try: + start_forum_engine() + processes['forum']['status'] = 'running' + logs.append("ForumEngine 启动完成") + forum_started = True + except Exception as exc: # pragma: no cover - 保底捕获 + error_msg = f"ForumEngine 启动失败: {exc}" + logs.append(error_msg) + errors.append(error_msg) + + if REPORT_ENGINE_AVAILABLE: + try: + if initialize_report_engine(): + logs.append("ReportEngine 初始化成功") + else: + msg = "ReportEngine 初始化失败" + logs.append(msg) + errors.append(msg) + except Exception as exc: # pragma: no cover + msg = f"ReportEngine 初始化异常: {exc}" + logs.append(msg) + errors.append(msg) + + if errors: + cleanup_processes() + processes['forum']['status'] = 'stopped' + if forum_started: + try: + stop_forum_engine() + except Exception: # pragma: no cover + logging.exception("停止ForumEngine失败") + return False, logs, errors + + return True, logs, [] + # 初始化ForumEngine的forum.log文件 def init_forum_log(): """初始化forum.log文件""" @@ -195,7 +408,13 @@ processes = { 'insight': {'process': None, 'port': 8501, 'status': 'stopped', 'output': [], 'log_file': None}, 'media': {'process': None, 'port': 8502, 'status': 'stopped', 'output': [], 'log_file': None}, 'query': {'process': None, 'port': 8503, 'status': 'stopped', 'output': [], 'log_file': None}, - 'forum': {'process': None, 'port': None, 'status': 'running', 'output': [], 'log_file': None} # Forum始终运行 + 'forum': {'process': None, 'port': None, 'status': 'stopped', 'output': [], 'log_file': None} # 启动后标记为 running +} + +STREAMLIT_SCRIPTS = { + 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py', + 'media': 'SingleEngineApp/media_engine_streamlit_app.py', + 'query': 'SingleEngineApp/query_engine_streamlit_app.py' } # 输出队列 @@ -449,8 +668,15 @@ def wait_for_app_startup(app_name, max_wait_time=30): def cleanup_processes(): """清理所有进程""" - for app_name in processes: + for app_name in STREAMLIT_SCRIPTS: stop_streamlit_app(app_name) + + processes['forum']['status'] = 'stopped' + try: + stop_forum_engine() + except Exception: # pragma: no cover + logging.exception("停止ForumEngine失败") + _set_system_state(started=False, starting=False) # 注册清理函数 atexit.register(cleanup_processes) @@ -478,20 +704,26 @@ def start_app(app_name): """启动指定应用""" if app_name not in processes: return jsonify({'success': False, 'message': '未知应用'}) - - script_paths = { - 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py', - 'media': 'SingleEngineApp/media_engine_streamlit_app.py', - 'query': 'SingleEngineApp/query_engine_streamlit_app.py' - } - + + if app_name == 'forum': + try: + start_forum_engine() + processes['forum']['status'] = 'running' + return jsonify({'success': True, 'message': 'ForumEngine已启动'}) + except Exception as exc: # pragma: no cover + logging.exception("手动启动ForumEngine失败") + return jsonify({'success': False, 'message': f'ForumEngine启动失败: {exc}'}) + + script_path = STREAMLIT_SCRIPTS.get(app_name) + if not script_path: + return jsonify({'success': False, 'message': '该应用不支持启动操作'}) + success, message = start_streamlit_app( - app_name, - script_paths[app_name], + app_name, + script_path, processes[app_name]['port'] ) - - + if success: # 等待应用启动 startup_success, startup_message = wait_for_app_startup(app_name, 15) @@ -505,7 +737,16 @@ def stop_app(app_name): """停止指定应用""" if app_name not in processes: return jsonify({'success': False, 'message': '未知应用'}) - + + if app_name == 'forum': + try: + stop_forum_engine() + processes['forum']['status'] = 'stopped' + return jsonify({'success': True, 'message': 'ForumEngine已停止'}) + except Exception as exc: # pragma: no cover + logging.exception("手动停止ForumEngine失败") + return jsonify({'success': False, 'message': f'ForumEngine停止失败: {exc}'}) + success, message = stop_streamlit_app(app_name) return jsonify({'success': success, 'message': message}) @@ -660,6 +901,80 @@ def search(): 'results': results }) + +@app.route('/api/config', methods=['GET']) +def get_config(): + """Expose selected configuration values to the frontend.""" + try: + config_values = read_config_values() + return jsonify({'success': True, 'config': config_values}) + except Exception as exc: + logging.exception("读取配置失败") + return jsonify({'success': False, 'message': f'读取配置失败: {exc}'}), 500 + + +@app.route('/api/config', methods=['POST']) +def update_config(): + """Update configuration values and persist them to config.py.""" + payload = request.get_json(silent=True) or {} + if not isinstance(payload, dict) or not payload: + return jsonify({'success': False, 'message': '请求体不能为空'}), 400 + + updates = {} + for key, value in payload.items(): + if key in CONFIG_KEYS: + updates[key] = value if value is not None else '' + + if not updates: + return jsonify({'success': False, 'message': '没有可更新的配置项'}), 400 + + try: + write_config_values(updates) + updated_config = read_config_values() + return jsonify({'success': True, 'config': updated_config}) + except Exception as exc: + logging.exception("更新配置失败") + return jsonify({'success': False, 'message': f'更新配置失败: {exc}'}), 500 + + +@app.route('/api/system/status') +def get_system_status(): + """返回系统启动状态。""" + state = _get_system_state() + return jsonify({ + 'success': True, + 'started': state['started'], + 'starting': state['starting'] + }) + + +@app.route('/api/system/start', methods=['POST']) +def start_system(): + """在接收到请求后启动完整系统。""" + allowed, message = _prepare_system_start() + if not allowed: + return jsonify({'success': False, 'message': message}), 400 + + try: + success, logs, errors = initialize_system_components() + if success: + _set_system_state(started=True) + return jsonify({'success': True, 'message': '系统启动成功', 'logs': logs}) + + _set_system_state(started=False) + return jsonify({ + 'success': False, + 'message': '系统启动失败', + 'logs': logs, + 'errors': errors + }), 500 + except Exception as exc: # pragma: no cover - 保底捕获 + logging.exception("系统启动过程中出现异常") + _set_system_state(started=False) + return jsonify({'success': False, 'message': f'系统启动异常: {exc}'}), 500 + finally: + _set_system_state(starting=False) + @socketio.on('connect') def handle_connect(): """客户端连接""" @@ -678,51 +993,12 @@ def handle_status_request(): }) if __name__ == '__main__': - # 启动时自动启动所有Streamlit应用 - print("正在启动Streamlit应用...") - - # 先停止ForumEngine监控器,避免文件占用冲突 - print("停止ForumEngine监控器以避免文件冲突...") - stop_forum_engine() - - script_paths = { - 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py', - 'media': 'SingleEngineApp/media_engine_streamlit_app.py', - 'query': 'SingleEngineApp/query_engine_streamlit_app.py' - } - - for app_name, script_path in script_paths.items(): - print(f"检查文件: {script_path}") - if os.path.exists(script_path): - print(f"启动 {app_name}...") - success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port']) - print(f"{app_name}: {message}") - - if success: - print(f"等待 {app_name} 启动完成...") - startup_success, startup_message = wait_for_app_startup(app_name, 30) - print(f"{app_name} 启动检查: {startup_message}") - else: - print(f"错误: {script_path} 不存在") - - start_forum_engine() - - # 初始化ReportEngine - if REPORT_ENGINE_AVAILABLE: - print("初始化ReportEngine...") - if initialize_report_engine(): - print("ReportEngine初始化成功") - print("ReportEngine文件基准已建立,开始监控文件变化") - else: - print("ReportEngine初始化失败") - + print("等待配置确认,系统将在前端指令后启动组件...") print("启动Flask服务器...") - + try: - # 启动Flask应用 socketio.run(app, host='0.0.0.0', port=5000, debug=False) except KeyboardInterrupt: print("\n正在关闭应用...") cleanup_processes() - diff --git a/templates/index.html b/templates/index.html index 0eda843..592b8bd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -44,10 +44,63 @@ letter-spacing: 1px; } + .search-row { + display: flex; + align-items: stretch; + gap: 12px; + max-width: 950px; + margin: 0 auto 10px; + } + + .config-button { + display: flex; + align-items: center; + justify-content: center; + padding: 0 24px; + border: 2px solid #000000; + background-color: #ffffff; + color: #000000; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.3s ease; + min-width: 120px; + } + + .config-button:hover { + background-color: #000000; + color: #ffffff; + } + + .config-password-wrapper { + display: flex; + align-items: center; + gap: 8px; + } + + .config-password-wrapper .config-field-input { + flex: 1; + } + + .config-password-toggle { + padding: 8px 14px; + border: 2px solid #000000; + background-color: #ffffff; + cursor: pointer; + font-size: 12px; + font-weight: bold; + transition: all 0.3s ease; + } + + .config-password-toggle:hover, + .config-password-toggle.revealed { + background-color: #000000; + color: #ffffff; + } + .search-box { display: flex; - max-width: 800px; - margin: 0 auto; + flex: 1; border: 2px solid #000000; } @@ -111,9 +164,10 @@ .upload-status { font-size: 12px; - margin-top: 10px; + margin: 10px auto 0; text-align: center; color: #666666; + max-width: 950px; } .upload-status.success { @@ -268,6 +322,207 @@ align-items: center; } + .config-modal-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.35); + display: none; + align-items: center; + justify-content: center; + z-index: 999; + padding: 20px; + } + + .config-modal-overlay.visible { + display: flex; + } + + .config-modal { + background-color: #ffffff; + border: 2px solid #000000; + width: 720px; + max-width: 90vw; + max-height: 85vh; + display: flex; + flex-direction: column; + box-shadow: 6px 6px 0 #000000; + } + + .config-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 2px solid #000000; + background-color: #ffffff; + } + + .config-modal-title { + font-size: 18px; + font-weight: bold; + } + + .config-modal-actions { + display: flex; + gap: 10px; + align-items: center; + } + + .config-close-button { + width: 32px; + height: 32px; + border: 2px solid #000000; + background-color: #ffffff; + font-size: 18px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + + .config-close-button:hover { + background-color: #000000; + color: #ffffff; + } + + .config-close-button:disabled { + opacity: 0.4; + cursor: not-allowed; + background-color: #f0f0f0; + color: #666666; + } + + .config-secondary-button { + padding: 8px 18px; + border: 2px solid #000000; + background-color: #ffffff; + color: #000000; + cursor: pointer; + font-size: 13px; + font-weight: bold; + transition: all 0.3s ease; + } + + .config-secondary-button:hover { + background-color: #f0f0f0; + } + + .config-modal-body { + padding: 20px; + overflow-y: auto; + } + + .config-group { + border: 2px solid #000000; + padding: 16px; + margin-bottom: 16px; + background-color: #ffffff; + } + + .config-group-title { + font-size: 15px; + font-weight: bold; + margin-bottom: 10px; + } + + .config-group-subtitle { + font-size: 12px; + color: #555555; + margin-bottom: 12px; + } + + .config-field { + display: flex; + flex-direction: column; + margin-bottom: 12px; + } + + .config-field-label { + font-size: 12px; + font-weight: bold; + margin-bottom: 6px; + } + + .config-field-input { + padding: 10px 12px; + border: 2px solid #000000; + font-size: 14px; + background-color: #ffffff; + } + + .config-field-input:focus { + outline: none; + border-color: #333333; + } + + .config-modal-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-top: 2px solid #000000; + background-color: #ffffff; + } + + .config-modal-footer-actions { + display: flex; + gap: 10px; + } + + .config-status-message { + font-size: 12px; + color: #555555; + } + + .config-status-message.error { + color: #8b4513; + } + + .config-status-message.success { + color: #4a6741; + } + + .config-save-button { + padding: 10px 24px; + border: none; + background-color: #000000; + color: #ffffff; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.3s ease; + } + + .config-save-button:hover { + background-color: #333333; + } + + .config-save-button:disabled { + background-color: #666666; + cursor: not-allowed; + } + + .config-start-button { + padding: 10px 24px; + border: none; + background-color: #000000; + color: #ffffff; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.3s ease; + } + + .config-start-button:hover { + background-color: #333333; + } + + .config-start-button:disabled { + background-color: #666666; + cursor: not-allowed; + } + .loading { display: inline-block; width: 12px; @@ -752,13 +1007,16 @@