http_routes.py
9.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
"""HTTP route registration for the BettaFish web API."""
from __future__ import annotations
import traceback
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Mapping, Protocol
from flask import Flask, jsonify, redirect, render_template, request
from loguru import logger
FrontendDevUrlResolver = Callable[[], str | None]
SearchRequestSubmitter = Callable[..., tuple[dict[str, Any], int]]
SocketEmit = Callable[[str, dict[str, Any]], None]
LogReader = Callable[[Path, str, int | None], list[str]]
LogWriter = Callable[[Path, str, str], None]
ForumStartHandler = Callable[..., bool]
ForumStopHandler = Callable[..., None]
ForumOutputGetter = Callable[..., dict[str, Any]]
ForumLogPayloadGetter = Callable[..., dict[str, Any]]
ForumLogHistoryGetter = Callable[..., dict[str, Any]]
ProcessStatusGetter = Callable[..., dict[str, dict[str, Any]]]
SystemStatusGetter = Callable[[], dict[str, Any]]
StreamlitAppStarter = Callable[..., tuple[bool, str]]
StreamlitStartupWaiter = Callable[[str, int], tuple[bool, str]]
StreamlitAppStopper = Callable[[str], tuple[bool, str]]
class SystemLifecycleContract(Protocol):
"""Protocol used by system routes to start and stop the platform."""
def start_system(self) -> tuple[dict[str, Any], int]:
...
def shutdown_system(self, *, cleanup_timeout: float = 6.0) -> tuple[dict[str, Any], int]:
...
@dataclass(frozen=True)
class HttpRouteDependencies:
"""Runtime services needed by extracted HTTP routes."""
frontend_dev_server_url: FrontendDevUrlResolver
log_dir: Path
read_log: LogReader
write_log: LogWriter
socket_emit: SocketEmit
get_process_status: ProcessStatusGetter
get_system_status: SystemStatusGetter
system_lifecycle: SystemLifecycleContract
streamlit_scripts: Mapping[str, str]
start_streamlit_app: StreamlitAppStarter
wait_for_app_startup: StreamlitStartupWaiter
stop_streamlit_app: StreamlitAppStopper
start_forum_engine: ForumStartHandler
stop_forum_engine: ForumStopHandler
get_forum_output: ForumOutputGetter
get_forum_log_payload: ForumLogPayloadGetter
get_forum_log_history: ForumLogHistoryGetter
submit_search_request: SearchRequestSubmitter
def register_http_routes(app: Flask, deps: HttpRouteDependencies) -> None:
"""Register runtime HTTP routes on the Flask application."""
@app.route("/")
def index():
"""涓婚〉"""
frontend_dev_url = deps.frontend_dev_server_url()
if frontend_dev_url:
return redirect(frontend_dev_url)
frontend_index = Path(app.static_folder) / "frontend" / "index.html"
if frontend_index.exists():
return app.send_static_file("frontend/index.html")
return render_template("index.html")
@app.route("/api/status")
def get_status():
"""Return the status of all managed apps."""
return jsonify(deps.get_process_status(include_output_lines=True))
@app.route("/api/start/<app_name>")
def start_app(app_name: str):
"""鍚姩鎸囧畾搴旂敤"""
process_status = deps.get_process_status()
if app_name not in process_status:
return jsonify({"success": False, "message": "鏈煡搴旂敤"})
if app_name == "forum":
try:
deps.start_forum_engine(emit_output=deps.socket_emit)
return jsonify({"success": True, "message": "ForumEngine已启动"})
except Exception as exc: # pragma: no cover
logger.exception("鎵嬪姩鍚姩ForumEngine澶辫触")
return jsonify({"success": False, "message": f"ForumEngine鍚姩澶辫触: {exc}"})
script_path = deps.streamlit_scripts.get(app_name)
if not script_path:
return jsonify({"success": False, "message": "璇ュ簲鐢ㄤ笉鏀寔鍚姩鎿嶄綔"})
success, message = deps.start_streamlit_app(
app_name,
script_path,
process_status[app_name]["port"],
log_dir=deps.log_dir,
emit_output=deps.socket_emit,
)
if success:
startup_success, startup_message = deps.wait_for_app_startup(app_name, 15)
if not startup_success:
message += f" 浣嗗惎鍔ㄦ鏌ュけ璐? {startup_message}"
return jsonify({"success": success, "message": message})
@app.route("/api/stop/<app_name>")
def stop_app(app_name: str):
"""鍋滄鎸囧畾搴旂敤"""
if app_name not in deps.get_process_status():
return jsonify({"success": False, "message": "鏈煡搴旂敤"})
if app_name == "forum":
try:
deps.stop_forum_engine(emit_output=deps.socket_emit)
return jsonify({"success": True, "message": "ForumEngine已停止"})
except Exception as exc: # pragma: no cover
logger.exception("鎵嬪姩鍋滄ForumEngine澶辫触")
return jsonify({"success": False, "message": f"ForumEngine鍋滄澶辫触: {exc}"})
success, message = deps.stop_streamlit_app(app_name)
return jsonify({"success": success, "message": message})
@app.route("/api/output/<app_name>")
def get_output(app_name: str):
"""鑾峰彇搴旂敤杈撳嚭"""
if app_name not in deps.get_process_status():
return jsonify({"success": False, "message": "鏈煡搴旂敤"})
if app_name == "forum":
try:
return jsonify(deps.get_forum_output(log_dir=deps.log_dir))
except Exception as exc:
return jsonify({"success": False, "message": f"璇诲彇forum鏃ュ織澶辫触: {str(exc)}"})
output_lines = deps.read_log(deps.log_dir, app_name)
return jsonify({"success": True, "output": output_lines})
@app.route("/api/test_log/<app_name>")
def test_log(app_name: str):
"""娴嬭瘯鏃ュ織鍐欏叆鍔熻兘"""
if app_name not in deps.get_process_status():
return jsonify({"success": False, "message": "鏈煡搴旂敤"})
test_msg = f"[{datetime.now().strftime('%H:%M:%S')}] 娴嬭瘯鏃ュ織娑堟伅 - {datetime.now()}"
deps.write_log(deps.log_dir, app_name, test_msg)
deps.socket_emit("console_output", {"app": app_name, "line": test_msg})
return jsonify({"success": True, "message": f"娴嬭瘯娑堟伅宸插啓鍏?{app_name} 鏃ュ織"})
@app.route("/api/forum/start")
def start_forum_monitoring_api():
"""鎵嬪姩鍚姩ForumEngine璁哄潧"""
try:
deps.start_forum_engine(emit_output=deps.socket_emit)
return jsonify({"success": True, "message": "ForumEngine论坛已启动"})
except Exception as exc:
return jsonify({"success": False, "message": f"鍚姩璁哄潧澶辫触: {str(exc)}"})
@app.route("/api/forum/stop")
def stop_forum_monitoring_api():
"""鎵嬪姩鍋滄ForumEngine璁哄潧"""
try:
deps.stop_forum_engine(emit_output=deps.socket_emit)
return jsonify({"success": True, "message": "ForumEngine论坛已停止"})
except Exception as exc:
return jsonify({"success": False, "message": f"鍋滄璁哄潧澶辫触: {str(exc)}"})
@app.route("/api/forum/log")
def get_forum_log():
"""鑾峰彇ForumEngine鐨刦orum.log鍐呭"""
try:
return jsonify(deps.get_forum_log_payload(log_dir=deps.log_dir))
except Exception as exc:
return jsonify({"success": False, "message": f"璇诲彇forum.log澶辫触: {str(exc)}"})
@app.route("/api/forum/log/history", methods=["POST"])
def get_forum_log_history_api():
"""鑾峰彇Forum鍘嗗彶鏃ュ織锛堟敮鎸佷粠鎸囧畾浣嶇疆寮€濮嬶級"""
try:
data = request.get_json() or {}
start_position = data.get("position", 0)
max_lines = data.get("max_lines", 1000)
return jsonify(
deps.get_forum_log_history(
log_dir=deps.log_dir,
start_position=start_position,
max_lines=max_lines,
)
)
except Exception as exc:
return jsonify({"success": False, "message": f"璇诲彇forum鍘嗗彶澶辫触: {str(exc)}"})
@app.route("/api/search", methods=["POST"])
def search():
"""统一搜索接口(异步分发版)"""
try:
data = request.get_json(silent=True) or {}
result, status_code = deps.submit_search_request(
payload=data,
)
return jsonify(result), status_code
except Exception as exc:
logger.exception(f"/api/search 执行失败: {exc}")
return (
jsonify(
{
"success": False,
"message": f"/api/search 执行失败: {exc}",
"traceback": traceback.format_exc(),
}
),
500,
)
@app.route("/api/system/status")
def get_system_status():
"""Return the current system startup state."""
return jsonify(deps.get_system_status())
@app.route("/api/system/start", methods=["POST"])
def start_system():
"""Start the full platform after receiving a request."""
payload, status_code = deps.system_lifecycle.start_system()
return jsonify(payload), status_code
@app.route("/api/system/shutdown", methods=["POST"])
def shutdown_system():
"""Gracefully stop all managed services and shut down the server."""
payload, status_code = deps.system_lifecycle.shutdown_system(cleanup_timeout=6.0)
return jsonify(payload), status_code