群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

群排行功能 - #6

Merged
sssysy merged 6 commits into
mainfrom
ranking_test
Jul 13, 2026
Merged

群排行功能#6
sssysy merged 6 commits into
mainfrom
ranking_test

Conversation

@sssysy

@sssysysssysy commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。

New Features:

  • 生成一张“今天玩什么”图片卡片,从用户的 Steam 库中随机推荐最多三个游戏。
  • 暴露一个新命令,从用户的游戏库中随机选择游戏并发送推荐图片。
  • 提供一个群组游戏时长排名命令,根据累计游玩时间返回聊天中的顶级玩家。

Enhancements:

  • 在专用数据库表中跟踪用户-游戏维度的游玩会话,并在状态轮询过程中更新这些记录。
  • 在管理控制台中注册新的游玩记录模型以便管理和查询。
  • 添加一个工具,用于将以秒为单位的时长转换为可读的中文字符串。

Documentation:

  • 更新 README 中的命令列表、截图和路线图,记录新的随机游玩和群组排名功能,并将其标记为已完成。
Original summary in English

Summary by Sourcery

Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.

New Features:

  • Generate a "今天玩什么" image card that randomly recommends up to three games from a user�s Steam library.
  • Expose a new command to randomly pick games from a user�s library and send the recommendation image.
  • Provide a group game-time ranking command that returns the top players in a chat based on accumulated playtime.

Enhancements:

  • Track per-user, per-game play sessions in a dedicated database table and update these records during status polling.
  • Register the new play record model in the admin console for management and querying.
  • Add a utility for converting durations in seconds into a human-readable Chinese string.

Documentation:

  • Update README command list, screenshots, and roadmap to document the new random play and group ranking features and mark them as completed.

Summary by CodeRabbit

  • 新功能
    • 新增“玩什么”命令,可从个人游戏库随机推荐最多 3 款游戏并生成推荐卡片。
    • 新增群游戏时长排行榜,展示群内前 5 名排行。
    • 自动记录游戏游玩时段,支持游玩记录管理。
    • 新增“社交相关”帮助分类及加好友、群排行说明。
  • 文档
    • 更新帮助菜单、命令说明及功能展示图片。
    • 完善“steam玩什么”和群排行相关使用说明。

@sourcery-ai

sourcery-aiBot commented Jul 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。

新增群游玩时长排行榜流程的时序图

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

新增随机游戏推荐图片流程的时序图

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
实现“今天玩什么”推荐卡片绘制,以及基于用户 Steam 游戏库的随机游戏选择,并通过新命令暴露该能力。
  • 为新的卡片渲染器加入玩法卡片布局常量、格式化辅助函数、渐变背景以及圆角封面贴图逻辑
  • 实现 draw_what_to_play,在主题画布上渲染 1–3 个推荐游戏,包括封面、名称截断以及格式化后的游玩时长
  • 新增 build_random_pick 服务,用于获取游戏库、随机抽取最多 3 款游戏、准备数据、调用 draw_what_to_play,并返回 JPEG 图片
  • 将 build_random_pick 接入 SteamLibarary 命令集合中,通过新的 玩什么 命令对外暴露,并包含错误处理和进度提示信息
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
引入 SteamPlayRecord 持久化,用于记录会话级别的游玩历史,并将其接入状态轮询和管理后台 UI。
  • 添加 SteamPlayRecord ORM 模型,包含 steamid64、appid、start_ts、end_ts 以及相应的类型变量
  • 实现 upsert_record 用于开启/结束会话,delete_record 用于清理记录,以及灵活的 get_records / get_records_by_steamids 查询辅助函数
  • 在 poll_service.update_game_record 中使用 SteamPlayRecord 持久化由状态变化推导出的游戏开始/结束事件,该逻辑由 poll_and_push_game_status 调用
  • 在数据库模块导出中注册 SteamPlayRecord,并新增管理后台页面用于管理游玩记录
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
新增群排行服务和命令,用于基于 SteamPlayRecord 计算每个用户的总游玩时长并展示排行榜。
  • 实现 get_group_ranking_list,将群绑定映射到用户 ID,批量获取已结束的游玩记录,按用户聚合游玩时长,并按降序排序
  • 创建 SteamRanking SV,提供 群排行/群排名 命令,校验群聊上下文,调用排行服务,使用新的时间格式化辅助函数格式化前 5 名结果,并处理错误情况
  • 新增 time_convert_s 工具函数,将秒级时长格式化为适合展示的人类可读字符串
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
更新文档和帮助,以体现新命令和新功能,包括“玩什么”和群排行。
  • 刷新 steam帮助 图片,并注明该图片可能不是最新
  • 扩展命令列表表格,包含 steam玩什么 和 steam群排行,以及社交命令章节
  • 在路线图中标记 steam玩什么 和 群游玩时长排行榜 已实现,并补充说明部分功能尚未完成绘图实现
