Skip to content

feat: add configurable message format for push notifications - #99

Closed
FFFold wants to merge 18 commits into
AstrBot-Elementary-School:masterfrom
FFFold:feat/message-format
Closed

feat: add configurable message format for push notifications#99
FFFold wants to merge 18 commits into
AstrBot-Elementary-School:masterfrom
FFFold:feat/message-format

Conversation

@FFFold

@FFFold FFFold commented Jul 18, 2026

Copy link
Copy Markdown

Closes #98

t2i推送图片示例:

图片1 图片2

为推送消息增加可配置的发送格式,解决 OneBot 平台固定使用合并转发的问题。新增 message_format 选项,支持三种模式:合并转发(默认)、直发、图片(t2i 渲染)。

Modifications / 改动点

配置层:

  • src/shared/constants.py — 新增 MessageFormat 枚举及 MESSAGE_FORMAT_* 常量和映射
  • src/infrastructure/config/models/plugin_config_models.pyGlobalConfig 新增 message_format 字段及序列化映射
  • src/infrastructure/config/models/runtime_settings.pySubscriptionDefaults 新增 message_format 字段
  • src/infrastructure/config/settings_builder.py — 读取 message_format 配置
  • src/domain/entities/user.pysubscription.py — 实体新增 message_format 字段
  • src/infrastructure/persistence/models.py — ORM 新增 message_format
  • src/infrastructure/persistence/migrations/V3_add_message_format.py — V3 数据库迁移
  • src/interfaces/handlers/config.pySESSION_DEFAULT_KEYS 加入 message_format
  • src/application/commands/set_user_settings_cmd.py — 注册 message_format 为合法设置项

分发层:

  • src/application/services/notification_dispatcher.py — 新增 _resolve_message_format,在 PreparedSubscriptionDispatchsend_to_sessionMessageContext 中贯通

发送层:

  • src/infrastructure/messaging/senders/base_sender.py — 新增 _components_to_single_chain_send_direct_send_as_imagesend_to_user 加入 IMAGE 模式分支
  • src/infrastructure/messaging/senders/onebot_sender.pysend_to_user 加入 DIRECT / IMAGE 模式分支
  • src/infrastructure/messaging/senders/provider.py — 适配 message_format 在 port 和 infra 间传递
  • src/infrastructure/messaging/senders/types.pyMessageContext 新增 message_format 字段
  • src/application/ports/message_sender.py — port 层 MessageContext 新增 message_format 字段

Web API 与前端:

  • src/interfaces/web_api.py — 订阅/用户 API 响应中序列化 message_format
  • pages/ — 设置页、订阅编辑、用户编辑分别新增"消息格式"下拉选项

AI 工具:

  • src/application/llmtools/xml_push.pyrss_push_xml_entry 工具暴露 message_format 参数
  • src/application/services/agent_xml_push_service.py — 解析、传递 message_format

模板:

  • templates/entry_card.html — 全新深色卡片风格 t2i 模板,修复右侧空白,提升渲染质量 (JPEG quality 40→85)

文档:

  • docs/usage/commands.mdai-tools.mdconfiguration.md
  • docs/project/architecture.mdapplication.mddispatch.mddomain-model.mdplatforms.md

测试:

  • tests/unit/infrastructure/test_onebot_sender_message_format.py — 新增 DIRECT/IMAGE 模式测试

  • tests/unit/application/test_notification_dispatcher_message_format.py — 新增 _resolve_message_format 测试

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

$ uv run python -m pytest tests/unit/ -v 2>&1 | tail -5 
690 passed, 30 failed (30 pre-existing Windows path failures)

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed them with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txt 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

为推送通知新增可配置的消息格式,包括合并转发、直发和图片卡片模式,并贯穿调度、发送、持久化和配置各层实现。

New Features:

  • 引入 MessageFormat 枚举和配置选项,用于控制按用户和按订阅的消息格式(合并转发、直发、图片)。
  • 新增图片(t2i)消息格式,将条目渲染为基于 HTML 的 JPEG 卡片,支持富文本和媒体预览,并在必要时自动回退到直发模式。
  • rss_push_xml_entry AI 工具以及 Web API 和控制面板 UI 中,将 message_format 暴露为安全的格式化参数。

Enhancements:

  • 扩展通知调度和发送上下文以携带有效的 message_format 和原始 HTML,确保在重试和代理推送过程中行为一致。
  • 更新 OneBot 发送器以遵循 message_format,支持直发单链路发送和图片卡片发送,而不是始终使用合并转发。
  • 编写 message_format 的语义与格式化流水线文档,包括各平台行为及其与 send_mode 的交互方式。

Deployment:

  • 新增 V3_add_message_format 数据库迁移,在用户和订阅表上添加 message_format 字段。

Tests:

  • 添加单元测试,覆盖 NotificationDispatcher 中的 message_format 解析,以及 OneBot 发送器中的直发/图片模式。
  • 更新数据库迁移测试,纳入新的 V3_add_message_format 迁移,并验证用户和订阅表的模式期望。
Original summary in English

Summary by Sourcery

Add configurable message formatting for push notifications, including merged forward, direct, and image card modes, and wire it through dispatch, sending, persistence, and configuration layers.

New Features:

  • Introduce a MessageFormat enum and configuration options to control per-user and per-subscription message format (merged forward, direct, image).
  • Add image (t2i) message format that renders entries as HTML-based JPEG cards with rich text and media previews, with automatic fallback to direct sending.
  • Expose message_format as a safe formatting parameter in the rss_push_xml_entry AI tool and through Web API and dashboard UI controls.

Enhancements:

  • Extend notification dispatch and sender contexts to carry effective message_format and raw HTML, ensuring consistent behavior across retries and agent pushes.
  • Update OneBot sender to respect message_format, supporting direct single-chain sends and image card sends instead of always using merged forward.
  • Document message_format semantics and formatting pipeline, including platform behavior and interaction with send_mode.

Deployment:

  • Add V3_add_message_format database migration to introduce message_format columns on user and subscription tables.

Tests:

  • Add unit tests covering message_format resolution in NotificationDispatcher and direct/image modes in the OneBot sender.
  • Update database migration tests to include the new V3_add_message_format migration and schema expectations for user and subscription tables.
Original summary in English

Summary by Sourcery

为推送通知新增可配置的消息格式,包括合并转发、直发和图片卡片模式,并贯穿调度、发送、持久化和配置各层实现。

New Features:

  • 引入 MessageFormat 枚举和配置选项,用于控制按用户和按订阅的消息格式(合并转发、直发、图片)。
  • 新增图片(t2i)消息格式,将条目渲染为基于 HTML 的 JPEG 卡片,支持富文本和媒体预览,并在必要时自动回退到直发模式。
  • rss_push_xml_entry AI 工具以及 Web API 和控制面板 UI 中,将 message_format 暴露为安全的格式化参数。

Enhancements:

  • 扩展通知调度和发送上下文以携带有效的 message_format 和原始 HTML,确保在重试和代理推送过程中行为一致。
  • 更新 OneBot 发送器以遵循 message_format,支持直发单链路发送和图片卡片发送,而不是始终使用合并转发。
  • 编写 message_format 的语义与格式化流水线文档,包括各平台行为及其与 send_mode 的交互方式。

Deployment:

  • 新增 V3_add_message_format 数据库迁移,在用户和订阅表上添加 message_format 字段。

Tests:

  • 添加单元测试,覆盖 NotificationDispatcher 中的 message_format 解析,以及 OneBot 发送器中的直发/图片模式。
  • 更新数据库迁移测试,纳入新的 V3_add_message_format 迁移,并验证用户和订阅表的模式期望。
Original summary in English

