666ghj

Implement comprehensive front-end settings UI.

@@ -16,6 +16,8 @@ import signal @@ -16,6 +16,8 @@ import signal
16 import atexit 16 import atexit
17 import requests 17 import requests
18 import logging 18 import logging
  19 +import importlib
  20 +import re
19 from pathlib import Path 21 from pathlib import Path
20 22
21 # 导入ReportEngine 23 # 导入ReportEngine
@@ -45,6 +47,217 @@ os.environ['PYTHONUTF8'] = '1' @@ -45,6 +47,217 @@ os.environ['PYTHONUTF8'] = '1'
45 LOG_DIR = Path('logs') 47 LOG_DIR = Path('logs')
46 LOG_DIR.mkdir(exist_ok=True) 48 LOG_DIR.mkdir(exist_ok=True)
47 49
  50 +CONFIG_MODULE_NAME = 'config'
  51 +CONFIG_FILE_PATH = Path(__file__).resolve().parent / 'config.py'
  52 +CONFIG_KEYS = [
  53 + 'DB_HOST',
  54 + 'DB_PORT',
  55 + 'DB_USER',
  56 + 'DB_PASSWORD',
  57 + 'DB_NAME',
  58 + 'DB_CHARSET',
  59 + 'INSIGHT_ENGINE_API_KEY',
  60 + 'INSIGHT_ENGINE_BASE_URL',
  61 + 'INSIGHT_ENGINE_MODEL_NAME',
  62 + 'MEDIA_ENGINE_API_KEY',
  63 + 'MEDIA_ENGINE_BASE_URL',
  64 + 'MEDIA_ENGINE_MODEL_NAME',
  65 + 'QUERY_ENGINE_API_KEY',
  66 + 'QUERY_ENGINE_BASE_URL',
  67 + 'QUERY_ENGINE_MODEL_NAME',
  68 + 'REPORT_ENGINE_API_KEY',
  69 + 'REPORT_ENGINE_BASE_URL',
  70 + 'REPORT_ENGINE_MODEL_NAME',
  71 + 'FORUM_HOST_API_KEY',
  72 + 'FORUM_HOST_BASE_URL',
  73 + 'FORUM_HOST_MODEL_NAME',
  74 + 'KEYWORD_OPTIMIZER_API_KEY',
  75 + 'KEYWORD_OPTIMIZER_BASE_URL',
  76 + 'KEYWORD_OPTIMIZER_MODEL_NAME',
  77 + 'TAVILY_API_KEY',
  78 + 'BOCHA_WEB_SEARCH_API_KEY'
  79 +]
  80 +
  81 +
  82 +def _load_config_module():
  83 + """Load or reload the config module to ensure latest values are available."""
  84 + importlib.invalidate_caches()
  85 + module = sys.modules.get(CONFIG_MODULE_NAME)
  86 + try:
  87 + if module is None:
  88 + module = importlib.import_module(CONFIG_MODULE_NAME)
  89 + else:
  90 + module = importlib.reload(module)
  91 + except ModuleNotFoundError:
  92 + return None
  93 + return module
  94 +
  95 +
  96 +def read_config_values():
  97 + """Return the current configuration values that are exposed to the frontend."""
  98 + module = _load_config_module()
  99 + if not module:
  100 + return {}
  101 +
  102 + values = {}
  103 + for key in CONFIG_KEYS:
  104 + value = getattr(module, key, '')
  105 + # Convert to string for uniform handling on the frontend.
  106 + if value is None:
  107 + values[key] = ''
  108 + else:
  109 + values[key] = str(value)
  110 + return values
  111 +
  112 +
  113 +def _serialize_config_value(value):
  114 + """Serialize Python values back to a config.py assignment-friendly string."""
  115 + if isinstance(value, bool):
  116 + return 'True' if value else 'False'
  117 + if isinstance(value, (int, float)):
  118 + return str(value)
  119 + if value is None:
  120 + return 'None'
  121 +
  122 + value_str = str(value)
  123 + escaped = value_str.replace('\\', '\\\\').replace('"', '\\"')
  124 + return f'"{escaped}"'
  125 +
  126 +
  127 +def write_config_values(updates):
  128 + """Persist configuration updates into config.py."""
  129 + if not CONFIG_FILE_PATH.exists():
  130 + raise FileNotFoundError("配置文件 config.py 不存在")
  131 +
  132 + content = CONFIG_FILE_PATH.read_text(encoding='utf-8')
  133 +
  134 + for key, raw_value in updates.items():
  135 + formatted_value = _serialize_config_value(raw_value)
  136 + pattern = re.compile(
  137 + rf'^(\s*{key}\s*=\s*)(["\'].*?["\']|None|True|False|[0-9\.-]+)(.*)$',
  138 + re.MULTILINE
  139 + )
  140 +
  141 + def replace(match):
  142 + prefix, _, suffix = match.groups()
  143 + return f"{prefix}{formatted_value}{suffix}"
  144 +
  145 + new_content, count = pattern.subn(replace, content, count=1)
  146 +
  147 + if count == 0:
  148 + # Append the new key if it was not present.
  149 + if not new_content.endswith('\n'):
  150 + new_content += '\n'
  151 + new_content += f'{key} = {formatted_value}\n'
  152 +
  153 + content = new_content
  154 +
  155 + CONFIG_FILE_PATH.write_text(content, encoding='utf-8')
  156 + # Reload the module so the rest of the app observes the new values when possible.
  157 + _load_config_module()
  158 +
  159 +
  160 +system_state_lock = threading.Lock()
  161 +system_state = {
  162 + 'started': False,
  163 + 'starting': False
  164 +}
  165 +
  166 +
  167 +def _set_system_state(*, started=None, starting=None):
  168 + """Safely update the cached system state flags."""
  169 + with system_state_lock:
  170 + if started is not None:
  171 + system_state['started'] = started
  172 + if starting is not None:
  173 + system_state['starting'] = starting
  174 +
  175 +
  176 +def _get_system_state():
  177 + """Return a shallow copy of the system state flags."""
  178 + with system_state_lock:
  179 + return system_state.copy()
  180 +
  181 +
  182 +def _prepare_system_start():
  183 + """Mark the system as starting if it is not already running or starting."""
  184 + with system_state_lock:
  185 + if system_state['started']:
  186 + return False, '系统已启动'
  187 + if system_state['starting']:
  188 + return False, '系统正在启动'
  189 + system_state['starting'] = True
  190 + return True, None
  191 +
  192 +
  193 +def initialize_system_components():
  194 + """启动所有依赖组件(Streamlit 子应用、ForumEngine、ReportEngine)。"""
  195 + logs = []
  196 + errors = []
  197 +
  198 + try:
  199 + stop_forum_engine()
  200 + logs.append("已停止 ForumEngine 监控器以避免文件冲突")
  201 + except Exception as exc: # pragma: no cover - 安全捕获
  202 + message = f"停止 ForumEngine 时发生异常: {exc}"
  203 + logs.append(message)
  204 + logging.exception(message)
  205 +
  206 + processes['forum']['status'] = 'stopped'
  207 +
  208 + for app_name, script_path in STREAMLIT_SCRIPTS.items():
  209 + logs.append(f"检查文件: {script_path}")
  210 + if os.path.exists(script_path):
  211 + success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port'])
  212 + logs.append(f"{app_name}: {message}")
  213 + if success:
  214 + startup_success, startup_message = wait_for_app_startup(app_name, 30)
  215 + logs.append(f"{app_name} 启动检查: {startup_message}")
  216 + if not startup_success:
  217 + errors.append(f"{app_name} 启动失败: {startup_message}")
  218 + else:
  219 + errors.append(f"{app_name} 启动失败: {message}")
  220 + else:
  221 + msg = f"文件不存在: {script_path}"
  222 + logs.append(f"错误: {msg}")
  223 + errors.append(f"{app_name}: {msg}")
  224 +
  225 + forum_started = False
  226 + try:
  227 + start_forum_engine()
  228 + processes['forum']['status'] = 'running'
  229 + logs.append("ForumEngine 启动完成")
  230 + forum_started = True
  231 + except Exception as exc: # pragma: no cover - 保底捕获
  232 + error_msg = f"ForumEngine 启动失败: {exc}"
  233 + logs.append(error_msg)
  234 + errors.append(error_msg)
  235 +
  236 + if REPORT_ENGINE_AVAILABLE:
  237 + try:
  238 + if initialize_report_engine():
  239 + logs.append("ReportEngine 初始化成功")
  240 + else:
  241 + msg = "ReportEngine 初始化失败"
  242 + logs.append(msg)
  243 + errors.append(msg)
  244 + except Exception as exc: # pragma: no cover
  245 + msg = f"ReportEngine 初始化异常: {exc}"
  246 + logs.append(msg)
  247 + errors.append(msg)
  248 +
  249 + if errors:
  250 + cleanup_processes()
  251 + processes['forum']['status'] = 'stopped'
  252 + if forum_started:
  253 + try:
  254 + stop_forum_engine()
  255 + except Exception: # pragma: no cover
  256 + logging.exception("停止ForumEngine失败")
  257 + return False, logs, errors
  258 +
  259 + return True, logs, []
  260 +
