666ghj

Implement comprehensive front-end settings UI.

... ... @@ -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,9 +668,16 @@ 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)
... ... @@ -479,19 +705,25 @@ 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],
script_path,
processes[app_name]['port']
)
if success:
# 等待应用启动
startup_success, startup_message = wait_for_app_startup(app_name, 15)
... ... @@ -506,6 +738,15 @@ 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()
... ...
... ... @@ -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,6 +1007,8 @@
<!-- 搜索框区域 -->
<div class="search-section">
<div class="search-title">微舆 - 致力于打造简洁通用的舆情分析平台</div>
<div class="search-row">
<button class="config-button" id="openConfigButton">LLM 配置</button>
<div class="search-box">
<input type="text" class="search-input" id="searchInput" placeholder="请输入要分析的内容...">
<button class="search-button" id="searchButton">开始</button>
... ... @@ -760,6 +1017,7 @@
<input type="file" id="templateFileInput" accept=".md,.txt" title="上传自定义报告模板(支持 .md 和 .txt 文件)">
</button>
</div>
</div>
<div class="upload-status" id="uploadStatus"></div>
</div>
... ... @@ -829,6 +1087,28 @@
</div>
</div>
<div class="config-modal-overlay" id="configModal">
<div class="config-modal">
<div class="config-modal-header">
<div class="config-modal-title">LLM 配置 - 与Config文件双向同步</div>
<div class="config-modal-actions">
<button class="config-secondary-button" id="refreshConfigButton">刷新</button>
<button class="config-close-button" id="closeConfigModal" aria-label="关闭配置窗口">×</button>
</div>
</div>
<div class="config-modal-body" id="configFormContainer">
<!-- 由脚本填充 -->
</div>
<div class="config-modal-footer">
<div class="config-status-message" id="configStatusMessage"></div>
<div class="config-modal-footer-actions">
<button class="config-save-button" id="saveConfigButton">保存</button>
<button class="config-start-button" id="startSystemButton">保存并启动系统</button>
</div>
</div>
</div>
</div>
<!-- 消息提示 -->
<div class="message" id="message"></div>
... ... @@ -840,10 +1120,98 @@
insight: 'stopped',
media: 'stopped',
query: 'stopped',
forum: 'running', // Forum Engine 默认运行
forum: 'stopped', // 前端启动后再标记为 running
report: 'stopped' // Report Engine
};
let customTemplate = ''; // 存储用户上传的自定义模板内容
let configValues = {};
let configDirty = false;
let configAutoRefreshTimer = null;
let systemStarted = false;
let systemStarting = false;
let configModalLocked = false;
const CONFIG_ENDPOINT = '/api/config';
const SYSTEM_STATUS_ENDPOINT = '/api/system/status';
const SYSTEM_START_ENDPOINT = '/api/system/start';
const START_BUTTON_DEFAULT_TEXT = '保存并启动系统';
const configFieldGroups = [
{
title: '数据库连接',
subtitle: '用于连接业务数据库的基本配置',
fields: [
{ key: 'DB_HOST', label: '主机地址' },
{ key: 'DB_PORT', label: '端口' },
{ key: 'DB_USER', label: '用户名' },
{ key: 'DB_PASSWORD', label: '密码', type: 'password' },
{ key: 'DB_NAME', label: '数据库名称' },
{ key: 'DB_CHARSET', label: '字符集' }
]
},
{
title: 'Insight Agent',
subtitle: '负责洞察分析的模型配置',
fields: [
{ key: 'INSIGHT_ENGINE_API_KEY', label: 'API Key' },
{ key: 'INSIGHT_ENGINE_BASE_URL', label: 'Base URL' },
{ key: 'INSIGHT_ENGINE_MODEL_NAME', label: '模型名称' }
]
},
{
title: 'Media Agent',
subtitle: '媒体内容理解与生成模型',
fields: [
{ key: 'MEDIA_ENGINE_API_KEY', label: 'API Key' },
{ key: 'MEDIA_ENGINE_BASE_URL', label: 'Base URL' },
{ key: 'MEDIA_ENGINE_MODEL_NAME', label: '模型名称' }
]
},
{
title: 'Query Agent',
subtitle: '负责搜索与信息汇总的模型配置',
fields: [
{ key: 'QUERY_ENGINE_API_KEY', label: 'API Key' },
{ key: 'QUERY_ENGINE_BASE_URL', label: 'Base URL' },
{ key: 'QUERY_ENGINE_MODEL_NAME', label: '模型名称' }
]
},
{
title: 'Report Agent',
subtitle: '报告生成使用的模型配置',
fields: [
{ key: 'REPORT_ENGINE_API_KEY', label: 'API Key' },
{ key: 'REPORT_ENGINE_BASE_URL', label: 'Base URL' },
{ key: 'REPORT_ENGINE_MODEL_NAME', label: '模型名称' }
]
},
{
title: 'Forum Host',
subtitle: '多智能体协同使用的模型配置',
fields: [
{ key: 'FORUM_HOST_API_KEY', label: 'API Key' },
{ key: 'FORUM_HOST_BASE_URL', label: 'Base URL' },
{ key: 'FORUM_HOST_MODEL_NAME', label: '模型名称' }
]
},
{
title: 'Keyword Optimizer',
subtitle: 'SQL / 关键词优化模型配置',
fields: [
{ key: 'KEYWORD_OPTIMIZER_API_KEY', label: 'API Key' },
{ key: 'KEYWORD_OPTIMIZER_BASE_URL', label: 'Base URL' },
{ key: 'KEYWORD_OPTIMIZER_MODEL_NAME', label: '模型名称' }
]
},
{
title: '外部检索工具',
subtitle: '联动搜索引擎、网站抓取等在线服务',
fields: [
{ key: 'TAVILY_API_KEY', label: 'Tavily API Key' },
{ key: 'BOCHA_WEB_SEARCH_API_KEY', label: 'Bocha API Key' }
]
}
];
// 应用名称映射
const appNames = {
... ... @@ -867,6 +1235,7 @@
document.addEventListener('DOMContentLoaded', function() {
initializeSocket();
initializeEventListeners();
ensureSystemReadyOnLoad();
updateTime();
setInterval(updateTime, 1000);
checkStatus();
... ... @@ -952,6 +1321,445 @@
switchToApp(app);
});
});
// LLM 配置弹窗
const openConfigButton = document.getElementById('openConfigButton');
if (openConfigButton) {
openConfigButton.addEventListener('click', () => openConfigModal({ lock: !systemStarted }));
}
const closeConfigButton = document.getElementById('closeConfigModal');
if (closeConfigButton) {
closeConfigButton.addEventListener('click', () => closeConfigModal());
}
const refreshConfigButton = document.getElementById('refreshConfigButton');
if (refreshConfigButton) {
refreshConfigButton.addEventListener('click', () => refreshConfigFromServer(true));
}
const saveConfigButton = document.getElementById('saveConfigButton');
if (saveConfigButton) {
saveConfigButton.addEventListener('click', () => saveConfigUpdates());
}
const startSystemButton = document.getElementById('startSystemButton');
if (startSystemButton) {
startSystemButton.addEventListener('click', () => startSystem());
}
const configModal = document.getElementById('configModal');
if (configModal) {
configModal.addEventListener('click', (event) => {
if (event.target === configModal) {
closeConfigModal();
}
});
}
const configFormContainer = document.getElementById('configFormContainer');
if (configFormContainer) {
configFormContainer.addEventListener('input', () => {
configDirty = true;
setConfigStatus('已修改,尚未保存');
});
}
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && isConfigModalVisible()) {
closeConfigModal();
}
});
}
function isConfigModalVisible() {
const modal = document.getElementById('configModal');
return modal ? modal.classList.contains('visible') : false;
}
function openConfigModal(options = {}) {
const { lock = false, message = '' } = options;
const modal = document.getElementById('configModal');
if (!modal) {
return;
}
configModalLocked = lock;
modal.classList.add('visible');
configDirty = false;
const initialMessage = message || '正在读取配置...';
setConfigStatus(initialMessage, '');
const messageAfterLoad = message || '';
refreshConfigFromServer(true, messageAfterLoad);
if (configAutoRefreshTimer) {
clearInterval(configAutoRefreshTimer);
}
configAutoRefreshTimer = setInterval(() => {
if (!configDirty) {
refreshConfigFromServer(false, messageAfterLoad);
}
}, 10000);
updateStartButtonState();
updateConfigCloseButton();
}
function closeConfigModal(force = false) {
if (!force && configModalLocked && !systemStarted) {
setConfigStatus('请先完成配置并启动系统', 'error');
showMessage('请先完成配置并启动系统', 'error');
return;
}
const modal = document.getElementById('configModal');
if (modal) {
modal.classList.remove('visible');
}
if (configAutoRefreshTimer) {
clearInterval(configAutoRefreshTimer);
configAutoRefreshTimer = null;
}
configDirty = false;
configModalLocked = false;
setConfigStatus('', '');
updateStartButtonState();
updateConfigCloseButton();
}
function refreshConfigFromServer(showFeedback = false, messageOverride = '') {
if (showFeedback && configDirty) {
const proceed = window.confirm('当前修改尚未保存,确定要刷新并放弃更改吗?');
if (!proceed) {
return;
}
}
fetch(CONFIG_ENDPOINT)
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || '读取配置失败');
}
configValues = data.config || {};
renderConfigForm(configValues);
configDirty = false;
if (messageOverride) {
setConfigStatus(messageOverride);
} else if (showFeedback) {
setConfigStatus('已加载最新配置');
} else {
setConfigStatus('已同步最新配置');
}
})
.catch(error => {
console.error(error);
setConfigStatus(`读取配置失败: ${error.message}`, 'error');
});
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderConfigForm(values) {
const container = document.getElementById('configFormContainer');
if (!container) {
return;
}
const sections = configFieldGroups.map(group => {
const fieldsHtml = group.fields.map(field => {
const value = values[field.key] !== undefined ? values[field.key] : '';
const safeValue = escapeHtml(String(value || ''));
const inputType = field.type === 'password' ? 'password' : (field.type || 'text');
const inputElement = `
<input
type="${inputType}"
class="config-field-input"
data-config-key="${field.key}"
data-field-type="${field.type || 'text'}"
value="${safeValue}"
placeholder="填写${field.label}"
autocomplete="${field.type === 'password' ? 'off' : 'on'}"
>
`;
const control = field.type === 'password'
? `
<div class="config-password-wrapper">
${inputElement}
<button type="button" class="config-password-toggle" data-target="${field.key}">显示</button>
</div>
`
: inputElement;
return `
<label class="config-field">
<span class="config-field-label">${field.label}</span>
${control}
</label>
`;
}).join('');
const subtitle = group.subtitle ? `<div class="config-group-subtitle">${group.subtitle}</div>` : '';
return `
<section class="config-group">
<div class="config-group-title">${group.title}</div>
${subtitle}
${fieldsHtml}
</section>
`;
}).join('');
container.innerHTML = sections;
attachConfigPasswordToggles();
}
function attachConfigPasswordToggles() {
const toggles = document.querySelectorAll('.config-password-toggle');
toggles.forEach(toggle => {
const key = toggle.dataset.target;
const input = document.querySelector(`.config-field-input[data-config-key="${key}"]`);
if (!input) {
return;
}
toggle.addEventListener('click', () => {
const reveal = input.getAttribute('type') === 'password';
input.setAttribute('type', reveal ? 'text' : 'password');
toggle.textContent = reveal ? '隐藏' : '显示';
toggle.classList.toggle('revealed', reveal);
});
});
}
function collectConfigUpdates() {
const inputs = document.querySelectorAll('#configFormContainer [data-config-key]');
const updates = {};
inputs.forEach(input => {
const key = input.dataset.configKey;
if (!key) {
return;
}
const fieldType = input.dataset.fieldType || 'text';
let value = input.value;
if (fieldType !== 'password' && typeof value === 'string') {
value = value.trim();
}
if (value !== '' && /PORT$/i.test(key)) {
const numeric = Number(value);
if (!Number.isNaN(numeric)) {
updates[key] = numeric;
return;
}
}
updates[key] = value;
});
return updates;
}
function setConfigStatus(message, type = '') {
const status = document.getElementById('configStatusMessage');
if (!status) {
return;
}
status.textContent = message || '';
status.classList.remove('error', 'success');
if (type) {
status.classList.add(type);
}
}
async function saveConfigUpdates(options = {}) {
const { silent = false } = options;
const saveButton = document.getElementById('saveConfigButton');
if (!silent && saveButton) {
saveButton.disabled = true;
saveButton.textContent = '保存中...';
}
if (!silent) {
setConfigStatus('正在保存配置...', '');
}
const updates = collectConfigUpdates();
try {
const response = await fetch(CONFIG_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || '保存失败');
}
configValues = data.config || {};
renderConfigForm(configValues);
configDirty = false;
if (silent) {
setConfigStatus('配置已保存', 'success');
} else {
setConfigStatus('配置已保存', 'success');
showMessage('配置已保存', 'success');
}
return true;
} catch (error) {
console.error(error);
setConfigStatus(`保存失败: ${error.message}`, 'error');
if (!silent) {
showMessage(`保存失败: ${error.message}`, 'error');
}
return false;
} finally {
if (!silent && saveButton) {
saveButton.disabled = false;
saveButton.textContent = '保存';
}
}
}
function updateStartButtonState() {
const startButton = document.getElementById('startSystemButton');
if (!startButton) {
return;
}
if (systemStarting) {
startButton.disabled = true;
startButton.textContent = '启动中...';
} else if (systemStarted) {
startButton.disabled = true;
startButton.textContent = '系统已启动';
} else {
startButton.disabled = false;
startButton.textContent = START_BUTTON_DEFAULT_TEXT;
}
}
function updateConfigCloseButton() {
const closeButton = document.getElementById('closeConfigModal');
if (!closeButton) {
return;
}
if (configModalLocked && !systemStarted) {
closeButton.setAttribute('disabled', 'disabled');
} else {
closeButton.removeAttribute('disabled');
}
}
function applySystemState(state) {
if (!state) {
return;
}
if (Object.prototype.hasOwnProperty.call(state, 'started')) {
systemStarted = !!state.started;
}
if (Object.prototype.hasOwnProperty.call(state, 'starting')) {
systemStarting = !!state.starting;
}
updateStartButtonState();
updateConfigCloseButton();
}
async function fetchSystemStatus() {
try {
const response = await fetch(SYSTEM_STATUS_ENDPOINT);
const data = await response.json();
if (data && data.success) {
applySystemState(data);
}
return data;
} catch (error) {
console.error('获取系统状态失败', error);
return null;
}
}
async function ensureSystemReadyOnLoad() {
const status = await fetchSystemStatus();
if (!status || !status.success) {
openConfigModal({
lock: true,
message: '无法获取系统状态,请检查配置后重试。'
});
return;
}
if (!status.started) {
openConfigModal({
lock: true,
message: '请先确认配置,然后点击“保存并启动系统”'
});
} else {
applySystemState(status);
configModalLocked = false;
}
}
async function startSystem() {
if (systemStarting) {
setConfigStatus('系统正在启动,请稍候...', '');
return;
}
systemStarting = true;
updateStartButtonState();
try {
if (configDirty) {
setConfigStatus('检测到未保存的修改,正在保存配置...', '');
const saved = await saveConfigUpdates({ silent: true });
if (!saved) {
systemStarting = false;
updateStartButtonState();
return;
}
}
setConfigStatus('正在启动系统...', '');
const response = await fetch(SYSTEM_START_ENDPOINT, { method: 'POST' });
const data = await response.json();
if (!response.ok || !data.success) {
const message = data && data.message ? data.message : '系统启动失败';
throw new Error(message);
}
showMessage('系统启动成功', 'success');
setConfigStatus('系统启动成功', 'success');
applySystemState({ started: true, starting: false });
configModalLocked = false;
setTimeout(() => {
closeConfigModal();
}, 800);
setTimeout(() => {
checkStatus();
}, 1000);
setTimeout(() => {
window.location.reload();
}, 1200);
} catch (error) {
setConfigStatus(`系统启动失败: ${error.message}`, 'error');
showMessage(`系统启动失败: ${error.message}`, 'error');
applySystemState({ started: false, starting: false });
} finally {
systemStarting = false;
updateStartButtonState();
await fetchSystemStatus();
}
}
// 执行搜索
... ...