Skip to content

release: v2.1.3 - #100

Merged
FlanChanXwO merged 3 commits into
masterfrom
codex/release-v2.1.3
Jul 23, 2026
Merged

release: v2.1.3#100
FlanChanXwO merged 3 commits into
masterfrom
codex/release-v2.1.3

Conversation

@FlanChanXwO

@FlanChanXwO FlanChanXwO commented Jul 23, 2026

Copy link
Copy Markdown
Member

发布 v2.1.3:将 PR #93 的 OneBot 合并转发节点 UIN 修复安全移植到当前 master(v2.1.2)上。\n\n### 内容\n- 合并转发节点在可可靠识别时写入 bot self_id,修复新版 NapCat / Lagrange 的 retcode=1200。\n- 主动推送仅在 bot 账号唯一时使用解析结果;多 bot 或未知场景保留 SDK 默认值,避免写错账号。\n- 更新 CHANGELOG.md 与 metadata.yaml 至 v2.1.3。\n- 修正数据目录测试对普通 checkout 路径的硬编码,使其可在 Git worktree 中稳定运行。\n\n### 验证\n- pytest tests/unit -q:724 passed, 2 skipped\n- pre-commit run --all-files:passed\n- 仓库体积:约 9 MB,低于 16 MB 上限\n\n- [x] 这不是破坏性变更。

Summary by Sourcery

发布 v2.1.3,改进 OneBot 转发消息处理,并在 QQ 平台上实现更安全的机器人账号解析。

Bug 修复:

  • 确保 OneBot 合并转发节点在可以可靠确定的情况下包含正确的机器人 QQ self_id,防止在新版 NapCat/Lagrange 实现上出现 retcode=1200 错误。
  • 当无法唯一识别机器人 self_id 时,回退为保留 SDK 默认 UIN,避免在多机器人或未知场景中出现错误归属。
  • 修复插件数据目录解析问题,使测试在 Git worktree 和非标准检出布局下也能正常运行。

功能改进:

  • 引入基于 provider 的机制,从已连接的 WebSocket 客户端解析机器人 self_id,用于主动推送场景,并接入到 bootstrap 和 OneBot sender 中。
  • 更新平台文档,说明 OneBot/aiocqhttp 转发节点 UIN 的行为和限制。

文档:

  • 在平台指南中记录更新后的 OneBot/aiocqhttp 转发节点行为以及默认 UIN 处理方式。

测试:

  • 为 OneBot sender 添加测试,用于覆盖命令响应、主动推送以及未知 self_id 情况下的 self_id 解析。
  • 添加 bootstrap 运行时测试,验证在仅有单一已连接账号时,机器人 self_id provider 才会返回值。
  • 调整路径相关测试,通过使用临时 AstrBot 根目录来避免硬编码检出路径。

杂项:

  • 将插件版本元数据和变更日志提升至 v2.1.3。
Original summary in English

Summary by Sourcery

Release v2.1.3 with improved OneBot forward-message handling and safer bot account resolution for QQ platforms.

Bug Fixes:

  • Ensure OneBot merged forward nodes include the correct bot QQ self_id when it can be reliably determined, preventing retcode=1200 failures on newer NapCat/Lagrange implementations.
  • Fallback to leaving the SDK default UIN when the bot self_id cannot be uniquely identified, avoiding misattribution in multi-bot or unknown scenarios.
  • Fix plugin data directory resolution so tests work correctly in Git worktree and non-standard checkout layouts.

Enhancements:

  • Introduce a provider-based mechanism to resolve bot self_id from connected WebSocket clients for active push scenarios and wire it into the bootstrap and OneBot sender.
  • Update platform documentation to describe OneBot/aiocqhttp forward-node UIN behavior and constraints.

Documentation:

  • Document updated OneBot/aiocqhttp behavior for forward nodes and default UIN handling in the platforms guide.

Tests:

  • Add OneBot sender tests covering self_id resolution for command responses, active pushes, and unknown self_id cases.
  • Add bootstrap runtime tests verifying that the bot self_id provider only returns a value when there is a single connected account.
  • Adjust path tests to avoid hard-coded checkout paths by using a temporary AstrBot root.

Chores:

  • Bump plugin version metadata and changelog to v2.1.3.

Summary by CodeRabbit

  • 新功能

    • OneBot 合并转发节点现在会在可靠识别机器人账号时写入正确的 QQ 号。
    • 支持从当前事件或已连接账号中解析机器人 QQ 号。
  • 问题修复

    • 修复合并转发节点缺少账号信息导致发送失败的问题。
    • 多账号或无法确认账号时不再误用其他账号,也不会伪造账号信息。
    • 保持原有直接发送及失败时回退文本的行为。
  • 文档

    • 补充 OneBot 合并转发节点账号字段的写入规则。
  • 版本

    • 版本更新至 2.1.3。

