Skip to content

Conversation

@LuisKlee
Copy link
Collaborator

@LuisKlee LuisKlee commented Jan 11, 2026

Summary by Sourcery

Update network handling for media and forwarded messages and clarify branch stability in the README.

Bug Fixes:

  • Replace custom urllib3 SSL adapter with aiohttp for downloading media as Base64, improving compatibility and async behavior.
  • Add timeout handling when fetching stranger info and return None on failure instead of raising.
  • Ensure forward message fetching always returns a structured placeholder message on errors or timeouts and safely handles empty adapter responses.

Enhancements:

  • Change forward message helper to return a list of message dicts instead of a single dict for better downstream consumption.

Documentation:

  • Document the roles and stability of the dev and classical branches in the README, including guidance on switching branches.

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 11, 2026

Reviewer's Guide

Refactors network and timeout handling in the napcat adapter utilities, adjusts forward-message return semantics and error handling, and updates the README to document dev/classical branch stability and usage guidance.

Sequence diagram for updated get_image_base64 network handling

sequenceDiagram
    participant Caller
    participant Utils as NapcatUtils
    participant Aiohttp as aiohttp_ClientSession
    participant Remote as RemoteServer

    Caller->>Utils: get_image_base64(url)
    activate Utils
    Utils->>Aiohttp: create ClientSession()
    activate Aiohttp
    Utils->>Aiohttp: session.get(url, timeout=10)
    Aiohttp->>Remote: HTTP GET url
    Remote-->>Aiohttp: HTTP response
    Aiohttp-->>Utils: response

    alt response.status != 200
        Utils-->>Caller: raise Exception HTTP_Error_status
    else response.status == 200
        Utils->>Aiohttp: response.read()
        Aiohttp-->>Utils: image_bytes
        Utils-->>Caller: base64_b64encode(image_bytes)
    end

    deactivate Aiohttp
    deactivate Utils

    opt any_exception
        Utils->>Utils: log error 图片下载失败
        Utils-->>Caller: re_raise_exception
    end
Loading

Sequence diagram for updated get_forward_message error handling and return type

sequenceDiagram
    participant Caller
    participant Utils as NapcatUtils
    participant Adapter as NapcatAdapter

    Caller->>Utils: get_forward_message(raw_message, adapter)
    activate Utils
    Utils->>Utils: extract forward_message_data from raw_message
    alt forward_message_data empty
        Utils->>Utils: log warning 转发消息内容为空
        Utils-->>Caller: None
    else forward_message_data present
        Utils->>Adapter: get_msg(forward_id)
        activate Adapter
        Adapter-->>Utils: response or timeout or error
        deactivate Adapter

        alt response is None
            Utils->>Utils: log error 获取转发消息失败,返回值为空
            Utils-->>Caller: [{sender:{nickname:系统}, message:[{type:text, data:[获取转发消息失败]}]}]
        else adapter timeout
            Utils->>Utils: log error 获取转发消息超时
            Utils-->>Caller: [{sender:{nickname:系统}, message:[{type:text, data:[获取转发消息超时]}]}]
        else adapter exception
            Utils->>Utils: log error 获取转发消息失败
            Utils-->>Caller: [{sender:{nickname:系统}, message:[{type:text, data:[获取转发消息失败_with_reason]}]}]
        else response ok
            Utils->>Utils: response_data = (response.data or {})
            alt response_data empty
                Utils->>Utils: log warning 转发消息内容为空或获取失败
                Utils-->>Caller: None
            else response_data present
                Utils->>Utils: log debug 转发消息原始格式
                Utils-->>Caller: response_data as list_of_message_nodes
            end
        end
        deactivate Utils
    end
Loading

File-Level Changes

Change Details Files
Switch image download helper to use asynchronous aiohttp instead of a custom urllib3 SSL adapter.
  • Remove custom SSLAdapter subclass based on urllib3.PoolManager and default SSL context overrides.
  • Refactor get_image_base64 to create an aiohttp.ClientSession and perform an async GET with a 10s timeout.
  • Read response bytes via await response.read() and continue returning base64-encoded content while preserving HTTP status validation and logging.
src/plugins/built_in/napcat_adapter/src/handlers/utils.py
Harden stranger-info and forward-message retrieval with timeouts, structured fallback values, and a stricter return type.
  • Add a timeout parameter (10.0s) to _call_adapter_api when fetching stranger info, and on failure log a warning and return None instead of propagating errors.
  • Change get_forward_message return type from optional dict to optional list of dicts, and on failure/timeout/exception return a synthetic system message list instead of None.
  • Ensure response data handling in get_forward_message is robust to missing/None responses by defaulting to an empty dict and keeping existing empty-content warnings.
src/plugins/built_in/napcat_adapter/src/handlers/utils.py
Clarify branch stability and usage instructions in the README.
  • Document that the dev branch is under active refactor and unstable, while the classical branch is the maintained stable line.
  • Add explicit git commands showing how to switch to the classical branch for stable usage and reserving dev for refactor/testing PRs.
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@LuisKlee LuisKlee merged commit fe57d79 into classical Jan 11, 2026
3 of 4 checks passed
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In get_image_base64, consider reusing an aiohttp.ClientSession instead of creating a new session on every call to avoid unnecessary connection overhead and improve performance under load.
  • get_forward_message now usually returns a list[dict] (including for error cases) but still returns None when response_data is empty; aligning all error/empty cases to a consistent list-based return shape would simplify caller handling and reduce the need for extra None checks.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_image_base64`, consider reusing an `aiohttp.ClientSession` instead of creating a new session on every call to avoid unnecessary connection overhead and improve performance under load.
- `get_forward_message` now usually returns a `list[dict]` (including for error cases) but still returns `None` when `response_data` is empty; aligning all error/empty cases to a consistent list-based return shape would simplify caller handling and reduce the need for extra `None` checks.

## Individual Comments

### Comment 1
<location> `src/plugins/built_in/napcat_adapter/src/handlers/utils.py:238-239` </location>
<code_context>
-            raise Exception(f"HTTP Error: {response.status}")
-        image_bytes = response.data
-        return base64.b64encode(image_bytes).decode("utf-8")
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url, timeout=10) as response:
+                if response.status != 200:
+                    raise Exception(f"HTTP Error: {response.status}")
</code_context>

<issue_to_address>
**suggestion:** Consider reusing an aiohttp.ClientSession and configuring SSL/timeouts explicitly instead of creating a new session per call.

Creating a new `ClientSession` on every `get_image_base64` call adds unnecessary overhead and can trigger aiohttp warnings. Prefer a shared or injected session, configured once with `ClientTimeout` and a `TCPConnector` (including SSL options), so you can preserve any required relaxed SSL/cipher/TLS settings while keeping the async API.

Suggested implementation:

```python
import aiohttp

# Shared HTTP client session for outbound requests from this module.
# NOTE: This avoids creating a new ClientSession per call, which is inefficient
# and may trigger aiohttp warnings. Configure timeout/SSL/connector here.
_HTTP_SESSION: aiohttp.ClientSession | None = None


async def get_http_session() -> aiohttp.ClientSession:
    """Get (or lazily create) a shared aiohttp ClientSession.

    Centralizes timeout/SSL/connector configuration so callers do not need to
    create their own sessions and we can preserve any custom TLS settings.
    """
    global _HTTP_SESSION

    if _HTTP_SESSION is None or _HTTP_SESSION.closed:
        timeout = aiohttp.ClientTimeout(total=10)
        connector = aiohttp.TCPConnector(
            # Set SSL options explicitly; adjust as needed for your environment.
            # ssl=None keeps default verification; set ssl=False to disable it.
            ssl=None,
            # You can tune connection limits here if needed.
            limit=100,
        )
        _HTTP_SESSION = aiohttp.ClientSession(
            timeout=timeout,
            connector=connector,
        )

    return _HTTP_SESSION

```

```python
    # sourcery skip: raise-specific-error
    """下载图片/视频并返回Base64"""
    logger.debug(f"下载图片: {url}")
    try:
        session = await get_http_session()
        async with session.get(url) as response:
            if response.status != 200:
                raise Exception(f"HTTP Error: {response.status}")
            image_bytes = await response.read()
            return base64.b64encode(image_bytes).decode("utf-8")
    except Exception as e:
        logger.error(f"图片下载失败: {e!s}")

```

If this function is not already declared as `async`, it must be updated to `async def ...` to allow `await get_http_session()`.  
If your project has a centralized lifecycle management (startup/shutdown hooks), you may also want to add logic there to gracefully close `_HTTP_SESSION` (e.g. `await _HTTP_SESSION.close()` on shutdown) rather than relying on process exit.
</issue_to_address>

### Comment 2
<location> `src/plugins/built_in/napcat_adapter/src/handlers/utils.py:375-381` </location>
<code_context>

 async def get_forward_message(
     raw_message: dict, *, adapter: "NapcatAdapter | None" = None
-) -> dict[str, Any] | None:
</code_context>

<issue_to_address>
**question (bug_risk):** The return type of get_forward_message is now a list, but some branches still return None; consider making the result shape more consistent.

The function now sometimes returns a synthetic single-element list on error/timeout, but still uses `None` when `response_data` is falsy ("转发消息内容为空或获取失败"). This forces callers to handle three shapes: a normal list from the API, a synthetic error list, and `None`. Consider standardizing on either a list for all non-success cases (e.g., a system message element) or consistently using `None` for all failures, and ensure this matches existing caller expectations to avoid subtle logic errors.
</issue_to_address>

### Comment 3
<location> `README.md:36` </location>
<code_context>
+> **分支状态说明**
+>
+> - `dev` 分支当前用于架构重构与大规模改动,功能不稳定,无法保证可用性。
+> - 现行可用的稳定版本已迁移至 `classical` 分支,并在该分支持续维护。
+> - 如需稳定使用,请切换到 `classical` 分支:
+>
</code_context>

<issue_to_address>
**nitpick (typo):** Consider adding “上” in “并在该分支持续维护” for more natural written Chinese.

This matches standard written usage and improves readability.

```suggestion
> - 现行可用的稳定版本已迁移至 `classical` 分支,并在该分支上持续维护。
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +238 to +239
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=10) as response:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider reusing an aiohttp.ClientSession and configuring SSL/timeouts explicitly instead of creating a new session per call.

Creating a new ClientSession on every get_image_base64 call adds unnecessary overhead and can trigger aiohttp warnings. Prefer a shared or injected session, configured once with ClientTimeout and a TCPConnector (including SSL options), so you can preserve any required relaxed SSL/cipher/TLS settings while keeping the async API.

Suggested implementation:

import aiohttp

# Shared HTTP client session for outbound requests from this module.
# NOTE: This avoids creating a new ClientSession per call, which is inefficient
# and may trigger aiohttp warnings. Configure timeout/SSL/connector here.
_HTTP_SESSION: aiohttp.ClientSession | None = None


async def get_http_session() -> aiohttp.ClientSession:
    """Get (or lazily create) a shared aiohttp ClientSession.

    Centralizes timeout/SSL/connector configuration so callers do not need to
    create their own sessions and we can preserve any custom TLS settings.
    """
    global _HTTP_SESSION

    if _HTTP_SESSION is None or _HTTP_SESSION.closed:
        timeout = aiohttp.ClientTimeout(total=10)
        connector = aiohttp.TCPConnector(
            # Set SSL options explicitly; adjust as needed for your environment.
            # ssl=None keeps default verification; set ssl=False to disable it.
            ssl=None,
            # You can tune connection limits here if needed.
            limit=100,
        )
        _HTTP_SESSION = aiohttp.ClientSession(
            timeout=timeout,
            connector=connector,
        )

    return _HTTP_SESSION
    # sourcery skip: raise-specific-error
    """下载图片/视频并返回Base64"""
    logger.debug(f"下载图片: {url}")
    try:
        session = await get_http_session()
        async with session.get(url) as response:
            if response.status != 200:
                raise Exception(f"HTTP Error: {response.status}")
            image_bytes = await response.read()
            return base64.b64encode(image_bytes).decode("utf-8")
    except Exception as e:
        logger.error(f"图片下载失败: {e!s}")

If this function is not already declared as async, it must be updated to async def ... to allow await get_http_session().
If your project has a centralized lifecycle management (startup/shutdown hooks), you may also want to add logic there to gracefully close _HTTP_SESSION (e.g. await _HTTP_SESSION.close() on shutdown) rather than relying on process exit.

Comment on lines 375 to 381

async def get_forward_message(
raw_message: dict, *, adapter: "NapcatAdapter | None" = None
) -> dict[str, Any] | None:
) -> list[dict[str, Any]] | None:
forward_message_data: dict = raw_message.get("data", {})
if not forward_message_data:
logger.warning("转发消息内容为空")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): The return type of get_forward_message is now a list, but some branches still return None; consider making the result shape more consistent.

The function now sometimes returns a synthetic single-element list on error/timeout, but still uses None when response_data is falsy ("转发消息内容为空或获取失败"). This forces callers to handle three shapes: a normal list from the API, a synthetic error list, and None. Consider standardizing on either a list for all non-success cases (e.g., a system message element) or consistently using None for all failures, and ensure this matches existing caller expectations to avoid subtle logic errors.

> **分支状态说明**
>
> - `dev` 分支当前用于架构重构与大规模改动,功能不稳定,无法保证可用性。
> - 现行可用的稳定版本已迁移至 `classical` 分支,并在该分支持续维护。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Consider adding “上” in “并在该分支持续维护” for more natural written Chinese.

This matches standard written usage and improves readability.

Suggested change
> - 现行可用的稳定版本已迁移至 `classical` 分支,并在该分支持续维护
> - 现行可用的稳定版本已迁移至 `classical` 分支,并在该分支上持续维护

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants