feat(web): 任务中心全局 Provider / 设置·布局·路由调整 / 视频解析页适配
- tasks.tsx:顶栏任务中心全局 Provider(SSE 任务进度,供各插件页共享) - 设置页与 AppLayout / client / router 调整,BF2042 插件页路由注册 - web_hub:dashboard / bot_settings 同步更新 - 视频解析页配合后端(缓存 tab / 任务槽进度) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
6af60a7d55
commit
968b3d1cfe
10 files changed
+804
-9
No files matched your search
@@ -41,6 +41,7 @@ from .web_plugin_registry import get_web_plugin_builder, list_web_plugins
|
||||
|
||||
from .dashboard import collect_dashboard
|
||||
|
||||
|
||||
# hexi/web_hub 为机器人核心模块(统一 Web 管理台),非 NoneBot 插件
|
||||
basic_path = Path(__file__).resolve().parent
|
||||
# hexi/web 是统一 Web 管理台前端(hexi/web/dist),不是插件目录下的 web
|
||||
@@ -333,6 +334,10 @@ async def _first_bot():
|
||||
|
||||
|
||||
def build_hub_app() -> FastAPI:
|
||||
# 延迟导入:hexi.core 是插件侧基础设施,模块级导入会触发它整套初始化
|
||||
# (见本模块文档"对 core 的引用一律函数内导入"的约定)
|
||||
from hexi.core.task_slots import TASKS
|
||||
|
||||
sync_admin()
|
||||
|
||||
app = FastAPI(title="HeXi Web Hub")
|
||||
@@ -575,6 +580,58 @@ def build_hub_app() -> FastAPI:
|
||||
|
||||
return StreamingResponse(gen(), media_type="text/event-stream")
|
||||
|
||||
@app.get("/api/tasks")
|
||||
async def list_tasks(_: dict = Depends(get_current_user)):
|
||||
"""任务槽的当前快照(新的在前);SSE 首帧同款,作轮询兜底用。"""
|
||||
return {"ok": True, "tasks": TASKS.snapshot()}
|
||||
|
||||
@app.get("/api/tasks/stream")
|
||||
async def tasks_stream(_: dict = Depends(get_current_user)):
|
||||
"""任务槽 SSE:连上先发一帧全量快照,之后每次任务变化再发一帧。
|
||||
|
||||
变化不频繁(按百分比桶节流),所以直接推送全量快照,不做增量协议。
|
||||
"""
|
||||
queue = TASKS.subscribe()
|
||||
|
||||
async def gen():
|
||||
try:
|
||||
yield f"data: {json.dumps({'ok': True, 'tasks': TASKS.snapshot()}, ensure_ascii=False)}\n\n"
|
||||
while True:
|
||||
try:
|
||||
tasks = await asyncio.wait_for(queue.get(), timeout=15)
|
||||
except asyncio.TimeoutError:
|
||||
yield ": ping\n\n" # ~15s 心跳保活
|
||||
continue
|
||||
yield f"data: {json.dumps({'ok': True, 'tasks': tasks}, ensure_ascii=False)}\n\n"
|
||||
finally:
|
||||
TASKS.unsubscribe(queue)
|
||||
|
||||
return StreamingResponse(gen(), media_type="text/event-stream")
|
||||
|
||||
@app.post("/api/tasks/clear")
|
||||
async def clear_tasks(_: dict = Depends(get_current_user)):
|
||||
"""清掉所有已结束的任务(在跑的不动)。"""
|
||||
removed = TASKS.clear_finished()
|
||||
return {"ok": True, "removed": removed}
|
||||
|
||||
@app.post("/api/tasks/{task_id}/dismiss")
|
||||
async def dismiss_task(task_id: str, _: dict = Depends(get_current_user)):
|
||||
"""从列表里删掉一条已结束的任务(在跑的先取消,删不了)。"""
|
||||
if not TASKS.dismiss(task_id):
|
||||
return {"ok": False, "msg": "任务不存在,或还在运行(在跑的只能先取消)"}
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/api/tasks/{task_id}/cancel")
|
||||
async def cancel_task(task_id: str, _: dict = Depends(get_current_user)):
|
||||
"""请求取消一个在跑的任务。
|
||||
|
||||
协作式:只置标志,任务体在下一个检查点收尾(见 task_slots 模块文档),
|
||||
所以接口马上返回不代表任务已经停了 —— 状态变化会从 SSE 推出去。
|
||||
"""
|
||||
if not TASKS.cancel(task_id):
|
||||
return {"ok": False, "msg": "任务不存在或已结束"}
|
||||
return {"ok": True}
|
||||
|
||||
@app.get("/api/logs/stream")
|
||||
async def logs_stream(
|
||||
since: int = 0,
|
||||
@@ -726,4 +783,4 @@ async def _startup_mount_web_plugins() -> None:
|
||||
@get_driver().on_startup
|
||||
async def _downgrade_uvicorn_access_logs() -> None:
|
||||
"""把 uvicorn.access 日志降级为 DEBUG(经 loguru 后按日志等级过滤)。"""
|
||||
_logging.getLogger("uvicorn.access").addFilter(_uvicorn_access_debug)
|
||||
_logging.getLogger("uvicorn.access").addFilter(_uvicorn_access_debug)
|
||||
Reference in new issue
Block a user