48 # 初始化ForumEngine的forum.log文件 261 # 初始化ForumEngine的forum.log文件
49 def init_forum_log(): 262 def init_forum_log():
50 """初始化forum.log文件""" 263 """初始化forum.log文件"""
@@ -195,7 +408,13 @@ processes = { @@ -195,7 +408,13 @@ processes = {
195 'insight': {'process': None, 'port': 8501, 'status': 'stopped', 'output': [], 'log_file': None}, 408 'insight': {'process': None, 'port': 8501, 'status': 'stopped', 'output': [], 'log_file': None},
196 'media': {'process': None, 'port': 8502, 'status': 'stopped', 'output': [], 'log_file': None}, 409 'media': {'process': None, 'port': 8502, 'status': 'stopped', 'output': [], 'log_file': None},
197 'query': {'process': None, 'port': 8503, 'status': 'stopped', 'output': [], 'log_file': None}, 410 'query': {'process': None, 'port': 8503, 'status': 'stopped', 'output': [], 'log_file': None},
198 - 'forum': {'process': None, 'port': None, 'status': 'running', 'output': [], 'log_file': None} # Forum始终运行 411 + 'forum': {'process': None, 'port': None, 'status': 'stopped', 'output': [], 'log_file': None} # 启动后标记为 running
  412 +}
  413 +
  414 +STREAMLIT_SCRIPTS = {
  415 + 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',
  416 + 'media': 'SingleEngineApp/media_engine_streamlit_app.py',
  417 + 'query': 'SingleEngineApp/query_engine_streamlit_app.py'
199 } 418 }
200 419
201 # 输出队列 420 # 输出队列
@@ -449,8 +668,15 @@ def wait_for_app_startup(app_name, max_wait_time=30): @@ -449,8 +668,15 @@ def wait_for_app_startup(app_name, max_wait_time=30):
449 668
450 def cleanup_processes(): 669 def cleanup_processes():
451 """清理所有进程""" 670 """清理所有进程"""
452 - for app_name in processes: 671 + for app_name in STREAMLIT_SCRIPTS:
453 stop_streamlit_app(app_name) 672 stop_streamlit_app(app_name)
  673 +
  674 + processes['forum']['status'] = 'stopped'
  675 + try:
  676 + stop_forum_engine()
  677 + except Exception: # pragma: no cover
  678 + logging.exception("停止ForumEngine失败")
  679 + _set_system_state(started=False, starting=False)
454 680
455 # 注册清理函数 681 # 注册清理函数
456 atexit.register(cleanup_processes) 682 atexit.register(cleanup_processes)
@@ -478,20 +704,26 @@ def start_app(app_name): @@ -478,20 +704,26 @@ def start_app(app_name):
478 """启动指定应用""" 704 """启动指定应用"""
479 if app_name not in processes: 705 if app_name not in processes:
480 return jsonify({'success': False, 'message': '未知应用'}) 706 return jsonify({'success': False, 'message': '未知应用'})
481 -  
482 - script_paths = {  
483 - 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',  
484 - 'media': 'SingleEngineApp/media_engine_streamlit_app.py',  
485 - 'query': 'SingleEngineApp/query_engine_streamlit_app.py'  
486 - }  
487 - 707 +
  708 + if app_name == 'forum':
  709 + try:
  710 + start_forum_engine()
  711 + processes['forum']['status'] = 'running'
  712 + return jsonify({'success': True, 'message': 'ForumEngine已启动'})
  713 + except Exception as exc: # pragma: no cover
  714 + logging.exception("手动启动ForumEngine失败")
  715 + return jsonify({'success': False, 'message': f'ForumEngine启动失败: {exc}'})
  716 +
  717 + script_path = STREAMLIT_SCRIPTS.get(app_name)
  718 + if not script_path:
  719 + return jsonify({'success': False, 'message': '该应用不支持启动操作'})
  720 +
