Repository: nonebot/nonebot2

Issue:
Bug: Quart 驱动器 WebSocket 上下文错误
### 操作系统

Windows

### Python 版本

Any

### NoneBot 版本

2.0.0

### 适配器

OneBot v11

### 协议端

/

### 描述问题

Related to https://github.com/nonebot/adapter-onebot/issues/68

### 复现步骤

在 Apscheduler 的 job 中调用 bot api 通过 websocket 发送内容

### 期望的结果

_No response_

### 截图或日志

_No response_


Discussion:

Resolving patch:
diff --git a/nonebot/drivers/quart.py b/nonebot/drivers/quart.py
--- a/nonebot/drivers/quart.py
+++ b/nonebot/drivers/quart.py
@@ -17,7 +17,18 @@
 
 import asyncio
 from functools import wraps
-from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
+from typing import (
+    Any,
+    Dict,
+    List,
+    Tuple,
+    Union,
+    TypeVar,
+    Callable,
+    Optional,
+    Coroutine,
+    cast,
+)
 
 from pydantic import BaseSettings
 
@@ -33,7 +44,8 @@
 try:
     import uvicorn
     from quart import request as _request
-    from quart import websocket as _websocket
+    from quart.ctx import WebsocketContext
+    from quart.globals import websocket_ctx
     from quart import Quart, Request, Response
     from quart.datastructures import FileStorage
     from quart import Websocket as QuartWebSocket
@@ -222,7 +234,8 @@ async def _handle_http(self, setup: HTTPServerSetup) -> Response:
         )
 
     async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
-        websocket: QuartWebSocket = _websocket
+        ctx = cast(WebsocketContext, websocket_ctx.copy())
+        websocket = websocket_ctx.websocket
 
         http_request = BaseRequest(
             websocket.method,
@@ -232,7 +245,7 @@ async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
             version=websocket.http_version,
         )
 
-        ws = WebSocket(request=http_request, websocket=websocket)
+        ws = WebSocket(request=http_request, websocket_ctx=ctx)
 
         await setup.handle_func(ws)
 
@@ -240,9 +253,13 @@ async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
 class WebSocket(BaseWebSocket):
     """Quart WebSocket Wrapper"""
 
-    def __init__(self, *, request: BaseRequest, websocket: QuartWebSocket):
+    def __init__(self, *, request: BaseRequest, websocket_ctx: WebsocketContext):
         super().__init__(request=request)
-        self.websocket = websocket
+        self.websocket_ctx = websocket_ctx
+
+    @property
+    def websocket(self) -> QuartWebSocket:
+        return self.websocket_ctx.websocket
 
     @property
     @overrides(BaseWebSocket)


Test patch:
diff --git a/tests/test_broadcast.py b/tests/test_broadcast.py
--- a/tests/test_broadcast.py
+++ b/tests/test_broadcast.py
@@ -234,7 +234,7 @@ async def test_preprocessor(
                 bot = ctx.create_bot()
                 event = make_fake_event()()
                 ctx.receive_event(bot, event)
-                ctx.should_call_send(event, "test", True, bot)
+                ctx.should_call_send(event, "test", True, bot=bot)
 
         assert runned, "run_preprocessor should runned"
 
@@ -346,7 +346,7 @@ async def test_postprocessor(
                 bot = ctx.create_bot()
                 event = make_fake_event()()
                 ctx.receive_event(bot, event)
-                ctx.should_call_send(event, "test", True, bot)
+                ctx.should_call_send(event, "test", True, bot=bot)
 
         assert runned, "run_postprocessor should runned"
 
diff --git a/tests/test_driver.py b/tests/test_driver.py
--- a/tests/test_driver.py
+++ b/tests/test_driver.py
@@ -1,6 +1,6 @@
 import json
 import asyncio
-from typing import Any, Set, cast
+from typing import Any, Set, Optional, cast
 
 import pytest
 from nonebug import App
@@ -154,6 +154,63 @@ async def _handle_ws(ws: WebSocket) -> None:
     await asyncio.sleep(1)
 
 
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    "driver",
+    [
+        pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
+        pytest.param("nonebot.drivers.quart:Driver", id="quart"),
+    ],
+    indirect=True,
+)
+async def test_cross_context(app: App, driver: Driver):
+    driver = cast(ReverseDriver, driver)
+
+    ws: Optional[WebSocket] = None
+    ws_ready = asyncio.Event()
+    ws_should_close = asyncio.Event()
+
+    async def background_task():
+        try:
+            await ws_ready.wait()
+            assert ws is not None
+
+            await ws.send("ping")
+            data = await ws.receive()
+            assert data == "pong"
+        finally:
+            ws_should_close.set()
+
+    task = asyncio.create_task(background_task())
+
+    async def _handle_ws(websocket: WebSocket) -> None:
+        nonlocal ws
+        await websocket.accept()
+        ws = websocket
+        ws_ready.set()
+
+        await ws_should_close.wait()
+        await websocket.close()
+
+    ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
+    driver.setup_websocket_server(ws_setup)
+
+    async with app.test_server(driver.asgi) as ctx:
+        client = ctx.get_client()
+
+        async with client.websocket_connect("/ws_test") as websocket:
+            try:
+                data = await websocket.receive_text()
+                assert data == "ping"
+                await websocket.send_text("pong")
+            except Exception as e:
+                if not e.args or "websocket.close" not in str(e.args[0]):
+                    raise
+
+    await task
+    await asyncio.sleep(1)
+
+
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
     "driver",