README.md
SteamUID/SteamHelp/help.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

Adds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates.

Sequence diagram for the new group playtime ranking flow

sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Loading

Sequence diagram for the new random game recommendation image flow

sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
Loading

File-Level Changes

ChangeDetailsFiles
Implement "今天玩什么" recommendation card drawing and random game selection from a user’s Steam library, exposed via a new command.
  • Introduce play card layout constants, formatting helpers, gradient background, and rounded cover paste logic for the new card renderer
  • Implement draw_what_to_play to render 1–3 recommended games with cover, name truncation, and formatted playtime on a themed canvas
  • Add build_random_pick service to fetch library, randomly sample up to 3 games, prepare data, call draw_what_to_play, and return a JPEG image
  • Wire build_random_pick into the SteamLibarary command set with a new 玩什么 command including error handling and progress message
SteamUID/utils/PIL/draw.py
SteamUID/SteamLibarary/library_service.py
SteamUID/SteamLibarary/__init__.py
Introduce SteamPlayRecord persistence for per-session play history and hook it into status polling and admin UI.
  • Add SteamPlayRecord ORM model with steamid64, appid, start_ts, end_ts and supporting type variable
  • Implement upsert_record for starting/ending sessions, delete_record for cleanup, and flexible get_records / get_records_by_steamids query helpers
  • Use SteamPlayRecord in poll_service.update_game_record to persist game start/end events derived from status changes, invoked from poll_and_push_game_status
  • Register SteamPlayRecord in database module exports and add an admin console page for managing play records
SteamUID/utils/database/models.py
SteamUID/SteamPoll/poll_service.py
SteamUID/utils/database/admin.py
SteamUID/utils/database/__init__.py
Add group ranking service and commands that compute per-user total playtime from SteamPlayRecord and present a leaderboard.
  • Implement get_group_ranking_list to map group binds to user IDs, batch fetch finished play records, aggregate durations per user, and sort descending
  • Create SteamRanking SV with 群排行/群排名 commands that validate group context, call the ranking service, format top-5 results using a new time formatting helper, and handle errors
  • Add time_convert_s utility to format durations in seconds into human-readable strings for display
SteamUID/SteamRanking/ranking_service.py
SteamUID/SteamRanking/__init__.py
SteamUID/utils/utils.py
Update documentation and help to reflect new commands and features, including "玩什么" and group ranking.
  • Refresh steam帮助 image and annotate that it may not be up to date
  • Extend command list tables to include steam玩什么 and steam群排行 plus social commands section
  • Mark steam玩什么 and 群游玩时长排行榜 as implemented in the roadmap and add a note about some features lacking drawing implementations
README.md
SteamUID/SteamHelp/help.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

coderabbitaiBot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sssysy, you've reached your PR review limit, so we couldn't start this review.

Next review available in:41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dec40f-8fdd-449e-bc98-d7daa4ea6e58

📥 Commits

Reviewing files that changed from the base of the PR and between 68b6744 and 66d05bf.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。

Changes

Steam 功能扩展

