Toggle navigation
Toggle navigation
This project
Loading...
Sign in
万朱浩
/
Venue-Ops
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
666ghj
2025-11-05 00:24:35 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
4b48156e583bef5c065914d8d108b0edf0355d21
4b48156e
1 parent
96be3c1b
Implement comprehensive front-end settings UI.
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1150 additions
and
66 deletions
app.py
templates/index.html
app.py
View file @
4b48156
...
...
@@ -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
()
...
...
templates/index.html
View file @
4b48156
...
...
@@ -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 @@
<!-- 搜索框区域 -->
<div
class=
"search-section"
>
<div
class=
"search-title"
>
微舆 - 致力于打造简洁通用的舆情分析平台
</div>
<div
class=
"search-box"
>
<input
type=
"text"
class=
"search-input"
id=
"searchInput"
placeholder=
"请输入要分析的内容..."
>
<button
class=
"search-button"
id=
"searchButton"
>
开始
</button>
<button
class=
"upload-button"
id=
"uploadButton"
>
上传模板
<input
type=
"file"
id=
"templateFileInput"
accept=
".md,.txt"
title=
"上传自定义报告模板(支持 .md 和 .txt 文件)"
>
</button>
<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>
<button
class=
"upload-button"
id=
"uploadButton"
>
上传模板
<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
,
'&'
)
.
replace
(
/</g
,
'<'
)
.
replace
(
/>/g
,
'>'
)
.
replace
(
/"/g
,
'"'
)
.
replace
(
/'/g
,
'''
);
}
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
();
}
}
// 执行搜索
...
...
Please
register
or
login
to post a comment