Github-2333-top and others added 3 commits July 24, 2026 00:40
OneBotMessageSender 构建合并转发 Node 时未传 uin,导致 Node.uin 默认
为 "0",序列化后 user_id="0"。新版 NapCat / Lagrange 会拒绝 user_id
为 0 或空的转发节点,返回 retcode=1200 ("forward node user_id/uin is
required"),使 RSS 推送到 QQ 群失败。

修复:
- onebot_sender: 新增 _resolve_bot_self_id,优先从 event.message_obj.self_id
  读取(命令响应场景),其次通过 get_bot_self_id provider 解析(主动推送
  场景),最后兜底非零占位 "10000";构建与回退 Node 时均传入 uin。
- bootstrap: 注册 set_bot_self_id_provider,从 CQHttp 的 _wsr_api_clients
  解析已连接 bot 的 self_id(types.py 已预留 hook 但未注册)。

同时将 bot_client 解析上移到节点构建之前,使 uin 在构建节点时即可用。

(cherry picked from commit 6d5f7af)
Copilot AI review requested due to automatic review settings July 23, 2026 16:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added area: docs Documentation changes area: backend Backend or core runtime changes area: tests Test changes release Release metadata or changelog changes labels Jul 23, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

实现了一种安全机制,用于在命令响应和主动推送两种场景中解析并注入 OneBot 机器人 self_id(QQ 号)到合并转发节点中;通过 bootstrap 注册全局 self_id provider;相应更新测试和文档;并将插件版本号提升至 v2.1.3。

Sequence diagram for resolving bot self_id during OneBot send_to_user

sequenceDiagram
    participant RSSJob
    participant OneBotMessageSender
    participant MessageContext
    participant get_bot_self_id
    participant _bot_self_id_provider

    RSSJob->>OneBotMessageSender: send_to_user(request, context)
    OneBotMessageSender->>OneBotMessageSender: _resolve_bot_self_id(context)
    alt event message has self_id and != 0
        OneBotMessageSender->>MessageContext: getattr(event.message_obj, self_id)
        OneBotMessageSender-->>OneBotMessageSender: return self_id
    else platform_name is set
        OneBotMessageSender->>get_bot_self_id: get_bot_self_id(platform_name)
        get_bot_self_id->>_bot_self_id_provider: _bot_self_id_provider(platform_name)
        _bot_self_id_provider-->>get_bot_self_id: self_id or ""
        get_bot_self_id-->>OneBotMessageSender: self_id or ""
    else cannot resolve
        OneBotMessageSender-->>OneBotMessageSender: return ""
    end
    OneBotMessageSender->>OneBotMessageSender: _build_forward_node(content, nickname, bot_self_id)
    Note over OneBotMessageSender: Only sets Node.uin when bot_self_id is non-empty
    OneBotMessageSender-->>RSSJob: SendResult
Loading

File-Level Changes

Change Details Files
仅在可以可靠获取的情况下,从消息上下文或 provider 中解析机器人 self_id,并将其应用到 OneBot 合并转发节点。
  • 扩展 OneBot sender 的导入以包含 get_bot_self_id,并新增 _resolve_bot_self_id 辅助方法,通过 event.self_id 或基于平台的 provider 推导机器人 QQ,严格过滤未知值和为 0 的值。
  • 引入 _build_forward_node 辅助方法,仅在存在非空 bot_self_id 时有条件地设置 Node.uin,否则回退到 SDK 默认行为。
  • 将 bot_self_id 解析整合进 send_to_user:每次发送时只解析一次,并在所有构建转发节点的路径中复用,包括媒体上传失败后的回退节点;当 self_id 未知时保持现有行为不变。
src/infrastructure/messaging/senders/onebot_sender.py
从 bootstrap 暴露全局 bot self_id provider,通过已连接的 aiocqhttp 客户端安全推导唯一的 self_id。
  • 更新 bootstrap 的导入以包含 set_bot_self_id_provider,并调整 _register_bot_client_provider 的文档字符串,覆盖用于主动推送的客户端和 self_id provider。
  • 实现 _resolve_bot_self_id:检查平台客户端的 _wsr_api_clients,在唯一且非空/非零的情况下返回该 self_id;当存在多个候选值时记录警告并返回空字符串,并通过 set_bot_self_id_provider 注册该结果。
  • 保留现有的 bot client 解析逻辑,同时一并注册客户端和 self_id provider。
bootstrap.py
收紧 get_bot_self_id 的语义,使调用方能够区分未知和已知的机器人 ID。
  • 将 get_bot_self_id 的行为改为在未注册任何 provider 时返回空字符串,而不是硬编码的占位值,确保调用方不会将假 ID 误认为真实 ID。
src/infrastructure/messaging/senders/types.py
加强围绕 OneBot sender self_id 处理、媒体本地路径、bot self_id provider 注册以及数据目录解析的单元测试。
  • 增强 OneBot sender 的测试:为测试用 Node stub 添加 uin 字段,确保 PreparedMedia 实例包含 local_path,并新增三个异步测试,分别覆盖基于 event 的 self_id、用于主动推送的基于 provider 的 self_id,以及在 self_id 解析失败或未知时保留默认 uin 的行为。
  • 新增一个 bootstrap 运行时测试:使用基于 SimpleNamespace 的 platform 和 platform_manager 假对象,验证已注册的 bot self_id provider 在存在唯一连接 self_id 时返回该值,在存在多个 self_id 时返回空字符串。
  • 重构插件数据目录测试:在 tmp_path 下构建一个合成的 AstrBot/tree,通过 monkeypatch 覆盖 PLUGIN_ROOT,并断言以工作树安全的方式避免使用插件本地的 astrbot 数据。
tests/unit/infrastructure/test_onebot_sender.py
tests/unit/test_bootstrap_runtime.py
tests/unit/infrastructure/test_paths.py
更新文档和元数据,以反映新的 OneBot 行为以及版本号提升至 v2.1.3。
  • 扩展平台总览文档中 OneBot/aiocqhttp 的这一行,描述合并转发节点现在会写入已确认的机器人 QQ uin,而在未知时保留 SDK 默认行为,并移除该单元格中此前与 Telegraph 相关的说明。
  • 在 changelog 中添加 v2.1.3 条目,记录 OneBot 合并转发节点修复、自身 self_id 解析策略以及此更改的非破坏性特征。
  • 在 metadata.yaml 中将插件版本字符串从 v2.1.2 更新为 v2.1.3。
docs/project/platforms.md
CHANGELOG.md
metadata.yaml

Tips and commands

Interacting with Sourcery

  • 触发新的代码审查: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub Issue: 在回复某条审查评论时请求 Sourcery 创建 issue。你也可以直接回复审查评论并写 @sourcery-ai issue 来从该评论生成一个 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题中任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 正文中任意位置写上 @sourcery-ai summary,即可在指定位置生成 PR 摘要。你也可以在 Pull Request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 Pull Request 中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 一次性解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论并不希望再看到它们,这会很有用。
  • 撤销所有 Sourcery 审查: 在 Pull Request 中评论 @sourcery-ai dismiss,即可撤销所有现有的 Sourcery 审查。如果你希望从一次新的审查开始,这尤其有用——别忘了再评论 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 自动生成的 Pull Request 摘要、审阅者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Implements a safe mechanism for resolving and injecting the OneBot bot self_id (QQ number) into merged forward nodes for both command-response and active-push scenarios, wires a global self_id provider via bootstrap, adjusts tests and docs accordingly, and bumps the plugin version to v2.1.3.

Sequence diagram for resolving bot self_id during OneBot send_to_user

sequenceDiagram
    participant RSSJob
    participant OneBotMessageSender
    participant MessageContext
    participant get_bot_self_id
    participant _bot_self_id_provider

    RSSJob->>OneBotMessageSender: send_to_user(request, context)
    OneBotMessageSender->>OneBotMessageSender: _resolve_bot_self_id(context)
    alt event message has self_id and != 0
        OneBotMessageSender->>MessageContext: getattr(event.message_obj, self_id)
        OneBotMessageSender-->>OneBotMessageSender: return self_id
    else platform_name is set
        OneBotMessageSender->>get_bot_self_id: get_bot_self_id(platform_name)
        get_bot_self_id->>_bot_self_id_provider: _bot_self_id_provider(platform_name)
        _bot_self_id_provider-->>get_bot_self_id: self_id or ""
        get_bot_self_id-->>OneBotMessageSender: self_id or ""
    else cannot resolve
        OneBotMessageSender-->>OneBotMessageSender: return ""
    end
    OneBotMessageSender->>OneBotMessageSender: _build_forward_node(content, nickname, bot_self_id)
    Note over OneBotMessageSender: Only sets Node.uin when bot_self_id is non-empty
    OneBotMessageSender-->>RSSJob: SendResult
Loading

File-Level Changes

Change Details Files
Resolve bot self_id from message context or provider and apply it to OneBot merged forward nodes only when reliably known.
  • Extend OneBot sender imports to include get_bot_self_id and add _resolve_bot_self_id helper to derive bot QQ from event.self_id or platform-based provider with strict checks against unknown/zero values.
  • Introduce _build_forward_node helper that conditionally sets Node.uin only when a non-empty bot_self_id is available, otherwise falling back to SDK defaults.
  • Integrate bot_self_id resolution into send_to_user by computing it once per send and using it for all forward-node construction paths, including fallback nodes after media upload failures, while keeping behavior unchanged when self_id is unknown.
src/infrastructure/messaging/senders/onebot_sender.py
Expose a global bot self_id provider from bootstrap that safely derives a unique self_id from connected aiocqhttp clients.
  • Update bootstrap imports to include set_bot_self_id_provider and adjust _register_bot_client_provider docstring to cover both client and self_id providers for active push.
  • Implement _resolve_bot_self_id that inspects the platform client’s _wsr_api_clients, returns the sole non-empty/non-zero self_id when unique, logs a warning and returns empty when multiple candidates exist, and registers it via set_bot_self_id_provider.
  • Keep existing bot client resolution logic and register both client and self_id providers together.
bootstrap.py
Tighten the semantics of get_bot_self_id so callers can distinguish between unknown and known bot IDs.
  • Change get_bot_self_id to return an empty string when no provider is registered instead of a hard-coded placeholder value, ensuring callers don’t mistake a fake ID for a real one.
src/infrastructure/messaging/senders/types.py
Strengthen unit tests around OneBot sender self_id handling, media local paths, the bot self_id provider registration, and data directory resolution.
  • Augment OneBot sender tests with a uin field on the test Node stub, ensure PreparedMedia instances include local_path, and add three async tests covering event-based self_id, provider-based self_id for active push, and preservation of default uin when self_id resolution fails or is unknown.
  • Add a bootstrap runtime test that verifies the registered bot self_id provider returns a single connected self_id when unique and an empty string when multiple self_ids are present, using SimpleNamespace-based fakes for platform and platform_manager.
  • Refactor the plugin data-dir test to build a synthetic AstrBot/tree under tmp_path, override PLUGIN_ROOT via monkeypatch, and assert that plugin-local astrbot data is avoided in a worktree-safe manner.
tests/unit/infrastructure/test_onebot_sender.py
tests/unit/test_bootstrap_runtime.py
tests/unit/infrastructure/test_paths.py
Update documentation and metadata to reflect the new OneBot behavior and version bump to v2.1.3.
  • Extend the OneBot/aiocqhttp row in the platforms overview docs to describe how merged forward nodes now write the confirmed bot QQ uin and retain the SDK default when unknown, removing the previous Telegraph-related note in that cell.
  • Add a 2.1.3 entry to the changelog documenting the OneBot merged-forward-node fix, self_id resolution strategy, and non-breaking nature of the change.
  • Bump the plugin version string from v2.1.2 to v2.1.3 in metadata.yaml.
docs/project/platforms.md
CHANGELOG.md
metadata.yaml

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

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

本次更新发布 2.1.3,新增 OneBot bot self_id 解析与合并转发节点 uin 条件注入,覆盖事件响应、主动推送及未知账号场景,并补充相关测试、文档和版本记录。

Changes

OneBot self_id 解析与转发节点构建

Layer / File(s) Summary
Provider 注册与账号解析
bootstrap.py, src/infrastructure/messaging/senders/types.py, tests/unit/test_bootstrap_runtime.py
注册 bot self_id provider,从事件连接或唯一已连接账号解析有效 self_id;无法确认时返回空字符串。
合并转发节点注入
src/infrastructure/messaging/senders/onebot_sender.py, tests/unit/infrastructure/test_onebot_sender.py
合并转发节点及文本回退节点仅在确认账号后设置 uin,否则保留 SDK 默认值。
版本说明与测试隔离
CHANGELOG.md, docs/project/platforms.md, metadata.yaml, tests/unit/infrastructure/test_paths.py
更新 2.1.3 版本信息、OneBot 行为说明,并使用临时目录隔离路径测试。

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MessageContext
  participant OneBotMessageSender
  participant SelfIdProvider
  participant ForwardNode
  MessageContext->>OneBotMessageSender: 提供事件 self_id
  OneBotMessageSender->>SelfIdProvider: 事件账号无效时解析 self_id
  SelfIdProvider-->>OneBotMessageSender: 返回唯一账号或空字符串
  OneBotMessageSender->>ForwardNode: 条件写入 uin
Loading

Suggested reviewers: copilot

Poem

一只兔子跳进消息流,
带着账号穿过节点楼。
已知便写入,未知不乱猜,
回退路径也稳稳守。
2.1.3,胡萝卜庆功喽!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题与本次版本发布及 v2.1.3 升级内容直接相关,能概括这次 PR 的主要变更。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 4 issues

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

## Individual Comments

### Comment 1
<location path="bootstrap.py" line_range="282-297" />
<code_context>
             )
         return None