Layer / File(s)Summary
游玩记录持久化与轮询同步
SteamUID/utils/database/models.py, SteamUID/SteamPoll/poll_service.py, SteamUID/utils/database/admin.py
新增 SteamPlayRecord 模型、查询与写入接口,并在轮询状态变化后记录游戏开始和结束时间。
游戏库随机推荐流程
SteamUID/utils/PIL/draw.py, SteamUID/SteamLibarary/..., SteamUID/SteamHelp/help.json, README.md
新增随机选择最多三款游戏、生成推荐卡片图片并通过“玩什么”命令发送的流程。
群游玩时长排行榜
SteamUID/SteamRanking/..., SteamUID/SteamHelp/help.json, README.md
新增群绑定与游玩记录汇总、排行榜命令及相关帮助说明。
时长格式化工具
SteamUID/utils/utils.py
新增秒数到中文可读时长的转换函数。

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
Loading
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Loading

Poem

我是兔兔蹦蹦跳,
随机挑三款游戏包。
记录开始和结束,
群里排行看得妙。
图片一亮,胡萝卜也笑!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed标题准确指向本次新增的群内游玩时长排行榜功能,与变更内容一致。
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ranking_test

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-aisourcery-aiBot 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 - 我发现了 3 个问题,并给出了一些整体性反馈:

  • build_random_pick 中,game_data 未定义,同时循环末尾存在多余的 }) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 picks_data),并移除多余的闭合括号。
  • 同样在 build_random_pick 中,pic_quality 在当前作用域中被引用但从未定义,而在 build_library_wall 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 NameError
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments-`build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record``end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None``end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts``end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>

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

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

  • In build_random_pick, game_data is not defined and there is an extra }) }) at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. picks_data) and clean up the stray closing braces.
  • Also in build_random_pick, pic_quality is referenced but never defined in this scope, unlike build_library_wall where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a NameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</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 on lines +57 to +66
async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败

build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。

Original comment in English

issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime

In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.

Comment on lines +608 to +617
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算

Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_recordend_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。

请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. 如果 upsert_record 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 ValueError 替换为领域特定的异常类型(例如 InvalidPlayRecordError),或者转换为你现有的错误/响应模式。
  2. 确保任何此前依赖插入 start_ts=Noneend_ts=None 的调用方都被更新为始终提供合法的 start_ts,或者在能够提供时再调用 upsert_record
  3. 如果 upsert_record 内部存在基于其他信息推导 start_ts/end_ts 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English

suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations

The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.

Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.

Suggested implementation:

@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
  1. If upsert_record is part of a public API that surfaces errors to callers, you may want to replace ValueError with a domain-specific exception type (e.g. InvalidPlayRecordError) or convert it to your existing error/response pattern.
  2. Ensure any callers that relied on inserting start_ts=None with end_ts=None are updated to always supply a valid start_ts, or to avoid calling upsert_record until they can.
  3. If there is logic inside upsert_record that derives start_ts/end_ts from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.

Comment on lines +47 to +51
for record in records:
uid = steamid_to_user.get(record.steamid64)
if uid is None:
continue
duration = record.end_ts - record.start_ts # type: ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃

get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:

  • 跳过 start_tsend_ts 为 None,或者 end_ts < start_ts 的记录;
  • 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
    这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English

issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash

In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:

  • Skip records where start_ts or end_ts is None, or end_ts < start_ts.
  • Optionally log or count these cases to detect upstream data issues.
    This keeps the ranking robust when bad data is present.

@sssysy
sssysy merged commit 39e184b into mainJul 13, 2026
1 of 2 checks passed

@coderabbitaicoderabbitaiBot 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)

184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222 现在先 flush_status_updates(update_list),再 update_game_record(push_list)。一旦任一条 upsert_record 失败或抛错,这次 gameid 变化已经写回 SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。

🤖 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 `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整
poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成
update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在
update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)

385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

建议为 group_id 补充索引以支撑排行榜查询。

get_binds_by_groupgroup_id 过滤,但 SteamBind.group_id 字段定义为 Optional[str] = Field(default=None, title="群ID"),未设置 index=True(对比 steamid64/user_id 均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。

♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")

请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。

🤖 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 `@SteamUID/utils/database/models.py` around lines 385 - 396, 为
SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
README.md (1)

105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新增图片缺少 alt 文本。

markdownlint 提示该图片缺少替代文本(MD045)。

♿ 建议补充 alt
 ## steam玩什么
-<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">
🤖 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 `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt
属性,保留现有图片地址和 width 设置不变。

Source: Linters/SAST tools

SteamUID/SteamLibarary/library_service.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

playtime 计算逻辑与 build_library_wall(32-38 行)完全重复。

同一段 playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever) 逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。

♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int:
return (
game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0)
)

随后在 build_library_wallbuild_random_pick 中分别调用 _get_total_playtime(game) 替换重复代码块。

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数
`_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新
`build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。
SteamUID/utils/PIL/draw.py (1)

678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

推荐卡数量小于 3 时布局不居中。

画布宽度固定为 _PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时 cx 始终从 SIDE_PAD 开始向右排列。当 picks 长度为 1 或 2(库存游戏不足 3 款时会发生,见 library_service.pypick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。

♻️ 建议:按实际卡片数量居中
 # —— 绘制卡片 ——
cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP)
+ n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+
for i, game in enumerate(picks):
- cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))

Also applies to: 754-770

🤖 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 `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card
positioning in draw_what_to_play so the rendered cards are horizontally centered
based on len(picks), while preserving the existing card width and gap. Compute a
centered starting x-coordinate from the fixed canvas width and actual card
count, then use it when calculating each card’s cx instead of always starting at
SIDE_PAD.
🤖 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 `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 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: 4c992a5c-c784-424f-a939-eaec03caba02

📥 Commits

Reviewing files that changed from the base of the PR and between e4572e2 and 68b6744.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • SteamUID/SteamHelp/help.json
  • SteamUID/SteamLibarary/__init__.py
  • SteamUID/SteamLibarary/library_service.py
  • SteamUID/SteamPoll/poll_service.py
  • SteamUID/SteamRanking/__init__.py
  • SteamUID/SteamRanking/ranking_service.py
  • SteamUID/utils/PIL/draw.py
  • SteamUID/utils/database/__init__.py
  • SteamUID/utils/database/admin.py
  • SteamUID/utils/database/models.py
  • SteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

Comment threadREADME.md
Comment on lines +79 to 88
#### 社交相关
| 命令 | 说明 |
|------|:------:|
| `@xx steam加好友123456` | 获取被 @ 用户的好友码 |
| `steam群排行` | 查看群游玩时长排行榜 |

#### 其他服务
| 命令 | 说明 |
|------|:------:|
| `steam帮助` | 呼出本插件帮助菜单 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15

Repository: sssysy/SteamUID

Length of output: 153


steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 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 `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.

Comment on lines +55 to +95


async def build_random_pick(steamid64: str) -> bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key = SteamConfig.get_config("SteamWebAPIKey").data
if not api_key:
raise SteamConfigError("请先配置 steam web api key")

library = await get_steamlibrary_by_steamid64(api_key, steamid64)
games = library.get("games")
if games is None:
raise SteamValidationError("获取 steam 游戏库列表失败")
if not games:
raise SteamValidationError("该 steam 账号暂无游戏库存")
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)

for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })

img = await draw_what_to_play(game_data)
img = img.convert("RGB")

buf = BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
return buf.getvalue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。

第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError

由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。

🐛 修复语法错误与变量初始化
 pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
}) })
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
asyncdefbuild_random_pick(steamid64: str) ->bytes:
"""从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。"""
api_key=SteamConfig.get_config("SteamWebAPIKey").data
ifnotapi_key:
raiseSteamConfigError("请先配置 steam web api key")
library=awaitget_steamlibrary_by_steamid64(api_key, steamid64)
games=library.get("games")
ifgamesisNone:
raiseSteamValidationError("获取 steam 游戏库列表失败")
ifnotgames:
raiseSteamValidationError("该 steam 账号暂无游戏库存")
pick_count=min(3, len(games))
picks=random.sample(games, pick_count)
game_data= []
forgameinpicks:
appid=str(game.get("appid", ""))
name=game.get("name", "未知游戏")
playtime= (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
})
img=awaitdraw_what_to_play(game_data)
img=img.convert("RGB")
buf=BytesIO()
img.save(buf, format="JPEG", quality=pic_quality, subsampling=0)
returnbuf.getvalue()
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🪛 Ruff (0.15.20)

[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-88: Expected a statement

(invalid-syntax)


[warning] 88-89: Expected a statement

(invalid-syntax)

🤖 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 `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。

Source: Linters/SAST tools

await bot.send("本群暂无游戏时长排行数据")
return

text = f"本群游戏时长排行:\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除多余的 f-string 前缀。

text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。

🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"
As per static analysis hints, Ruff flags `[error] 28-28: f-string without any placeholders` (F541).
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text=f"本群游戏时长排行:\n"
text="本群游戏时长排行:\n"
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 28-28: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 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 `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.

Source: Linters/SAST tools

Comment on lines +20 to +23
# 第1步:取群绑定列表,构建映射
binds = await SteamBind.get_binds_by_group(group_id)
if not binds:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。

对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_groupwith_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.pygroup_ranking 的分支逻辑),不利于故障排查。

🐛 建议修复
 binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifnotbinds:
return []
# 第1步:取群绑定列表,构建映射
binds=awaitSteamBind.get_binds_by_group(group_id)
ifbindsisNone:
raiseSteamError("查询群绑定信息失败,请稍后重试")
ifnotbinds:
return []
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 20-20: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 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 `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.

Comment on lines +597 to +652
class SteamPlayRecord(BaseIDModel, table=True):
"""Steam游戏游玩记录表(用于游戏排行榜及衍生功能)"""
__table_args__: Dict[str, Any] = {"extend_existing": True}

steamid64: str = Field(default=None, index=True, title="SteamID64")
appid: str = Field(default=None, index=True, title="游戏AppID")
start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)")
end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)")

@classmethod
@with_session
async def upsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] = None,
end_ts: Optional[int] = None,
) -> int:
"""
写入游玩记录(upsert)。
- 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。
- 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL
定位进行中的记录并写入 end_ts,start_ts 此时忽略。
一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录,
因此该定位方式不会产生歧义。
返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。
"""
if end_ts is None:
session.add(
cls(
steamid64=steamid64, # type: ignore
appid=appid, # type: ignore
start_ts=start_ts, # type: ignore
end_ts=None, # type: ignore
)
)
return 0

# 结束游戏:查找进行中的记录
stmt = (
select(cls)
.where(
cls.steamid64 == steamid64,
cls.appid == appid,
cls.end_ts.is_(None), # type: ignore
)
.order_by(cls.id.desc()) # type: ignore
)
result = await session.execute(stmt)
existing = result.scalars().first()
if existing is None:
return -1
existing.end_ts = end_ts
session.add(existing)
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUID

Repository: sssysy/SteamUID

Length of output: 8887


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'

Repository: sssysy/SteamUID

Length of output: 13449


🌐 Web query:

SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation

💡 Result:

In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].

Citations:


开始游戏分支补上 start_ts 非空校验
upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 598-598: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 598-598: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 599-599: Mutable default value for class attribute

(RUF012)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 617-617: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 618-618: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 619-619: Docstring contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF002)


[warning] 620-620: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 621-621: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 623-623: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 636-636: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)

🤖 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 `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。



def time_convert_s(seconds: int) -> str:
"""将秒数转换为人类可读的时长,如 1天2小时30分45秒"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

修正文档字符串中的全角逗号

Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。

建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
"""将秒数转换为人类可读的时长如 1天2小时30分45秒"""
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 80-80: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)

🤖 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 `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.

Source: Linters/SAST tools

@sssysy
sssysy deleted the ranking_test branch July 14, 2026 13:28
Sign up for freeto 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.

1 participant

@sssysy