Skip to content

feat: 支持自动报告静默分析与多格式本地归档留存,新增黑白名单发信权限管控#124

Draft
jkfujr wants to merge 8 commits intoSXP-Simon:mainfrom
jkfujr:main
Draft

feat: 支持自动报告静默分析与多格式本地归档留存,新增黑白名单发信权限管控#124
jkfujr wants to merge 8 commits intoSXP-Simon:mainfrom
jkfujr:main

Conversation

@jkfujr
Copy link

@jkfujr jkfujr commented Mar 23, 2026

主要对插件中的“自动发信分发机制”和“报告生成落地流转”进行了彻底的基础设施级重构与能力增强,在不破坏任何既有工作流且完美兼容旧配置的基础上,补齐了后台静默流转和自定义落盘的场景需求。

主要特性

1. 细粒度发信控制 (静默分析支持)

  • 全局静默开关:在“自动分析”配置中新增 auto_analysis_send_report。关闭后定时任务会在后台跑完分析与存档工作,彻底切断所有消息下发管道。
  • 自定义群聊黑白名单:新增 send_report_modesend_report_list 选项。现在调度器会自动计算 (全局发信打开) && (群白名单匹配) 双重鉴权;不符合推送名单的群,会在底层被阻断外发进入安全的静默工作模式。

2. 本地报告统一落盘与留存管理 (report_storage)

  • 全量格式归档留底:原本系统仅支持利用临时文件缓存或对 PDF 提供硬盘储存。现将输出配置升级至全局通用的 report_storage。无论是 Image(转为 .png)、Text(转为 .md) 还是 PDF,都会在此进行统一的长久落地持久化记录。
  • 配置向下兼容机制:重构了 config_manager.py 的读取策略。即使老用户不升级本地配置或重置,依然会自动平滑读取旧版 pdf_output_dir 等字段组合出兼容路径。
  • “阅后即焚”开关:新增 enable_local_storage,提供给介意磁盘空间占用的用户。启用关闭后,系统即便产生了用于发信的临时文件(如 PDF、临时图片图片),在发信抛出或被静默阻尼后,自身会立即自动抹除本地残留文件,释放 IO 及存储空间。

其他增强优化

  • 面板文案修正:前置了“报告外部 HTML 模板”的相关声明,向用户指明该设定仅适用于 Image 与 PDF 格式的渲染排版,以消除文本模式(Text)用户的潜在歧义。

Summary by Sourcery

Introduce fine-grained automatic report delivery controls and a unified, configurable local report archiving mechanism while preserving backward compatibility with existing PDF settings.

New Features:

  • Add global toggle and per-group whitelist/blacklist controls for automatic analysis report sending, including support for silent background processing without message delivery.
  • Persist generated reports in a unified local storage directory across image (PNG), text (Markdown), and PDF formats with configurable filename patterns and output paths.

Enhancements:

  • Refactor configuration accessors to migrate from PDF-specific settings to generalized report storage options with automatic fallback to legacy fields.
  • Extend the reporting dispatcher to support silent-mode execution, centralized local archiving, and optional disk cleanup when local storage is disabled.
  • Allow custom browser path configuration to be managed under the unified report storage configuration group.

jkfujr added 4 commits March 24, 2026 01:14
1. 配置层:新增 `auto_analysis_send_report` 控制静默分析模式。
2. 配置层:废弃独立的 `pdf` 专属配置,并升级为统一的 `report_storage`(向下兼容读取老配置),负责所有分析格式的落盘参数。
3. 调度层:向派发方法注入 `silent_mode` 标识,触发静默分析时不推送给群聊。
4. 分发层:统筹 dispatcher 逻辑扩展,新增 `_save_to_local_binary` 及 `_save_to_local_text`,实现 Image 和 Text 生成报告的同时向本地持久化归档目录保存 `.png` 取代临时文件以及保存 `.md` 文件。
5. 分发层:处理拦截逻辑,当开启静默模式时截断向 message_sender 的下发动作。
1. 添加配置项说明:由于原有外部报告 HTML 模板仅适用图片与 PDF 的 UI 渲染,在配置界面增加适用范围说明,防止文本报告时引发歧义。
2. 添加统一开关:在 `report_storage` 下增加 `enable_local_storage` 归档留存开关。
3. 在分发器中结合开关状态,支持静音生成或阅后即扫把临时文件清理,防止垃圾堆叠。
1. 配置新增:在 `auto_analysis` 下增加 `send_report_mode` 模式选择和 `send_report_list` 控制名单。
2. 配置加载:在管理类补充 `is_group_allowed_to_send_report` 提供鉴权。
3. 调度扩展:修改 `auto_scheduler` 定时任务,计算 `silent_mode` 从单一的总开关升级为 `总开关 && 白名单鉴权通过`,阻断不需要被推送信件的群进行打扰。
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 23, 2026

Reviewer's Guide

Refactors the auto-analysis reporting pipeline to add global silent-send controls with group-level whitelist/blacklist, and introduces a unified, backward-compatible report storage layer that persistently archives image/text/PDF outputs with optional local-retention toggling.

Sequence diagram for auto-analysis dispatch with silent mode and unified storage

sequenceDiagram
    actor Admin
    participant AutoScheduler
    participant ConfigManager
    participant ReportDispatcher
    participant ReportGenerator
    participant MessageSender
    participant LocalStorage

    Admin->>ConfigManager: set_auto_analysis_send_report(enabled)
    Admin->>ConfigManager: set_send_report_mode(mode)
    Admin->>ConfigManager: set_send_report_list(group_list)
    Admin->>ConfigManager: set_enable_local_storage(enabled)
    Admin->>ConfigManager: set_report_output_dir(directory)
    Admin->>ConfigManager: set_report_filename_format(format_str)

    AutoScheduler->>ConfigManager: get_auto_analysis_send_report()
    ConfigManager-->>AutoScheduler: auto_analysis_send_report
    AutoScheduler->>ConfigManager: is_group_allowed_to_send_report(group_id)
    ConfigManager-->>AutoScheduler: allowed
    AutoScheduler->>AutoScheduler: should_send = auto_analysis_send_report && allowed

    AutoScheduler->>ReportDispatcher: dispatch(group_id, analysis_result, platform_id, silent_mode = not should_send)
    activate ReportDispatcher
    ReportDispatcher->>ConfigManager: get_output_format()
    ConfigManager-->>ReportDispatcher: output_format

    alt output_format == image
        ReportDispatcher->>ReportDispatcher: _dispatch_image(..., silent_mode)
        activate ReportDispatcher
        ReportDispatcher->>ReportGenerator: generate_image_report(group_id, analysis_result)
        ReportGenerator-->>ReportDispatcher: image_url
        ReportDispatcher->>ReportDispatcher: _save_to_local_binary(group_id, image_url, .png)
        ReportDispatcher->>ConfigManager: get_enable_local_storage()
        ConfigManager-->>ReportDispatcher: enable_local_storage
        alt silent_mode == True
            ReportDispatcher-->>ReportDispatcher: log 静默不推送
        else silent_mode == False
            ReportDispatcher->>MessageSender: send_image_smart(group_id, image_url, caption, platform_id)
            MessageSender-->>ReportDispatcher: sent
        end
        deactivate ReportDispatcher
    else output_format == pdf
        ReportDispatcher->>ReportDispatcher: _dispatch_pdf(..., silent_mode)
        activate ReportDispatcher
        ReportDispatcher->>ReportGenerator: generate_pdf_report(group_id, analysis_result, platform_id)
        ReportGenerator-->>ReportDispatcher: pdf_path
        alt silent_mode == True
            ReportDispatcher-->>ReportDispatcher: log 静默不推送
        else silent_mode == False
            ReportDispatcher->>MessageSender: send_pdf(group_id, pdf_path, caption, platform_id)
            MessageSender-->>ReportDispatcher: sent
        end
        ReportDispatcher->>ConfigManager: get_enable_local_storage()
        ConfigManager-->>ReportDispatcher: enable_local_storage
        alt enable_local_storage == False
            ReportDispatcher->>LocalStorage: delete pdf_path
        else enable_local_storage == True
            ReportDispatcher-->>LocalStorage: keep pdf_path (already stored by generator)
        end
        deactivate ReportDispatcher
    else output_format == text
        ReportDispatcher->>ReportDispatcher: _dispatch_text(..., silent_mode)
        activate ReportDispatcher
        ReportDispatcher->>ReportGenerator: generate_text_report(analysis_result)
        ReportGenerator-->>ReportDispatcher: text_report
        ReportDispatcher->>ReportDispatcher: _save_to_local_text(group_id, text_report, .md)
        ReportDispatcher->>ConfigManager: get_enable_local_storage()
        ConfigManager-->>ReportDispatcher: enable_local_storage
        alt silent_mode == True
            ReportDispatcher-->>ReportDispatcher: log 静默不推送
        else silent_mode == False
            ReportDispatcher->>MessageSender: send_text(group_id, text_body, platform_id)
            MessageSender-->>ReportDispatcher: sent
        end
        deactivate ReportDispatcher
    end

    ReportDispatcher-->>AutoScheduler: dispatch result
    deactivate ReportDispatcher
