Repository: nonebot/nonebot2

Issue:
Bug: on_called_api 后处理异常的逻辑问题
### 操作系统

Windows

### Python 版本

3.10.11

### NoneBot 版本

2.1.0

### 适配器

adapter-qqguild 0.3.0

### 协议端

QQ-Guild

### 描述问题

这应该是一个 `on_called_api` 处理逻辑的问题

## 场景

在 [adapter-qqguild](https://github.com/nonebot/adapter-qqguild) 中发送图片之类的消息会引发审核异常 `AuditException`
为使插件层面不受该异常影响而通过 `on_called_api` 处理审核事件:

```python3
from typing import Any, Dict, Optional

from nonebot.exception import MockApiException
from nonebot.adapters.qqguild.bot import Bot as QQGuildBot
from nonebot.adapters.qqguild.exception import AuditException
from nonebot.adapters.qqguild.event import MessageAuditPassEvent, MessageAuditRejectEvent


@QQGuildBot.on_called_api
async def handle_api_result(
        bot: QQGuildBot,
        exception: Optional[Exception],
        api: str,
        data: Dict[str, Any],
        result: Any
):
    """获取消息发送后审核状态并自动处理 AuditException 事件"""
    if not isinstance(bot, QQGuildBot):
        return

    if exception is None:
        return

    if isinstance(exception, AuditException) and api == 'post_messages':
        audit_result = await exception.get_audit_result(timeout=30)
        
        if isinstance(audit_result, MessageAuditPassEvent):
            raise MockApiException(result=...)
        elif isinstance(audit_result, MessageAuditRejectEvent):
            raise exception
        else:
           ...
```

预期是能拦截 `AuditException` 并直接返回审核通过后的发送消息返回内容
实际上即使在 `on_called_api` 中抛出 `MockApiException` 后 `bot.call_api` 仍然抛出了 `AuditException`
原因是在第 108 行捕获 `MockApiException` 后仅修改了返回的 `result` 值, 对 `exception` 并未作处理, 然后 120 行直接抛出了 `adapter._call_api` 中的异常

https://github.com/nonebot/nonebot2/blob/ede1a20c53b05b332b4ace770172eef1f9936c77/nonebot/internal/adapter/bot.py#L96-L121

在 api calling hook 中, `MockApiException` 应当明确表示阻止本次 API 调用或修改本次调用返回值, 并返回自定义内容, 上述场景中即使在 `on_called_api` 中抛出 `MockApiException` 后却仍然继续抛出原异常是不符直觉和逻辑的


### 复现步骤

略

### 期望的结果

_No response_

### 截图或日志

_No response_


Discussion:

Resolving patch:
diff --git a/nonebot/internal/adapter/bot.py b/nonebot/internal/adapter/bot.py
--- a/nonebot/internal/adapter/bot.py
+++ b/nonebot/internal/adapter/bot.py
@@ -106,7 +106,10 @@ async def call_api(self, api: str, **data: Any) -> Any:
                 logger.debug("Running CalledAPI hooks...")
                 await asyncio.gather(*coros)
             except MockApiException as e:
+                # mock api result
                 result = e.result
+                # ignore exception
+                exception = None
                 logger.debug(
                     f"Calling API {api} result is mocked. Return {result} instead."
                 )


Test patch:
diff --git a/tests/test_adapters/test_bot.py b/tests/test_adapters/test_bot.py
new file mode 100644
--- /dev/null
+++ b/tests/test_adapters/test_bot.py
@@ -0,0 +1,152 @@
+from typing import Any, Dict, Optional
+
+import pytest
+from nonebug import App
+
+from nonebot.adapters import Bot
+from nonebot.exception import MockApiException
+
+
+@pytest.mark.asyncio
+async def test_bot_call_api(app: App):
+    async with app.test_api() as ctx:
+        bot = ctx.create_bot()
+        ctx.should_call_api("test", {}, True)
+        result = await bot.call_api("test")
+
+    assert result is True
+
+    async with app.test_api() as ctx:
+        bot = ctx.create_bot()
+        ctx.should_call_api("test", {}, exception=RuntimeError("test"))
+        with pytest.raises(RuntimeError, match="test"):
+            await bot.call_api("test")
+
+
+@pytest.mark.asyncio
+async def test_bot_calling_api_hook_simple(app: App):
+    runned: bool = False
+
+    async def calling_api_hook(bot: Bot, api: str, data: Dict[str, Any]):
+        nonlocal runned
+        runned = True
+
+    hooks = set()
+
+    with pytest.MonkeyPatch.context() as m:
+        m.setattr(Bot, "_calling_api_hook", hooks)
+
+        Bot.on_calling_api(calling_api_hook)
+
+        assert hooks == {calling_api_hook}
+
+        async with app.test_api() as ctx:
+            bot = ctx.create_bot()
+            ctx.should_call_api("test", {}, True)
+            result = await bot.call_api("test")
+
+        assert runned is True
+        assert result is True
+
+
+@pytest.mark.asyncio
+async def test_bot_calling_api_hook_mock(app: App):
+    runned: bool = False
+
+    async def calling_api_hook(bot: Bot, api: str, data: Dict[str, Any]):
+        nonlocal runned
+        runned = True
+
+        raise MockApiException(False)
+
+    hooks = set()
+
+    with pytest.MonkeyPatch.context() as m:
+        m.setattr(Bot, "_calling_api_hook", hooks)
+
+        Bot.on_calling_api(calling_api_hook)
+
+        assert hooks == {calling_api_hook}
+
+        async with app.test_api() as ctx:
+            bot = ctx.create_bot()
+            result = await bot.call_api("test")
+
+        assert runned is True
+        assert result is False
+
+
+@pytest.mark.asyncio
+async def test_bot_called_api_hook_simple(app: App):
+    runned: bool = False
+
+    async def called_api_hook(
+        bot: Bot,
+        exception: Optional[Exception],
+        api: str,
+        data: Dict[str, Any],
+        result: Any,
+    ):
+        nonlocal runned
+        runned = True
+
+    hooks = set()
+
+    with pytest.MonkeyPatch.context() as m:
+        m.setattr(Bot, "_called_api_hook", hooks)
+
+        Bot.on_called_api(called_api_hook)
+
+        assert hooks == {called_api_hook}
+
+        async with app.test_api() as ctx:
+            bot = ctx.create_bot()
+            ctx.should_call_api("test", {}, True)
+            result = await bot.call_api("test")
+
+        assert runned is True
+        assert result is True
+
+
+@pytest.mark.asyncio
+async def test_bot_called_api_hook_mock(app: App):
+    runned: bool = False
+
+    async def called_api_hook(
+        bot: Bot,
+        exception: Optional[Exception],
+        api: str,
+        data: Dict[str, Any],
+        result: Any,
+    ):
+        nonlocal runned
+        runned = True
+
+        raise MockApiException(False)
+
+    hooks = set()
+
+    with pytest.MonkeyPatch.context() as m:
+        m.setattr(Bot, "_called_api_hook", hooks)
+
+        Bot.on_called_api(called_api_hook)
+
+        assert hooks == {called_api_hook}
+
+        async with app.test_api() as ctx:
+            bot = ctx.create_bot()
+            ctx.should_call_api("test", {}, True)
+            result = await bot.call_api("test")
+
+        assert runned is True
+        assert result is False
+
+        runned = False
+
+        async with app.test_api() as ctx:
+            bot = ctx.create_bot()
+            ctx.should_call_api("test", {}, exception=RuntimeError("test"))
+            result = await bot.call_api("test")
+
+        assert runned is True
+        assert result is False