+    def _resolve_bot_self_id(platform_name: str) -> str:
+        """从已连接的反向 WebSocket 客户端中解析 bot 的 self_id(QQ 号)。
+
+        aiocqhttp 的 CQHttp 实例以 self_id 为键保存已连接的 API 客户端
+        (``_wsr_api_clients``)。只有连接中恰有一个非零 self_id 时才返回,
+        避免多 bot 平台把其他账号误写入合并转发节点;解析失败时返回空串,
+        由调用方保留 SDK 的默认值。
+        """
+        client = _resolve_bot_client(platform_name)
+        if client is None:
+            return ""
+        api_clients = getattr(client, "_wsr_api_clients", None)
+        if isinstance(api_clients, dict):
+            self_ids = [
+                str(self_id).strip()
+                for self_id in list(api_clients)
+                if str(self_id or "").strip() not in {"", "0"}
+            ]
+            if len(self_ids) == 1:
+                return self_ids[0]
+            if len(self_ids) > 1:
+                logger.warning(
+                    "无法唯一解析 bot self_id,保留合并转发节点默认值: "
</code_context>
<issue_to_address>
**suggestion:** self_id 提取逻辑做了一些可以简化的额外工作,影响清晰度和效率。

在 `_resolve_bot_self_id` 中,下列列表推导式

```python
self_ids = [
    str(self_id).strip()
    for self_id in list(api_clients)
    if str(self_id or "").strip() not in {"", "0"}
]
```
包含多余的 `str(...)` 调用以及不必要的 `list(api_clients)` 转换。你可以直接遍历 `api_clients`(或 `api_clients.keys()`),对每个键只归一化一次,再应用非空 / 非零过滤。这样可以在保持现有唯一性检查逻辑不变的前提下,提高可读性和效率。

```suggestion
        api_clients = getattr(client, "_wsr_api_clients", None)
        if isinstance(api_clients, dict):
            self_ids: list[str] = []
            for self_id in api_clients:
                normalized = str(self_id or "").strip()
                if normalized and normalized != "0":
                    self_ids.append(normalized)
            if len(self_ids) == 1:
                return self_ids[0]
            if len(self_ids) > 1:
                logger.warning(
                    "无法唯一解析 bot self_id,保留合并转发节点默认值: "
                    "platform=%s, candidates=%s",
                    platform_name,
                    self_ids,
                )
```
</issue_to_address>

### Comment 2
<location path="tests/unit/infrastructure/test_onebot_sender.py" line_range="417-426" />
<code_context>
+@pytest.mark.asyncio
</code_context>
<issue_to_address>
**suggestion (testing):** 请增加一个同时存在 event.self_id 和 provider self_id 的测试用例,以验证优先级。

当前测试已经覆盖了仅事件提供 self_id、仅 provider 提供 self_id,以及未知 `self_id` 的情况,但还缺少验证「事件值应覆盖 provider 值」这一优先级规则的用例。建议新增一个用例,在其中 `context.event.message_obj.self_id``get_bot_self_id` 返回值不同,并断言转发节点的 `uin` 使用事件中的值,以防止将来修改不小心颠倒这个优先级。

建议实现如下:

```python
    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)


@pytest.mark.asyncio
async def test_onebot_sender_prefers_event_self_id_over_provider(monkeypatch):
    """当事件和 provider 都提供 self_id 时,应优先使用事件中的 bot QQ 号写入转发节点。"""
    sender = OneBotMessageSender()
    calls: list[tuple[str, list]] = []

    async def fake_send_chain(session_id: str, chain: list, **_kwargs):
        calls.append((session_id, chain))
        return SendResult(ok=True)

    # provider 返回的 bot self_id 与事件中的不同,用于验证优先级
    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)
    monkeypatch.setattr(sender, "get_bot_self_id", lambda context: "222222")

    # 构造同时具有 event.self_id 和 provider self_id 的上下文
    # 这里假设已有的测试工具/fixture可以设置 event.message_obj.self_id
    context = make_dummy_context()  # 占位:请替换为项目中实际的上下文构造方式
    context.event.message_obj.self_id = "111111"

    # 触发构建转发消息的逻辑(占位:请替换为项目中实际的发送/构建调用)
    await sender.send(context, "测试带转发节点的消息")

    # 断言转发节点的 uin 使用事件中的 self_id,而不是 provider 返回的值
    assert calls, "预期 fake_send_chain 被调用以生成转发节点"

    forward_nodes = [
        segment for segment in calls[0][1]
        if getattr(segment, "type", None) == "node"
    ]
    assert forward_nodes, "预期消息链中包含至少一个转发节点"

    for node in forward_nodes:
        # uin 字段应该等于事件中的 self_id,而不是 get_bot_self_id 返回的值
        assert getattr(node, "data", {}).get("uin") == "111111"

```

1.`make_dummy_context()` 替换为项目中实际用于构造上下文 / 事件的 helper 或 fixture,确保能够设置 `context.event.message_obj.self_id`2.`await sender.send(context, "测试带转发节点的消息")` 替换为实际会走到「构建转发节点」逻辑的调用(例如发送包含转发 / 引用 / 多段消息的接口)。
3. `forward_nodes` 的筛选条件和 `node.data["uin"]` 的访问方式需要根据现有消息段结构调整:如果转发节点类型、字段名不同,请改为匹配当前实现(例如 `type == "forward"` 或使用 `segment.data["sender"]["uin"]` 等)。
4. 如现有代码中 `get_bot_self_id` 的签名不同(例如需要 `provider``context` 或其他参数),请相应调整 `monkeypatch.setattr(sender, "get_bot_self_id", ...)` 的 lambda 以匹配实际签名。
</issue_to_address>

### Comment 3
<location path="tests/unit/test_bootstrap_runtime.py" line_range="36-45" />
<code_context>
+def test_bot_self_id_provider_returns_only_a_unique_connected_account(monkeypatch):
</code_context>
<issue_to_address>
**suggestion (testing):** 请增加一个在没有 API 客户端或只有零值 self_id 时的用例,以验证 provider 会返回空字符串。

该测试已经覆盖了单个非零 self_id 和多个 self_id 的场景。为了完整覆盖 `_resolve_bot_self_id`,建议再添加 `_wsr_api_clients` 为空,以及仅包含零值(例如 `{}``{"0": object()}`)两种情况,并在这两种情况下都断言 provider 返回 `""`,以符合文档中描述的回退行为。

建议实现如下:

```python
from unittest.mock import AsyncMock, MagicMock

import pytest

from nonebot_plugin_saa.bootstrap.runtime import _resolve_bot_self_id

```

```python
        def meta(self):


def test_bot_self_id_provider_returns_empty_string_for_missing_or_zero_self_ids(monkeypatch):
    """当不存在已连接 bot 或仅存在零值 self_id 时,应返回空字符串作为回退行为。"""

    class FakePlatform:
        def __init__(self, self_ids: list[str]):
            # 仿照运行时结构,构造 _wsr_api_clients 字典
            self._client = SimpleNamespace(
                _wsr_api_clients={self_id: object() for self_id in self_ids}
            )

        def meta(self):
            # 假定 _resolve_bot_self_id 只关心 _client._wsr_api_clients,
            # 这里返回 self 以满足可能的接口需求
            return self

    # 情况一:没有任何已连接的 API client
    empty_platform = FakePlatform([])
    assert _resolve_bot_self_id(empty_platform) == ""

    # 情况二:仅存在零值 / 占位 self_id(例如 "0")
    zero_only_platform = FakePlatform(["0"])
    assert _resolve_bot_self_id(zero_only_platform) == ""

```

上述实现假定:
1. `_resolve_bot_self_id` 定义在 `nonebot_plugin_saa.bootstrap.runtime` 中,并接收平台 / 适配器实例(或一个其 `meta()` / `_client._wsr_api_clients` 结构与真实运行时一致的对象)。
2. `_resolve_bot_self_id` 通过 `platform._client._wsr_api_clients`(或经由 `platform.meta()`)读取映射,并在映射为空或仅包含类似零值的键(例如 `"0"`)时返回 `""`。

为了让这个测试与你的代码库精准对齐:
1. 如果 `_resolve_bot_self_id` 位于其他模块,请调整导入路径。
2. 如果 `_resolve_bot_self_id` 期望的对象结构不同(如 `platform.meta().client._wsr_api_clients`),请相应修改 `FakePlatform.meta()` 以镜像真实的运行时对象结构。
3. 如果你文档中约定的「零值」 self_id 有不同表示方式(例如 `None``""` 或数值 `0`),建议扩展测试以覆盖这些表示形式,并同样断言返回值为 `""`。
</issue_to_address>

### Comment 4
<location path="tests/unit/infrastructure/test_paths.py" line_range="8-16" />
<code_context>
-def test_plugin_data_dir_avoids_plugin_local_astrbot_data(monkeypatch):
-    plugin_root = paths.PLUGIN_ROOT
-    astrbot_root = plugin_root.parents[2]
+def test_plugin_data_dir_avoids_plugin_local_astrbot_data(monkeypatch, tmp_path):
+    """本地插件目录运行时,数据应回落到实际 AstrBot 根目录。"""
+    astrbot_root = tmp_path / "astrbot"
+    plugin_root = astrbot_root / "data" / "plugins" / paths.PLUGIN_NAME
+    plugin_root.mkdir(parents=True)
     plugin_local_data = plugin_root / "data" / "plugin_data"

+    monkeypatch.setattr(paths, "PLUGIN_ROOT", plugin_root)
     monkeypatch.setattr(
         paths, "_resolve_explicit_astrbot_data_dir", lambda: plugin_local_data
     )
</code_context>
<issue_to_address>
**suggestion (testing):** 建议显式断言解析出的数据目录位于构造的 AstrBot 根目录下,以使测试意图更清晰。

使用 `tmp_path` 是一个不错的改进。为了更完整地体现预期行为,建议在断言 `paths.PLUGIN_DATA_DIR` 不等于 `plugin_local_data` 之外,再额外断言 `paths.PLUGIN_DATA_DIR` 位于构造出的 `astrbot_root` 之下。这样可以在测试中同时记录并确保「避免使用插件本地目录」和「回落到 AstrBot 根目录」这两层语义。
</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.
Original comment in English

Hey - I've found 4 issues

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

## Individual Comments

### Comment 1
<location path="bootstrap.py" line_range="282-297" />
<code_context>
             )
         return None

+    def _resolve_bot_self_id(platform_name: str) -> str:
+        """从已连接的反向 WebSocket 客户端中解析 bot 的 self_id(QQ 号)。
+
+        aiocqhttp 的 CQHttp 实例以 self_id 为键保存已连接的 API 客户端
+        (``_wsr_api_clients``)。只有连接中恰有一个非零 self_id 时才返回,
+        避免多 bot 平台把其他账号误写入合并转发节点;解析失败时返回空串,
+        由调用方保留 SDK 的默认值。
+        """
+        client = _resolve_bot_client(platform_name)
+        if client is None:
+            return ""
+        api_clients = getattr(client, "_wsr_api_clients", None)
+        if isinstance(api_clients, dict):
+            self_ids = [
+                str(self_id).strip()
+                for self_id in list(api_clients)
+                if str(self_id or "").strip() not in {"", "0"}
+            ]
+            if len(self_ids) == 1:
+                return self_ids[0]
+            if len(self_ids) > 1:
+                logger.warning(
+                    "无法唯一解析 bot self_id,保留合并转发节点默认值: "
</code_context>
<issue_to_address>
**suggestion:** The self_id extraction logic does extra work that could be simplified for clarity and efficiency.

In `_resolve_bot_self_id`, the list comprehension

```python
self_ids = [
    str(self_id).strip()
    for self_id in list(api_clients)
    if str(self_id or "").strip() not in {"", "0"}
]
```
performs redundant `str(...)` calls and an unnecessary `list(api_clients)` conversion. You can iterate directly over `api_clients` (or `api_clients.keys()`), normalize once per key, and then apply the non-empty/non-zero filter. This keeps the uniqueness check logic the same while improving readability and efficiency.

```suggestion
        api_clients = getattr(client, "_wsr_api_clients", None)
        if isinstance(api_clients, dict):
            self_ids: list[str] = []
            for self_id in api_clients:
                normalized = str(self_id or "").strip()
                if normalized and normalized != "0":
                    self_ids.append(normalized)
            if len(self_ids) == 1:
                return self_ids[0]
            if len(self_ids) > 1:
                logger.warning(
                    "无法唯一解析 bot self_id,保留合并转发节点默认值: "
                    "platform=%s, candidates=%s",
                    platform_name,
                    self_ids,
                )
```
</issue_to_address>

### Comment 2
<location path="tests/unit/infrastructure/test_onebot_sender.py" line_range="417-426" />
<code_context>
+@pytest.mark.asyncio
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test where both event.self_id and provider self_id exist to verify precedence

Current tests cover event-only, provider-only, and unknown `self_id` cases, but not the precedence rule where the event value should override the provider value. Please add a test where `context.event.message_obj.self_id` and `get_bot_self_id` return different values, and assert that the forward node’s `uin` uses the event value to protect against future changes that might invert this precedence.

Suggested implementation:

```python
    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)


@pytest.mark.asyncio
async def test_onebot_sender_prefers_event_self_id_over_provider(monkeypatch):
    """当事件和 provider 都提供 self_id 时,应优先使用事件中的 bot QQ 号写入转发节点。"""
    sender = OneBotMessageSender()
    calls: list[tuple[str, list]] = []

    async def fake_send_chain(session_id: str, chain: list, **_kwargs):
        calls.append((session_id, chain))
        return SendResult(ok=True)

    # provider 返回的 bot self_id 与事件中的不同,用于验证优先级
    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)
    monkeypatch.setattr(sender, "get_bot_self_id", lambda context: "222222")

    # 构造同时具有 event.self_id 和 provider self_id 的上下文
    # 这里假设已有的测试工具/fixture可以设置 event.message_obj.self_id
    context = make_dummy_context()  # 占位:请替换为项目中实际的上下文构造方式
    context.event.message_obj.self_id = "111111"

    # 触发构建转发消息的逻辑(占位:请替换为项目中实际的发送/构建调用)
    await sender.send(context, "测试带转发节点的消息")

    # 断言转发节点的 uin 使用事件中的 self_id,而不是 provider 返回的值
    assert calls, "预期 fake_send_chain 被调用以生成转发节点"

    forward_nodes = [
        segment for segment in calls[0][1]
        if getattr(segment, "type", None) == "node"
    ]
    assert forward_nodes, "预期消息链中包含至少一个转发节点"

    for node in forward_nodes:
        # uin 字段应该等于事件中的 self_id,而不是 get_bot_self_id 返回的值
        assert getattr(node, "data", {}).get("uin") == "111111"

```

1.`make_dummy_context()` 替换为项目中实际用于构造上下文/事件的 helper 或 fixture,确保能够设置 `context.event.message_obj.self_id`2.`await sender.send(context, "测试带转发节点的消息")` 替换为实际会走到「构建转发节点」逻辑的调用(例如发送包含转发/引用/多段消息的接口)。
3. `forward_nodes` 的筛选条件和 `node.data["uin"]` 的访问方式需要根据现有消息段结构调整:如果转发节点类型、字段名不同,请改为匹配当前实现(例如 `type == "forward"` 或使用 `segment.data["sender"]["uin"]` 等)。
4. 如现有代码中 `get_bot_self_id` 的签名不同(例如需要 `provider``context` 或其他参数),请相应调整 `monkeypatch.setattr(sender, "get_bot_self_id", ...)` 的 lambda 以匹配实际签名。
</issue_to_address>

### Comment 3
<location path="tests/unit/test_bootstrap_runtime.py" line_range="36-45" />
<code_context>
+def test_bot_self_id_provider_returns_only_a_unique_connected_account(monkeypatch):
</code_context>
<issue_to_address>
**suggestion (testing):** Add a case where no API clients or only zero-valued self_ids exist to verify provider returns an empty string

This test already covers the single non-zero and multiple self_id scenarios. To fully exercise `_resolve_bot_self_id`, please also add coverage for when `_wsr_api_clients` is empty and when it only contains a zero-like entry (e.g. `{}` and `{"0": object()}`), asserting that the provider returns `""` in both cases, consistent with the documented fallback behavior.

Suggested implementation:

```python
from unittest.mock import AsyncMock, MagicMock

import pytest

from nonebot_plugin_saa.bootstrap.runtime import _resolve_bot_self_id

```

```python
        def meta(self):


def test_bot_self_id_provider_returns_empty_string_for_missing_or_zero_self_ids(monkeypatch):
    """当不存在已连接 bot 或仅存在零值 self_id 时,应返回空字符串作为回退行为。"""

    class FakePlatform:
        def __init__(self, self_ids: list[str]):
            # 仿照运行时结构,构造 _wsr_api_clients 字典
            self._client = SimpleNamespace(
                _wsr_api_clients={self_id: object() for self_id in self_ids}
            )

        def meta(self):
            # 假定 _resolve_bot_self_id 只关心 _client._wsr_api_clients,
            # 这里返回 self 以满足可能的接口需求
            return self

    # 情况一:没有任何已连接的 API client
    empty_platform = FakePlatform([])
    assert _resolve_bot_self_id(empty_platform) == ""

    # 情况二:仅存在零值 / 占位 self_id(例如 "0")
    zero_only_platform = FakePlatform(["0"])
    assert _resolve_bot_self_id(zero_only_platform) == ""

```

The above implementation assumes:
1. `_resolve_bot_self_id` is defined in `nonebot_plugin_saa.bootstrap.runtime` and accepts the platform/adapter instance (or an object whose `meta()`/`_client._wsr_api_clients` matches the real runtime structure).
2. `_resolve_bot_self_id` reads `platform._client._wsr_api_clients` (or through `platform.meta()`), and returns `""` when the mapping is empty or when only a zero-like key (e.g. `"0"`) is present.

To align this test precisely with your codebase:
1. Adjust the import path for `_resolve_bot_self_id` if it lives in a different module.
2. If `_resolve_bot_self_id` expects a different object shape (e.g. `platform.meta().client._wsr_api_clients`), adapt `FakePlatform.meta()` accordingly to mirror the real runtime object graph.
3. If your documented "zero-like" self_id is represented differently (e.g. `None`, `""`, or numeric `0`), extend the test to include those specific representations and still assert `""` as the fallback return value.
</issue_to_address>

### Comment 4
<location path="tests/unit/infrastructure/test_paths.py" line_range="8-16" />
<code_context>
-def test_plugin_data_dir_avoids_plugin_local_astrbot_data(monkeypatch):
-    plugin_root = paths.PLUGIN_ROOT
-    astrbot_root = plugin_root.parents[2]
+def test_plugin_data_dir_avoids_plugin_local_astrbot_data(monkeypatch, tmp_path):
+    """本地插件目录运行时,数据应回落到实际 AstrBot 根目录。"""
+    astrbot_root = tmp_path / "astrbot"
+    plugin_root = astrbot_root / "data" / "plugins" / paths.PLUGIN_NAME
+    plugin_root.mkdir(parents=True)
     plugin_local_data = plugin_root / "data" / "plugin_data"

+    monkeypatch.setattr(paths, "PLUGIN_ROOT", plugin_root)
     monkeypatch.setattr(
         paths, "_resolve_explicit_astrbot_data_dir", lambda: plugin_local_data
     )
</code_context>
<issue_to_address>
**suggestion (testing):** Assert explicitly that the resolved data dir is under the constructed AstrBot root to make the intent clearer

Using `tmp_path` is a good improvement. To fully capture the intended behavior, please also assert that `paths.PLUGIN_DATA_DIR` is located under the constructed `astrbot_root`, in addition to not being equal to `plugin_local_data`. This will document and enforce both "avoid plugin-local" and "fall back to AstrBot root" semantics in the test.
</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 thread bootstrap.py
Comment on lines +282 to +297
api_clients = getattr(client, "_wsr_api_clients", None)
if isinstance(api_clients, dict):
self_ids = [
str(self_id).strip()
for self_id in list(api_clients)
if str(self_id or "").strip() not in {"", "0"}
]
if len(self_ids) == 1:
return self_ids[0]
if len(self_ids) > 1:
logger.warning(
"无法唯一解析 bot self_id,保留合并转发节点默认值: "
"platform=%s, candidates=%s",
platform_name,
self_ids,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: self_id 提取逻辑做了一些可以简化的额外工作,影响清晰度和效率。

_resolve_bot_self_id 中,下列列表推导式

self_ids = [
    str(self_id).strip()
    for self_id in list(api_clients)
    if str(self_id or "").strip() not in {"", "0"}
]

包含多余的 str(...) 调用以及不必要的 list(api_clients) 转换。你可以直接遍历 api_clients(或 api_clients.keys()),对每个键只归一化一次,再应用非空 / 非零过滤。这样可以在保持现有唯一性检查逻辑不变的前提下,提高可读性和效率。

Suggested change
api_clients = getattr(client, "_wsr_api_clients", None)
if isinstance(api_clients, dict):
self_ids = [
str(self_id).strip()
for self_id in list(api_clients)
if str(self_id or "").strip() not in {"", "0"}
]
if len(self_ids) == 1:
return self_ids[0]
if len(self_ids) > 1:
logger.warning(
"无法唯一解析 bot self_id,保留合并转发节点默认值: "
"platform=%s, candidates=%s",
platform_name,
self_ids,
)
api_clients = getattr(client, "_wsr_api_clients", None)
if isinstance(api_clients, dict):
self_ids: list[str] = []
for self_id in api_clients:
normalized = str(self_id or "").strip()
if normalized and normalized != "0":
self_ids.append(normalized)
if len(self_ids) == 1:
return self_ids[0]
if len(self_ids) > 1:
logger.warning(
"无法唯一解析 bot self_id,保留合并转发节点默认值: "
"platform=%s, candidates=%s",
platform_name,
self_ids,
)
Original comment in English

suggestion: The self_id extraction logic does extra work that could be simplified for clarity and efficiency.

In _resolve_bot_self_id, the list comprehension

self_ids = [
    str(self_id).strip()
    for self_id in list(api_clients)
    if str(self_id or "").strip() not in {"", "0"}
]

performs redundant str(...) calls and an unnecessary list(api_clients) conversion. You can iterate directly over api_clients (or api_clients.keys()), normalize once per key, and then apply the non-empty/non-zero filter. This keeps the uniqueness check logic the same while improving readability and efficiency.

Suggested change
api_clients = getattr(client, "_wsr_api_clients", None)
if isinstance(api_clients, dict):
self_ids = [
str(self_id).strip()
for self_id in list(api_clients)
if str(self_id or "").strip() not in {"", "0"}
]
if len(self_ids) == 1:
return self_ids[0]
if len(self_ids) > 1:
logger.warning(
"无法唯一解析 bot self_id,保留合并转发节点默认值: "
"platform=%s, candidates=%s",
platform_name,
self_ids,
)
api_clients = getattr(client, "_wsr_api_clients", None)
if isinstance(api_clients, dict):
self_ids: list[str] = []
for self_id in api_clients:
normalized = str(self_id or "").strip()
if normalized and normalized != "0":
self_ids.append(normalized)
if len(self_ids) == 1:
return self_ids[0]
if len(self_ids) > 1:
logger.warning(
"无法唯一解析 bot self_id,保留合并转发节点默认值: "
"platform=%s, candidates=%s",
platform_name,
self_ids,
)

Comment on lines +417 to +426
@pytest.mark.asyncio
async def test_onebot_sender_uses_event_self_id_for_forward_nodes(monkeypatch):
"""命令响应场景应把事件中的 bot QQ 号写入转发节点。"""
sender = OneBotMessageSender()
calls: list[tuple[str, list]] = []

async def fake_send_chain(session_id: str, chain: list, **_kwargs):
calls.append((session_id, chain))
return SendResult(ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): 请增加一个同时存在 event.self_id 和 provider self_id 的测试用例,以验证优先级。

当前测试已经覆盖了仅事件提供 self_id、仅 provider 提供 self_id,以及未知 self_id 的情况,但还缺少验证「事件值应覆盖 provider 值」这一优先级规则的用例。建议新增一个用例,在其中 context.event.message_obj.self_idget_bot_self_id 返回值不同,并断言转发节点的 uin 使用事件中的值,以防止将来修改不小心颠倒这个优先级。

建议实现如下:

    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)


@pytest.mark.asyncio
async def test_onebot_sender_prefers_event_self_id_over_provider(monkeypatch):
    """当事件和 provider 都提供 self_id 时,应优先使用事件中的 bot QQ 号写入转发节点。"""
    sender = OneBotMessageSender()
    calls: list[tuple[str, list]] = []

    async def fake_send_chain(session_id: str, chain: list, **_kwargs):
        calls.append((session_id, chain))
        return SendResult(ok=True)

    # provider 返回的 bot self_id 与事件中的不同,用于验证优先级
    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)
    monkeypatch.setattr(sender, "get_bot_self_id", lambda context: "222222")

    # 构造同时具有 event.self_id 和 provider self_id 的上下文
    # 这里假设已有的测试工具/fixture可以设置 event.message_obj.self_id
    context = make_dummy_context()  # 占位:请替换为项目中实际的上下文构造方式
    context.event.message_obj.self_id = "111111"

    # 触发构建转发消息的逻辑(占位:请替换为项目中实际的发送/构建调用)
    await sender.send(context, "测试带转发节点的消息")

    # 断言转发节点的 uin 使用事件中的 self_id,而不是 provider 返回的值
    assert calls, "预期 fake_send_chain 被调用以生成转发节点"

    forward_nodes = [
        segment for segment in calls[0][1]
        if getattr(segment, "type", None) == "node"
    ]
    assert forward_nodes, "预期消息链中包含至少一个转发节点"

    for node in forward_nodes:
        # uin 字段应该等于事件中的 self_id,而不是 get_bot_self_id 返回的值
        assert getattr(node, "data", {}).get("uin") == "111111"
  1. make_dummy_context() 替换为项目中实际用于构造上下文 / 事件的 helper 或 fixture,确保能够设置 context.event.message_obj.self_id
  2. await sender.send(context, "测试带转发节点的消息") 替换为实际会走到「构建转发节点」逻辑的调用(例如发送包含转发 / 引用 / 多段消息的接口)。
  3. forward_nodes 的筛选条件和 node.data["uin"] 的访问方式需要根据现有消息段结构调整:如果转发节点类型、字段名不同,请改为匹配当前实现(例如 type == "forward" 或使用 segment.data["sender"]["uin"] 等)。
  4. 如现有代码中 get_bot_self_id 的签名不同(例如需要 providercontext 或其他参数),请相应调整 monkeypatch.setattr(sender, "get_bot_self_id", ...) 的 lambda 以匹配实际签名。