Loading

Updated class diagram for config, scheduler, and report dispatch

classDiagram
    class ConfigManager {
        +bool get_enable_auto_analysis()
        +bool get_auto_analysis_send_report()
        +str get_send_report_mode()
        +list~str~ get_send_report_list()
        +bool is_group_allowed_to_send_report(group_id_or_umo)
        +str get_output_format()
        +bool get_enable_local_storage()
        +str get_report_output_dir()
        +str get_report_filename_format()
        +str get_browser_path()
        +void set_enable_auto_analysis(enabled)
        +void set_auto_analysis_send_report(enabled)
        +void set_send_report_mode(mode)
        +void set_send_report_list(group_list)
        +void set_enable_local_storage(enabled)
        +void set_report_output_dir(directory)
        +void set_report_filename_format(format_str)
        +void set_browser_path(path)
    }

    class ReportDispatcher {
        +ConfigManager config_manager
        +ReportGenerator report_generator
        +MessageSender message_sender
        +async dispatch(group_id, analysis_result, platform_id, silent_mode)
        +async _dispatch_image(group_id, analysis_result, platform_id, silent_mode)
        +async _dispatch_pdf(group_id, analysis_result, platform_id, silent_mode)
        +async _dispatch_text(group_id, analysis_result, platform_id, silent_mode)
        -str _get_archive_path(group_id, extension)
        -void _save_to_local_binary(group_id, image_url, ext)
        -void _save_to_local_text(group_id, text, ext)
        -_get_onebot_adapter(platform_id)
    }

    class AutoScheduler {
        +ConfigManager config_manager
        +ReportDispatcher report_dispatcher
        +async _perform_auto_analysis_for_group(group_id, target_platform_id)
        +async _perform_incremental_final_report_for_group(group_id, target_platform_id)
    }

    class ReportGenerator {
        +ConfigManager config_manager
        +async generate_pdf_report(group_id, analysis_result, platform_id)
        +async generate_image_report(group_id, analysis_result)
        +str generate_text_report(analysis_result)
    }

    class MessageSender {
        +async send_image_smart(group_id, image_url, caption, platform_id)
        +async send_pdf(group_id, pdf_path, caption, platform_id)
        +async send_text(group_id, text, platform_id)
    }

    AutoScheduler --> ConfigManager : uses
    AutoScheduler --> ReportDispatcher : uses
    ReportDispatcher --> ConfigManager : reads_settings
    ReportDispatcher --> ReportGenerator : generates_reports
    ReportDispatcher --> MessageSender : sends_messages
    ReportGenerator --> ConfigManager : uses_storage_settings
Loading

File-Level Changes

Change Details Files
Introduce fine-grained auto-analysis send controls, including a global toggle and group-level whitelist/blacklist evaluation.
  • Add getters/setters for auto_analysis_send_report, send_report_mode, and send_report_list in the config manager with sane defaults and persistence.
  • Implement is_group_allowed_to_send_report to normalize various group ID formats and apply whitelist/blacklist/none logic.
  • Update auto-analysis scheduler entry points to compute should_send based on the global switch and group allowance, then pass silent_mode into the dispatcher.
src/infrastructure/config/config_manager.py
src/infrastructure/scheduler/auto_scheduler.py
Add unified report storage configuration with backward-compatible path and filename handling plus optional local-retention disabling.
  • Replace PDF-specific get/set functions with report_storage-based get_report_output_dir, get_report_filename_format, and corresponding setters, while reading legacy pdf_output_dir/pdf_filename_format as fallback.
  • Introduce enable_local_storage flag and get/set helpers to gate whether artifacts are kept on disk or deleted after use.
  • Route browser_path configuration through report_storage while preserving existing pdf group values when not overridden.
src/infrastructure/config/config_manager.py
_conf_schema.json
Extend the report dispatcher to support silent mode and centralized on-disk archiving for all report formats.
  • Thread a silent_mode flag through dispatch and the _dispatch_image/_dispatch_pdf/_dispatch_text helpers, affecting logging and whether messages are actually sent.
  • Add _get_archive_path plus _save_to_local_binary/_save_to_local_text to persist PNG/Markdown outputs into the unified report directory when local storage is enabled.
  • Ensure PDF generation is archived by the generator, and conditionally delete generated PDFs when local storage is disabled to support “burn after reading” semantics.
src/infrastructure/reporting/dispatcher.py
src/infrastructure/reporting/generators.py

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

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

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

  • In _dispatch_pdf, the except Exception as e: pass around Path(pdf_path).unlink silently swallows errors and drops the exception variable; consider at least logging a debug/warning with the path to aid diagnosing file cleanup issues.
  • The group ID matching logic in is_group_allowed_to_send_report is quite dense and string-manipulation-heavy; adding small helper functions or early-return branches (e.g., separating :/# handling) would make it easier to reason about future changes and reduce the chance of subtle matching bugs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_dispatch_pdf`, the `except Exception as e: pass` around `Path(pdf_path).unlink` silently swallows errors and drops the exception variable; consider at least logging a debug/warning with the path to aid diagnosing file cleanup issues.
- The group ID matching logic in `is_group_allowed_to_send_report` is quite dense and string-manipulation-heavy; adding small helper functions or early-return branches (e.g., separating `:`/`#` handling) would make it easier to reason about future changes and reduce the chance of subtle matching bugs.

## Individual Comments

### Comment 1
<location path="src/infrastructure/reporting/dispatcher.py" line_range="135" />
<code_context>
         return await self._dispatch_text(group_id, analysis_result, platform_id)

     async def _dispatch_pdf(
</code_context>
<issue_to_address>
**issue (bug_risk):** silent_mode is lost when _dispatch_image falls back to text, causing unexpected message sending in silent runs

In `_dispatch_image`, the fallback to `_dispatch_text` doesn’t pass `silent_mode`, so a call made with `silent_mode=True` will still send a text message on image failure:

```python
return await self._dispatch_text(group_id, analysis_result, platform_id)
```
This should instead pass the flag:

```python
return await self._dispatch_text(group_id, analysis_result, platform_id, silent_mode)
```
so silent mode is respected consistently on fallback as well.
</issue_to_address>

### Comment 2
<location path="src/infrastructure/reporting/dispatcher.py" line_range="169-175" />
<code_context>
+                )
+            
+            # 如果不启用本地存储,且发信结束(或静默阻断),应当清理 Generator 生成出来的临时文件
+            if not self.config_manager.get_enable_local_storage():
+                try:
+                    Path(pdf_path).unlink(missing_ok=True)
+                    logger.debug(f"[{trace_id}] 本地存储归档未启用,清理PDF缓存({pdf_path})")
+                except Exception as e:
+                    pass
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Exceptions when deleting temporary PDF files are silently swallowed, which can hide filesystem issues

In `_dispatch_pdf`, when local storage is disabled you delete the generated PDF but catch all exceptions and silently ignore them:

```python
if not self.config_manager.get_enable_local_storage():
    try:
        Path(pdf_path).unlink(missing_ok=True)
        logger.debug(...)
    except Exception as e:
        pass
```
This can hide real filesystem problems (e.g., permissions, bad paths) and make them difficult to diagnose. Please either narrow the caught exception type (e.g., to `OSError`) and/or log the exception (debug or warning) instead of silently passing.

```suggestion
            # 如果不启用本地存储,且发信结束(或静默阻断),应当清理 Generator 生成出来的临时文件
            if not self.config_manager.get_enable_local_storage():
                try:
                    Path(pdf_path).unlink(missing_ok=True)
                    logger.debug(f"[{trace_id}] 本地存储归档未启用,清理PDF缓存({pdf_path})")
                except OSError as e:
                    # missing_ok=True 已经忽略文件不存在的情况;其他 OSError 需要记录,避免静默失败
                    logger.warning(
                        f"[{trace_id}] 本地存储归档未启用,清理PDF缓存失败({pdf_path}): {e!r}"
                    )
```
</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.

- fix(dispatcher): 单独处理 `_dispatch_image` 降级发送丢失 `silent_mode` 参数的问题,防止静默模式被意外打破
- fix(dispatcher): 更换 `_dispatch_pdf` 清理临时文件的报错吞咽方法,捕获具体的 OSError 并留下警告日志,帮助追溯环境文件权限或路径错误
- refactor(config_manager): 封装提取 `_match_umo_rule`,简化 `is_group_allowed` 与 `is_group_allowed_to_send_report` 校验底层机制,分离 `:` 和 `#` 复杂的验证体系,使校验系统更具健壮性
@jkfujr jkfujr marked this pull request as draft March 24, 2026 03:56
@fluidcat
Copy link

有个小建议可以参考下:
或许可以把静默模式去掉,改成增加一个「分析报告接收人名单」,改动更小不用本地存储,后续查看报告也更方便~

@jkfujr
Copy link
Author

jkfujr commented Mar 26, 2026

或许可以把静默模式去掉,改成增加一个「分析报告接收人名单」,改动更小不用本地存储,后续查看报告也更方便~

也不赖,我研究下

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants