Skip to content

release: v2.1.4 - #101

Merged
FlanChanXwO merged 84 commits into
masterfrom
dev
Jul 24, 2026
Merged

release: v2.1.4#101
FlanChanXwO merged 84 commits into
masterfrom
dev

Conversation

@FlanChanXwO

@FlanChanXwO FlanChanXwO commented Jul 24, 2026

Copy link
Copy Markdown
Member

发布 v2.1.4:将 dev 分支的 GIF 转码内存优化发布到 master,解决高分辨率视频转 GIF 时 FFmpeg 可能被 OOM 杀死的问题。

Modifications / 改动点

  • 新增 media.gif_transcode_profile:默认 compatibility(长边 960px、15 FPS、128 色),另提供 balanced 与显式保留旧行为的 quality 档位;非法值会明确报配置错误。
  • GIF 在 palette 生成前完成等比缩放和降帧,缓存键按档位隔离;压缩候选不再回升到原视频尺寸或 30 FPS,显著降低单次转码峰值内存。
  • GIF、MP4、HLS 的 FFmpeg 调用统一使用受控子进程执行器:抑制非必要日志、stderr 落临时文件、超时/取消时结束子进程并清理半成品。
  • 补充 GIF 任务可观测日志、配置与平台文档,并同步 schema 提示和测试断言。
  • 版本提升至 v2.1.4,更新 CHANGELOG.md

默认档位会降低 GIF 的分辨率、帧率和色彩数以保障 3 GiB 容器的稳定性;普通视频原始推送不受影响。需要保留旧 GIF 输出行为时可显式设为 quality

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

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

  • git diff --check:passed
  • ruff check .:passed
  • ruff format --check .:895 files already formatted
  • python -m pytest tests/unit -q:735 passed, 1 skipped

Checklist / 检查清单

  • 😊 本次发布范围已确认,默认档位和画质影响已在上方说明。
  • 👀 改动已经过完整单元测试与静态检查。
  • 🤓 未新增运行时依赖。
  • 😮 未引入恶意代码。

Summary by Sourcery

通过引入可配置的 GIF 转码配置档(profile)并统一 FFmpeg 进程处理方式,在媒体转换过程中降低内存占用并提升可观测性。

New Features:

  • 新增 media.gif_transcode_profile 配置项,提供 compatibilitybalancedquality 预设,用于控制 GIF 的分辨率、帧率和色深。
  • 通过运行时媒体设置以及消息发送行为暴露 GIF 转码配置档,使 GIF 转换在各平台上都能遵循启动时的配置。

Bug Fixes:

  • 在将高分辨率静音视频转码为 GIF 时,通过降低默认 GIF 输出的资源占用,并取消在受限容器中生成 30 FPS、全分辨率调色板,缓解 OOM(内存耗尽)被杀问题。

Enhancements:

  • 优化 GIF 和压缩 GIF 转码流水线,在生成调色板之前进行缩放和降采样,根据配置档隔离缓存键,防止候选结果超出所选配置档的限制。
  • 统一 GIF、MP4 和 HLS 工作流中的 FFmpeg 子进程执行逻辑,加入受控日志输出,将 stderr 捕获到临时文件,并提供健壮的超时/取消处理机制以清理部分生成的输出。
  • 提升 GIF 转码的可观测性,使用 FFprobe 记录流元数据,并在不将媒体内容加载进内存的情况下产出结构化的成功/失败消息。
  • 扩展 AstrBot 引导流程和配置模式(schema),对 gif_transcode_profile 的取值进行严格校验,并在平台与领域模型文档中记录各配置档的语义。

Documentation:

  • 更新 CHANGELOG 以及平台/领域文档,描述 GIF 转码配置档、其资源特性以及新的 FFmpeg 进程处理方式。
  • 扩展配置模式文档,加入 gif_transcode_profile 的可选值、默认值以及关于内存影响的使用指引。

Tests:

  • 新增单元测试,用于覆盖 FFmpeg runner 行为、GIF 配置档过滤器构造、按配置档进行缓存隔离、压缩 GIF 候选上限、视频流探测以及 gif_transcode_profile 的配置映射/校验。
  • 调整现有的媒体下载器、发送器、引导流程和配置往返测试,以断言 GIF 转码配置档的传播以及新的缓存语义。

Chores:

  • 引入 GIF_TRANSCODE_PROFILE 常量,以便在基础设施和配置层之间共享使用,并将插件元数据版本提升至 v2.1.4
Original summary in English

Summary by Sourcery

Introduce configurable GIF transcoding profiles and unify FFmpeg process handling to reduce memory usage and improve observability during media conversions.

New Features:

  • Add media.gif_transcode_profile configuration with compatibility, balanced, and quality presets that control GIF resolution, frame rate, and color depth.
  • Expose GIF transcode profile through runtime media settings and message sender behavior so GIF conversion respects startup configuration across platforms.

Bug Fixes:

  • Mitigate OOM kills when transcoding high-resolution silent videos to GIF by lowering default GIF output resource usage and eliminating 30 FPS, full-resolution palette generation in constrained containers.

Enhancements:

  • Refine GIF and compressed GIF transcoding pipelines to scale and downsample before palette generation, isolating cache keys by profile and preventing candidates from exceeding the selected profile limits.
  • Unify FFmpeg subprocess execution for GIF, MP4, and HLS workflows with controlled logging, stderr captured to temporary files, and robust timeout/cancellation handling that cleans up partial outputs.
  • Enhance GIF transcode observability with FFprobe-based stream metadata logging and structured success/failure messages without loading media content into memory.
  • Expand AstrBot bootstrap and configuration schema to validate gif_transcode_profile values strictly and document profile semantics in platform and domain-model docs.

Documentation:

  • Update CHANGELOG and platform/domain documentation to describe GIF transcode profiles, their resource characteristics, and the new FFmpeg process handling.
  • Extend configuration schema documentation to include gif_transcode_profile options, defaults, and guidance on memory impact.

Tests:

  • Add unit tests for FFmpeg runner behavior, GIF profile filter construction, cache isolation by profile, compressed GIF candidate limits, video stream probing, and configuration mapping/validation of gif_transcode_profile.
  • Adjust existing media downloader, sender, bootstrap, and config round-trip tests to assert propagation of GIF transcode profile and new caching semantics.

Chores:

  • Introduce GIF_TRANSCODE_PROFILE constants for shared use across infrastructure and configuration layers and bump plugin metadata version to v2.1.4.

Summary by CodeRabbit

  • 新功能

    • 新增 GIF 转码质量档位:compatibilitybalancedquality
    • 默认使用 compatibility,并支持按档位控制尺寸、帧率与颜色数。
    • GIF 转码缓存按质量档位隔离,避免复用不匹配的旧文件。
  • 错误修复

    • 无效的 GIF 转码档位会明确提示配置错误。
    • 改善高分辨率无声视频转 GIF 的内存占用,降低容器内存溢出风险。
    • FFmpeg 转码在取消、超时或失败时会终止进程并清理临时文件。

FlanChanXwO and others added 30 commits May 27, 2026 01:22
* fix(media): harden predownload and config handling

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* docs: refresh help image and project docs

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docs: update pull request template

* ci: remove unused knowledgebase script

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix(media): improve media detection and platform sending

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(media): address platform review feedback

* fix(media): address review feedback

* refactor(media): add table rendering pipeline

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* feat(pages): add push retry and data self-healing

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(pages): address web api review feedback

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* refactor(pages): split dashboard modules

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix(pages): improve multi-select filters

* fix(pages): address filter review comments
* fix(kb): reconcile route knowledge manifest

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(kb): address manifest review feedback

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
… upload (#74)

* fix(db): drop legacy link_preview column and add napcat stream upload

修复 Issue #58:旧库残留的 link_preview NOT NULL 列导致 /sub 创建订阅失败。
新增 V2 迁移在启动时安全删除该列(幂等)。

同时把 OneBot 的 prefer_local_video 配置替换为 napcat_stream_mode
(disabled/fallback/always):媒体已统一预下载,发送统一使用本地文件;
新增 NapCat 流式上传模块,支持发送前预上传或失败后流式重试,避免大文件 OOM。
主动推送场景通过 platform_manager 解析 bot 客户端,旧 prefer_local_video
配置在启动自愈时迁移为对应 napcat_stream_mode。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(config): replace stale FFmpegSettings export with MediaRuntimeSettings

FFmpegSettings was renamed to MediaRuntimeSettings in the napcat_stream
refactor but the __init__.py re-exports in config/ and models/ still
referenced the old name, causing ImportError at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* feat: bundle Noto Sans SC font and remove hardcoded system font paths

Ship NotoSansSC-subset.otf (~14 MB) in assets/fonts/ so the table
image renderer works out-of-the-box with CJK text on any OS. Remove
the six hardcoded absolute system font paths from _iter_font_candidates()
to eliminate platform-dependent font probing.

- Add assets/fonts/NotoSansSC-subset.otf (Noto Sans SC Subset Variable OTF)
- Add assets/fonts/OFL.txt (SIL Open Font License 1.1)
- Remove hardcoded macOS/Linux system font paths from table_image_renderer.py
- Update font type annotations to support FreeTypeFont | ImageFont union
- Add tests for bundled font discovery and system path verification
- Update docs/project/formatting.md to reflect bundled font

Font search priority: env vars > assets/fonts/ > Pillow default fallback

* feat: runtime font download instead of bundled CJK font

Replace the 14 MB bundled NotoSansSC-subset.otf with runtime download
from jsDelivr CDN. Font is downloaded at plugin startup, verified with
SHA256 + size checks, and atomically written to persistent data dir.

When no CJK font is available (download failed or offline), tables
fall back to plain text (A | B | C) instead of using Pillow default
font, preventing broken CJK rendering.

- Add font_manager.py with async download, double-check lock, atomic write
- Remove ImageFont.load_default() fallback from table_image_renderer
- Add _warned_no_font + early return None when no font
- Add get_runtime_font_dir() to font candidate priority list
- Wire await ensure_table_font() in bootstrap startup flow
- Add 8 font_manager tests + update table_renderer tests
- Fix conftest.py namespace package conflict across feature branches
- Update formatting.md docs

Closes #71

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
启用表格转图时,字体下载在 bootstrap 中由同步 await 改为后台
asyncio.create_task 预取,首次表格渲染前再按需等待就绪(未配置下载的
环境直接回退纯文本,不发起网络请求)。同时补全缺失的表格转图总开关
EntryTextFormatter.configure_table_to_image,修复其原本不存在却被
bootstrap 调用导致的启动 AttributeError。
命令处理器参数均带默认值(args: GreedyStr = ""),框架解析时只读默认值
不读注解,greedy 合并分支恒不触发——该注解功能上是惰性的,等价于 str。
故将注解统一改为 str,文件上传监听改用框架原生
filter.event_message_type(filter.EventMessageType.ALL),删除整个
astrbot_compat 兼容层并同步重写相关回归测试。
将 telegraph_proxy 从顶层 media 配置移入 telegram_strategy 模板,与
enable_telegraph/telegraph_token 同源。发送时由 TelegramSender 从策略
解析并传入 TelegraphClient(留空即直连,不再继承通用 HTTP 代理),修复
此前 _get_telegraph_proxy 定义却从未被调用的死配置问题,并移除相关死链路。
启动配置自愈新增迁移:旧 media.telegraph_proxy 会自动写入首个 telegram
策略模板项。
新增/完善 docs/usage 下的命令、配置、AI 工具、Plugin Pages 与兼容性文档;
commands.md 补 sub_stop 的 rss_stop 别名并修正 rsshelp 别名,configuration.md
随 telegraph_proxy 归位 Telegram 策略同步调整。CHANGELOG 2.0.3 由 22 条精简为
面向用户的高层要点。一并清理已内置字体后多余的 assets/fonts/OFL.txt,并在
metadata.yaml 声明最低 AstrBot 版本 >=4.24.0。
messaging 包通过 __getattr__ 懒加载所有导出,导致 __all__ 中 31 个名字在
类型检查器看来「未定义」(reportUnsupportedDunderAll)。新增 if TYPE_CHECKING
块真实导入这些符号:类型检查时静态可见、告警清零,运行时该块不执行,原有
懒加载行为完全保留。
订阅查重原先用 (user_id, feed_id) 忽略 target_session,导致同一用户在不同
群聊订阅同一 Feed 被误判为「您已经订阅了此源」。将仓储方法
get_by_user_and_feed 收紧为 get_by_user_feed_session(user_id, feed_id,
target_session),/sub 与导入两处查重均带上会话;同时避免多会话行存在时
scalar_one_or_none 误抛。新增回归测试覆盖「不同会话各自成功、相同会话仍拒绝」。
将此前压缩成扁平要点的 2.0.3 条目还原为 Keep a Changelog 分节格式,并把
「媒体缓存 GC / 完整性阈值 / 平台降级策略不再作为用户配置项暴露」从 Changed
提取为独立的 Removed 段。
- maintenance.md 新增「仓库体积」章节:插件仓库总大小严格不得超过 16 MB,
  大体积资源应按需下载而非纳入仓库。
- engineering-principles.md 代码组织新增:每个函数/方法必须有确切的中文
  docstring,并与实现保持一致。
- font_manager: ensure_table_font_runtime 引入模块级缓存 _cached_verified_font
  + _verify_lock,首次校验后命中缓存,避免按 <table> 反复跑 15MB 全量 SHA256
  阻塞事件循环;ensure_table_font 在校验/下载成功后同步更新缓存。
- subscription_repository_impl: get_by_user_feed_session 改用 limit(1) +
  scalars().first(),(user_id, feed_id, target_session) 无唯一约束时若存在
  重复行不再抛 MultipleResultsFound。
- 补 font_manager 缓存回归测试与缓存重置 fixture。
* perf(rendering): 字体改为后台预取+按需加载,不再阻塞插件启动

启用表格转图时,字体下载在 bootstrap 中由同步 await 改为后台
asyncio.create_task 预取,首次表格渲染前再按需等待就绪(未配置下载的
环境直接回退纯文本,不发起网络请求)。同时补全缺失的表格转图总开关
EntryTextFormatter.configure_table_to_image,修复其原本不存在却被
bootstrap 调用导致的启动 AttributeError。

* refactor(interfaces): 移除无效的 GreedyStr 兼容层 astrbot_compat

命令处理器参数均带默认值(args: GreedyStr = ""),框架解析时只读默认值
不读注解,greedy 合并分支恒不触发——该注解功能上是惰性的,等价于 str。
故将注解统一改为 str,文件上传监听改用框架原生
filter.event_message_type(filter.EventMessageType.ALL),删除整个
astrbot_compat 兼容层并同步重写相关回归测试。

* refactor(config): telegraph_proxy 归位 Telegram 策略并接通

将 telegraph_proxy 从顶层 media 配置移入 telegram_strategy 模板,与
enable_telegraph/telegraph_token 同源。发送时由 TelegramSender 从策略
解析并传入 TelegraphClient(留空即直连,不再继承通用 HTTP 代理),修复
此前 _get_telegraph_proxy 定义却从未被调用的死配置问题,并移除相关死链路。
启动配置自愈新增迁移:旧 media.telegraph_proxy 会自动写入首个 telegram
策略模板项。

* docs: 整理 usage 文档与 README,精简 CHANGELOG 2.0.3

新增/完善 docs/usage 下的命令、配置、AI 工具、Plugin Pages 与兼容性文档;
commands.md 补 sub_stop 的 rss_stop 别名并修正 rsshelp 别名,configuration.md
随 telegraph_proxy 归位 Telegram 策略同步调整。CHANGELOG 2.0.3 由 22 条精简为
面向用户的高层要点。一并清理已内置字体后多余的 assets/fonts/OFL.txt,并在
metadata.yaml 声明最低 AstrBot 版本 >=4.24.0。

* fix(messaging): 用 TYPE_CHECKING 块消除 __all__ 懒加载告警

messaging 包通过 __getattr__ 懒加载所有导出,导致 __all__ 中 31 个名字在
类型检查器看来「未定义」(reportUnsupportedDunderAll)。新增 if TYPE_CHECKING
块真实导入这些符号:类型检查时静态可见、告警清零,运行时该块不执行,原有
懒加载行为完全保留。

* fix(subscription): 查重按会话隔离,允许多群订阅同一源 (#70)

订阅查重原先用 (user_id, feed_id) 忽略 target_session,导致同一用户在不同
群聊订阅同一 Feed 被误判为「您已经订阅了此源」。将仓储方法
get_by_user_and_feed 收紧为 get_by_user_feed_session(user_id, feed_id,
target_session),/sub 与导入两处查重均带上会话;同时避免多会话行存在时
scalar_one_or_none 误抛。新增回归测试覆盖「不同会话各自成功、相同会话仍拒绝」。

* docs(changelog): 2.0.3 恢复 Added/Changed/Removed/Fixed 分节结构

将此前压缩成扁平要点的 2.0.3 条目还原为 Keep a Changelog 分节格式,并把
「媒体缓存 GC / 完整性阈值 / 平台降级策略不再作为用户配置项暴露」从 Changed
提取为独立的 Removed 段。

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docs(dev): 新增仓库体积上限与函数中文 docstring 规范

- maintenance.md 新增「仓库体积」章节:插件仓库总大小严格不得超过 16 MB,
  大体积资源应按需下载而非纳入仓库。
- engineering-principles.md 代码组织新增:每个函数/方法必须有确切的中文
  docstring,并与实现保持一致。

* fix: 缓存字体校验结果并防御订阅查重重复行

- font_manager: ensure_table_font_runtime 引入模块级缓存 _cached_verified_font
  + _verify_lock,首次校验后命中缓存,避免按 <table> 反复跑 15MB 全量 SHA256
  阻塞事件循环;ensure_table_font 在校验/下载成功后同步更新缓存。
- subscription_repository_impl: get_by_user_feed_session 改用 limit(1) +
  scalars().first(),(user_id, feed_id, target_session) 无唯一约束时若存在
  重复行不再抛 MultipleResultsFound。
- 补 font_manager 缓存回归测试与缓存重置 fixture。

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix(media): 实现媒体反代并修复下载崩溃,新增 GIF 大小限制压缩

反代(修复严重回归):
- #74 给 base_sender 加了反代管线,把 image_relay_base_url/media_relay_base_url
  传给 MediaDownloader.get_or_download(_prepared),但下载器从未接收这两个参数,
  导致每次媒体下载 TypeError 被吞、download_failed=True,所有媒体静默发不出去。
- 在 get_or_download / get_or_download_prepared 加这两个参数并真正接通:新增
  _build_relay_url(支持 https://wsrv.nl/https://wsrv.nl/?url= 两种形式)和
  _select_relay_base(图片优先图片反代、非图片走通用反代),在抓取处"先反代再
  回源"。缓存 key 与 original_url 始终保持原始 URL,m3u8 不走反代,两个反代为空
  时行为与之前一致。

ffmpeg:
- 新增 FFmpegTool.transcode_to_gif_under_limit,逐步降分辨率/帧率把视频转为不超过
  指定字节数的 GIF;media_downloader 在生成 GIF 变体时调用。

测试:
- 新增反代回归测试与 GIF 压缩测试;修正 test_base_sender_ffmpeg 中因 #74 起就过时
  的预期 call dict(补 relay 键)。

* refactor(media): 按评审意见调整 relay 类型标注与 GIF 体积校验

- relay 辅助函数与下载器参数(_build_relay_url / _select_relay_base /
  image_relay_base_url / media_relay_base_url)类型标注改为 str | None,
  如实反映内部 `or ""` 容忍 None 的用法。
- transcode_to_gif_under_limit 每次尝试只读取一次 output_path.stat().st_size
  (缓存快速路径与转码后校验各缓存一次),减少重复 syscall。
- 帮助图副标题改为 "Everything is RSSable",并重新生成 light/dark 帮助图。
- README 顶部加 Last Commit 徽章,新增「命令一览」段贴帮助图。
- docs/usage/commands.md 补全 RSSHub Routes 知识库命令的中文别名。
- CHANGELOG 2.0.3 补记媒体反代(image/media relay,先反代再回源、缓存键保持原始
  URL)与无声视频转 GIF 体积上限压缩,刷新发版内容。
FlanChanXwO and others added 15 commits July 25, 2026 01:05
# Conflicts:
#	pages/dashboard/store/modules/charts.js
#	tests/frontend/overview-charts.test.mjs
# Conflicts:
#	README.md
#	docs/project/platforms.md
#	src/shared/constants.py
#	tests/unit/infrastructure/test_media_send_planner.py
# Conflicts:
#	README.md
#	docs/project/platforms.md
#	src/shared/constants.py
#	tests/unit/infrastructure/test_media_send_planner.py
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.13 → v0.15.20](astral-sh/ruff-pre-commit@v0.15.13...v0.15.20)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 17:26

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 24, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

引入可配置的 GIF 转码配置(profile)并使用更安全的默认值;将所有 FFmpeg 调用重构为统一的、具备内存感知能力的子进程执行器;在配置/消息发送器/下载器中贯穿新的配置,并为 v2.1.4 版本扩展测试、文档和变更日志。

带配置和统一 FFmpeg 运行器的 GIF 转码序列图

sequenceDiagram
    actor User
    participant DefaultMessageSender
    participant MediaDownloader
    participant FFmpegTool
    participant FFmpegProcess as ffmpeg_subprocess

    User->>DefaultMessageSender: send_message(...)
    DefaultMessageSender->>DefaultMessageSender: _resolve_gif_transcode_decision(...)
    alt should_transcode_gif
        DefaultMessageSender->>DefaultMessageSender: _get_gif_transcode_profile()
        DefaultMessageSender->>MediaDownloader: get_or_download_prepared(..., gif_transcode_profile)
        MediaDownloader->>MediaDownloader: _append_gif_variants(..., profile=gif_transcode_profile)
        MediaDownloader->>FFmpegTool: transcode_to_gif_under_limit(..., profile)
        FFmpegTool->>FFmpegTool: _get_gif_transcode_profile(profile)
        FFmpegTool->>FFmpegTool: _build_gif_filter(...)
        FFmpegTool->>FFmpegTool: _run_ffmpeg(args, timeout_seconds)
        FFmpegTool->>FFmpegProcess: asyncio.create_subprocess_exec(...)
        FFmpegProcess-->>FFmpegTool: returncode, stderr_tail
        alt result is None (timeout)
            FFmpegTool-->>MediaDownloader: None
            MediaDownloader-->>DefaultMessageSender: no_gif_variant
        else result.returncode != 0
            FFmpegTool-->>MediaDownloader: None
            MediaDownloader-->>DefaultMessageSender: no_gif_variant
        else success
            FFmpegTool-->>MediaDownloader: gif_path
            MediaDownloader-->>DefaultMessageSender: PreparedMedia with gif_variant
        end
    else no_gif_transcode
        DefaultMessageSender-->>User: send without gif
    end
Loading

File-Level Changes

Change Details Files
添加 GIF 转码配置,并在 FFmpeg GIF/压缩 GIF 相关流程中一致地应用,包括缓存和日志。
  • 引入 _GifTranscodeProfile 数据类和配置注册表(compatibility/balanced/quality),支持用于缓存键的配置信息。
  • 通过辅助函数构建 GIF 滤镜链,在生成调色板之前先缩放并降低 FPS;其中 quality 配置保持旧有行为,其他配置在处理小视频时避免放大分辨率。
  • 更新 GIF 和压缩 GIF 转码函数,使其接收 profile 参数,使用与配置相关的缓存键,并在进行视频元数据探测(使用 ffprobe 时)时记录包含配置、尺寸、FPS 和耗时的开始/成功日志。
src/infrastructure/utils/ffmpeg_helper.py
src/shared/constants.py
docs/project/platforms.md
docs/project/domain-model.md
统一 GIF/MP4/HLS 的 FFmpeg/FFprobe 子进程处理方式,通过受控的日志、超时和取消语义来降低内存使用并避免孤儿进程。
  • 添加 _FFmpegRunResult 以及相关辅助方法,以将 FFmpeg 的 stderr 重定向到临时文件,只读取受限长度的尾部内容用于日志。
  • 确保在超时和 asyncio 任务取消时能够杀死并等待 FFmpeg 进程结束,并清理 GIF、压缩 GIF、MP4 转码以及 m3u8 下载路径中产生的部分输出文件。
  • 在所有转码和下载辅助方法中标准化 FFmpeg 参数,统一使用 -hide_banner -nostats -loglevel error,并弃用 stdout 输出。
src/infrastructure/utils/ffmpeg_helper.py
gif_transcode_profile 设置从配置一路传递到运行时设置、启动流程、消息发送器和下载器,使所有 GIF 处理都使用经过校验的配置以及配置特定的缓存。
  • 扩展 MediaConfigMediaSettingssettings_builder,加入 gif_transcode_profile 字段,并基于 GIF_TRANSCODE_PROFILE_OPTIONS 进行严格校验,提供合理的默认值。
  • 在启动过程中将 gif_transcode_profileApplicationSettings 传入 DefaultMessageSender.configure_behavior,在发送器上存储该配置,并向下传播到 MediaDownloader.get_or_download/_append_gif_variants 以及 FFmpegTool 的转码调用中。
  • 在 GIF 变体的媒体缓存键中包含该配置,以保证不同配置不会共享缓存输出。
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/media/media_downloader.py
bootstrap.py
tests/unit/infrastructure/test_media_downloader.py
tests/unit/infrastructure/test_base_sender_ffmpeg.py
tests/unit/application/test_settings.py
tests/unit/infrastructure/test_config_round_trip.py
tests/unit/test_bootstrap_runtime.py
改进测试脚手架,并新增针对 FFmpeg 运行器、GIF 配置、缓存行为和配置/Schema 的测试用例。
  • 优化 tests/conftest.py 中对 AstrBot 的模拟,避免在测试收集阶段依赖完整运行时,并为不关心 ffprobe 的测试添加禁用 GIF 可观测性探针的 fixture。
  • 新增 _probe_video_stream_info 的单元测试,以及针对 FFmpeg 运行器行为(stderr 尾部读取、超时/取消)、GIF 配置滤镜构造、配置隔离的 GIF 缓存和受配置约束的压缩 GIF 备选项的测试。
  • 更新现有测试,以断言 gif_transcode_profile 的传递、媒体缓存键的变更,以及针对新设置的 Schema 默认值/可选项。
tests/conftest.py
tests/unit/infrastructure/test_ffmpeg_helper.py
tests/unit/infrastructure/test_media_downloader.py
tests/unit/infrastructure/test_base_sender_ffmpeg.py
tests/unit/application/test_settings.py
tests/unit/infrastructure/test_config_round_trip.py
tests/unit/test_conf_schema.py
tests/unit/test_bootstrap_runtime.py
为 v2.1.4 版本记录并文档化新的行为。
  • CHANGELOG.md 中新增 v2.1.4 章节,描述新的 gif_transcode_profile、FFmpeg 进程变更,以及针对高分辨率 GIF 转码负载的 OOM 修复。
  • 扩展平台和领域模型文档,说明 gif_transcode_profile 的语义、配置表(分辨率/FPS/颜色)、缓存隔离,以及 FFmpeg 可观测性方面的保证。
  • metadata.yaml 中将插件版本从 v2.1.3 升级到 v2.1.4,并让配置 Schema 与新的 media.gif_transcode_profile 字段保持同步。
CHANGELOG.md
docs/project/platforms.md
docs/project/domain-model.md
metadata.yaml
_conf_schema.json

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

Introduce configurable GIF transcode profiles with safer defaults, refactor FFmpeg invocations into a unified, memory‑aware subprocess runner, propagate the new profile through config/sender/downloader, and extend tests/docs/changelog for the v2.1.4 release.

Sequence diagram for GIF transcode with profiles and unified FFmpeg runner

sequenceDiagram
    actor User
    participant DefaultMessageSender
    participant MediaDownloader
    participant FFmpegTool
    participant FFmpegProcess as ffmpeg_subprocess

    User->>DefaultMessageSender: send_message(...)
    DefaultMessageSender->>DefaultMessageSender: _resolve_gif_transcode_decision(...)
    alt should_transcode_gif
        DefaultMessageSender->>DefaultMessageSender: _get_gif_transcode_profile()
        DefaultMessageSender->>MediaDownloader: get_or_download_prepared(..., gif_transcode_profile)
        MediaDownloader->>MediaDownloader: _append_gif_variants(..., profile=gif_transcode_profile)
        MediaDownloader->>FFmpegTool: transcode_to_gif_under_limit(..., profile)
        FFmpegTool->>FFmpegTool: _get_gif_transcode_profile(profile)
        FFmpegTool->>FFmpegTool: _build_gif_filter(...)
        FFmpegTool->>FFmpegTool: _run_ffmpeg(args, timeout_seconds)
        FFmpegTool->>FFmpegProcess: asyncio.create_subprocess_exec(...)
        FFmpegProcess-->>FFmpegTool: returncode, stderr_tail
        alt result is None (timeout)
            FFmpegTool-->>MediaDownloader: None
            MediaDownloader-->>DefaultMessageSender: no_gif_variant
        else result.returncode != 0
            FFmpegTool-->>MediaDownloader: None
            MediaDownloader-->>DefaultMessageSender: no_gif_variant
        else success
            FFmpegTool-->>MediaDownloader: gif_path
            MediaDownloader-->>DefaultMessageSender: PreparedMedia with gif_variant
        end
    else no_gif_transcode
        DefaultMessageSender-->>User: send without gif
    end
Loading

File-Level Changes

Change Details Files
Add GIF transcode profiles and apply them consistently in FFmpeg GIF/compressed-GIF flows, including caching and logging.
  • Introduce _GifTranscodeProfile dataclass and a profile registry (compatibility/balanced/quality) with cache-key support.
  • Build GIF filter chains via helper functions that scale and drop FPS before palette generation, with quality profile preserving legacy behavior and others avoiding upscaling small videos.
  • Update GIF and compressed-GIF transcode functions to take a profile parameter, use profile-aware cache keys, and log start/success with profile, size, FPS, and elapsed time while probing video metadata via ffprobe where available.
src/infrastructure/utils/ffmpeg_helper.py
src/shared/constants.py
docs/project/platforms.md
docs/project/domain-model.md
Unify FFmpeg/FFprobe subprocess handling for GIF/MP4/HLS with controlled logging, timeout, and cancellation semantics to reduce memory usage and orphan processes.
  • Add _FFmpegRunResult and helpers to run FFmpeg with stderr redirected to a temporary file, reading only a bounded tail for logging.
  • Ensure timeouts and asyncio task cancellations kill and await FFmpeg processes, cleaning up partial output files for GIF, compressed GIF, MP4 transcode, and m3u8 download paths.
  • Standardize FFmpeg arguments to use -hide_banner -nostats -loglevel error and drop stdout across transcode and download helpers.
src/infrastructure/utils/ffmpeg_helper.py
Thread the gif_transcode_profile setting from configuration through runtime settings, bootstrap, sender, and downloader so all GIF work uses a validated profile and profile-specific cache.
  • Extend MediaConfig, MediaSettings, and settings_builder to include gif_transcode_profile with strict validation against GIF_TRANSCODE_PROFILE_OPTIONS and sensible defaults.
  • Pass gif_transcode_profile from ApplicationSettings into DefaultMessageSender.configure_behavior during bootstrap, store it on the sender, and propagate it into MediaDownloader.get_or_download/_append_gif_variants and FFmpegTool transcode calls.
  • Include the profile in media cache keys for GIF variants so different profiles do not share cached outputs.
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/media/media_downloader.py
bootstrap.py
tests/unit/infrastructure/test_media_downloader.py
tests/unit/infrastructure/test_base_sender_ffmpeg.py
tests/unit/application/test_settings.py
tests/unit/infrastructure/test_config_round_trip.py
tests/unit/test_bootstrap_runtime.py
Improve test scaffolding and add targeted tests for the new FFmpeg runner, GIF profiles, cache behavior, and configuration/schema.
  • Refine AstrBot mocking in tests/conftest.py to avoid depending on the full runtime during collection, and add fixtures that disable GIF observability probes in tests that don’t care about ffprobe.
  • Add unit tests for _probe_video_stream_info, the FFmpeg runner behavior (stderr tail reading, timeout/cancellation), GIF profile filter construction, profile-isolated GIF caches, and profile-bounded compressed GIF candidates.
  • Update existing tests to assert gif_transcode_profile propagation, media cache key changes, and schema defaults/options for the new setting.
tests/conftest.py
tests/unit/infrastructure/test_ffmpeg_helper.py
tests/unit/infrastructure/test_media_downloader.py
tests/unit/infrastructure/test_base_sender_ffmpeg.py
tests/unit/application/test_settings.py
tests/unit/infrastructure/test_config_round_trip.py
tests/unit/test_conf_schema.py
tests/unit/test_bootstrap_runtime.py
Document and version the new behavior for the v2.1.4 release.
  • Add v2.1.4 section to CHANGELOG.md describing the new gif_transcode_profile, FFmpeg process changes, and the OOM fix for high-resolution GIF transcode workloads.
  • Extend platform and domain-model docs to explain gif_transcode_profile semantics, profile table (resolution/FPS/colors), cache isolation, and FFmpeg observability guarantees.
  • Bump plugin version from v2.1.3 to v2.1.4 in metadata.yaml and keep the configuration schema in sync with the new media.gif_transcode_profile field.
CHANGELOG.md
docs/project/platforms.md
docs/project/domain-model.md
metadata.yaml
_conf_schema.json

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 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

新增 media.gif_transcode_profile 三档配置,默认使用 compatibility,并将档位贯穿配置、消息发送、媒体下载、GIF 缓存及 FFmpeg 转码流程。FFmpeg 同时统一了错误、超时、取消和临时文件清理处理。

Changes

GIF 转码档位配置

Layer / File(s) Summary
配置契约与校验
_conf_schema.json, src/shared/constants.py, src/infrastructure/config/..., tests/unit/application/test_settings.py, tests/unit/infrastructure/test_config_round_trip.py, tests/unit/test_conf_schema.py
新增 compatibilitybalancedquality 三档配置,默认值为 compatibility,并对未知值进行校验。
发送器与媒体下载传播
bootstrap.py, src/infrastructure/messaging/senders/base_sender.py, src/infrastructure/media/media_downloader.py, tests/unit/infrastructure/test_base_sender_ffmpeg.py, tests/unit/infrastructure/test_media_downloader.py, tests/unit/test_bootstrap_runtime.py
启动配置通过消息发送器传递至媒体下载器,GIF 及压缩 GIF 缓存键按档位隔离。
FFmpeg 档位与子进程执行
src/infrastructure/utils/ffmpeg_helper.py, tests/unit/infrastructure/test_ffmpeg_helper.py
新增分档 GIF 滤镜和压缩候选,统一 GIF、MP4、HLS 的 FFmpeg 执行、错误尾部读取、超时取消和半成品清理。
文档与版本同步
docs/project/domain-model.md, docs/project/platforms.md, CHANGELOG.md, metadata.yaml, tests/conftest.py
更新平台语义、变更记录和版本号,并调整测试导入阶段的 AstrBot mocks。

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ApplicationSettings
  participant DefaultMessageSender
  participant MediaDownloader
  participant FFmpegTool
  participant FFmpegProcess
  ApplicationSettings->>DefaultMessageSender: 配置 gif_transcode_profile
  DefaultMessageSender->>MediaDownloader: 透传 GIF 档位
  MediaDownloader->>FFmpegTool: 请求 GIF 转码与压缩
  FFmpegTool->>FFmpegProcess: 执行统一 FFmpeg 子进程
  FFmpegProcess-->>FFmpegTool: 返回状态码和 stderr 尾部
  FFmpegTool-->>MediaDownloader: 返回 GIF 输出或失败结果
Loading

Possibly related PRs

Suggested reviewers: copilot

Poem

小兔挥耳调三档,
GIF 轻快换新装。
FFmpeg 稳稳跑,
超时取消不慌张。
缓存分层留清香,
新版月光正闪亮。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.77% 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.4,与变更内容一致。
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.

嗨,我在这里给了一些总体反馈:

  • GIF 转码档位的校验逻辑现在在 MediaConfig.field_validatorsettings_builder._get_gif_transcode_profile 中各实现了一份,可以考虑收敛到单一工具函数或常量来避免未来档位调整时出现遗漏或不一致。
  • _run_ffmpeg 目前用 None 同时表示超时和创建子进程失败(如 process is None),建议在返回值或日志中区分这两种情况,以便更精确地排查 FFmpeg 启动问题与转码耗时问题。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- GIF 转码档位的校验逻辑现在在 `MediaConfig.field_validator``settings_builder._get_gif_transcode_profile` 中各实现了一份,可以考虑收敛到单一工具函数或常量来避免未来档位调整时出现遗漏或不一致。
- `_run_ffmpeg` 目前用 `None` 同时表示超时和创建子进程失败(如 `process is None`),建议在返回值或日志中区分这两种情况,以便更精确地排查 FFmpeg 启动问题与转码耗时问题。

Sourcery 对开源项目免费——如果你觉得我们的代码审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的审查建议。
Original comment in English

Hey - I've left some high level feedback:

  • GIF 转码档位的校验逻辑现在在 MediaConfig.field_validatorsettings_builder._get_gif_transcode_profile 中各实现了一份,可以考虑收敛到单一工具函数或常量来避免未来档位调整时出现遗漏或不一致。
  • _run_ffmpeg 目前用 None 同时表示超时和创建子进程失败(如 process is None),建议在返回值或日志中区分这两种情况,以便更精确地排查 FFmpeg 启动问题与转码耗时问题。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- GIF 转码档位的校验逻辑现在在 `MediaConfig.field_validator``settings_builder._get_gif_transcode_profile` 中各实现了一份,可以考虑收敛到单一工具函数或常量来避免未来档位调整时出现遗漏或不一致。
- `_run_ffmpeg` 目前用 `None` 同时表示超时和创建子进程失败(如 `process is None`),建议在返回值或日志中区分这两种情况,以便更精确地排查 FFmpeg 启动问题与转码耗时问题。

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.

@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: 1

🤖 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 `@src/infrastructure/utils/ffmpeg_helper.py`:
- Around line 236-258: 为 `_probe_video_stream_info()` 中的 `process.communicate()`
增加约 10 秒超时,使用 `asyncio.wait_for` 包装调用;捕获 `asyncio.TimeoutError` 后通过现有的
`FFmpegTool._stop_ffmpeg_process(process)` 清理进程并返回 `None`,同时保持取消处理和其他异常分支不变。
🪄 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 Plus

Run ID: a535d575-28e4-468f-8cab-9e1dd2e7ff2f

📥 Commits

Reviewing files that changed from the base of the PR and between d1d556b and ed46d53.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • _conf_schema.json
  • bootstrap.py
  • docs/project/domain-model.md
  • docs/project/platforms.md
  • metadata.yaml
  • src/infrastructure/config/models/plugin_config_models.py
  • src/infrastructure/config/models/runtime_settings.py
  • src/infrastructure/config/settings_builder.py
  • src/infrastructure/media/media_downloader.py
  • src/infrastructure/messaging/senders/base_sender.py
  • src/infrastructure/utils/ffmpeg_helper.py
  • src/shared/constants.py
  • tests/conftest.py
  • tests/unit/application/test_settings.py
  • tests/unit/infrastructure/test_base_sender_ffmpeg.py
  • tests/unit/infrastructure/test_config_round_trip.py
  • tests/unit/infrastructure/test_ffmpeg_helper.py
  • tests/unit/infrastructure/test_media_downloader.py
  • tests/unit/test_bootstrap_runtime.py
  • tests/unit/test_conf_schema.py

Comment on lines +236 to +258
process: asyncio.subprocess.Process | None = None
try:
process = await asyncio.create_subprocess_exec(
ffprobe_exe,
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,r_frame_rate",
"-of",
"json",
str(source_path),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
stdout, _stderr = await process.communicate()
except asyncio.CancelledError:
if process is not None:
await FFmpegTool._stop_ffmpeg_process(process)
raise
except (OSError, ValueError):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '_probe_video_stream_info' --type=py

Repository: FlanChanXwO/astrbot_plugin_rsshub

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -u

echo "== files =="
git ls-files | sed -n '1,120p' | grep -n '\.py$' | head -100

echo "== locate ffmpeg_helper =="
fd -a 'ffmpeg_helper.py|ffmpeg.*\.py' . | sed 's#^\./##'

echo "== search symbols =="
rg -n -C3 'probe_video_stream_info|has_audio_stream|has_valid_video_stream|gif_transcode_timeout|transcode_to_gif|_run_ffmpeg|asyncio\.wait_for|create_subprocess_exec' --type=py . || true

Repository: FlanChanXwO/astrbot_plugin_rsshub

Length of output: 50389


🏁 Script executed:

#!/bin/bash
set -u

echo "== ffmpeg_helper relevant methods =="
sed -n '80,150p' src/infrastructure/utils/ffmpeg_helper.py
sed -n '200,280p' src/infrastructure/utils/ffmpeg_helper.py
sed -n '330,480p' src/infrastructure/utils/ffmpeg_helper.py

echo "== exact _probe_video_stream_info definition =="
python3 - <<'PY'
import ast, pathlib
p = pathlib.Path("src/infrastructure/utils/ffmpeg_helper.py")
src = p.read_text()
tree = ast.parse(src)
for node in tree.body:
    if isinstance(node, ast.ClassDef) and node.name == "FFmpegTool":
        for member in node.body:
            if isinstance(member, ast.AsyncFunctionDef) and member.name == "_probe_video_stream_info":
                start = member.lineno
                end = member.end_lineno or 0
                print(f"{p}:{start}-{end}")
                for n, line in enumerate(src.splitlines(), 1):
                    if 230 <= n <= 265:
                        print(f"{n:4d}: {line}")
PY

Repository: FlanChanXwO/astrbot_plugin_rsshub

Length of output: 12438


🏁 Script executed:

#!/bin/bash
set -u

echo "== transcode_to_gif callers and internal probes =="
sed -n '310,420p' src/infrastructure/utils/ffmpeg_helper.py
sed -n '500,620p' src/infrastructure/utils/ffmpeg_helper.py

echo "== source tree imports/usages of private method only =="
rg -n '_probe_video_stream_info' --type=py src tests || true

echo "== probe methods timeout lines =="
python3 - <<'PY'
from pathlib import Path
src = Path("src/infrastructure/utils/ffmpeg_helper.py").read_text()
for name in ["_probe_video_stream_info", "has_audio_stream", "has_valid_video_stream", "_run_ffmpeg", "transcode_to_gif"]:
    idx = src.find(f"async def {name}")
    if idx != -1:
        line = src[:idx].count("\n") + 1
        print(f"{name}: line {line}")
PY

Repository: FlanChanXwO/astrbot_plugin_rsshub

Length of output: 9644


🏁 Script executed:

#!/bin/bash
set -u

echo "== transcode_to_gif body =="
sed -n '880,1080p' src/infrastructure/utils/ffmpeg_helper.py

echo "== static verifier: timeout wrapping check =="
python3 - <<'PY'
import ast, pathlib
src = ast.parse(pathlib.Path("src/infrastructure/utils/ffmpeg_helper.py").read_text())
probe = video = audio = run = transcode = None
for node in ast.walk(src):
    if isinstance(node, ast.AsyncFunctionDef):
        if node.name == "_probe_video_stream_info":
            probe = node
        elif node.name == "has_audio_stream":
            audio = node
        elif node.name == "has_valid_video_stream":
            video = node
        elif node.name == "_run_ffmpeg":
            run = node
        elif node.name == "transcode_to_gif":
            transcode = node

def uses_wait_for_func(func):
    for node in ast.walk(func):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
            if node.func.attr == "wait_for":
                return True
    return False

def has_timeout_except(func):
    for node in ast.walk(func):
        if isinstance(node, ast.ExceptHandler) and isinstance(node.type, ast.Attribute):
            if node.type.attr in {"TimeoutError", "OSError", "ValueError"}:
                return True
    return False

print({
    "_probe_video_stream_info_wait_for": uses_wait_for_func(probe),
    "_probe_video_stream_info_timeout_except": has_timeout_except(probe),
    "has_audio_stream_wait_for": uses_wait_for_func(audio),
    "has_audio_stream_timeout_except": has_timeout_except(audio),
    "has_valid_video_stream_wait_for": uses_wait_for_func(video),
    "has_valid_video_stream_timeout_except": has_timeout_except(video),
    "_run_ffmpeg_wait_for": uses_wait_for_func(run),
    "_run_ffmpeg_timeout_except": has_timeout_except(run),
})
PY

Repository: FlanChanXwO/astrbot_plugin_rsshub

Length of output: 7677


_probe_video_stream_info() 添加 process.communicate() 超时。

当前只有 GIF 转码 _run_ffmpeg()timeout_seconds 控制;transcode_to_gif() 在转码前后两次调用该探测方法时,如果 ffprobe 卡住会无限阻塞并占用进程资源。加一个短超时,如 asyncio.wait_for(process.communicate(), timeout=10) 并在 asyncio.TimeoutError 中清理进程、返回 None,与同类探测方法的超时治理方式保持一致。

🤖 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/infrastructure/utils/ffmpeg_helper.py` around lines 236 - 258, 为
`_probe_video_stream_info()` 中的 `process.communicate()` 增加约 10 秒超时,使用
`asyncio.wait_for` 包装调用;捕获 `asyncio.TimeoutError` 后通过现有的
`FFmpegTool._stop_ffmpeg_process(process)` 清理进程并返回 `None`,同时保持取消处理和其他异常分支不变。

@FlanChanXwO
FlanChanXwO merged commit a047f46 into master Jul 24, 2026
5 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in astrbot_plugin_rsshub Jul 24, 2026
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