Summary by Sourcery

Add configurable message formatting for push notifications, including merged forward, direct, and image card modes, and wire it through dispatch, sending, persistence, and configuration layers.

New Features:

  • Introduce a MessageFormat enum and configuration options to control per-user and per-subscription message format (merged forward, direct, image).
  • Add image (t2i) message format that renders entries as HTML-based JPEG cards with rich text and media previews, with automatic fallback to direct sending.
  • Expose message_format as a safe formatting parameter in the rss_push_xml_entry AI tool and through Web API and dashboard UI controls.

Enhancements:

  • Extend notification dispatch and sender contexts to carry effective message_format and raw HTML, ensuring consistent behavior across retries and agent pushes.
  • Update OneBot sender to respect message_format, supporting direct single-chain sends and image card sends instead of always using merged forward.
  • Document message_format semantics and formatting pipeline, including platform behavior and interaction with send_mode.

Deployment:

  • Add V3_add_message_format database migration to introduce message_format columns on user and subscription tables.

Tests:

  • Add unit tests covering message_format resolution in NotificationDispatcher and direct/image modes in the OneBot sender.
  • Update database migration tests to include the new V3_add_message_format migration and schema expectations for user and subscription tables.
Original summary in English

Summary by Sourcery

为推送通知新增可配置的消息格式,包括合并转发、直发和图片卡片模式,并贯穿调度、发送、持久化和配置各层实现。

New Features:

  • 引入 MessageFormat 枚举和配置选项,用于控制按用户和按订阅的消息格式(合并转发、直发、图片)。
  • 新增图片(t2i)消息格式,将条目渲染为基于 HTML 的 JPEG 卡片,支持富文本和媒体预览,并在必要时自动回退到直发模式。
  • rss_push_xml_entry AI 工具以及 Web API 和控制面板 UI 中,将 message_format 暴露为安全的格式化参数。

Enhancements:

  • 扩展通知调度和发送上下文以携带有效的 message_format 和原始 HTML,确保在重试和代理推送过程中行为一致。
  • 更新 OneBot 发送器以遵循 message_format,支持直发单链路发送和图片卡片发送,而不是始终使用合并转发。
  • 编写 message_format 的语义与格式化流水线文档,包括各平台行为及其与 send_mode 的交互方式。

Deployment:

  • 新增 V3_add_message_format 数据库迁移,在用户和订阅表上添加 message_format 字段。

Tests:

  • 添加单元测试,覆盖 NotificationDispatcher 中的 message_format 解析,以及 OneBot 发送器中的直发/图片模式。
  • 更新数据库迁移测试,纳入新的 V3_add_message_format 迁移,并验证用户和订阅表的模式期望。
Original summary in English

Summary by Sourcery

Add configurable message formatting for push notifications, including merged forward, direct, and image card modes, and wire it through dispatch, sending, persistence, and configuration layers.

New Features:

  • Introduce a MessageFormat enum and configuration options to control per-user and per-subscription message format (merged forward, direct, image).
  • Add image (t2i) message format that renders entries as HTML-based JPEG cards with rich text and media previews, with automatic fallback to direct sending.
  • Expose message_format as a safe formatting parameter in the rss_push_xml_entry AI tool and through Web API and dashboard UI controls.

Enhancements:

  • Extend notification dispatch and sender contexts to carry effective message_format and raw HTML, ensuring consistent behavior across retries and agent pushes.
  • Update OneBot sender to respect message_format, supporting direct single-chain sends and image card sends instead of always using merged forward.
  • Document message_format semantics and formatting pipeline, including platform behavior and interaction with send_mode.

Deployment:

  • Add V3_add_message_format database migration to introduce message_format columns on user and subscription tables.

Tests:

  • Add unit tests covering message_format resolution in NotificationDispatcher and direct/image modes in the OneBot sender.
  • Update database migration tests to include the new V3_add_message_format migration and schema expectations for user and subscription tables.
Original summary in English

Summary by Sourcery

为推送通知新增可配置的消息格式,包括合并转发、直发和图片卡片模式,并贯穿调度、发送、持久化和配置各层实现。

New Features:

  • 引入 MessageFormat 枚举和配置选项,用于控制按用户和按订阅的消息格式(合并转发、直发、图片)。
  • 新增图片(t2i)消息格式,将条目渲染为基于 HTML 的 JPEG 卡片,支持富文本和媒体预览,并在必要时自动回退到直发模式。
  • rss_push_xml_entry AI 工具以及 Web API 和控制面板 UI 中,将 message_format 暴露为安全的格式化参数。

Enhancements:

  • 扩展通知调度和发送上下文以携带有效的 message_format 和原始 HTML,确保在重试和代理推送过程中行为一致。
  • 更新 OneBot 发送器以遵循 message_format,支持直发单链路发送和图片卡片发送,而不是始终使用合并转发。
  • 编写 message_format 的语义与格式化流水线文档,包括各平台行为及其与 send_mode 的交互方式。

Deployment:

  • 新增 V3_add_message_format 数据库迁移,在用户和订阅表上添加 message_format 字段。

Tests:

  • 添加单元测试,覆盖 NotificationDispatcher 中的 message_format 解析,以及 OneBot 发送器中的直发/图片模式。
  • 更新数据库迁移测试,纳入新的 V3_add_message_format 迁移,并验证用户和订阅表的模式期望。
Original summary in English

Summary by Sourcery

Add configurable message formatting for push notifications, including merged forward, direct, and image card modes, and wire it through dispatch, sending, persistence, and configuration layers.

New Features:

  • Introduce a MessageFormat enum and configuration options to control per-user and per-subscription message format (merged forward, direct, image).
  • Add image (t2i) message format that renders entries as HTML-based JPEG cards with rich text and media previews, with automatic fallback to direct sending.
  • Expose message_format as a safe formatting parameter in the rss_push_xml_entry AI tool and through Web API and dashboard UI controls.

Enhancements:

  • Extend notification dispatch and sender contexts to carry effective message_format and raw HTML, ensuring consistent behavior across retries and agent pushes.
  • Update OneBot sender to respect message_format, supporting direct single-chain sends and image card sends instead of always using merged forward.
  • Document message_format semantics and formatting pipeline, including platform behavior and interaction with send_mode.

Deployment:

  • Add V3_add_message_format database migration to introduce message_format columns on user and subscription tables.

Tests:

  • Add unit tests covering message_format resolution in NotificationDispatcher and direct/image modes in the OneBot sender.
  • Update database migration tests to include the new V3_add_message_format migration and schema expectations for user and subscription tables.

Summary by CodeRabbit

  • 新功能

    • 新增消息格式设置:支持合并转发、直发和图片三种模式。
    • 可在订阅、用户及默认配置中选择消息格式,并支持继承设置。
    • 图片模式会将条目渲染为图片卡片;不兼容平台将自动回退为直发。
    • XML 即时推送支持指定安全的消息格式参数。
  • 改进

    • 消息格式配置现可通过命令、控制面板及订阅导出进行管理。
    • 完善跨平台兼容、默认值处理和历史数据迁移支持。
  • 文档

    • 更新消息格式、推送参数及平台兼容规则说明。

FFFold added 12 commits July 18, 2026 16:56
…h messages

新增 message_format 配置项,支持三种发送格式:
- merged_forward(默认):保持当前合并转发行为
- direct:直接发送消息组件,不走合并转发
- image:使用 AstrBot t2i 服务渲染为图片发送

配置层级与 send_mode 同级:_conf_schema.json 全局默认 → 订阅/用户/会话级可覆盖。
OneBot 平台支持所有三种格式,其他平台 merged_forward 自动回退为 direct。

新增文件:
- V3_add_message_format.py: 数据库迁移
- entry_card.html: t2i 渲染模板
- test_onebot_sender_message_format.py: OneBot 格式测试
- test_notification_dispatcher_message_format.py: dispatcher 解析测试
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

新增 message_format 配置,支持合并转发、直发和图片模式,并贯通配置界面、持久化、订阅分发、即时推送及消息发送实现。

Changes

消息格式功能

Layer / File(s) Summary
消息格式契约与持久化
src/shared/constants.py, src/domain/entities/*, src/infrastructure/config/..., src/infrastructure/persistence/..., src/application/dto/*
新增消息格式枚举、领域字段、全局配置、数据库迁移、ORM 字段及仓储映射。
配置入口与管理界面
src/application/commands/*, src/interfaces/*, pages/dashboard/...
新增用户、订阅、会话和全局默认消息格式的校验、读写、API 返回及 Dashboard 控件。
推送解析与工具传递
src/application/llmtools/xml_push.py, src/application/services/*, src/infrastructure/messaging/senders/provider.py
即时推送支持消息格式参数;分发器按订阅、用户和默认配置解析最终格式,并传入发送请求。
直发与图片发送实现
src/infrastructure/messaging/senders/*, templates/entry_card.html
新增单链直发、HTML 卡片图片渲染及 OneBot 格式分流,图片渲染失败时回退到直发。
文档与行为验证
docs/*, tests/unit/*
补充消息格式语义和工具边界文档,并覆盖解析、迁移、工具调用及 OneBot 发送行为。

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant Config as Dashboard/配置
  participant Dispatcher as NotificationDispatcher
  participant Sender as MessageSender
  participant T2I as t2i服务
  Config->>Dispatcher: 保存并解析 message_format
  Dispatcher->>Sender: 传递 SendRequest.message_format
  Sender->>T2I: 图片模式渲染 entry_card.html
  T2I-->>Sender: 返回卡片图片
  Sender-->>Dispatcher: 发送结果
Loading

Possibly related PRs

Suggested reviewers: flanchanxwo

Poem

小兔捧来新格式,
合并、直发与图片。
配置一路跳进云,
卡片映着 RSS 春。
兔耳一抖,消息准时到。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.81% 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
Linked Issues check ✅ Passed 代码实现了 #98 要求的 merged_forward、direct、image 三种发送模式,并贯通配置、发送链路和界面。
Out of Scope Changes check ✅ Passed 未见明显与消息格式功能无关的改动,变更主要集中在配置、发送、持久化、文档和测试。
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题简洁且准确概括了本次变更的核心:为推送通知新增可配置的消息格式。

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.

@github-actions github-actions Bot added area: docs Documentation changes area: frontend Plugin Pages or frontend changes area: backend Backend or core runtime changes area: tests Test changes labels Jul 18, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown

审查者指南

实现了通知的可配置消息格式(merged forward、direct、image),并将该选项贯穿到配置/默认值、领域实体、持久化和迁移、分发流水线、消息发送方(包括 OneBot)、Web API/UI、AI XML 推送工具、模板化 t2i 渲染,以及测试/文档中。

带有 t2i 渲染和回退机制的 IMAGE message_format 发送时序图

sequenceDiagram
    participant ND as NotificationDispatcher
    participant SP as MessageSenderProvider
    participant BS as BaseSender
    participant HR as html_renderer

    ND->>SP: send_to_session(..., message_format=MESSAGE_FORMAT_IMAGE)
    SP->>BS: send_to_user(request, context.message_format)
    BS->>BS: _send_as_image(request, prepared_media, context)
    BS->>HR: render_custom_template(tmpl_str, tmpl_data, options)
    alt render succeeds
        HR-->>BS: rendered
        BS->>BS: _build_components(...)
        BS->>BS: _send_chain(session_id, chain)
    else render fails or empty
        HR-->>BS: [exception or empty]
        BS->>BS: _send_direct(request, prepared_media, context)
        BS->>BS: _components_to_single_chain(components, request.message)
        BS->>BS: _send_chain(session_id, chain)
    end
    BS-->>SP: SendResult
    SP-->>ND: SendResult
Loading

在分发流水线中解析生效 message_format 的流程图

flowchart TD
    subgraph ConfigAndDomain
        SD[SubscriptionDefaults.message_format]
        U[User.message_format]
        S[Subscription.message_format]
    end

    ND[NotificationDispatcher]
    RMF[_resolve_message_format]
    DFLT[_message_format_from_subscription_defaults]
    PREP[PreparedSubscriptionDispatch.effective_message_format]
    MC[MessageContext.message_format]

    SD --> DFLT
    DFLT --> ND

    ND --> RMF
    S --> RMF
    U --> RMF

    RMF --> PREP
    PREP --> MC
Loading

文件级变更

变更 详情 文件
在配置、领域、持久化和 API 中,将 message_format 添加为一等可配置选项。
  • 引入 MessageFormat IntEnum 以及相关常量/映射,用于字符串和整型表示,包括默认值和允许集合。
  • 扩展全局和运行时订阅默认配置模型,增加 message_format 字符串字段,并实现数据库序列化/反序列化以及 settings builder 接线。
  • UserSubscription 领域实体、ORM 模型、导出 DTO,以及 CLI/用户设置命令中添加 message_format 字段,包括校验范围和生效键列表。
  • 实现 V3_add_message_format 迁移以及数据库测试,为 rsshub_userrsshub_sub 增加 message_format 列,设置合理默认值并提供基线覆盖。
  • 通过 Web API 响应、会话默认键以及仪表盘/前端状态,暴露用户、订阅和全局设置的 message_format
src/shared/constants.py
src/infrastructure/config/models/plugin_config_models.py
src/infrastructure/config/models/runtime_settings.py
src/infrastructure/config/settings_builder.py
src/domain/entities/user.py
src/domain/entities/subscription.py
src/infrastructure/persistence/models.py
src/infrastructure/persistence/migrations/V3_add_message_format.py
src/infrastructure/persistence/user_repository_impl.py
src/infrastructure/persistence/subscription_repository_impl.py
src/application/commands/get_user_settings_cmd.py
src/application/commands/set_user_settings_cmd.py
src/application/dto/user_dto.py
src/application/dto/subscription_export_record.py
src/interfaces/handlers/config.py
src/interfaces/web_api.py
src/infrastructure/persistence/__tests__/*
docs/usage/commands.md
pages/dashboard/**/*
在通知分发流水线中传播解析出的 message_format,包括代理 XML 推送和重试。
  • PreparedSubscriptionDispatch 中添加 effective_message_format,并通过 _message_format_from_subscription_defaults_resolve_message_format 使用用户/订阅值的继承语义进行解析。
  • 确保在推送历史中记录 message_format,与 send_mode/style 一起存储,并传递到 send_to_session_send_to_sessiondispatch_agent_entry
  • 扩展 AgentXmlPushOptions 和 XML 推送解析,使其接受 message_format 字符串,将其映射为整型,并在 dry-run 和实际推送时传递给 NotificationDispatcher
  • 在未显式提供格式时(例如代理重试),设置 MESSAGE_FORMAT_DEFAULT
src/application/services/notification_dispatcher.py
src/application/services/agent_xml_push_service.py
src/application/llmtools/xml_push.py
tests/unit/application/test_notification_dispatcher_message_format.py
tests/unit/application/test_llmtools.py
docs/project/application.md
docs/project/architecture.md
docs/project/dispatch.md
docs/project/domain-model.md
docs/project/platforms.md
docs/usage/ai-tools.md
docs/usage/configuration.md
实现 message_format 感知的发送行为,包括直接单链发送以及带优雅回退的 t2i 图片渲染。
  • 在基础 sender 中添加 _components_to_single_chain 帮助方法,将组件扁平化为单一链条(text/media/tail 元素),并提供默认文本回退。
  • 实现 _send_direct,构建组件并以单一链条形式发送;实现 _send_as_image,通过 html_renderer 渲染 entry_card.html 为图片,附加链接文本和媒体附件,并在任意失败或缺失模板时回退到 _send_direct
  • 更新 base_sender.send_to_user,在 MESSAGE_FORMAT_IMAGE 时分支,并在 MessageContext 请求图像格式时调用 _send_as_image
  • 更新 OneBot sender,以支持 MESSAGE_FORMAT_DIRECTMESSAGE_FORMAT_IMAGE:在 direct 模式下构建单链直接消息,在 image 模式下委托给 _send_as_image,包括空消息处理。
  • 从 ports 层的 MessageContextmessage_format 传递到 sender provider,再传递到基础设施层 sender 所使用的 MessageContext,在缺失时默认使用 MESSAGE_FORMAT_DEFAULT
src/infrastructure/messaging/senders/base_sender.py
src/infrastructure/messaging/senders/onebot_sender.py
src/infrastructure/messaging/senders/provider.py
src/application/ports/message_sender.py
src/infrastructure/messaging/senders/types.py
templates/entry_card.html
tests/unit/infrastructure/test_onebot_sender_message_format.py
message_format 暴露给 AI 工具,并确保文档/测试保持一致。
  • 扩展 rss_push_xml_entry LLM 工具 schema,加入 message_format 枚举,并将该参数转发到 agent_xml_push_service.push_entry
  • 更新测试,断言 message_format 出现在 LLM 工具 JSON schema 中,并且调用在 message_formatNone 时也能正确传递该参数。
  • 刷新文档,描述 message_format 语义、平台行为以及新的图片模式渲染流水线,并将 message_format 列为 AI 工具的安全可配置参数。
src/application/llmtools/xml_push.py
tests/unit/application/test_llmtools.py
docs/usage/ai-tools.md
docs/project/application.md
docs/project/architecture.md
docs/project/platforms.md
docs/project/dispatch.md
docs/project/domain-model.md
docs/usage/configuration.md

与关联 Issue 的对照评估

Issue 目标 是否已满足 说明
#98 引入可配置的 message_format/forward_mode 选项,与 send_mode 并行(默认为 merged_forward),可在全局、用户级和订阅级设置,并被持久化、通过 API/CLI/UI 暴露,以及在分发推送时使用。
#98 实现 direct 消息格式,尤其是在 OneBot/QQ 上,将 RSS 条目以普通消息链直接发送(不使用 merged-forward/Nodes 容器),同时在链内部仍使用现有内容选择(send_mode)。
#98 实现 image 消息格式,通过 AstrBot 的 t2i 服务(基于 HTML 模板)将条目内容渲染为图片,与链接和媒体附件一并发送,并在 t2i 渲染或模板加载失败时回退到 direct 发送。

可能相关的 Issue


提示和命令

与 Sourcery 交互

  • 触发新审查: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 通过回复审查评论,让 Sourcery 从该评论创建 issue。你也可以回复审查评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 Pull Request 标题: 在 PR 标题中任意位置写入 @sourcery-ai 即可随时生成标题。你也可以在 PR 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 PR 正文中任意位置写入 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 PR 中评论 @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 以触发新审查!

自定义你的体验

访问你的 dashboard 可以:

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

获取帮助

Original review guide in English

Reviewer's Guide

Implements configurable message formats for notifications (merged forward, direct, image) and wires this option through config/defaults, domain entities, persistence and migrations, dispatch pipeline, message senders (including OneBot), Web API/UI, AI XML push tooling, templated t2i rendering, and tests/docs.

Sequence diagram for IMAGE message_format sending with t2i rendering and fallback

sequenceDiagram
    participant ND as NotificationDispatcher
    participant SP as MessageSenderProvider
    participant BS as BaseSender
    participant HR as html_renderer

    ND->>SP: send_to_session(..., message_format=MESSAGE_FORMAT_IMAGE)
    SP->>BS: send_to_user(request, context.message_format)
    BS->>BS: _send_as_image(request, prepared_media, context)
    BS->>HR: render_custom_template(tmpl_str, tmpl_data, options)
    alt render succeeds
        HR-->>BS: rendered
        BS->>BS: _build_components(...)
        BS->>BS: _send_chain(session_id, chain)
    else render fails or empty
        HR-->>BS: [exception or empty]
        BS->>BS: _send_direct(request, prepared_media, context)
        BS->>BS: _components_to_single_chain(components, request.message)
        BS->>BS: _send_chain(session_id, chain)
    end
    BS-->>SP: SendResult
    SP-->>ND: SendResult
Loading

Flow diagram for resolving effective message_format in dispatch pipeline

flowchart TD
    subgraph ConfigAndDomain
        SD[SubscriptionDefaults.message_format]
        U[User.message_format]
        S[Subscription.message_format]
    end

    ND[NotificationDispatcher]
    RMF[_resolve_message_format]
    DFLT[_message_format_from_subscription_defaults]
    PREP[PreparedSubscriptionDispatch.effective_message_format]
    MC[MessageContext.message_format]

    SD --> DFLT
    DFLT --> ND

    ND --> RMF
    S --> RMF
    U --> RMF

    RMF --> PREP
    PREP --> MC
Loading

File-Level Changes

Change Details Files
Add message_format as a first-class configurable option across config, domain, persistence, and APIs.
  • Introduce MessageFormat IntEnum and related constants/mappings for string and int representations, including defaults and allowed sets.
  • Extend global and runtime subscription default config models to include a message_format string field with DB serialization/deserialization and settings builder wiring.
  • Add message_format fields to User and Subscription domain entities, ORM models, export DTOs, and CLI/user settings commands, including validation ranges and effectivity key lists.
  • Implement V3_add_message_format migration plus database tests to add message_format columns to rsshub_user and rsshub_sub with proper defaults and baseline coverage.
  • Expose message_format via Web API responses, session default keys, and dashboard/store state for users, subscriptions, and global settings.
src/shared/constants.py
src/infrastructure/config/models/plugin_config_models.py
src/infrastructure/config/models/runtime_settings.py
src/infrastructure/config/settings_builder.py
src/domain/entities/user.py
src/domain/entities/subscription.py
src/infrastructure/persistence/models.py
src/infrastructure/persistence/migrations/V3_add_message_format.py
src/infrastructure/persistence/user_repository_impl.py
src/infrastructure/persistence/subscription_repository_impl.py
src/application/commands/get_user_settings_cmd.py
src/application/commands/set_user_settings_cmd.py
src/application/dto/user_dto.py
src/application/dto/subscription_export_record.py
src/interfaces/handlers/config.py
src/interfaces/web_api.py
src/infrastructure/persistence/__tests__/*
docs/usage/commands.md
pages/dashboard/**/*
Propagate resolved message_format through the notification dispatch pipeline, including agent XML pushes and retries.
  • Add effective_message_format to PreparedSubscriptionDispatch and resolve it via _message_format_from_subscription_defaults and _resolve_message_format using inheritance semantics for user/subscription values.
  • Ensure message_format is recorded alongside send_mode/style in push history and is passed into send_to_session, _send_to_session, and dispatch_agent_entry.
  • Extend AgentXmlPushOptions and XML push resolution to accept message_format strings, map them to ints, and pass them through to NotificationDispatcher for both dry-run and actual pushes.
  • Set MESSAGE_FORMAT_DEFAULT where an explicit format is not provided (e.g., agent retries).
src/application/services/notification_dispatcher.py
src/application/services/agent_xml_push_service.py
src/application/llmtools/xml_push.py
tests/unit/application/test_notification_dispatcher_message_format.py
tests/unit/application/test_llmtools.py
docs/project/application.md
docs/project/architecture.md
docs/project/dispatch.md
docs/project/domain-model.md
docs/project/platforms.md
docs/usage/ai-tools.md
docs/usage/configuration.md
Implement message_format-aware sending behavior, including direct single-chain sends and t2i image rendering with graceful fallback.
  • In the base sender, add _components_to_single_chain helper to flatten components into a single chain of text/media/tail elements with default text fallback.
  • Implement _send_direct to build components and send them as a single chain, and _send_as_image to render entry_card.html via html_renderer into an image, append link text and media attachments, and fall back to _send_direct on any failure or missing template.
  • Update base_sender.send_to_user to branch on MESSAGE_FORMAT_IMAGE and invoke _send_as_image when requested via MessageContext.
  • Update OneBot sender to honor MESSAGE_FORMAT_DIRECT and MESSAGE_FORMAT_IMAGE by building a single-chain direct message or delegating to _send_as_image, including empty-message handling.
  • Wire message_format from the ports-layer MessageContext through the sender provider into MessageContext used by infra senders, defaulting to MESSAGE_FORMAT_DEFAULT when absent.
src/infrastructure/messaging/senders/base_sender.py
src/infrastructure/messaging/senders/onebot_sender.py
src/infrastructure/messaging/senders/provider.py
src/application/ports/message_sender.py
src/infrastructure/messaging/senders/types.py
templates/entry_card.html
tests/unit/infrastructure/test_onebot_sender_message_format.py
Expose message_format to AI tooling and ensure docs/tests remain consistent.
  • Extend rss_push_xml_entry LLM tool schema to include a message_format enum and forward the parameter to agent_xml_push_service.push_entry.
  • Update tests to assert message_format appears in the LLM tool JSON schema and that calls pass message_format through even when None.
  • Refresh documentation to describe message_format semantics, platform behavior, and the new image mode rendering pipeline and to list message_format as a safe, configurable parameter for AI tools.
src/application/llmtools/xml_push.py
tests/unit/application/test_llmtools.py
docs/usage/ai-tools.md
docs/project/application.md
docs/project/architecture.md
docs/project/platforms.md
docs/project/dispatch.md
docs/project/domain-model.md
docs/usage/configuration.md

Assessment against linked issues

Issue Objective Addressed Explanation
#98 Introduce a configurable message_format/forward_mode option alongside send_mode (defaulting to merged_forward) that can be set globally, per user, and per subscription, and is persisted, exposed via APIs/CLI/UI, and used when dispatching pushes.
#98 Implement a direct message format where, especially on OneBot/QQ, RSS entries are sent as plain message chains directly (without merged-forward/Nodes container), while still using existing content selection (send_mode) inside the chain.
#98 Implement an image message format that renders entry content into an image via AstrBot’s t2i service (HTML template-based), sends it along with links and media attachments, and falls back to direct sending if t2i rendering or template loading fails.

Possibly linked issues


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

@FFFold
FFFold marked this pull request as ready for review July 18, 2026 10:11
@FFFold
FFFold requested a review from FlanChanXwO as a code owner July 18, 2026 10:11

@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 - 我发现了 5 个问题,并且给了一些整体性的反馈:

  • _send_as_image 中对 entry_card.html 的查找是从 __file__ 向上走固定层级的父目录,这在以后重构时会非常脆弱;建议使用 importlib.resources 或者一个集中管理模板路径的工具函数来定位模板,而不是硬编码相对路径的层级深度。
  • message_format 的字符串/整数映射逻辑在多个地方重复出现(例如 MESSAGE_FORMAT_STRING_MAPGlobalConfig._MESSAGE_FORMAT_MAPagent_xml_push_service 里的 _resolve_push_options);把这些逻辑抽到共享的 helper 中,或者复用枚举/常量映射,可以降低映射关系逐渐不一致的风险。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- `_send_as_image` 中对 `entry_card.html` 的查找是从 `__file__` 向上走固定层级的父目录,这在以后重构时会非常脆弱;建议使用 `importlib.resources` 或者一个集中管理模板路径的工具函数来定位模板,而不是硬编码相对路径的层级深度。
- `message_format` 的字符串/整数映射逻辑在多个地方重复出现(例如 `MESSAGE_FORMAT_STRING_MAP``GlobalConfig._MESSAGE_FORMAT_MAP``agent_xml_push_service` 里的 `_resolve_push_options`);把这些逻辑抽到共享的 helper 中,或者复用枚举/常量映射,可以降低映射关系逐渐不一致的风险。

## Individual Comments

### Comment 1
<location path="src/infrastructure/messaging/senders/base_sender.py" line_range="1484-1493" />
<code_context>
+        entry_title = getattr(context, "entry_title", "")
+        entry_link = getattr(context, "entry_link", "")
+
+        media_previews = []
+        if prepared_media:
+            for pm in prepared_media:
+                if pm.media_type == "image" and pm.original_url:
+                    media_previews.append(
+                        {
+                            "type": "image",
</code_context>
<issue_to_address>
**suggestion:** 模板已经支持视频媒体,但 `_send_as_image` 只收集图片用于预览。

由于 `_send_as_image` 只会为 `pm.media_type == "image"` 的条目添加预览,模板里 `item.type == 'video'` 的分支实际上从未被使用。可以考虑要么扩展这里的逻辑,把视频也纳入(例如以 `type: "video"` 追加到 `media_previews`),要么移除模板中与视频相关的路径,避免产生死代码。

```suggestion
        media_previews = []
        if prepared_media:
            for pm in prepared_media:
                if pm.original_url and pm.media_type in ("image", "video"):
                    media_previews.append(
                        {
                            "type": pm.media_type,
                            "url": pm.original_url,
                        }
                    )
```
</issue_to_address>

### Comment 2
<location path="src/application/services/agent_xml_push_service.py" line_range="206-215" />
<code_context>
         allowed={SEND_MODE_LINK_ONLY, SEND_MODE_AUTO, SEND_MODE_DIRECT},
         fallback=SEND_MODE_AUTO,
     )
+    message_format_value = _parse_mapped_int(
+        message_format,
+        mapping={
+            "merged_forward": 0,
+            "merged-forward": 0,
+            "merged": 0,
+            "合并转发": 0,
+            "direct": 1,
+            "直发": 1,
+            "image": 2,
+            "图片": 2,
+            "t2i": 2,
+        },
+        allowed={0, 1, 2},
+        fallback=0,
+    )
     style_value = _parse_mapped_int(
</code_context>
<issue_to_address>
**suggestion:** message format 的映射中直接硬编码了数值,而不是复用已有的共享常量。

这里的映射应该使用 `shared/constants.py` 中的 `MessageFormat` / `MESSAGE_FORMAT_*` 常量,而不是裸写 `0/1/2`。请导入并引用 `MESSAGE_FORMAT_MERGED_FORWARD``MESSAGE_FORMAT_DIRECT``MESSAGE_FORMAT_IMAGE`,在 `mapping``allowed` 中都使用这些常量,以避免枚举变化时出现偏差,并使其与全局约定保持一致。

建议实现如下:

```python
    message_format_value = _parse_mapped_int(
        message_format,
        mapping={
            "merged_forward": MESSAGE_FORMAT_MERGED_FORWARD,
            "merged-forward": MESSAGE_FORMAT_MERGED_FORWARD,
            "merged": MESSAGE_FORMAT_MERGED_FORWARD,
            "合并转发": MESSAGE_FORMAT_MERGED_FORWARD,
            "direct": MESSAGE_FORMAT_DIRECT,
            "直发": MESSAGE_FORMAT_DIRECT,
            "image": MESSAGE_FORMAT_IMAGE,
            "图片": MESSAGE_FORMAT_IMAGE,
            "t2i": MESSAGE_FORMAT_IMAGE,
        },
        allowed={MESSAGE_FORMAT_MERGED_FORWARD, MESSAGE_FORMAT_DIRECT, MESSAGE_FORMAT_IMAGE},
        fallback=MESSAGE_FORMAT_MERGED_FORWARD,
    )

```

为了完整实现该建议,还需要:

1. 确保在 `src/application/services/agent_xml_push_service.py` 顶部导入了 `MESSAGE_FORMAT_MERGED_FORWARD``MESSAGE_FORMAT_DIRECT``MESSAGE_FORMAT_IMAGE`,例如:
   - `from shared.constants import MESSAGE_FORMAT_MERGED_FORWARD, MESSAGE_FORMAT_DIRECT, MESSAGE_FORMAT_IMAGE`
2. 如果该文件已经从 `shared/constants.py` 导入了其他与 message format 相关的常量,请将这三个常量添加到已有的导入语句中,而不是单独新增一行,以保持当前代码风格一致。
</issue_to_address>

### Comment 3
<location path="src/application/services/notification_dispatcher.py" line_range="1251-1256" />
<code_context>
                         entry_link=entry_link,
                         platform_name=target.platform_name or "",
                         send_mode=self._normalize_send_mode_value(send_mode),
+                        message_format=message_format or MESSAGE_FORMAT_DEFAULT,
                         style=style,
                         sender_strategy=sender_strategy,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 依赖 `or` 来处理 `message_format` 默认值,未来在默认值变为非 0 时可能会带来问题。

目前由于 `MESSAGE_FORMAT_DEFAULT``0`,这种写法对 `None` 和当前已有的合法值来说是可行的。但如果默认值未来变为非 0,那么这里会把上游有意传入的 `0` 给“吃掉”。改为 `message_format if message_format is not None else MESSAGE_FORMAT_DEFAULT` 可以更清晰地表达意图,并避免以后对 “0” 与“默认值” 之间含义的混淆。

```suggestion
                        entry_link=entry_link,
                        platform_name=target.platform_name or "",
                        send_mode=self._normalize_send_mode_value(send_mode),
                        message_format=message_format if message_format is not None else MESSAGE_FORMAT_DEFAULT,
                        style=style,
                        sender_strategy=sender_strategy,
```
</issue_to_address>

### Comment 4
<location path="tests/unit/infrastructure/test_onebot_sender_message_format.py" line_range="1-10" />
<code_context>
+"""V3 迁移:在 rsshub_user 和 rsshub_sub 表中添加 message_format 列"""
+
+from __future__ import annotations
+
+from ...utils import get_logger
</code_context>
<issue_to_address>
**suggestion (testing):** 加强 DIRECT 模式的测试,断言实际的 chain 内容以及默认文本回退逻辑。

当前测试只验证了确实发送了一个 chain,但没有检查它的具体内容。考虑到 `_components_to_single_chain` 中的逻辑比较复杂(裁剪、media 与尾部的处理、`default_text` 等),建议断言生成的 chain 恰好由一个内容为 `"Hello"` 且没有额外字段的 Plain 节点组成(例如通过 `_Plain` shim 检查 `sent_chain[0][1]`)。另外也可以补充一个用例:`message=""` 但组件列表包含媒体内容,以验证在存在媒体时不会注入 `default_text`。
</issue_to_address>

### Comment 5
<location path="docs/project/domain-model.md" line_range="15" />
<code_context>
 | 配置继承标记 | `-100` | 订阅继承用户配置,用户继承全局默认 | 不恢复 `use_sub_config` / `use_user_config`|
 | 用户状态 | `1` / `-1` | `USER_STATE_USER` / `USER_STATE_BANNED` | 旧非负状态统一视为普通用户。 |
 | `send_mode` | `-1` / `0` / `1` | 仅链接 / 自动 / 直接发送 |`1=Telegraph` 归一化为 `0`,旧 `2=直接消息` 归一化为 `1`|
+| `message_format` | `0` / `1` / `2` | 合并转发 / 直发 / 图片 | OneBot 支持全部三种格式;其他平台 `0` 自动回落为 `1`。 |
 | `style` | `0` / `1` / `2` | 自动或平台经典 / RSSRT / original |`flowerss=1` 迁移为 `0`,不恢复 flowerss UI 文案。 |
 | 显示类字段 | 整数状态 | `display_author``display_via``display_title``display_entry_tags``display_media` | 需要支持继承,不能简化为 `true/false`|
</code_context>
<issue_to_address>
**nitpick (typo):** 建议与前文 `message_format` 的说明用词保持一致(“回退” vs “回落”)。

在 `docs/usage/commands.md` 中写的是 “其他平台选 `0` 自动回退为 `1`”,而这里写的是 “自动回落为 `1`”。两处描述的是同一个行为,建议统一用词(例如都用“回退”或都用“回落”),以避免产生歧义。

```suggestion
| `message_format` | `0` / `1` / `2` | 合并转发 / 直发 / 图片 | OneBot 支持全部三种格式;其他平台 `0` 自动回退为 `1`。 |
```
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得这些 review 有帮助,可以考虑分享给别人 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进之后的 review。
Original comment in English

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

  • The entry_card.html lookup in _send_as_image walks up a fixed number of parents from __file__, which is brittle to future refactors; consider using importlib.resources or a central helper to locate templates instead of hardcoding the relative path depth.
  • message_format string/int mapping logic is duplicated in several places (e.g., MESSAGE_FORMAT_STRING_MAP, GlobalConfig._MESSAGE_FORMAT_MAP, _resolve_push_options in agent_xml_push_service); consolidating these into shared helpers or reusing the enum/constant maps would reduce the risk of the mappings drifting out of sync.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `entry_card.html` lookup in `_send_as_image` walks up a fixed number of parents from `__file__`, which is brittle to future refactors; consider using `importlib.resources` or a central helper to locate templates instead of hardcoding the relative path depth.
- `message_format` string/int mapping logic is duplicated in several places (e.g., `MESSAGE_FORMAT_STRING_MAP`, `GlobalConfig._MESSAGE_FORMAT_MAP`, `_resolve_push_options` in `agent_xml_push_service`); consolidating these into shared helpers or reusing the enum/constant maps would reduce the risk of the mappings drifting out of sync.

## Individual Comments

### Comment 1
<location path="src/infrastructure/messaging/senders/base_sender.py" line_range="1484-1493" />
<code_context>
+        entry_title = getattr(context, "entry_title", "")
+        entry_link = getattr(context, "entry_link", "")
+
+        media_previews = []
+        if prepared_media:
+            for pm in prepared_media:
+                if pm.media_type == "image" and pm.original_url:
+                    media_previews.append(
+                        {
+                            "type": "image",
</code_context>
<issue_to_address>
**suggestion:** Template supports video media but `_send_as_image` only collects images for previews.

Since `_send_as_image` only adds entries where `pm.media_type == "image"`, the template’s `item.type == 'video'` branch is never used. Either extend this logic to include video items (e.g. append them to `media_previews` with `type: "video"`) or remove the video-specific template path to avoid dead code.

```suggestion
        media_previews = []
        if prepared_media:
            for pm in prepared_media:
                if pm.original_url and pm.media_type in ("image", "video"):
                    media_previews.append(
                        {
                            "type": pm.media_type,
                            "url": pm.original_url,
                        }
                    )
```
</issue_to_address>

### Comment 2
<location path="src/application/services/agent_xml_push_service.py" line_range="206-215" />
<code_context>
         allowed={SEND_MODE_LINK_ONLY, SEND_MODE_AUTO, SEND_MODE_DIRECT},
         fallback=SEND_MODE_AUTO,
     )
+    message_format_value = _parse_mapped_int(
+        message_format,
+        mapping={
+            "merged_forward": 0,
+            "merged-forward": 0,
+            "merged": 0,
+            "合并转发": 0,
+            "direct": 1,
+            "直发": 1,
+            "image": 2,
+            "图片": 2,
+            "t2i": 2,
+        },
+        allowed={0, 1, 2},
+        fallback=0,
+    )
     style_value = _parse_mapped_int(
</code_context>
<issue_to_address>
**suggestion:** Message format mapping hard-codes numeric values instead of reusing shared constants.

This mapping should use the `MessageFormat` / `MESSAGE_FORMAT_*` constants from `shared/constants.py` instead of raw values `0/1/2`. Import and reference `MESSAGE_FORMAT_MERGED_FORWARD`, `MESSAGE_FORMAT_DIRECT`, and `MESSAGE_FORMAT_IMAGE` for both `mapping` and `allowed` to avoid drift if the enum changes and to keep this aligned with the global contract.

Suggested implementation:

```python
    message_format_value = _parse_mapped_int(
        message_format,
        mapping={
            "merged_forward": MESSAGE_FORMAT_MERGED_FORWARD,
            "merged-forward": MESSAGE_FORMAT_MERGED_FORWARD,
            "merged": MESSAGE_FORMAT_MERGED_FORWARD,
            "合并转发": MESSAGE_FORMAT_MERGED_FORWARD,
            "direct": MESSAGE_FORMAT_DIRECT,
            "直发": MESSAGE_FORMAT_DIRECT,
            "image": MESSAGE_FORMAT_IMAGE,
            "图片": MESSAGE_FORMAT_IMAGE,
            "t2i": MESSAGE_FORMAT_IMAGE,
        },
        allowed={MESSAGE_FORMAT_MERGED_FORWARD, MESSAGE_FORMAT_DIRECT, MESSAGE_FORMAT_IMAGE},
        fallback=MESSAGE_FORMAT_MERGED_FORWARD,
    )

```

To fully implement the suggestion, you should also:

1. Ensure `MESSAGE_FORMAT_MERGED_FORWARD`, `MESSAGE_FORMAT_DIRECT`, and `MESSAGE_FORMAT_IMAGE` are imported at the top of `src/application/services/agent_xml_push_service.py`, for example:
   - `from shared.constants import MESSAGE_FORMAT_MERGED_FORWARD, MESSAGE_FORMAT_DIRECT, MESSAGE_FORMAT_IMAGE`
2. If the file already imports other message format–related constants from `shared/constants.py`, add these three to the existing import line instead of introducing a new one, to match the current style.
</issue_to_address>

### Comment 3
<location path="src/application/services/notification_dispatcher.py" line_range="1251-1256" />
<code_context>
                         entry_link=entry_link,
                         platform_name=target.platform_name or "",
                         send_mode=self._normalize_send_mode_value(send_mode),
+                        message_format=message_format or MESSAGE_FORMAT_DEFAULT,
                         style=style,
                         sender_strategy=sender_strategy,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Relying on `or` for `message_format` defaults may obscure future non-zero defaults.

Because `MESSAGE_FORMAT_DEFAULT` is currently `0`, this works for now for `None` and the existing valid values. But if the default ever becomes non-zero, this could hide a deliberate `0` passed from upstream. Using `message_format if message_format is not None else MESSAGE_FORMAT_DEFAULT` makes the intent explicit and avoids future ambiguity around `0` vs. the default.

```suggestion
                        entry_link=entry_link,
                        platform_name=target.platform_name or "",
                        send_mode=self._normalize_send_mode_value(send_mode),
                        message_format=message_format if message_format is not None else MESSAGE_FORMAT_DEFAULT,
                        style=style,
                        sender_strategy=sender_strategy,
```
</issue_to_address>

### Comment 4
<location path="tests/unit/infrastructure/test_onebot_sender_message_format.py" line_range="1-10" />
<code_context>
+"""V3 迁移:在 rsshub_user 和 rsshub_sub 表中添加 message_format 列"""
+
+from __future__ import annotations
+
+from ...utils import get_logger
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen DIRECT mode test by asserting the actual chain content and default text fallback

This test only checks that a chain was sent, not what it contains. Given the non-trivial logic in `_components_to_single_chain` (trimming, media vs tail, `default_text`), please assert that the resulting chain is exactly one Plain node with `"Hello"` and no extras (e.g., by inspecting `sent_chain[0][1]` with the `_Plain` shim). Also consider adding a test where `message=""` but the component list contains media, to verify that `default_text` is not injected when media is present.
</issue_to_address>

### Comment 5
<location path="docs/project/domain-model.md" line_range="15" />
<code_context>
 | 配置继承标记 | `-100` | 订阅继承用户配置,用户继承全局默认 | 不恢复 `use_sub_config` / `use_user_config`|
 | 用户状态 | `1` / `-1` | `USER_STATE_USER` / `USER_STATE_BANNED` | 旧非负状态统一视为普通用户。 |
 | `send_mode` | `-1` / `0` / `1` | 仅链接 / 自动 / 直接发送 |`1=Telegraph` 归一化为 `0`,旧 `2=直接消息` 归一化为 `1`|
+| `message_format` | `0` / `1` / `2` | 合并转发 / 直发 / 图片 | OneBot 支持全部三种格式;其他平台 `0` 自动回落为 `1`。 |
 | `style` | `0` / `1` / `2` | 自动或平台经典 / RSSRT / original |`flowerss=1` 迁移为 `0`,不恢复 flowerss UI 文案。 |
 | 显示类字段 | 整数状态 | `display_author``display_via``display_title``display_entry_tags``display_media` | 需要支持继承,不能简化为 `true/false`|
</code_context>
<issue_to_address>
**nitpick (typo):** Consider making the wording consistent with the earlier `message_format` description ("回退" vs "回落").

In `docs/usage/commands.md` it says “其他平台选 `0` 自动回退为 `1`”, while here it says “自动回落为 `1`”. Since they describe the same behavior, please standardize on one verb (e.g., both “回退” or both “回落”) to avoid ambiguity.

```suggestion
| `message_format` | `0` / `1` / `2` | 合并转发 / 直发 / 图片 | OneBot 支持全部三种格式;其他平台 `0` 自动回退为 `1`。 |
```
</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 src/infrastructure/messaging/senders/base_sender.py
Comment thread src/application/services/agent_xml_push_service.py Outdated
Comment thread src/application/services/notification_dispatcher.py
Comment thread tests/unit/infrastructure/test_onebot_sender_message_format.py
Comment thread docs/project/domain-model.md Outdated

@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.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/application/services/notification_dispatcher.py (1)

1254-1254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

修复基于 or 运算符的假值(falsy)判定陷阱。

使用 or 进行默认值回退时,如果 message_format 参数值为 0(即 MESSAGE_FORMAT_MERGED_FORWARD),会被判定为假值(falsy),从而引发短路逻辑并回退到 MESSAGE_FORMAT_DEFAULT。虽然目前 MESSAGE_FORMAT_DEFAULT 恰好也是 0 使得结果碰巧正确,但如果未来默认值发生更改(例如改为 1),这里将产生严重的配置覆写 Bug。建议使用 is not None 进行显式判定。

♻️ 建议的修复
-                        message_format=message_format or MESSAGE_FORMAT_DEFAULT,
+                        message_format=message_format if message_format is not None else MESSAGE_FORMAT_DEFAULT,
🤖 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 `@src/application/services/notification_dispatcher.py` at line 1254, Update the
message_format fallback in the notification dispatch construction to distinguish
None from valid falsy values such as 0; use an explicit is-not-None check so
MESSAGE_FORMAT_MERGED_FORWARD is preserved and only an absent value falls back
to MESSAGE_FORMAT_DEFAULT.
tests/unit/infrastructure/test_user_repository_impl.py (1)

44-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

补充 message_format 的非默认往返断言。

当前 User(id="u1")existing.message_format 都是 -100;即使映射代码被删除,测试仍可能通过。请设置一个非默认值并断言保存后的实体及 ORM 值。

建议修改
 user = User(id="u1")
 user.notify = 1
+user.message_format = 2
 ...
 assert saved.notify == 1
+assert saved.message_format == 2
+assert existing.message_format == 2
🤖 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 `@tests/unit/infrastructure/test_user_repository_impl.py` at line 44, Update
the test setup for User and existing in the repository test to use a non-default
message_format value instead of -100, then assert that the saved domain entity
and ORM record both preserve that value.
🤖 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.

Inline comments:
In `@docs/project/dispatch.md`:
- Line 62: 修正文档中的配置对象描述,将 `message_format` 明确表述为与 `EffectivePushOptions`
独立的配置值;除非同步调整 `notification_dispatcher.py` 中 `_resolve_message_format()` 和
`_resolve_effective_push_options()` 的实现,否则不要将其描述为 `EffectivePushOptions` 的字段。

In `@src/application/services/notification_dispatcher.py`:
- Line 1440: 更新 dispatch_pending_retries 中的重试逻辑:提取 sub 后获取其 user 上下文,通过
self._resolve_message_format(sub, user) 动态解析并传递 message_format,同时保留实际配置的
send_mode。更新 retry_push_history_once 在 _target_from_history 恢复上下文后获取订阅配置,传递解析出的
message_format 和 send_mode,移除两处对 MESSAGE_FORMAT_DEFAULT 及默认发送模式的硬编码;涉及
src/application/services/notification_dispatcher.py#L1440-L1440 和 `#L1545-L1545`。

In `@src/infrastructure/messaging/senders/base_sender.py`:
- Around line 1484-1493: 更新 base_sender 中构建 media_previews 的逻辑:不要将
pm.original_url 直接用于图片预览,改用已下载内容生成的数据 URI 或受控中继地址;同时为视频媒体补充无 URL 的占位项,使
templates/entry_card.html 的视频分支能够命中。保留现有图片筛选逻辑,并确保预览地址不受订阅源直接控制。

In `@src/infrastructure/messaging/senders/onebot_sender.py`:
- Around line 124-136: 保留下载失败媒体的原始链接:在
src/infrastructure/messaging/senders/onebot_sender.py#L124-L136 的
_build_components 调用中移除显式的 failed_urls=[],并让
src/infrastructure/messaging/senders/base_sender.py#L1454-L1461 与 `#L1548-L1555`
的相关调用使用可收集失败链接的默认行为;确保 _build_components 能自动记录失败媒体 URL,避免静默丢失。

In `@src/interfaces/handlers/config.py`:
- Line 15: 统一会话配置中 message_format 的表示,修正 handle_sub_set_session 与
notification_dispatcher.py 中 _message_format_from_subscription_defaults
的不一致解析。让配置边界统一转换为同一格式,或使解析器同时支持整数 0/1/2,并对其他非法值明确拒绝,避免其静默回退为默认的合并转发。

---

Nitpick comments:
In `@src/application/services/notification_dispatcher.py`:
- Line 1254: Update the message_format fallback in the notification dispatch
construction to distinguish None from valid falsy values such as 0; use an
explicit is-not-None check so MESSAGE_FORMAT_MERGED_FORWARD is preserved and
only an absent value falls back to MESSAGE_FORMAT_DEFAULT.

In `@tests/unit/infrastructure/test_user_repository_impl.py`:
- Line 44: Update the test setup for User and existing in the repository test to
use a non-default message_format value instead of -100, then assert that the
saved domain entity and ORM record both preserve that value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb623294-fdd6-4bc8-9981-5449aa3d8208

📥 Commits

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

📒 Files selected for processing (46)
  • docs/project/application.md
  • docs/project/architecture.md
  • docs/project/dispatch.md
  • docs/project/domain-model.md
  • docs/project/platforms.md
  • docs/usage/ai-tools.md
  • docs/usage/commands.md
  • docs/usage/configuration.md
  • pages/dashboard/components/overlays/main-panel.js
  • pages/dashboard/components/overlays/user-panel.js
  • pages/dashboard/components/pages/settings.js
  • pages/dashboard/store/helpers.js
  • pages/dashboard/store/modules/subscriptions.js
  • pages/dashboard/store/modules/users.js
  • pages/dashboard/store/state.js
  • src/application/commands/get_user_settings_cmd.py
  • src/application/commands/set_user_settings_cmd.py
  • src/application/dto/subscription_export_record.py
  • src/application/dto/user_dto.py
  • src/application/llmtools/xml_push.py
  • src/application/ports/message_sender.py
  • src/application/services/agent_xml_push_service.py
  • src/application/services/notification_dispatcher.py
  • src/domain/entities/subscription.py
  • src/domain/entities/user.py
  • src/infrastructure/config/models/plugin_config_models.py
  • src/infrastructure/config/models/runtime_settings.py
  • src/infrastructure/config/settings_builder.py
  • src/infrastructure/messaging/senders/base_sender.py
  • src/infrastructure/messaging/senders/onebot_sender.py
  • src/infrastructure/messaging/senders/provider.py
  • src/infrastructure/messaging/senders/types.py
  • src/infrastructure/persistence/migrations/V3_add_message_format.py
  • src/infrastructure/persistence/models.py
  • src/infrastructure/persistence/subscription_repository_impl.py
  • src/infrastructure/persistence/user_repository_impl.py
  • src/interfaces/handlers/config.py
  • src/interfaces/web_api.py
  • src/shared/constants.py
  • templates/entry_card.html
  • tests/unit/application/test_llmtools.py
  • tests/unit/application/test_notification_dispatcher_message_format.py
  • tests/unit/infrastructure/test_database_manager.py
  • tests/unit/infrastructure/test_onebot_sender_message_format.py
  • tests/unit/infrastructure/test_subscription_repository_impl.py
  • tests/unit/infrastructure/test_user_repository_impl.py

Comment thread docs/project/dispatch.md
Comment thread src/application/services/notification_dispatcher.py
Comment thread src/infrastructure/messaging/senders/base_sender.py
Comment thread src/infrastructure/messaging/senders/onebot_sender.py
Comment thread src/interfaces/handlers/config.py
@FFFold
FFFold force-pushed the feat/message-format branch from a29166b to 939bf8f Compare July 18, 2026 10:25
@FFFold
FFFold force-pushed the feat/message-format branch 3 times, most recently from b8dfd11 to 0f2ea6e Compare July 19, 2026 00:07
@FFFold

FFFold commented Jul 19, 2026

Copy link
Copy Markdown
Author

@sourcery-ai summary

@FFFold

FFFold commented Jul 19, 2026

Copy link
Copy Markdown
Author

@sourcery-ai title

@sourcery-ai sourcery-ai Bot changed the title Feat/message format feat: add configurable message format for push notifications Jul 19, 2026
@FFFold
FFFold force-pushed the feat/message-format branch from 3de4c14 to 35da201 Compare July 19, 2026 00:29
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: frontend Plugin Pages or frontend changes area: tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: 提供更丰富的发送模式

2 participants