Skip to content

fix: send local aiocqhttp files as base64 - #10169

Open
iuiu-py wants to merge 1 commit into
AstrBotDevs:masterfrom
iuiu-py:fix/9626-aiocqhttp-file-base64
Open

iuiu-py wants to merge 1 commit into
AstrBotDevs:masterfrom
iuiu-py:fix/9626-aiocqhttp-file-base64

Conversation

@iuiu-py

@iuiu-py iuiu-py commented Sep 21, 2026

Copy link
Copy Markdown

Motivation

Fixes #9626.

When AstrBot runs in a container, aiocqhttp/OneBot protocol adapters such as NapCat may not share the AstrBot filesystem. Image and Record segments are already encoded in-process, but local File segments were converted to file:// URIs. The protocol side then tried to open a path from a different container and failed with ENOENT.

Changes

  • Encode readable local File contents as a OneBot base64:// payload, matching the existing image/audio behavior.
  • Derive the displayed file name from the local path when the component name is empty.
  • Preserve the existing URL/callback/file-URI serialization when the contents cannot be read.

Testing

  • Added unit tests covering explicit names, derived names, and the fallback to the existing file:// payload.
  • Confirmed the local-file tests failed before the fix and all pass afterward.
  • Ran uv run pytest tests/unit/test_aiocqhttp_file.py -q (3 passed).
  • Ran the focused aiocqhttp suite: uv run pytest tests/unit/test_aiocqhttp_group_info.py tests/unit/test_aiocqhttp_poke.py tests/unit/test_aiocqhttp_reply.py tests/unit/test_aiocqhttp_file.py -q (18 passed).
  • Ran Ruff format/check on the touched files and git diff --check.

Summary by Sourcery

Ensure local OneBot file segments remain transferable when protocol adapters run in separate containers.

Bug Fixes:

  • Serialize readable local File segments as OneBot base64 payloads so they work across isolated filesystems.
  • Preserve file-reference serialization when local file contents cannot be read.

Enhancements:

  • Derive missing file names from local paths during serialization.

Tests:

  • Add coverage for base64 serialization, path-derived names, and unavailable-file fallback.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py" line_range="51-64" />
<code_context>
         if isinstance(segment, File):
             # For File segments, we need to handle the file differently
+            try:
+                file_path = pathlib.Path(await segment.get_file())
+                if file_path.is_file():
+                    file_data = base64.b64encode(file_path.read_bytes()).decode()
+                    return {
+                        "type": "file",
</code_context>
<issue_to_address>
**issue (broader_impact):** URL-backed `File` segments are downloaded into AstrBot's local filesystem and serialized as `base64://` instead of preserving their original URL. A failed URL download also escapes the `OSError` fallback because `get_file()` can raise non-`OSError` exceptions, so the existing URL serialization is not retained.

**Triggers:** When a `File` is constructed with an HTTP(S) URL rather than a local path.

**Suggested fix:** Only attempt the local-file base64 path when the component has a local `file_` source; otherwise serialize the URL through the existing `to_dict()` path, and handle download failures consistently.

```suggestion
            if segment.file_:
                try:
                    file_path = pathlib.Path(await segment.get_file())
                    if file_path.is_file():
                        file_data = base64.b64encode(file_path.read_bytes()).decode()
                        return {
                            "type": "file",
                            "data": {
                                "name": segment.name or file_path.name,
                                "file": f"base64://{file_data}",
                            },
                        }
                except Exception:
                    # Fall back to a file reference if the contents are unavailable.
                    pass
```
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py" line_range="54" />
<code_context>
+            try:
+                file_path = pathlib.Path(await segment.get_file())
+                if file_path.is_file():
+                    file_data = base64.b64encode(file_path.read_bytes()).decode()
+                    return {
+                        "type": "file",
</code_context>
<issue_to_address>
**issue (performance):** `read_bytes()` performs a complete synchronous filesystem read inside the async message serialization coroutine, blocking the event loop for the entire read and base64 conversion. Sending a large local file therefore stalls unrelated bot tasks and message handling.

**Triggers:** When a local file is large or the filesystem is slow, such as a mounted volume in a container.

**Suggested fix:** Read and encode the file using an async file API or run the blocking read/base64 work via `asyncio.to_thread`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and this changes file handling from sending a reference to transmitting the local file contents in the outbound message. If the path or destination handling is wrong, file data can be exposed or an incorrect message can be sent, and reverting cannot retract what was already delivered.

Blocking findings: astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py:64, astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py:54


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +51 to +64
try:
file_path = pathlib.Path(await segment.get_file())
if file_path.is_file():
file_data = base64.b64encode(file_path.read_bytes()).decode()
return {
"type": "file",
"data": {
"name": segment.name or file_path.name,
"file": f"base64://{file_data}",
},
}
except OSError:
# Fall back to a file reference if the contents are unavailable.
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (broader_impact): URL-backed File segments are downloaded into AstrBot's local filesystem and serialized as base64:// instead of preserving their original URL. A failed URL download also escapes the OSError fallback because get_file() can raise non-OSError exceptions, so the existing URL serialization is not retained.

Triggers: When a File is constructed with an HTTP(S) URL rather than a local path.

Suggested fix: Only attempt the local-file base64 path when the component has a local file_ source; otherwise serialize the URL through the existing to_dict() path, and handle download failures consistently.

Suggested change
try:
file_path = pathlib.Path(await segment.get_file())
if file_path.is_file():
file_data = base64.b64encode(file_path.read_bytes()).decode()
return {
"type": "file",
"data": {
"name": segment.name or file_path.name,
"file": f"base64://{file_data}",
},
}
except OSError:
# Fall back to a file reference if the contents are unavailable.
pass
if segment.file_:
try:
file_path = pathlib.Path(await segment.get_file())
if file_path.is_file():
file_data = base64.b64encode(file_path.read_bytes()).decode()
return {
"type": "file",
"data": {
"name": segment.name or file_path.name,
"file": f"base64://{file_data}",
},
}
except Exception:
# Fall back to a file reference if the contents are unavailable.
pass

try:
file_path = pathlib.Path(await segment.get_file())
if file_path.is_file():
file_data = base64.b64encode(file_path.read_bytes()).decode()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (performance): read_bytes() performs a complete synchronous filesystem read inside the async message serialization coroutine, blocking the event loop for the entire read and base64 conversion. Sending a large local file therefore stalls unrelated bot tasks and message handling.

Triggers: When a local file is large or the filesystem is slow, such as a mounted volume in a container.

Suggested fix: Read and encode the file using an async file API or run the blocking read/base64 work via asyncio.to_thread.

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.

[Bug] send_message_to_user 莫名报 no such file,即使 file 存在且权限正确

1 participant