linux-web: B/S 架构——采集引擎后台线程 + aiohttp WebSocket 实时推送 + Web 仪表盘 + systemd 部署

This commit is contained in:
lookt
2026-09-15 20:09:46 +08:00
parent 2c72de1dba
commit f6bdc5a983
8 changed files with 633 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""
B/S 服务端:时间线采集引擎(后台线程)+ WebSocket 实时推送 + Web 仪表盘
Linux 服务器常驻运行: python -m src.web.server (或 systemd,见 deploy/a-stock-timeline.service
端口默认 8100,可用环境变量 PORT 覆盖。
"""
import asyncio
import json
import os
from collections import deque
from pathlib import Path
from aiohttp import WSMsgType, web
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from src.realtime.collector import TimelineCollector # noqa: E402
PORT = int(os.environ.get('PORT', '8100'))
ROOT = Path(__file__).resolve().parent.parent.parent
WEB_DIR = Path(__file__).resolve().parent
RECENT_MAX = 500
recent = deque(maxlen=RECENT_MAX) # 最近事件(内存)
clients = set() # 活跃 WS 连接
async def broadcast(event: dict):
recent.append(event)
payload = json.dumps(event, ensure_ascii=False)
dead = set()
for ws in clients:
try:
await ws.send_str(payload)
except Exception:
dead.add(ws)
for ws in dead:
clients.discard(ws)
async def collector_task(_app):
"""把阻塞式采集循环放进线程池,事件桥接到 asyncio 广播"""
loop = asyncio.get_running_loop()
collector = TimelineCollector(emit=lambda rec: loop.call_soon_threadsafe(
asyncio.ensure_future, broadcast(rec)))
async def poll():
while True:
await loop.run_in_executor(None, collector.one_cycle)
await asyncio.sleep(60)
task = asyncio.create_task(poll())
# 启动即推最近历史(从 jsonl 恢复)
feed = ROOT / 'data' / 'realtime_feed.jsonl'
if feed.exists():
try:
lines = feed.read_text(encoding='utf-8').strip().splitlines()[-200:]
for line in lines:
try:
recent.append(json.loads(line))
except json.JSONDecodeError:
pass
except OSError:
pass
yield
task.cancel()
async def index(_request):
return web.FileResponse(WEB_DIR / 'index.html')
async def recent_events(_request):
return web.json_response({'events': list(recent)})
async def stats(_request):
return web.json_response({'clients': len(clients), 'buffered': len(recent)})
async def ws_handler(request):
ws = web.WebSocketResponse(heartbeat=30)
await ws.prepare(request)
clients.add(ws)
await ws.send_str(json.dumps({'kind': '连接成功',
'data': {'msg': '实时推送已连接', 'buffered': len(recent)}},
ensure_ascii=False))
try:
async for msg in ws:
if msg.type == WSMsgType.ERROR:
break
finally:
clients.discard(ws)
return ws
def build_app():
app = web.Application()
app.router.add_get('/', index)
app.router.add_get('/api/recent', recent_events)
app.router.add_get('/api/stats', stats)
app.router.add_get('/ws', ws_handler)
app.cleanup_ctx.append(collector_task)
return app
if __name__ == '__main__':
print('JQuant 时间线 B/S 服务启动: http://0.0.0.0:{} WS: /ws'.format(PORT))
web.run_app(build_app(), host='0.0.0.0', port=PORT)