Original comment in English

suggestion (testing): Add a test where both event.self_id and provider self_id exist to verify precedence

Current tests cover event-only, provider-only, and unknown self_id cases, but not the precedence rule where the event value should override the provider value. Please add a test where context.event.message_obj.self_id and get_bot_self_id return different values, and assert that the forward node’s uin uses the event value to protect against future changes that might invert this precedence.

Suggested implementation:

    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)


@pytest.mark.asyncio
async def test_onebot_sender_prefers_event_self_id_over_provider(monkeypatch):
    """当事件和 provider 都提供 self_id 时,应优先使用事件中的 bot QQ 号写入转发节点。"""
    sender = OneBotMessageSender()
    calls: list[tuple[str, list]] = []

    async def fake_send_chain(session_id: str, chain: list, **_kwargs):
        calls.append((session_id, chain))
        return SendResult(ok=True)

    # provider 返回的 bot self_id 与事件中的不同,用于验证优先级
    monkeypatch.setattr(sender, "_send_chain", fake_send_chain)
    monkeypatch.setattr(sender, "get_bot_self_id", lambda context: "222222")

    # 构造同时具有 event.self_id 和 provider self_id 的上下文
    # 这里假设已有的测试工具/fixture可以设置 event.message_obj.self_id
    context = make_dummy_context()  # 占位:请替换为项目中实际的上下文构造方式
    context.event.message_obj.self_id = "111111"

    # 触发构建转发消息的逻辑(占位:请替换为项目中实际的发送/构建调用)
    await sender.send(context, "测试带转发节点的消息")

    # 断言转发节点的 uin 使用事件中的 self_id,而不是 provider 返回的值
    assert calls, "预期 fake_send_chain 被调用以生成转发节点"

    forward_nodes = [
        segment for segment in calls[0][1]
        if getattr(segment, "type", None) == "node"
    ]
    assert forward_nodes, "预期消息链中包含至少一个转发节点"

    for node in forward_nodes:
        # uin 字段应该等于事件中的 self_id,而不是 get_bot_self_id 返回的值
        assert getattr(node, "data", {}).get("uin") == "111111"
  1. make_dummy_context() 替换为项目中实际用于构造上下文/事件的 helper 或 fixture,确保能够设置 context.event.message_obj.self_id
  2. await sender.send(context, "测试带转发节点的消息") 替换为实际会走到「构建转发节点」逻辑的调用(例如发送包含转发/引用/多段消息的接口)。
  3. forward_nodes 的筛选条件和 node.data["uin"] 的访问方式需要根据现有消息段结构调整:如果转发节点类型、字段名不同,请改为匹配当前实现(例如 type == "forward" 或使用 segment.data["sender"]["uin"] 等)。
  4. 如现有代码中 get_bot_self_id 的签名不同(例如需要 providercontext 或其他参数),请相应调整 monkeypatch.setattr(sender, "get_bot_self_id", ...) 的 lambda 以匹配实际签名。

Comment on lines +36 to +45
def test_bot_self_id_provider_returns_only_a_unique_connected_account(monkeypatch):
"""多 bot 连接时不应把任意账号误用为合并转发节点 UIN。"""
providers: dict[str, object] = {}

class FakePlatform:
def __init__(self, self_ids: list[str]):
self._client = SimpleNamespace(
_wsr_api_clients={self_id: object() for self_id in self_ids}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): 请增加一个在没有 API 客户端或只有零值 self_id 时的用例,以验证 provider 会返回空字符串。

该测试已经覆盖了单个非零 self_id 和多个 self_id 的场景。为了完整覆盖 _resolve_bot_self_id,建议再添加 _wsr_api_clients 为空,以及仅包含零值(例如 {}{"0": object()})两种情况,并在这两种情况下都断言 provider 返回 "",以符合文档中描述的回退行为。

建议实现如下:

from unittest.mock import AsyncMock, MagicMock

import pytest

from nonebot_plugin_saa.bootstrap.runtime import _resolve_bot_self_id
        def meta(self):


def test_bot_self_id_provider_returns_empty_string_for_missing_or_zero_self_ids(monkeypatch):
    """当不存在已连接 bot 或仅存在零值 self_id 时,应返回空字符串作为回退行为。"""

    class FakePlatform:
        def __init__(self, self_ids: list[str]):
            # 仿照运行时结构,构造 _wsr_api_clients 字典
            self._client = SimpleNamespace(
                _wsr_api_clients={self_id: object() for self_id in self_ids}
            )

        def meta(self):
            # 假定 _resolve_bot_self_id 只关心 _client._wsr_api_clients,
            # 这里返回 self 以满足可能的接口需求
            return self

    # 情况一:没有任何已连接的 API client
    empty_platform = FakePlatform([])
    assert _resolve_bot_self_id(empty_platform) == ""

    # 情况二:仅存在零值 / 占位 self_id(例如 "0")
    zero_only_platform = FakePlatform(["0"])
    assert _resolve_bot_self_id(zero_only_platform) == ""

上述实现假定:

  1. _resolve_bot_self_id 定义在 nonebot_plugin_saa.bootstrap.runtime 中,并接收平台 / 适配器实例(或一个其 meta() / _client._wsr_api_clients 结构与真实运行时一致的对象)。
  2. _resolve_bot_self_id 通过 platform._client._wsr_api_clients(或经由 platform.meta())读取映射,并在映射为空或仅包含类似零值的键(例如 "0")时返回 ""

为了让这个测试与你的代码库精准对齐:

  1. 如果 _resolve_bot_self_id 位于其他模块,请调整导入路径。
  2. 如果 _resolve_bot_self_id 期望的对象结构不同(如 platform.meta().client._wsr_api_clients),请相应修改 FakePlatform.meta() 以镜像真实的运行时对象结构。
  3. 如果你文档中约定的「零值」 self_id 有不同表示方式(例如 None"" 或数值 0),建议扩展测试以覆盖这些表示形式,并同样断言返回值为 ""
Original comment in English

suggestion (testing): Add a case where no API clients or only zero-valued self_ids exist to verify provider returns an empty string

This test already covers the single non-zero and multiple self_id scenarios. To fully exercise _resolve_bot_self_id, please also add coverage for when _wsr_api_clients is empty and when it only contains a zero-like entry (e.g. {} and {"0": object()}), asserting that the provider returns "" in both cases, consistent with the documented fallback behavior.

Suggested implementation:

from unittest.mock import AsyncMock, MagicMock

import pytest

from nonebot_plugin_saa.bootstrap.runtime import _resolve_bot_self_id
        def meta(self):


def test_bot_self_id_provider_returns_empty_string_for_missing_or_zero_self_ids(monkeypatch):
    """当不存在已连接 bot 或仅存在零值 self_id 时,应返回空字符串作为回退行为。"""

    class FakePlatform:
        def __init__(self, self_ids: list[str]):
            # 仿照运行时结构,构造 _wsr_api_clients 字典
            self._client = SimpleNamespace(
                _wsr_api_clients={self_id: object() for self_id in self_ids}
            )

        def meta(self):
            # 假定 _resolve_bot_self_id 只关心 _client._wsr_api_clients,
            # 这里返回 self 以满足可能的接口需求
            return self

    # 情况一:没有任何已连接的 API client
    empty_platform = FakePlatform([])
    assert _resolve_bot_self_id(empty_platform) == ""

    # 情况二:仅存在零值 / 占位 self_id(例如 "0")
    zero_only_platform = FakePlatform(["0"])
    assert _resolve_bot_self_id(zero_only_platform) == ""

The above implementation assumes:

  1. _resolve_bot_self_id is defined in nonebot_plugin_saa.bootstrap.runtime and accepts the platform/adapter instance (or an object whose meta()/_client._wsr_api_clients matches the real runtime structure).
  2. _resolve_bot_self_id reads platform._client._wsr_api_clients (or through platform.meta()), and returns "" when the mapping is empty or when only a zero-like key (e.g. "0") is present.

To align this test precisely with your codebase:

  1. Adjust the import path for _resolve_bot_self_id if it lives in a different module.
  2. If _resolve_bot_self_id expects a different object shape (e.g. platform.meta().client._wsr_api_clients), adapt FakePlatform.meta() accordingly to mirror the real runtime object graph.
  3. If your documented "zero-like" self_id is represented differently (e.g. None, "", or numeric 0), extend the test to include those specific representations and still assert "" as the fallback return value.

Comment on lines +8 to 16
def test_plugin_data_dir_avoids_plugin_local_astrbot_data(monkeypatch, tmp_path):
"""本地插件目录运行时,数据应回落到实际 AstrBot 根目录。"""
astrbot_root = tmp_path / "astrbot"
plugin_root = astrbot_root / "data" / "plugins" / paths.PLUGIN_NAME
plugin_root.mkdir(parents=True)
plugin_local_data = plugin_root / "data" / "plugin_data"

monkeypatch.setattr(paths, "PLUGIN_ROOT", plugin_root)
monkeypatch.setattr(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): 建议显式断言解析出的数据目录位于构造的 AstrBot 根目录下,以使测试意图更清晰。

使用 tmp_path 是一个不错的改进。为了更完整地体现预期行为,建议在断言 paths.PLUGIN_DATA_DIR 不等于 plugin_local_data 之外,再额外断言 paths.PLUGIN_DATA_DIR 位于构造出的 astrbot_root 之下。这样可以在测试中同时记录并确保「避免使用插件本地目录」和「回落到 AstrBot 根目录」这两层语义。

Original comment in English

suggestion (testing): Assert explicitly that the resolved data dir is under the constructed AstrBot root to make the intent clearer

Using tmp_path is a good improvement. To fully capture the intended behavior, please also assert that paths.PLUGIN_DATA_DIR is located under the constructed astrbot_root, in addition to not being equal to plugin_local_data. This will document and enforce both "avoid plugin-local" and "fall back to AstrBot root" semantics in the test.

@FlanChanXwO
FlanChanXwO merged commit d1d556b into master Jul 23, 2026
4 of 5 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in astrbot_plugin_rsshub Jul 23, 2026
@FlanChanXwO
FlanChanXwO deleted the codex/release-v2.1.3 branch July 23, 2026 16:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
bootstrap.py (1)

271-301: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

_resolve_bot_self_id 补充与 _resolve_bot_client 一致的异常保护。

_resolve_bot_self_id 直接访问 aiocqhttp 内部私有属性 _wsr_api_clients 并做列表推导,但没有像同级的 _resolve_bot_client(Lines 251-269)那样用 try/except 包裹。一旦该属性的内部结构在未来版本中发生非预期变化(例如键类型变化导致 str() 转换异常),异常会一路传播到 send_to_user 的 try 块中,被外层 except Exception 捕获,导致本可成功的发送被误判为失败,而不是安全回退到空字符串。

_wsr_api_clients 属于 aiocqhttp 的私有实现细节(未出现在官方文档 API 中),建议同时为该依赖增加与 _resolve_bot_client 一致的防御性处理。

🛡️ 建议修复
     def _resolve_bot_self_id(platform_name: str) -> str:
         """从已连接的反向 WebSocket 客户端中解析 bot 的 self_id(QQ 号)。
         ...
         """
-        client = _resolve_bot_client(platform_name)
-        if client is None:
-            return ""
-        api_clients = getattr(client, "_wsr_api_clients", None)
-        if isinstance(api_clients, dict):
-            self_ids = [
-                str(self_id).strip()
-                for self_id in list(api_clients)
-                if str(self_id or "").strip() not in {"", "0"}
-            ]
-            if len(self_ids) == 1:
-                return self_ids[0]
-            if len(self_ids) > 1:
-                logger.warning(
-                    "无法唯一解析 bot self_id,保留合并转发节点默认值: "
-                    "platform=%s, candidates=%s",
-                    platform_name,
-                    self_ids,
-                )
-        return ""
+        try:
+            client = _resolve_bot_client(platform_name)
+            if client is None:
+                return ""
+            api_clients = getattr(client, "_wsr_api_clients", None)
+            if isinstance(api_clients, dict):
+                self_ids = [
+                    str(self_id).strip()
+                    for self_id in list(api_clients)
+                    if str(self_id or "").strip() not in {"", "0"}
+                ]
+                if len(self_ids) == 1:
+                    return self_ids[0]
+                if len(self_ids) > 1:
+                    logger.warning(
+                        "无法唯一解析 bot self_id,保留合并转发节点默认值: "
+                        "platform=%s, candidates=%s",
+                        platform_name,
+                        self_ids,
+                    )
+        except Exception as exc:
+            logger.debug(
+                "解析 bot self_id 失败: platform=%s, err=%s", platform_name, exc
+            )
+        return ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bootstrap.py` around lines 271 - 301, 为 _resolve_bot_self_id 增加与
_resolve_bot_client 一致的 try/except 防御,将访问 _wsr_api_clients、类型检查及 self_ids
列表推导包裹其中;发生任何非预期异常时记录适当日志并返回空字符串,确保解析失败不会向 send_to_user 传播异常。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@bootstrap.py`:
- Around line 271-301: 为 _resolve_bot_self_id 增加与 _resolve_bot_client 一致的
try/except 防御,将访问 _wsr_api_clients、类型检查及 self_ids
列表推导包裹其中;发生任何非预期异常时记录适当日志并返回空字符串,确保解析失败不会向 send_to_user 传播异常。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 333ded1e-8711-4781-8701-515267a210f7

📥 Commits

Reviewing files that changed from the base of the PR and between cea30b8 and a4952b2.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • bootstrap.py
  • docs/project/platforms.md
  • metadata.yaml
  • src/infrastructure/messaging/senders/onebot_sender.py
  • src/infrastructure/messaging/senders/types.py
  • tests/unit/infrastructure/test_onebot_sender.py
  • tests/unit/infrastructure/test_paths.py
  • tests/unit/test_bootstrap_runtime.py

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

Labels

area: backend Backend or core runtime changes area: docs Documentation changes area: tests Test changes release Release metadata or changelog changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants