release: v2.1.4 - #101
Conversation
* 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 段。
for more information, see https://pre-commit.ci
- 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 体积上限压缩,刷新发版内容。
# 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>
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your Experience打开你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideIntroduce 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 runnersequenceDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthrough新增 ChangesGIF 转码档位配置
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 输出或失败结果
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
嗨,我在这里给了一些总体反馈:
- GIF 转码档位的校验逻辑现在在
MediaConfig.field_validator和settings_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 启动问题与转码耗时问题。帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的审查建议。
Original comment in English
Hey - I've left some high level feedback:
- GIF 转码档位的校验逻辑现在在
MediaConfig.field_validator和settings_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 启动问题与转码耗时问题。Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
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
📒 Files selected for processing (21)
CHANGELOG.md_conf_schema.jsonbootstrap.pydocs/project/domain-model.mddocs/project/platforms.mdmetadata.yamlsrc/infrastructure/config/models/plugin_config_models.pysrc/infrastructure/config/models/runtime_settings.pysrc/infrastructure/config/settings_builder.pysrc/infrastructure/media/media_downloader.pysrc/infrastructure/messaging/senders/base_sender.pysrc/infrastructure/utils/ffmpeg_helper.pysrc/shared/constants.pytests/conftest.pytests/unit/application/test_settings.pytests/unit/infrastructure/test_base_sender_ffmpeg.pytests/unit/infrastructure/test_config_round_trip.pytests/unit/infrastructure/test_ffmpeg_helper.pytests/unit/infrastructure/test_media_downloader.pytests/unit/test_bootstrap_runtime.pytests/unit/test_conf_schema.py
| 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 |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP -C3 '_probe_video_stream_info' --type=pyRepository: 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 . || trueRepository: 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}")
PYRepository: 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}")
PYRepository: 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),
})
PYRepository: 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`,同时保持取消处理和其他异常分支不变。
发布 v2.1.4:将 dev 分支的 GIF 转码内存优化发布到 master,解决高分辨率视频转 GIF 时 FFmpeg 可能被 OOM 杀死的问题。
Modifications / 改动点
media.gif_transcode_profile:默认compatibility(长边 960px、15 FPS、128 色),另提供balanced与显式保留旧行为的quality档位;非法值会明确报配置错误。v2.1.4,更新CHANGELOG.md。默认档位会降低 GIF 的分辨率、帧率和色彩数以保障 3 GiB 容器的稳定性;普通视频原始推送不受影响。需要保留旧 GIF 输出行为时可显式设为
quality。Screenshots or Test Results / 运行截图或测试结果
git diff --check:passedruff check .:passedruff format --check .:895 files already formattedpython -m pytest tests/unit -q:735 passed, 1 skippedChecklist / 检查清单
Summary by Sourcery
通过引入可配置的 GIF 转码配置档(profile)并统一 FFmpeg 进程处理方式,在媒体转换过程中降低内存占用并提升可观测性。
New Features:
media.gif_transcode_profile配置项,提供compatibility、balanced和quality预设,用于控制 GIF 的分辨率、帧率和色深。Bug Fixes:
Enhancements:
stderr捕获到临时文件,并提供健壮的超时/取消处理机制以清理部分生成的输出。gif_transcode_profile的取值进行严格校验,并在平台与领域模型文档中记录各配置档的语义。Documentation:
gif_transcode_profile的可选值、默认值以及关于内存影响的使用指引。Tests:
gif_transcode_profile的配置映射/校验。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:
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores:
Summary by CodeRabbit
新功能
compatibility、balanced和quality。compatibility,并支持按档位控制尺寸、帧率与颜色数。错误修复