488 success, message = start_streamlit_app( 721 success, message = start_streamlit_app(
489 - app_name,  
490 - script_paths[app_name], 722 + app_name,
  723 + script_path,
491 processes[app_name]['port'] 724 processes[app_name]['port']
492 ) 725 )
493 -  
494 - 726 +
495 if success: 727 if success:
496 # 等待应用启动 728 # 等待应用启动
497 startup_success, startup_message = wait_for_app_startup(app_name, 15) 729 startup_success, startup_message = wait_for_app_startup(app_name, 15)
@@ -505,7 +737,16 @@ def stop_app(app_name): @@ -505,7 +737,16 @@ def stop_app(app_name):
505 """停止指定应用""" 737 """停止指定应用"""
506 if app_name not in processes: 738 if app_name not in processes:
507 return jsonify({'success': False, 'message': '未知应用'}) 739 return jsonify({'success': False, 'message': '未知应用'})
508 - 740 +
  741 + if app_name == 'forum':
  742 + try:
  743 + stop_forum_engine()
  744 + processes['forum']['status'] = 'stopped'
  745 + return jsonify({'success': True, 'message': 'ForumEngine已停止'})
  746 + except Exception as exc: # pragma: no cover
  747 + logging.exception("手动停止ForumEngine失败")
  748 + return jsonify({'success': False, 'message': f'ForumEngine停止失败: {exc}'})
  749 +
509 success, message = stop_streamlit_app(app_name) 750 success, message = stop_streamlit_app(app_name)
510 return jsonify({'success': success, 'message': message}) 751 return jsonify({'success': success, 'message': message})
511 752
@@ -660,6 +901,80 @@ def search(): @@ -660,6 +901,80 @@ def search():
660 'results': results 901 'results': results
661 }) 902 })
662 903
  904 +
  905 +@app.route('/api/config', methods=['GET'])
  906 +def get_config():
  907 + """Expose selected configuration values to the frontend."""
  908 + try:
  909 + config_values = read_config_values()
  910 + return jsonify({'success': True, 'config': config_values})
  911 + except Exception as exc:
  912 + logging.exception("读取配置失败")
  913 + return jsonify({'success': False, 'message': f'读取配置失败: {exc}'}), 500
  914 +
  915 +
  916 +@app.route('/api/config', methods=['POST'])
  917 +def update_config():
  918 + """Update configuration values and persist them to config.py."""
  919 + payload = request.get_json(silent=True) or {}
  920 + if not isinstance(payload, dict) or not payload:
  921 + return jsonify({'success': False, 'message': '请求体不能为空'}), 400
  922 +
  923 + updates = {}
  924 + for key, value in payload.items():
  925 + if key in CONFIG_KEYS:
  926 + updates[key] = value if value is not None else ''
  927 +
  928 + if not updates:
  929 + return jsonify({'success': False, 'message': '没有可更新的配置项'}), 400
  930 +
  931 + try:
  932 + write_config_values(updates)
  933 + updated_config = read_config_values()
  934 + return jsonify({'success': True, 'config': updated_config})
  935 + except Exception as exc:
  936 + logging.exception("更新配置失败")
  937 + return jsonify({'success': False, 'message': f'更新配置失败: {exc}'}), 500
  938 +
  939 +
  940 +@app.route('/api/system/status')
  941 +def get_system_status():
  942 + """返回系统启动状态。"""
  943 + state = _get_system_state()
  944 + return jsonify({
  945 + 'success': True,
  946 + 'started': state['started'],
  947 + 'starting': state['starting']
  948 + })
  949 +
  950 +
  951 +@app.route('/api/system/start', methods=['POST'])
  952 +def start_system():
  953 + """在接收到请求后启动完整系统。"""
  954 + allowed, message = _prepare_system_start()
  955 + if not allowed:
  956 + return jsonify({'success': False, 'message': message}), 400
  957 +
  958 + try:
  959 + success, logs, errors = initialize_system_components()
  960 + if success:
  961 + _set_system_state(started=True)
  962 + return jsonify({'success': True, 'message': '系统启动成功', 'logs': logs})
  963 +
  964 + _set_system_state(started=False)
  965 + return jsonify({
  966 + 'success': False,
  967 + 'message': '系统启动失败',
  968 + 'logs': logs,
  969 + 'errors': errors
  970 + }), 500
  971 + except Exception as exc: # pragma: no cover - 保底捕获
  972 + logging.exception("系统启动过程中出现异常")
  973 + _set_system_state(started=False)
  974 + return jsonify({'success': False, 'message': f'系统启动异常: {exc}'}), 500
  975 + finally:
  976 + _set_system_state(starting=False)
  977 +
663 @socketio.on('connect') 978 @socketio.on('connect')
664 def handle_connect(): 979 def handle_connect():
665 """客户端连接""" 980 """客户端连接"""
@@ -678,51 +993,12 @@ def handle_status_request(): @@ -678,51 +993,12 @@ def handle_status_request():
678 }) 993 })
679 994
680 if __name__ == '__main__': 995 if __name__ == '__main__':
681 - # 启动时自动启动所有Streamlit应用  
682 - print("正在启动Streamlit应用...")  
683 -  
684 - # 先停止ForumEngine监控器,避免文件占用冲突  
685 - print("停止ForumEngine监控器以避免文件冲突...")  
686 - stop_forum_engine()  
687 -  
688 - script_paths = {  
689 - 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',  
690 - 'media': 'SingleEngineApp/media_engine_streamlit_app.py',  
691 - 'query': 'SingleEngineApp/query_engine_streamlit_app.py'  
692 - }  
693 -  
694 - for app_name, script_path in script_paths.items():  
695 - print(f"检查文件: {script_path}")  
696 - if os.path.exists(script_path):  
697 - print(f"启动 {app_name}...")  
698 - success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port'])  
699 - print(f"{app_name}: {message}")  
700 -  
701 - if success:  
702 - print(f"等待 {app_name} 启动完成...")  
703 - startup_success, startup_message = wait_for_app_startup(app_name, 30)  
704 - print(f"{app_name} 启动检查: {startup_message}")  
705 - else:  
706 - print(f"错误: {script_path} 不存在")  
707 -  
708 - start_forum_engine()  
709 -  
710 - # 初始化ReportEngine  
711 - if REPORT_ENGINE_AVAILABLE:  
712 - print("初始化ReportEngine...")  
713 - if initialize_report_engine():  
714 - print("ReportEngine初始化成功")  
715 - print("ReportEngine文件基准已建立,开始监控文件变化")  
716 - else:  
717 - print("ReportEngine初始化失败")  
718 - 996 + print("等待配置确认,系统将在前端指令后启动组件...")
719 print("启动Flask服务器...") 997 print("启动Flask服务器...")
720 - 998 +
721 try: 999 try:
722 - # 启动Flask应用  
723 socketio.run(app, host='0.0.0.0', port=5000, debug=False) 1000 socketio.run(app, host='0.0.0.0', port=5000, debug=False)
724 except KeyboardInterrupt: 1001 except KeyboardInterrupt:
725 print("\n正在关闭应用...") 1002 print("\n正在关闭应用...")
726 cleanup_processes() 1003 cleanup_processes()
727 -  
728 1004
@@ -44,10 +44,63 @@ @@ -44,10 +44,63 @@
44 letter-spacing: 1px; 44 letter-spacing: 1px;
45 } 45 }
46 46
  47 + .search-row {
  48 + display: flex;
  49 + align-items: stretch;
  50 + gap: 12px;
  51 + max-width: 950px;
  52 + margin: 0 auto 10px;
  53 + }
  54 +
  55 + .config-button {
  56 + display: flex;
  57 + align-items: center;
  58 + justify-content: center;
  59 + padding: 0 24px;
  60 + border: 2px solid #000000;
  61 + background-color: #ffffff;
  62 + color: #000000;
  63 + cursor: pointer;
  64 + font-size: 14px;
  65 + font-weight: bold;
  66 + transition: all 0.3s ease;
  67 + min-width: 120px;
  68 + }
  69 +
  70 + .config-button:hover {
  71 + background-color: #000000;
  72 + color: #ffffff;
  73 + }
  74 +
  75 + .config-password-wrapper {
  76 + display: flex;
  77 + align-items: center;
  78 + gap: 8px;
  79 + }
  80 +
  81 + .config-password-wrapper .config-field-input {
  82 + flex: 1;
  83 + }
  84 +
  85 + .config-password-toggle {
  86 + padding: 8px 14px;
  87 + border: 2px solid #000000;
  88 + background-color: #ffffff;
  89 + cursor: pointer;
  90 + font-size: 12px;
  91 + font-weight: bold;
  92 + transition: all 0.3s ease;
  93 + }
  94 +
  95 + .config-password-toggle:hover,
  96 + .config-password-toggle.revealed {
  97 + background-color: #000000;
  98 + color: #ffffff;
  99 + }
  100 +
47 .search-box { 101 .search-box {
48 display: flex; 102 display: flex;
49 - max-width: 800px;  
50 - margin: 0 auto; 103 + flex: 1;
51 border: 2px solid #000000; 104 border: 2px solid #000000;
52 } 105 }
53 106
@@ -111,9 +164,10 @@ @@ -111,9 +164,10 @@
111 164
112 .upload-status { 165 .upload-status {
113 font-size: 12px; 166 font-size: 12px;
114 - margin-top: 10px; 167 + margin: 10px auto 0;
115 text-align: center; 168 text-align: center;
116 color: #666666; 169 color: #666666;
  170 + max-width: 950px;
117 } 171 }
118 172
119 .upload-status.success { 173 .upload-status.success {
@@ -268,6 +322,207 @@ @@ -268,6 +322,207 @@
268 align-items: center; 322 align-items: center;
269 } 323 }
270 324
  325 + .config-modal-overlay {
  326 + position: fixed;
  327 + inset: 0;
  328 + background-color: rgba(0, 0, 0, 0.35);
  329 + display: none;
  330 + align-items: center;
  331 + justify-content: center;
  332 + z-index: 999;
  333 + padding: 20px;
  334 + }
  335 +
  336 + .config-modal-overlay.visible {
  337 + display: flex;
  338 + }
  339 +
  340 + .config-modal {
  341 + background-color: #ffffff;
  342 + border: 2px solid #000000;
  343 + width: 720px;
  344 + max-width: 90vw;
  345 + max-height: 85vh;
  346 + display: flex;
  347 + flex-direction: column;
  348 + box-shadow: 6px 6px 0 #000000;
  349 + }
  350 +
  351 + .config-modal-header {
  352 + display: flex;
  353 + justify-content: space-between;
  354 + align-items: center;
  355 + padding: 16px 20px;
  356 + border-bottom: 2px solid #000000;
  357 + background-color: #ffffff;
  358 + }
  359 +
  360 + .config-modal-title {
  361 + font-size: 18px;
  362 + font-weight: bold;
  363 + }
  364 +
  365 + .config-modal-actions {
  366 + display: flex;
  367 + gap: 10px;
  368 + align-items: center;
  369 + }
  370 +
  371 + .config-close-button {
  372 + width: 32px;
  373 + height: 32px;
  374 + border: 2px solid #000000;
  375 + background-color: #ffffff;
  376 + font-size: 18px;
  377 + line-height: 1;
  378 + cursor: pointer;
  379 + display: flex;
  380 + align-items: center;
  381 + justify-content: center;
  382 + }
  383 +
  384 + .config-close-button:hover {
  385 + background-color: #000000;
  386 + color: #ffffff;
  387 + }
  388 +
  389 + .config-close-button:disabled {
  390 + opacity: 0.4;
  391 + cursor: not-allowed;
  392 + background-color: #f0f0f0;
  393 + color: #666666;
  394 + }
  395 +
  396 + .config-secondary-button {
  397 + padding: 8px 18px;
  398 + border: 2px solid #000000;
  399 + background-color: #ffffff;
  400 + color: #000000;
  401 + cursor: pointer;
  402 + font-size: 13px;
  403 + font-weight: bold;
  404 + transition: all 0.3s ease;
  405 + }
  406 +
  407 + .config-secondary-button:hover {
  408 + background-color: #f0f0f0;
  409 + }
  410 +
  411 + .config-modal-body {
  412 + padding: 20px;
  413 + overflow-y: auto;
  414 + }
  415 +
  416 + .config-group {
  417 + border: 2px solid #000000;
  418 + padding: 16px;
  419 + margin-bottom: 16px;
  420 + background-color: #ffffff;
  421 + }
  422 +
  423 + .config-group-title {
  424 + font-size: 15px;
  425 + font-weight: bold;
  426 + margin-bottom: 10px;
  427 + }
  428 +
  429 + .config-group-subtitle {
  430 + font-size: 12px;
  431 + color: #555555;
  432 + margin-bottom: 12px;
  433 + }
  434 +
  435 + .config-field {
  436 + display: flex;
  437 + flex-direction: column;
  438 + margin-bottom: 12px;
  439 + }
  440 +
  441 + .config-field-label {
  442 + font-size: 12px;
  443 + font-weight: bold;
  444 + margin-bottom: 6px;
  445 + }
  446 +
  447 + .config-field-input {
  448 + padding: 10px 12px;
  449 + border: 2px solid #000000;
  450 + font-size: 14px;
  451 + background-color: #ffffff;
  452 + }
  453 +
  454 + .config-field-input:focus {
  455 + outline: none;
  456 + border-color: #333333;
  457 + }
  458 +
  459 + .config-modal-footer {
  460 + display: flex;
  461 + justify-content: space-between;
  462 + align-items: center;
  463 + padding: 16px 20px;
  464 + border-top: 2px solid #000000;
  465 + background-color: #ffffff;
  466 + }
  467 +
  468 + .config-modal-footer-actions {
  469 + display: flex;
  470 + gap: 10px;
  471 + }
  472 +
  473 + .config-status-message {
  474 + font-size: 12px;
  475 + color: #555555;
  476 + }
  477 +
  478 + .config-status-message.error {
  479 + color: #8b4513;
  480 + }
  481 +
  482 + .config-status-message.success {
  483 + color: #4a6741;
  484 + }
  485 +
  486 + .config-save-button {
  487 + padding: 10px 24px;
  488 + border: none;
  489 + background-color: #000000;
  490 + color: #ffffff;
  491 + cursor: pointer;
  492 + font-size: 14px;
  493 + font-weight: bold;
  494 + transition: all 0.3s ease;
  495 + }
  496 +
  497 + .config-save-button:hover {
  498 + background-color: #333333;
  499 + }
  500 +
  501 + .config-save-button:disabled {
  502 + background-color: #666666;
  503 + cursor: not-allowed;
  504 + }
  505 +
  506 + .config-start-button {
  507 + padding: 10px 24px;
  508 + border: none;
  509 + background-color: #000000;
  510 + color: #ffffff;
  511 + cursor: pointer;
  512 + font-size: 14px;
  513 + font-weight: bold;
  514 + transition: all 0.3s ease;
  515 + }
  516 +
  517 + .config-start-button:hover {
  518 + background-color: #333333;
  519 + }
  520 +
  521 + .config-start-button:disabled {
  522 + background-color: #666666;
  523 + cursor: not-allowed;
  524 + }
  525 +
271 .loading { 526 .loading {
272 display: inline-block; 527 display: inline-block;
273 width: 12px; 528 width: 12px;
@@ -752,13 +1007,16 @@ @@ -752,13 +1007,16 @@
752 <!-- 搜索框区域 --> 1007 <!-- 搜索框区域 -->
753 <div class="search-section"> 1008 <div class="search-section">
754 <div class="search-title">微舆 - 致力于打造简洁通用的舆情分析平台</div> 1009 <div class="search-title">微舆 - 致力于打造简洁通用的舆情分析平台</div>
755 - <div class="search-box">  
756 - <input type="text" class="search-input" id="searchInput" placeholder="请输入要分析的内容...">  
757 - <button class="search-button" id="searchButton">开始</button>  
758 - <button class="upload-button" id="uploadButton">  
759 - 上传模板  
760 - <input type="file" id="templateFileInput" accept=".md,.txt" title="上传自定义报告模板(支持 .md 和 .txt 文件)">  
761 - </button> 1010 + <div class="search-row">
  1011 + <button class="config-button" id="openConfigButton">LLM 配置</button>
  1012 + <div class="search-box">
  1013 + <input type="text" class="search-input" id="searchInput" placeholder="请输入要分析的内容...">
  1014 + <button class="search-button" id="searchButton">开始</button>
  1015 + <button class="upload-button" id="uploadButton">
  1016 + 上传模板
  1017 + <input type="file" id="templateFileInput" accept=".md,.txt" title="上传自定义报告模板(支持 .md 和 .txt 文件)">
  1018 + </button>
  1019 + </div>
762 </div> 1020 </div>
763 <div class="upload-status" id="uploadStatus"></div> 1021 <div class="upload-status" id="uploadStatus"></div>
764 </div> 1022 </div>
@@ -829,6 +1087,28 @@ @@ -829,6 +1087,28 @@
829 </div> 1087 </div>
830 </div> 1088 </div>
831 1089
  1090 + <div class="config-modal-overlay" id="configModal">
  1091 + <div class="config-modal">
  1092 + <div class="config-modal-header">
  1093 + <div class="config-modal-title">LLM 配置 - 与Config文件双向同步</div>
  1094 + <div class="config-modal-actions">
  1095 + <button class="config-secondary-button" id="refreshConfigButton">刷新</button>
  1096 + <button class="config-close-button" id="closeConfigModal" aria-label="关闭配置窗口">×</button>
  1097 + </div>
  1098 + </div>
  1099 + <div class="config-modal-body" id="configFormContainer">
  1100 + <!-- 由脚本填充 -->
  1101 + </div>
  1102 + <div class="config-modal-footer">
  1103 + <div class="config-status-message" id="configStatusMessage"></div>
  1104 + <div class="config-modal-footer-actions">
  1105 + <button class="config-save-button" id="saveConfigButton">保存</button>
  1106 + <button class="config-start-button" id="startSystemButton">保存并启动系统</button>
  1107 + </div>
  1108 + </div>
  1109 + </div>
  1110 + </div>
  1111 +
832 <!-- 消息提示 --> 1112 <!-- 消息提示 -->
833 <div class="message" id="message"></div> 1113 <div class="message" id="message"></div>
834 1114
@@ -840,10 +1120,98 @@ @@ -840,10 +1120,98 @@
840 insight: 'stopped', 1120 insight: 'stopped',
841 media: 'stopped', 1121 media: 'stopped',
842 query: 'stopped', 1122 query: 'stopped',
843 - forum: 'running', // Forum Engine 默认运行 1123 + forum: 'stopped', // 前端启动后再标记为 running
844 report: 'stopped' // Report Engine 1124 report: 'stopped' // Report Engine
845 }; 1125 };
846 let customTemplate = ''; // 存储用户上传的自定义模板内容 1126 let customTemplate = ''; // 存储用户上传的自定义模板内容
  1127 + let configValues = {};
  1128 + let configDirty = false;
  1129 + let configAutoRefreshTimer = null;
  1130 + let systemStarted = false;
  1131 + let systemStarting = false;
  1132 + let configModalLocked = false;
  1133 +
  1134 + const CONFIG_ENDPOINT = '/api/config';
  1135 + const SYSTEM_STATUS_ENDPOINT = '/api/system/status';
  1136 + const SYSTEM_START_ENDPOINT = '/api/system/start';
  1137 + const START_BUTTON_DEFAULT_TEXT = '保存并启动系统';
  1138 +
  1139 + const configFieldGroups = [
  1140 + {
  1141 + title: '数据库连接',
  1142 + subtitle: '用于连接业务数据库的基本配置',
  1143 + fields: [
  1144 + { key: 'DB_HOST', label: '主机地址' },
  1145 + { key: 'DB_PORT', label: '端口' },
  1146 + { key: 'DB_USER', label: '用户名' },
  1147 + { key: 'DB_PASSWORD', label: '密码', type: 'password' },
  1148 + { key: 'DB_NAME', label: '数据库名称' },
  1149 + { key: 'DB_CHARSET', label: '字符集' }
  1150 + ]
  1151 + },
  1152 + {
  1153 + title: 'Insight Agent',
  1154 + subtitle: '负责洞察分析的模型配置',
  1155 + fields: [
  1156 + { key: 'INSIGHT_ENGINE_API_KEY', label: 'API Key' },
  1157 + { key: 'INSIGHT_ENGINE_BASE_URL', label: 'Base URL' },
  1158 + { key: 'INSIGHT_ENGINE_MODEL_NAME', label: '模型名称' }
  1159 + ]
  1160 + },
  1161 + {
  1162 + title: 'Media Agent',
  1163 + subtitle: '媒体内容理解与生成模型',
  1164 + fields: [
  1165 + { key: 'MEDIA_ENGINE_API_KEY', label: 'API Key' },
  1166 + { key: 'MEDIA_ENGINE_BASE_URL', label: 'Base URL' },
  1167 + { key: 'MEDIA_ENGINE_MODEL_NAME', label: '模型名称' }
  1168 + ]
  1169 + },
  1170 + {
  1171 + title: 'Query Agent',
  1172 + subtitle: '负责搜索与信息汇总的模型配置',
  1173 + fields: [
  1174 + { key: 'QUERY_ENGINE_API_KEY', label: 'API Key' },
  1175 + { key: 'QUERY_ENGINE_BASE_URL', label: 'Base URL' },
  1176 + { key: 'QUERY_ENGINE_MODEL_NAME', label: '模型名称' }
  1177 + ]
  1178 + },
  1179 + {
  1180 + title: 'Report Agent',
  1181 + subtitle: '报告生成使用的模型配置',
  1182 + fields: [
  1183 + { key: 'REPORT_ENGINE_API_KEY', label: 'API Key' },
  1184 + { key: 'REPORT_ENGINE_BASE_URL', label: 'Base URL' },
  1185 + { key: 'REPORT_ENGINE_MODEL_NAME', label: '模型名称' }
  1186 + ]
  1187 + },
  1188 + {
  1189 + title: 'Forum Host',
  1190 + subtitle: '多智能体协同使用的模型配置',
  1191 + fields: [
  1192 + { key: 'FORUM_HOST_API_KEY', label: 'API Key' },
  1193 + { key: 'FORUM_HOST_BASE_URL', label: 'Base URL' },
  1194 + { key: 'FORUM_HOST_MODEL_NAME', label: '模型名称' }
  1195 + ]
  1196 + },
  1197 + {
  1198 + title: 'Keyword Optimizer',
  1199 + subtitle: 'SQL / 关键词优化模型配置',
  1200 + fields: [
  1201 + { key: 'KEYWORD_OPTIMIZER_API_KEY', label: 'API Key' },
  1202 + { key: 'KEYWORD_OPTIMIZER_BASE_URL', label: 'Base URL' },
  1203 + { key: 'KEYWORD_OPTIMIZER_MODEL_NAME', label: '模型名称' }
  1204 + ]
  1205 + },
  1206 + {
  1207 + title: '外部检索工具',
  1208 + subtitle: '联动搜索引擎、网站抓取等在线服务',
  1209 + fields: [
  1210 + { key: 'TAVILY_API_KEY', label: 'Tavily API Key' },
  1211 + { key: 'BOCHA_WEB_SEARCH_API_KEY', label: 'Bocha API Key' }
  1212 + ]
  1213 + }
  1214 + ];
847 1215
848 // 应用名称映射 1216 // 应用名称映射
849 const appNames = { 1217 const appNames = {
@@ -867,6 +1235,7 @@ @@ -867,6 +1235,7 @@
867 document.addEventListener('DOMContentLoaded', function() { 1235 document.addEventListener('DOMContentLoaded', function() {
868 initializeSocket(); 1236 initializeSocket();
869 initializeEventListeners(); 1237 initializeEventListeners();
  1238 + ensureSystemReadyOnLoad();
870 updateTime(); 1239 updateTime();
871 setInterval(updateTime, 1000); 1240 setInterval(updateTime, 1000);
872 checkStatus(); 1241 checkStatus();
@@ -952,6 +1321,445 @@ @@ -952,6 +1321,445 @@
952 switchToApp(app); 1321 switchToApp(app);
953 }); 1322 });
954 }); 1323 });
  1324 +
  1325 + // LLM 配置弹窗
  1326 + const openConfigButton = document.getElementById('openConfigButton');
  1327 + if (openConfigButton) {
  1328 + openConfigButton.addEventListener('click', () => openConfigModal({ lock: !systemStarted }));
  1329 + }
  1330 +
  1331 + const closeConfigButton = document.getElementById('closeConfigModal');
  1332 + if (closeConfigButton) {
  1333 + closeConfigButton.addEventListener('click', () => closeConfigModal());
  1334 + }
  1335 +
  1336 + const refreshConfigButton = document.getElementById('refreshConfigButton');
  1337 + if (refreshConfigButton) {
  1338 + refreshConfigButton.addEventListener('click', () => refreshConfigFromServer(true));
  1339 + }
  1340 +
  1341 + const saveConfigButton = document.getElementById('saveConfigButton');
  1342 + if (saveConfigButton) {
  1343 + saveConfigButton.addEventListener('click', () => saveConfigUpdates());
  1344 + }
  1345 +
  1346 + const startSystemButton = document.getElementById('startSystemButton');
  1347 + if (startSystemButton) {
  1348 + startSystemButton.addEventListener('click', () => startSystem());
  1349 + }
  1350 +
  1351 + const configModal = document.getElementById('configModal');
  1352 + if (configModal) {
  1353 + configModal.addEventListener('click', (event) => {
  1354 + if (event.target === configModal) {
  1355 + closeConfigModal();
  1356 + }
  1357 + });
  1358 + }
  1359 +
  1360 + const configFormContainer = document.getElementById('configFormContainer');
  1361 + if (configFormContainer) {
  1362 + configFormContainer.addEventListener('input', () => {
  1363 + configDirty = true;
  1364 + setConfigStatus('已修改,尚未保存');
  1365 + });
  1366 + }
  1367 +
  1368 + document.addEventListener('keydown', function(event) {
  1369 + if (event.key === 'Escape' && isConfigModalVisible()) {
  1370 + closeConfigModal();
  1371 + }
  1372 + });
  1373 + }
  1374 +
  1375 + function isConfigModalVisible() {
  1376 + const modal = document.getElementById('configModal');
  1377 + return modal ? modal.classList.contains('visible') : false;
  1378 + }
  1379 +
  1380 + function openConfigModal(options = {}) {
  1381 + const { lock = false, message = '' } = options;
  1382 + const modal = document.getElementById('configModal');
  1383 + if (!modal) {
  1384 + return;
  1385 + }
  1386 +
  1387 + configModalLocked = lock;
  1388 + modal.classList.add('visible');
  1389 + configDirty = false;
  1390 +
  1391 + const initialMessage = message || '正在读取配置...';
  1392 + setConfigStatus(initialMessage, '');
  1393 +
  1394 + const messageAfterLoad = message || '';
  1395 +
  1396 + refreshConfigFromServer(true, messageAfterLoad);
  1397 +
  1398 + if (configAutoRefreshTimer) {
  1399 + clearInterval(configAutoRefreshTimer);
  1400 + }
  1401 + configAutoRefreshTimer = setInterval(() => {
  1402 + if (!configDirty) {
  1403 + refreshConfigFromServer(false, messageAfterLoad);
  1404 + }
  1405 + }, 10000);
  1406 +
  1407 + updateStartButtonState();
  1408 + updateConfigCloseButton();
  1409 + }
  1410 +
  1411 + function closeConfigModal(force = false) {
  1412 + if (!force && configModalLocked && !systemStarted) {
  1413 + setConfigStatus('请先完成配置并启动系统', 'error');
  1414 + showMessage('请先完成配置并启动系统', 'error');
  1415 + return;
  1416 + }
  1417 +
  1418 + const modal = document.getElementById('configModal');
  1419 + if (modal) {
  1420 + modal.classList.remove('visible');
  1421 + }
  1422 + if (configAutoRefreshTimer) {
  1423 + clearInterval(configAutoRefreshTimer);
  1424 + configAutoRefreshTimer = null;
  1425 + }
  1426 + configDirty = false;
  1427 + configModalLocked = false;
  1428 + setConfigStatus('', '');
  1429 + updateStartButtonState();
  1430 + updateConfigCloseButton();
  1431 + }
  1432 +
  1433 + function refreshConfigFromServer(showFeedback = false, messageOverride = '') {
  1434 + if (showFeedback && configDirty) {
  1435 + const proceed = window.confirm('当前修改尚未保存,确定要刷新并放弃更改吗?');
  1436 + if (!proceed) {
  1437 + return;
  1438 + }
  1439 + }
  1440 + fetch(CONFIG_ENDPOINT)
  1441 + .then(response => response.json())
  1442 + .then(data => {
  1443 + if (!data.success) {
  1444 + throw new Error(data.message || '读取配置失败');
  1445 + }
  1446 + configValues = data.config || {};
  1447 + renderConfigForm(configValues);
  1448 + configDirty = false;
  1449 + if (messageOverride) {
  1450 + setConfigStatus(messageOverride);
  1451 + } else if (showFeedback) {
  1452 + setConfigStatus('已加载最新配置');
  1453 + } else {
  1454 + setConfigStatus('已同步最新配置');
  1455 + }
  1456 + })
  1457 + .catch(error => {
  1458 + console.error(error);
  1459 + setConfigStatus(`读取配置失败: ${error.message}`, 'error');
  1460 + });
  1461 + }
  1462 +
  1463 + function escapeHtml(str) {
  1464 + return str.replace(/&/g, '&amp;')
  1465 + .replace(/</g, '&lt;')
  1466 + .replace(/>/g, '&gt;')
  1467 + .replace(/"/g, '&quot;')
  1468 + .replace(/'/g, '&#39;');
  1469 + }
  1470 +
  1471 + function renderConfigForm(values) {
  1472 + const container = document.getElementById('configFormContainer');
  1473 + if (!container) {
  1474 + return;
  1475 + }
  1476 +
  1477 + const sections = configFieldGroups.map(group => {
  1478 + const fieldsHtml = group.fields.map(field => {
  1479 + const value = values[field.key] !== undefined ? values[field.key] : '';
  1480 + const safeValue = escapeHtml(String(value || ''));
  1481 + const inputType = field.type === 'password' ? 'password' : (field.type || 'text');
  1482 + const inputElement = `
  1483 + <input
  1484 + type="${inputType}"
  1485 + class="config-field-input"
  1486 + data-config-key="${field.key}"
  1487 + data-field-type="${field.type || 'text'}"
  1488 + value="${safeValue}"
  1489 + placeholder="填写${field.label}"
  1490 + autocomplete="${field.type === 'password' ? 'off' : 'on'}"
  1491 + >
  1492 + `;
  1493 +
  1494 + const control = field.type === 'password'
  1495 + ? `
  1496 + <div class="config-password-wrapper">
  1497 + ${inputElement}
  1498 + <button type="button" class="config-password-toggle" data-target="${field.key}">显示</button>
  1499 + </div>
  1500 + `
  1501 + : inputElement;
  1502 +
  1503 + return `
  1504 + <label class="config-field">
  1505 + <span class="config-field-label">${field.label}</span>
  1506 + ${control}
  1507 + </label>
  1508 + `;
  1509 + }).join('');
  1510 +
  1511 + const subtitle = group.subtitle ? `<div class="config-group-subtitle">${group.subtitle}</div>` : '';
  1512 +
  1513 + return `
  1514 + <section class="config-group">
  1515 + <div class="config-group-title">${group.title}</div>
  1516 + ${subtitle}
  1517 + ${fieldsHtml}
  1518 + </section>
  1519 + `;
  1520 + }).join('');
  1521 +
  1522 + container.innerHTML = sections;
  1523 + attachConfigPasswordToggles();
  1524 + }
  1525 +
  1526 + function attachConfigPasswordToggles() {
  1527 + const toggles = document.querySelectorAll('.config-password-toggle');
  1528 + toggles.forEach(toggle => {
  1529 + const key = toggle.dataset.target;
  1530 + const input = document.querySelector(`.config-field-input[data-config-key="${key}"]`);
  1531 + if (!input) {
  1532 + return;
  1533 + }
  1534 + toggle.addEventListener('click', () => {
  1535 + const reveal = input.getAttribute('type') === 'password';
  1536 + input.setAttribute('type', reveal ? 'text' : 'password');
  1537 + toggle.textContent = reveal ? '隐藏' : '显示';
  1538 + toggle.classList.toggle('revealed', reveal);
  1539 + });
  1540 + });
  1541 + }
  1542 +
  1543 + function collectConfigUpdates() {
  1544 + const inputs = document.querySelectorAll('#configFormContainer [data-config-key]');
  1545 + const updates = {};
  1546 + inputs.forEach(input => {
  1547 + const key = input.dataset.configKey;
  1548 + if (!key) {
  1549 + return;
  1550 + }
  1551 + const fieldType = input.dataset.fieldType || 'text';
  1552 + let value = input.value;
  1553 + if (fieldType !== 'password' && typeof value === 'string') {
  1554 + value = value.trim();
  1555 + }
  1556 +
  1557 + if (value !== '' && /PORT$/i.test(key)) {
  1558 + const numeric = Number(value);
  1559 + if (!Number.isNaN(numeric)) {
  1560 + updates[key] = numeric;
  1561 + return;
  1562 + }
  1563 + }
  1564 +
  1565 + updates[key] = value;
  1566 + });
  1567 + return updates;
  1568 + }
  1569 +
  1570 + function setConfigStatus(message, type = '') {
  1571 + const status = document.getElementById('configStatusMessage');
  1572 + if (!status) {
  1573 + return;
  1574 + }
  1575 + status.textContent = message || '';
  1576 + status.classList.remove('error', 'success');
  1577 + if (type) {
  1578 + status.classList.add(type);
  1579 + }
  1580 + }
  1581 +
  1582 + async function saveConfigUpdates(options = {}) {
  1583 + const { silent = false } = options;
  1584 + const saveButton = document.getElementById('saveConfigButton');
  1585 +
  1586 + if (!silent && saveButton) {
  1587 + saveButton.disabled = true;
  1588 + saveButton.textContent = '保存中...';
  1589 + }
  1590 + if (!silent) {
  1591 + setConfigStatus('正在保存配置...', '');
  1592 + }
  1593 +
  1594 + const updates = collectConfigUpdates();
  1595 +
  1596 + try {
  1597 + const response = await fetch(CONFIG_ENDPOINT, {
  1598 + method: 'POST',
  1599 + headers: { 'Content-Type': 'application/json' },
  1600 + body: JSON.stringify(updates)
  1601 + });
  1602 + const data = await response.json();
  1603 + if (!data.success) {
  1604 + throw new Error(data.message || '保存失败');
  1605 + }
  1606 + configValues = data.config || {};
  1607 + renderConfigForm(configValues);
  1608 + configDirty = false;
  1609 + if (silent) {
  1610 + setConfigStatus('配置已保存', 'success');
  1611 + } else {
  1612 + setConfigStatus('配置已保存', 'success');
  1613 + showMessage('配置已保存', 'success');
  1614 + }
  1615 + return true;
  1616 + } catch (error) {
  1617 + console.error(error);
  1618 + setConfigStatus(`保存失败: ${error.message}`, 'error');
  1619 + if (!silent) {
  1620 + showMessage(`保存失败: ${error.message}`, 'error');
  1621 + }
  1622 + return false;
  1623 + } finally {
  1624 + if (!silent && saveButton) {
  1625 + saveButton.disabled = false;
  1626 + saveButton.textContent = '保存';
  1627 + }
  1628 + }
  1629 + }
  1630 +
  1631 + function updateStartButtonState() {
  1632 + const startButton = document.getElementById('startSystemButton');
  1633 + if (!startButton) {
  1634 + return;
  1635 + }
  1636 +
  1637 + if (systemStarting) {
  1638 + startButton.disabled = true;
  1639 + startButton.textContent = '启动中...';
  1640 + } else if (systemStarted) {
  1641 + startButton.disabled = true;
  1642 + startButton.textContent = '系统已启动';
  1643 + } else {
  1644 + startButton.disabled = false;
  1645 + startButton.textContent = START_BUTTON_DEFAULT_TEXT;
  1646 + }
  1647 + }
  1648 +
  1649 + function updateConfigCloseButton() {
  1650 + const closeButton = document.getElementById('closeConfigModal');
  1651 + if (!closeButton) {
  1652 + return;
  1653 + }
  1654 + if (configModalLocked && !systemStarted) {
  1655 + closeButton.setAttribute('disabled', 'disabled');
  1656 + } else {
  1657 + closeButton.removeAttribute('disabled');
  1658 + }
  1659 + }
  1660 +
  1661 + function applySystemState(state) {
  1662 + if (!state) {
  1663 + return;
  1664 + }
  1665 + if (Object.prototype.hasOwnProperty.call(state, 'started')) {
  1666 + systemStarted = !!state.started;
  1667 + }
  1668 + if (Object.prototype.hasOwnProperty.call(state, 'starting')) {
  1669 + systemStarting = !!state.starting;
  1670 + }
  1671 + updateStartButtonState();
  1672 + updateConfigCloseButton();
  1673 + }
  1674 +
  1675 + async function fetchSystemStatus() {
  1676 + try {
  1677 + const response = await fetch(SYSTEM_STATUS_ENDPOINT);
  1678 + const data = await response.json();
  1679 + if (data && data.success) {
  1680 + applySystemState(data);
  1681 + }
  1682 + return data;
  1683 + } catch (error) {
  1684 + console.error('获取系统状态失败', error);
  1685 + return null;
  1686 + }
  1687 + }
  1688 +
  1689 + async function ensureSystemReadyOnLoad() {
  1690 + const status = await fetchSystemStatus();
  1691 + if (!status || !status.success) {
  1692 + openConfigModal({
  1693 + lock: true,
  1694 + message: '无法获取系统状态,请检查配置后重试。'
  1695 + });
  1696 + return;
  1697 + }
  1698 +
  1699 + if (!status.started) {
  1700 + openConfigModal({
  1701 + lock: true,
  1702 + message: '请先确认配置,然后点击“保存并启动系统”'
  1703 + });
  1704 + } else {
  1705 + applySystemState(status);
  1706 + configModalLocked = false;
  1707 + }
  1708 + }
  1709 +
  1710 + async function startSystem() {
  1711 + if (systemStarting) {
  1712 + setConfigStatus('系统正在启动,请稍候...', '');
  1713 + return;
  1714 + }
  1715 +
  1716 + systemStarting = true;
  1717 + updateStartButtonState();
  1718 +
  1719 + try {
  1720 + if (configDirty) {
  1721 + setConfigStatus('检测到未保存的修改,正在保存配置...', '');
  1722 + const saved = await saveConfigUpdates({ silent: true });
  1723 + if (!saved) {
  1724 + systemStarting = false;
  1725 + updateStartButtonState();
  1726 + return;
  1727 + }
  1728 + }
  1729 +
  1730 + setConfigStatus('正在启动系统...', '');
  1731 + const response = await fetch(SYSTEM_START_ENDPOINT, { method: 'POST' });
  1732 + const data = await response.json();
  1733 + if (!response.ok || !data.success) {
  1734 + const message = data && data.message ? data.message : '系统启动失败';
  1735 + throw new Error(message);
  1736 + }
  1737 +
  1738 + showMessage('系统启动成功', 'success');
  1739 + setConfigStatus('系统启动成功', 'success');
  1740 + applySystemState({ started: true, starting: false });
  1741 + configModalLocked = false;
  1742 +
  1743 + setTimeout(() => {
  1744 + closeConfigModal();
  1745 + }, 800);
  1746 +
  1747 + setTimeout(() => {
  1748 + checkStatus();
  1749 + }, 1000);
  1750 +
  1751 + setTimeout(() => {
  1752 + window.location.reload();
  1753 + }, 1200);
  1754 + } catch (error) {
  1755 + setConfigStatus(`系统启动失败: ${error.message}`, 'error');
  1756 + showMessage(`系统启动失败: ${error.message}`, 'error');
  1757 + applySystemState({ started: false, starting: false });
  1758 + } finally {
  1759 + systemStarting = false;
  1760 + updateStartButtonState();
  1761 + await fetchSystemStatus();
  1762 + }
955 } 1763 }
956 1764
957 // 执行搜索 1765 // 执行搜索