Skip to content

feat(api): 实现 Dashboard API 客户端集成#10

Open
Disaster-Terminator wants to merge 2 commits intomainfrom
feature/dashboard-api
Open

feat(api): 实现 Dashboard API 客户端集成#10
Disaster-Terminator wants to merge 2 commits intomainfrom
feature/dashboard-api

Conversation

@Disaster-Terminator
Copy link
Owner

@Disaster-Terminator Disaster-Terminator commented Feb 25, 2026

概述

实现 Dashboard API 客户端集成,通过 API 优先获取积分数据,提高数据获取的可靠性和性能。

变更内容

新增文件

  • src/api/__init__.py - API 模块入口
  • src/api/models.py - Dashboard 数据模型 (dataclass)
  • src/api/dashboard_client.py - Dashboard API 客户端
  • tests/unit/test_dashboard_client.py - 单元测试 (13 个测试用例)

修改文件

  • src/account/points_detector.py - 集成 DashboardClient,优先使用 API
  • pyproject.toml - 添加 respx 测试依赖

核心功能

DashboardClient

  • API 端点: https://rewards.bing.com/api/getuserinfo?type=1
  • 双重数据源: API 调用优先,HTML fallback 备用
  • 重试机制: 最多 2 次重试,间隔 1 秒
  • 会话过期处理: 401/403 错误自动触发 HTML fallback
  • 超时控制: 10 秒请求超时

异常处理

  • 使用具体异常类型 (httpx.RequestError, httpx.TimeoutException)
  • 避免宽泛的 Exception 捕获,防止掩盖系统错误

测试覆盖

测试场景 状态
API 成功响应
API 错误响应 fallback
401/403 会话过期 fallback
HTML fallback 成功/失败
重试逻辑
超时处理
网络错误处理

验证结果

  • ✅ 13 个单元测试全部通过
  • ✅ mypy --strict 类型检查通过

Summary by Sourcery

引入可复用、带类型的 Dashboard API 客户端和数据模型,并将其集成到积分检测中,以优先使用基于 API 的获取方式,并具备稳健的回退机制。

New Features:

  • 添加带错误类型的 DashboardClient,用于消费 Microsoft Rewards Dashboard API,包括对积分和搜索计数器的支持。
  • 通过新的 api 包对与 dashboard 相关的客户端和数据模型进行暴露,以便在整个项目中复用。

Enhancements:

  • 更新积分检测逻辑,优先通过 Dashboard API 获取当前积分,并加入超时处理;在必要时回退到现有的 HTML 解析,同时收紧任务状态解析的类型,并改进日志记录。
  • 通过使用带类型的状态字典和本地进度变量来改进任务状态解析,使完成状态检查更加清晰、安全。

Build:

  • 添加 httpx 作为运行时依赖,以及 respx 作为开发依赖,用于在测试中对 HTTP 客户端进行模拟。
  • 更新 Ruff 的 isort 配置,将新的 apiconstants 包识别为一方模块(first-party modules)。

Documentation:

  • 添加 CURRENT_TASK 文档,描述 Dashboard API 集成的目标、设计和验收标准。

Tests:

  • DashboardClient 添加全面的单元测试,覆盖 API 成功场景、各种回退路径、重试行为、超时、网络错误、Cookie 过滤以及数据模型解析的边界情况。
Original summary in English

Summary by Sourcery

Introduce a reusable, typed Dashboard API client and data models, and integrate it into points detection to prefer API-based retrieval with robust fallbacks.

New Features:

  • Add a typed DashboardClient with error type for consuming the Microsoft Rewards dashboard API, including support for points and search counters.
  • Expose dashboard-related client and data models via a new api package for reuse across the project.

Enhancements:

  • Update points detection to prefer fetching current points via the Dashboard API with timeout handling and fall back to existing HTML parsing, while tightening task status parsing types and logging.
  • Improve task status parsing by using typed status dictionaries and local progress variables to make completion checks clearer and safer.

Build:

  • Add httpx as a runtime dependency and respx as a dev dependency for HTTP client mocking in tests.
  • Update Ruff isort configuration to recognize the new api and constants packages as first-party modules.

Documentation:

  • Add CURRENT_TASK documentation describing the Dashboard API integration goals, design, and acceptance criteria.

Tests:

  • Add comprehensive unit tests for DashboardClient covering API success, various fallback paths, retry behavior, timeouts, network errors, cookie filtering, and data model parsing edge cases.
Original summary in English

Summary by Sourcery

引入可复用、带类型的 Dashboard API 客户端和数据模型,并将其集成到积分检测中,以优先使用基于 API 的获取方式,并具备稳健的回退机制。

New Features:

  • 添加带错误类型的 DashboardClient,用于消费 Microsoft Rewards Dashboard API,包括对积分和搜索计数器的支持。
  • 通过新的 api 包对与 dashboard 相关的客户端和数据模型进行暴露,以便在整个项目中复用。

Enhancements:

  • 更新积分检测逻辑,优先通过 Dashboard API 获取当前积分,并加入超时处理;在必要时回退到现有的 HTML 解析,同时收紧任务状态解析的类型,并改进日志记录。
  • 通过使用带类型的状态字典和本地进度变量来改进任务状态解析,使完成状态检查更加清晰、安全。

Build:

  • 添加 httpx 作为运行时依赖,以及 respx 作为开发依赖,用于在测试中对 HTTP 客户端进行模拟。
  • 更新 Ruff 的 isort 配置,将新的 apiconstants 包识别为一方模块(first-party modules)。

Documentation:

  • 添加 CURRENT_TASK 文档,描述 Dashboard API 集成的目标、设计和验收标准。

Tests:

  • DashboardClient 添加全面的单元测试,覆盖 API 成功场景、各种回退路径、重试行为、超时、网络错误、Cookie 过滤以及数据模型解析的边界情况。
Original summary in English

Summary by Sourcery

Introduce a reusable, typed Dashboard API client and data models, and integrate it into points detection to prefer API-based retrieval with robust fallbacks.

New Features:

  • Add a typed DashboardClient with error type for consuming the Microsoft Rewards dashboard API, including support for points and search counters.
  • Expose dashboard-related client and data models via a new api package for reuse across the project.

Enhancements:

  • Update points detection to prefer fetching current points via the Dashboard API with timeout handling and fall back to existing HTML parsing, while tightening task status parsing types and logging.
  • Improve task status parsing by using typed status dictionaries and local progress variables to make completion checks clearer and safer.

Build:

  • Add httpx as a runtime dependency and respx as a dev dependency for HTTP client mocking in tests.
  • Update Ruff isort configuration to recognize the new api and constants packages as first-party modules.

Documentation:

  • Add CURRENT_TASK documentation describing the Dashboard API integration goals, design, and acceptance criteria.

Tests:

  • Add comprehensive unit tests for DashboardClient covering API success, various fallback paths, retry behavior, timeouts, network errors, cookie filtering, and data model parsing edge cases.

新功能:

  • 添加类型化的 DashboardClient 以及相关数据模型,用于消费 Bing Rewards Dashboard API,包括对积分、搜索计数器、促销活动以及打卡卡片(punch cards)的支持。
  • 通过新的 api 包对外暴露 Dashboard API 客户端及模型,以便在整个项目中复用。

增强优化:

  • 更新积分检测逻辑,优先通过 Dashboard API 获取当前积分,在失败时回退到 HTML 解析,并改进日志记录和类型注解。
  • 调整任务状态解析逻辑,使用局部变量存储进度值,并对返回的状态字典进行更严格的类型约束。

构建:

  • respx 添加为开发依赖,用于在测试中对 HTTP 客户端进行模拟(mock)。

文档:

  • 添加 CURRENT_TASK 文档,描述 Dashboard API 集成的目标、设计和验收标准。

测试:

  • DashboardClient 添加全面的单元测试,覆盖成功的 API 调用、各种回退场景、重试逻辑、超时以及网络错误等情况。
Original summary in English

Summary by Sourcery

引入可复用、带类型的 Dashboard API 客户端和数据模型,并将其集成到积分检测中,以优先使用基于 API 的获取方式,并具备稳健的回退机制。

New Features:

  • 添加带错误类型的 DashboardClient,用于消费 Microsoft Rewards Dashboard API,包括对积分和搜索计数器的支持。
  • 通过新的 api 包对与 dashboard 相关的客户端和数据模型进行暴露,以便在整个项目中复用。

Enhancements:

  • 更新积分检测逻辑,优先通过 Dashboard API 获取当前积分,并加入超时处理;在必要时回退到现有的 HTML 解析,同时收紧任务状态解析的类型,并改进日志记录。
  • 通过使用带类型的状态字典和本地进度变量来改进任务状态解析,使完成状态检查更加清晰、安全。

Build:

  • 添加 httpx 作为运行时依赖,以及 respx 作为开发依赖,用于在测试中对 HTTP 客户端进行模拟。
  • 更新 Ruff 的 isort 配置,将新的 apiconstants 包识别为一方模块(first-party modules)。

Documentation:

  • 添加 CURRENT_TASK 文档,描述 Dashboard API 集成的目标、设计和验收标准。

Tests:

  • DashboardClient 添加全面的单元测试,覆盖 API 成功场景、各种回退路径、重试行为、超时、网络错误、Cookie 过滤以及数据模型解析的边界情况。
Original summary in English

Summary by Sourcery

Introduce a reusable, typed Dashboard API client and data models, and integrate it into points detection to prefer API-based retrieval with robust fallbacks.

New Features:

  • Add a typed DashboardClient with error type for consuming the Microsoft Rewards dashboard API, including support for points and search counters.
  • Expose dashboard-related client and data models via a new api package for reuse across the project.

Enhancements:

  • Update points detection to prefer fetching current points via the Dashboard API with timeout handling and fall back to existing HTML parsing, while tightening task status parsing types and logging.
  • Improve task status parsing by using typed status dictionaries and local progress variables to make completion checks clearer and safer.

Build:

  • Add httpx as a runtime dependency and respx as a dev dependency for HTTP client mocking in tests.
  • Update Ruff isort configuration to recognize the new api and constants packages as first-party modules.

Documentation:

  • Add CURRENT_TASK documentation describing the Dashboard API integration goals, design, and acceptance criteria.

Tests:

  • Add comprehensive unit tests for DashboardClient covering API success, various fallback paths, retry behavior, timeouts, network errors, cookie filtering, and data model parsing edge cases.
Original summary in English

Summary by Sourcery

引入可复用、带类型的 Dashboard API 客户端和数据模型,并将其集成到积分检测中,以优先使用基于 API 的获取方式,并具备稳健的回退机制。

New Features:

  • 添加带错误类型的 DashboardClient,用于消费 Microsoft Rewards Dashboard API,包括对积分和搜索计数器的支持。
  • 通过新的 api 包对与 dashboard 相关的客户端和数据模型进行暴露,以便在整个项目中复用。

Enhancements:

  • 更新积分检测逻辑,优先通过 Dashboard API 获取当前积分,并加入超时处理;在必要时回退到现有的 HTML 解析,同时收紧任务状态解析的类型,并改进日志记录。
  • 通过使用带类型的状态字典和本地进度变量来改进任务状态解析,使完成状态检查更加清晰、安全。

Build:

  • 添加 httpx 作为运行时依赖,以及 respx 作为开发依赖,用于在测试中对 HTTP 客户端进行模拟。
  • 更新 Ruff 的 isort 配置,将新的 apiconstants 包识别为一方模块(first-party modules)。

Documentation:

  • 添加 CURRENT_TASK 文档,描述 Dashboard API 集成的目标、设计和验收标准。

Tests:

  • DashboardClient 添加全面的单元测试,覆盖 API 成功场景、各种回退路径、重试行为、超时、网络错误、Cookie 过滤以及数据模型解析的边界情况。
Original summary in English

Summary by Sourcery

Introduce a reusable, typed Dashboard API client and data models, and integrate it into points detection to prefer API-based retrieval with robust fallbacks.

New Features:

  • Add a typed DashboardClient with error type for consuming the Microsoft Rewards dashboard API, including support for points and search counters.
  • Expose dashboard-related client and data models via a new api package for reuse across the project.

Enhancements:

  • Update points detection to prefer fetching current points via the Dashboard API with timeout handling and fall back to existing HTML parsing, while tightening task status parsing types and logging.
  • Improve task status parsing by using typed status dictionaries and local progress variables to make completion checks clearer and safer.

Build:

  • Add httpx as a runtime dependency and respx as a dev dependency for HTTP client mocking in tests.
  • Update Ruff isort configuration to recognize the new api and constants packages as first-party modules.

Documentation:

  • Add CURRENT_TASK documentation describing the Dashboard API integration goals, design, and acceptance criteria.

Tests:

  • Add comprehensive unit tests for DashboardClient covering API success, various fallback paths, retry behavior, timeouts, network errors, cookie filtering, and data model parsing edge cases.

Copilot AI review requested due to automatic review settings February 25, 2026 18:17
@qodo-code-review
Copy link

Review Summary by Qodo

Implement Dashboard API client with dual data source strategy

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Implement Dashboard API client with dual data source strategy
  - API-first approach with HTML fallback for reliability
  - Retry mechanism (2 retries, 1-second delay) and timeout handling
  - Session expiration detection (401/403) triggers automatic fallback
• Integrate DashboardClient into PointsDetector for points retrieval
  - Prioritize API calls over HTML parsing
  - Graceful error handling with specific exception types
• Add comprehensive data models for Dashboard API responses
  - Dataclass-based models with camelCase to snake_case conversion
  - Support for user status, search counters, promotions, and punch cards
• Comprehensive test coverage with 13 unit tests
  - Mock API responses, retry logic, timeout, and network error scenarios
  - HTML fallback validation for all failure cases
Diagram
flowchart LR
  A["PointsDetector"] -->|"try API first"| B["DashboardClient"]
  B -->|"success"| C["Return Points"]
  B -->|"failure/timeout"| D["HTML Fallback"]
  D -->|"success"| C
  D -->|"failure"| E["DashboardError"]
  B -->|"401/403"| D
  F["API Models"] -->|"parse response"| B
Loading

Grey Divider

File Changes

1. src/api/__init__.py ✨ Enhancement +24/-0

API module initialization and exports

• New API module initialization file
• Exports DashboardClient, DashboardError, and all data model classes
• Provides clean public API for the api package

src/api/init.py


2. src/api/dashboard_client.py ✨ Enhancement +176/-0

Dashboard API client with fallback strategy

• Implements DashboardClient class for API communication
• Dual data source strategy: API calls with HTML fallback
• Retry mechanism with configurable max retries and delay
• Session expiration handling (401/403 errors trigger fallback)
• Specific exception handling for httpx errors (TimeoutException, RequestError)
• Methods: get_dashboard_data(), get_current_points(), get_search_counters()

src/api/dashboard_client.py


3. src/api/models.py ✨ Enhancement +143/-0

Dashboard API data models with camelCase conversion

• Dataclass-based models for Dashboard API responses
• Models: DashboardData, UserStatus, LevelInfo, SearchCounters, SearchCounter, Promotion, PunchCard
• Automatic camelCase to snake_case conversion via _transform_dict utility
• from_dict() class methods for JSON deserialization
• Default factory fields for safe initialization

src/api/models.py


View more (3)
4. src/account/points_detector.py ✨ Enhancement +29/-10

Integrate Dashboard API with HTML fallback

• Integrate DashboardClient for API-first points retrieval
• Add try-except block to attempt API call before HTML parsing
• Graceful fallback to HTML parsing on DashboardError or general exceptions
• Improve type hints for status dictionary in _check_task_status method
• Add null check for points_text before parsing
• Refactor progress value extraction for better type safety

src/account/points_detector.py


5. tests/unit/test_dashboard_client.py 🧪 Tests +292/-0

Comprehensive Dashboard API client unit tests

• 13 comprehensive unit tests for DashboardClient
• Test scenarios: API success, error responses, 401/403 fallback, HTML fallback
• Retry logic validation with side_effect mocking
• Timeout and network error handling tests
• Data model parsing tests with missing fields
• Uses respx for HTTP mocking and pytest fixtures for test setup

tests/unit/test_dashboard_client.py


6. pyproject.toml Dependencies +1/-0

Add respx test dependency

• Add respx>=0.21.0 to dev dependencies
• Required for HTTP mocking in Dashboard API client tests

pyproject.toml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 25, 2026

Code Review by Qodo

🐞 Bugs (17) 📘 Rule violations (8) 📎 Requirement gaps (0)

Grey Divider


Action required

1. List items unchecked 🐞 Bug ⛯ Reliability ⭐ New
Description
多个 from_dict 直接对列表元素调用 *.from_dict(),未验证元素是否为 dict;当 API 返回包含 null/标量的数组时会触发 TypeError 并导致整个 API
解析失败。该问题会降低 API-first 的可靠性并频繁触发 fallback。
Code

src/api/models.py[R59-69]

+        pc_raw = data.get("pc_search") or []
+        if not isinstance(pc_raw, list):
+            pc_raw = []
+
+        mobile_raw = data.get("mobile_search") or []
+        if not isinstance(mobile_raw, list):
+            mobile_raw = []
+
+        pc_search = [SearchCounter.from_dict(item) for item in pc_raw]
+        mobile_search = [SearchCounter.from_dict(item) for item in mobile_raw]
+        return cls(pc_search=pc_search, mobile_search=mobile_search)
Evidence
SearchCounters.from_dict 对 pc_search/mobile_search 的列表元素不做 dict 校验,直接传入
SearchCounter.from_dict;DashboardData.from_dict 中对
more_promotions/punch_cards/streak_bonus_promotions 等也存在同样的“逐元素不校验”解析方式,因此只要外部 API 某个数组混入
null/标量即可导致解析异常。

src/api/models.py[59-69]
src/api/models.py[219-238]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`from_dict` 解析外部 API 返回值时,对列表字段只校验“是否为 list”,但不校验 list 内元素类型,导致一旦数组中混入 `null`/标量(外部 API 常见边界情况),就会在 `*.from_dict(item)` 处抛出 `TypeError`,从而让 API 解析整体失败。

### Issue Context
该客户端面向外部 API(不可控),需要更强的容错来维持 API-first 的稳定性,并减少无谓 fallback。

### Fix Focus Areas
- src/api/models.py[54-70]
- src/api/models.py[211-239]

### Suggested change
- 将类似 `X.from_dict(item) for item in items` 改为仅当 `isinstance(item, dict)` 时才解析,否则跳过。
- 视需要在跳过时 `logger.debug(...)`(若不希望模型层引入 logger,可保持静默跳过)。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Nested dict unchecked 🐞 Bug ✓ Correctness ⭐ New
Description
UserStatus/DashboardData 的嵌套对象字段未校验是否为 dict 就传入子模型 from_dict;当服务端返回 null/标量时会在
_filter_dataclass_fields(data, cls) 处触发异常。该问题会导致解析脆弱并放大外部 API 的偶发异常。
Code

src/api/models.py[R170-176]

+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "UserStatus":
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        level_info = LevelInfo.from_dict(data.get("level_info", {}))
+        counters = SearchCounters.from_dict(data.get("counters", {}))
+        return cls(
Evidence
UserStatus.from_dict 直接将 data.get("level_info") / data.get("counters") 传给子模型 from_dict;这些子模型的
from_dict 内部会把入参当作 dict 处理(调用 .items()),因此一旦入参为 None/标量就会抛异常。

src/api/models.py[170-176]
src/api/models.py[27-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
嵌套对象字段(如 `level_info` / `counters` / `streak_promotion`)在解析时未校验类型,直接传入子模型 `from_dict`。当 API 返回 `null`/标量时会导致 `_filter_dataclass_fields(...).items()` 抛异常。

### Issue Context
外部 API 的字段类型在边界场景(AB/灰度/服务端 bug)下可能出现 `null` 或类型变化;模型层应尽量做到“坏数据不炸全局”。

### Fix Focus Areas
- src/api/models.py[170-192]
- src/api/models.py[229-233]

### Suggested change
- 在 `UserStatus.from_dict` 中:
 - `level_info_raw = data.get("level_info")`; 若非 dict 则 `{}`
 - `counters_raw = data.get("counters")`; 若非 dict 则 `{}`
- 在 `DashboardData.from_dict` 中:
 - 解析 `streak_promotion` 前先校验其为 dict。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unscoped Cookie header 🐞 Bug ⛨ Security
Description
DashboardClient 通过 context.cookies() 获取同一浏览器上下文的所有 Cookie 并手动拼接为单个 Cookie header 发往
rewards.bing.com,这会绕过浏览器的同源 Cookie 约束,存在跨域 Cookie 泄露风险。建议仅发送 rewards.bing.com 域相关 Cookie。
Code

src/api/dashboard_client.py[R86-112]

+    async def _get_cookies_header(self) -> str:
+        """
+        从 Page context 获取 cookies 字符串
+
+        Returns:
+            cookies 字符串
+        """
+        cookies = await self._page.context.cookies()
+        return "; ".join(f"{c['name']}={c['value']}" for c in cookies)
+
+    async def _call_api(self) -> DashboardData:
+        """
+        调用 Dashboard API
+
+        Returns:
+            DashboardData 对象
+
+        Raises:
+            DashboardError: API 调用失败
+        """
+        headers = {
+            "Referer": REWARDS_URLS["dashboard"],
+            "Cookie": await self._get_cookies_header(),
+            "Accept": "application/json",
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+        }
+
Evidence
代码未对 cookies 做任何 domain/path
过滤;而项目常量中包含多个不同域名(bing.com、login.live.com、rewards.microsoft.com、rewards.bing.com)。在同一 Playwright
context 下很可能同时存在多域 Cookie,被拼接后会全部发送到 rewards.bing.com。

src/api/dashboard_client.py[86-111]
src/constants/urls.py[20-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
当前实现把 Playwright context 中的所有 cookie 拼接成 `Cookie` header 发送到 `rewards.bing.com`,会把非目标域 cookie 一并发送,存在跨域 cookie 泄露风险。
### Issue Context
项目会访问多个域(`www.bing.com` / `login.live.com` / `rewards.microsoft.com` / `rewards.bing.com`),同一 context 下可能同时存在这些域的 cookie。
### Fix Focus Areas
- src/api/dashboard_client.py[86-112]
- src/constants/urls.py[20-44]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (8)
4. Broad Exception in points 📘 Rule violation ⛯ Reliability
Description
get_current_points() catches Exception and silently falls back to HTML parsing, which can mask
programming/logic errors and make failures harder to diagnose. This reduces reliability by treating
unexpected exceptions as normal control flow.
Code

src/account/points_detector.py[R103-104]

+            except Exception as e:
+                logger.warning(f"API 调用异常: {e},使用 HTML 解析作为备用")
Evidence
The compliance checklist requires robust error handling without swallowing unexpected errors; the
added except Exception as e: logs a generic warning and continues with a fallback path.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/account/points_detector.py[93-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/account/points_detector.py` uses `except Exception` around the Dashboard API call and then falls back to HTML parsing. This can hide unexpected bugs and makes failures harder to debug.
## Issue Context
The fallback behavior should trigger only for known/expected failures (e.g., `DashboardError` or specific network/timeouts). Unexpected exceptions should be logged with stack trace and re-raised (or handled explicitly) rather than treated as a normal fallback.
## Fix Focus Areas
- src/account/points_detector.py[93-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Broad Exception in points 📘 Rule violation ⛯ Reliability
Description
get_current_points() catches Exception and silently falls back to HTML parsing, which can mask
programming/logic errors and make failures harder to diagnose. This reduces reliability by treating
unexpected exceptions as normal control flow.
Code

src/account/points_detector.py[R103-104]

+            except Exception as e:
+                logger.warning(f"API 调用异常: {e},使用 HTML 解析作为备用")
Evidence
The compliance checklist requires robust error handling without swallowing unexpected errors; the
added except Exception as e: logs a generic warning and continues with a fallback path.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/account/points_detector.py[93-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/account/points_detector.py` uses `except Exception` around the Dashboard API call and then falls back to HTML parsing. This can hide unexpected bugs and makes failures harder to debug.
## Issue Context
The fallback behavior should trigger only for known/expected failures (e.g., `DashboardError` or specific network/timeouts). Unexpected exceptions should be logged with stack trace and re-raised (or handled explicitly) rather than treated as a normal fallback.
## Fix Focus Areas
- src/account/points_detector.py[93-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. _html_fallback() catches Exception 📘 Rule violation ⛯ Reliability
Description
_html_fallback() catches Exception and returns None, potentially swallowing unexpected errors
and making failures non-actionable. This can prevent proper diagnosis of real parsing/logic bugs in
production.
Code

src/api/dashboard_client.py[R117-119]

+        except Exception as e:
+            logger.warning(f"HTML fallback failed: {e}")
+            return None
Evidence
The checklist requires robust error handling with meaningful context and avoiding swallowed
exceptions; the added broad exception handler logs a warning and suppresses the error by returning
None.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/api/dashboard_client.py[103-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DashboardClient._html_fallback()` catches all exceptions and returns `None`, which can hide real bugs and reduce diagnosability.
## Issue Context
HTML fallback should only suppress known/expected parsing failures. Unexpected exceptions should not be treated as a normal fallback outcome.
## Fix Focus Areas
- src/api/dashboard_client.py[103-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. _html_fallback() catches Exception 📘 Rule violation ⛯ Reliability
Description
_html_fallback() catches Exception and returns None, potentially swallowing unexpected errors
and making failures non-actionable. This can prevent proper diagnosis of real parsing/logic bugs in
production.
Code

src/api/dashboard_client.py[R117-119]

+        except Exception as e:
+            logger.warning(f"HTML fallback failed: {e}")
+            return None
Evidence
The checklist requires robust error handling with meaningful context and avoiding swallowed
exceptions; the added broad exception handler logs a warning and suppresses the error by returning
None.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/api/dashboard_client.py[103-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DashboardClient._html_fallback()` catches all exceptions and returns `None`, which can hide real bugs and reduce diagnosability.
## Issue Context
HTML fallback should only suppress known/expected parsing failures. Unexpected exceptions should not be treated as a normal fallback outcome.
## Fix Focus Areas
- src/api/dashboard_client.py[103-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. httpx未进核心依赖 🐞 Bug ⛯ Reliability
Description
DashboardClient 在运行时直接 import/使用 httpx,但 pyproject 的核心 dependencies 未包含 httpx;按文档/requirements
的“生产模式 pip install -e .”安装会在导入阶段触发 ImportError,导致积分检测功能不可用。
Code

src/api/dashboard_client.py[R9-10]

+import httpx
+from playwright.async_api import Page
Evidence
DashboardClient 模块级依赖 httpx;但 httpx 仅在 dev extra 中声明,核心 dependencies 缺失,生产安装(不带 extra)将无法导入该模块。

src/api/dashboard_client.py[5-12]
pyproject.toml[5-18]
pyproject.toml[20-27]
README.md[156-161]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DashboardClient` 在运行时依赖 `httpx`,但 `httpx` 当前只在 `dev` extra 中声明,导致生产安装(`pip install -e .`)导入 `api.dashboard_client` 直接失败。
### Issue Context
README/requirements 明确区分“核心依赖(用户模式)”与 “dev”。当前积分检测已在运行路径中 import `DashboardClient`。
### Fix Focus Areas
- pyproject.toml[5-28]
- src/api/dashboard_client.py[5-12]
- README.md[156-161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. httpx未进核心依赖 🐞 Bug ⛯ Reliability
Description
DashboardClient 在运行时直接 import/使用 httpx,但 pyproject 的核心 dependencies 未包含 httpx;按文档/requirements
的“生产模式 pip install -e .”安装会在导入阶段触发 ImportError,导致积分检测功能不可用。
Code

src/api/dashboard_client.py[R9-10]

+import httpx
+from playwright.async_api import Page
Evidence
DashboardClient 模块级依赖 httpx;但 httpx 仅在 dev extra 中声明,核心 dependencies 缺失,生产安装(不带 extra)将无法导入该模块。

src/api/dashboard_client.py[5-12]
pyproject.toml[5-18]
pyproject.toml[20-27]
README.md[156-161]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DashboardClient` 在运行时依赖 `httpx`,但 `httpx` 当前只在 `dev` extra 中声明,导致生产安装(`pip install -e .`)导入 `api.dashboard_client` 直接失败。
### Issue Context
README/requirements 明确区分“核心依赖(用户模式)”与 “dev”。当前积分检测已在运行路径中 import `DashboardClient`。
### Fix Focus Areas
- pyproject.toml[5-28]
- src/api/dashboard_client.py[5-12]
- README.md[156-161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. JSON/结构异常不触发fallback 🐞 Bug ⛯ Reliability
Description
_call_api 仅将 HTTPStatus/Timeout/RequestError 转为 DashboardError;当 response.json() 解码失败或返回结构非 dict
时会抛出 ValueError/TypeError,get_dashboard_data 只捕获 DashboardError,导致重试与 HTML fallback 全部失效并向上抛异常。
Code

src/api/dashboard_client.py[R66-75]

+        try:
+            async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
+                response = await client.get(self.API_URL, headers=headers)
+                response.raise_for_status()
+                data = response.json()
+                
+                if "dashboard" not in data:
+                    raise DashboardError("Invalid API response: missing 'dashboard' field")
+                
+                return self._parse_dashboard(data["dashboard"])
Evidence
response.json() 及后续的 "dashboard" in data/data["dashboard"] 依赖返回为 JSON dict,但异常未被包装为
DashboardError;而上层 get_dashboard_dataexcept DashboardError,因此这类异常会绕过 fallback。

src/api/dashboard_client.py[66-82]
src/api/dashboard_client.py[131-146]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_call_api()` 只捕获 httpx 的网络/状态码异常;`response.json()` 解码失败、响应不是 dict、`data["dashboard"]` 不是 dict、以及 `DashboardData.from_dict` 的解析异常都会以非 `DashboardError` 形式抛出,导致 `get_dashboard_data()` 的 fallback 逻辑失效。
### Issue Context
`get_dashboard_data()` 的异常处理只覆盖 `DashboardError`。为了达到“API 优先 + HTML fallback”的目标,应该保证 API 路径的所有可预期失败都统一归一化为 `DashboardError`。
### Fix Focus Areas
- src/api/dashboard_client.py[59-95]
- src/api/dashboard_client.py[121-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. JSON/结构异常不触发fallback 🐞 Bug ⛯ Reliability
Description
_call_api 仅将 HTTPStatus/Timeout/RequestError 转为 DashboardError;当 response.json() 解码失败或返回结构非 dict
时会抛出 ValueError/TypeError,get_dashboard_data 只捕获 DashboardError,导致重试与 HTML fallback 全部失效并向上抛异常。
Code

src/api/dashboard_client.py[R66-75]

+        try:
+            async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
+                response = await client.get(self.API_URL, headers=headers)
+                response.raise_for_status()
+                data = response.json()
+                
+                if "dashboard" not in data:
+                    raise DashboardError("Invalid API response: missing 'dashboard' field")
+                
+                return self._parse_dashboard(data["dashboard"])
Evidence
response.json() 及后续的 "dashboard" in data/data["dashboard"] 依赖返回为 JSON dict,但异常未被包装为
DashboardError;而上层 get_dashboard_dataexcept DashboardError,因此这类异常会绕过 fallback。

src/api/dashboard_client.py[66-82]
src/api/dashboard_client.py[131-146]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_call_api()` 只捕获 httpx 的网络/状态码异常;`response.json()` 解码失败、响应不是 dict、`data["dashboard"]` 不是 dict、以及 `DashboardData.from_dict` 的解析异常都会以非 `DashboardError` 形式抛出,导致 `get_dashboard_data()` 的 fallback 逻辑失效。
### Issue Context
`get_dashboard_data()` 的异常处理只覆盖 `DashboardError`。为了达到“API 优先 + HTML fallback”的目标,应该保证 API 路径的所有可预期失败都统一归一化为 `DashboardError`。
### Fix Focus Areas
- src/api/dashboard_client.py[59-95]
- src/api/dashboard_client.py[121-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

12. Cookie filtering may omit 🐞 Bug ⛯ Reliability ⭐ New
Description
Cookie Header 仅允许 rewards.bing.com/.rewards.bing.com 的严格相等匹配,可能遗漏对 rewards.bing.com 同样生效的父域
Cookie(例如 .bing.com)。一旦认证关键 Cookie 落在父域上,API 会更易出现 401/403 并退化为频繁走 HTML fallback。
Code

src/api/dashboard_client.py[R106-115]

+        all_cookies = await self._page.context.cookies()
+        api_domain = urlparse(self._api_url).netloc
+
+        allowed_domains = {
+            api_domain,
+            f".{api_domain}",
+        }
+
+        filtered_cookies = [c for c in all_cookies if c["domain"] in allowed_domains]
+        return "; ".join(f"{c['name']}={c['value']}" for c in filtered_cookies)
Evidence
DashboardClient 的过滤逻辑只保留 domain 完全等于 api_domain 或 .api_domain 的 cookie;仓库中已有为 .bing.com 设置 cookie
的逻辑,说明运行时确实可能存在父域 cookie,而 rewards.bing.com 是 bing.com 的子域,父域 cookie 在浏览器语义下通常同样适用。当前单测也把“排除
bing.com cookie”固化为预期,这会掩盖未来真实认证依赖父域 cookie 时的失败。

src/api/dashboard_client.py[106-115]
src/browser/simulator.py[346-357]
tests/unit/test_dashboard_client.py[527-546]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_get_cookies_header()` 采用“domain 严格相等”的过滤方式,可能遗漏对 `https://rewards.bing.com/...` 同样适用的父域 cookies(如 `.bing.com`)。若认证/会话关键 cookie 位于父域,将导致 API 调用更容易失败并退化为 HTML fallback。

### Issue Context
你们的目标是提升可靠性/性能;如果 cookie 选择不完整,API 路径可能长期不可用。

### Fix Focus Areas
- src/api/dashboard_client.py[95-116]
- tests/unit/test_dashboard_client.py[527-546]

### Suggested change
- 优先改为使用 Playwright 的 URL 作用域 cookie 选择:例如 `await self._page.context.cookies([self._api_url])`(或等价的 urls 参数形式),让 Playwright 按浏览器规则返回“对该 URL 生效”的 cookies。
- 如果仍要手动过滤,使用 suffix 匹配(cookie_domain 去掉前导点后为 api_domain 的后缀)而不是严格相等。
- 更新/新增单测覆盖:父域 cookie(如 `.bing.com`)在目标 URL 上应被包含(若采用 URL 作用域 cookies)。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. get_current_points() swallows errors 📘 Rule violation ✓ Correctness
Description
get_current_points() suppresses DashboardError and returns None without logging, creating
silent failures and losing debugging context. This makes it difficult to understand why API
retrieval failed when investigating issues.
Code

src/api/dashboard_client.py[R162-166]

+        try:
+            data = await self.get_dashboard_data()
+            return data.user_status.available_points
+        except DashboardError:
+            return None
Evidence
The checklist forbids silent failures; the new method catches DashboardError and returns None
without any log/metrics, removing actionable context about the failure.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/api/dashboard_client.py[155-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DashboardClient.get_current_points()` catches `DashboardError` and returns `None` without logging, causing silent failures.
## Issue Context
Callers need enough context to debug API failures (e.g., which failure occurred, whether fallback was used, etc.).
## Fix Focus Areas
- src/api/dashboard_client.py[155-166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Points logged at info 📘 Rule violation ⛨ Security
Description
New log lines emit the user’s current points value, which can be considered sensitive account data
and may violate the no-sensitive-data-in-logs requirement. If logs are shared (support bundles/CI
artifacts), this can leak per-account information.
Code

src/account/points_detector.py[R99-104]

+                    api_points: int | None = await asyncio.wait_for(
+                        client.get_current_points(), timeout=15.0
+                    )
+                if api_points is not None and api_points >= 0:
+                    logger.info(f"✓ 从 API 获取积分: {api_points:,}")
+                    return int(api_points)
Evidence
The secure logging rule prohibits logging sensitive/personal user information; the added code logs
the exact points value (api_points) at info level during normal operation.

Rule 5: Generic: Secure Logging Practices
src/account/points_detector.py[99-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New code logs the exact current points value, which may be sensitive account information under secure logging requirements.
## Issue Context
`PointsDetector.get_current_points()` now prefers the Dashboard API and logs the retrieved points at `info` level.
## Fix Focus Areas
- src/account/points_detector.py[99-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (9)
15. 401/403判断过于脆弱 🐞 Bug ⛯ Reliability
Description
会话过期逻辑通过匹配 DashboardError 文本包含 “HTTP error: 401/403” 来触发 fallback,该判断与错误字符串格式强耦合,未来改动错误信息或包装方式时容易失效。
Code

src/api/dashboard_client.py[R134-140]

+            except DashboardError as e:
+                if "HTTP error: 401" in str(e) or "HTTP error: 403" in str(e):
+                    # 会话过期,直接 fallback
+                    fallback_data = await self._html_fallback()
+                    if fallback_data:
+                        return fallback_data
+                    raise
Evidence
_call_api() 把状态码编码进异常字符串;get_dashboard_data() 再通过 substring 判断 401/403,属于非结构化契约,维护成本高且易回归。

src/api/dashboard_client.py[77-82]
src/api/dashboard_client.py[134-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
401/403 的 fallback 触发条件依赖异常字符串内容,属于脆弱实现。
### Issue Context
当前 `_call_api()` 已经拿到了 `e.response.status_code`,但在包装时丢失了结构化信息。
### Fix Focus Areas
- src/api/dashboard_client.py[66-82]
- src/api/dashboard_client.py[131-141]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. DashboardError lacks API context 📘 Rule violation ⛯ Reliability
Description
Several raised DashboardError messages omit key operational context (e.g., URL, attempt/retry
number), making failures harder to diagnose and reducing the actionability of logs and exceptions.
This weakens robustness for production debugging and edge-case investigation.
Code

src/api/dashboard_client.py[R130-147]

+            except httpx.HTTPStatusError as e:
+                raise DashboardError(
+                    f"HTTP error: {e.response.status_code}", status_code=e.response.status_code
+                ) from e
+            except httpx.TimeoutException as e:
+                if attempt == 0:
+                    logger.debug("Request timeout, retrying once...")
+                    last_error = e
+                    continue
+                raise DashboardError("API timeout") from e
+            except httpx.RequestError as e:
+                if attempt == 0:
+                    logger.debug(f"Network error, retrying once: {e}")
+                    last_error = e
+                    continue
+                raise DashboardError(f"Network error: {e}") from last_error
+            except (json.JSONDecodeError, TypeError, KeyError, ValueError) as e:
+                raise DashboardError(f"Parse error: {e}") from e
Evidence
The robust error handling rule requires meaningful context about what failed and why; the new
exceptions frequently only include generic labels like HTTP error:  / API timeout / `Parse
error: <err>` without indicating which operation/endpoint/attempt failed.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/api/dashboard_client.py[130-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Raised `DashboardError` messages are missing actionable context like which endpoint was called and which attempt failed, reducing debuggability.
## Issue Context
`DashboardClient._call_api()` wraps multiple failure modes but emits relatively context-poor error messages.
## Fix Focus Areas
- src/api/dashboard_client.py[130-147]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. dataclass解包易被额外字段击穿 🐞 Bug ⛯ Reliability
Description
多个模型 from_dict 直接 cls(**data),一旦 API/HTML 中包含未在 dataclass 声明的键(后端扩展/字段新增很常见),会抛出 TypeError
导致解析失败,降低“API 优先”的稳定性。
Code

src/api/models.py[R29-34]

+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> 'SearchCounter':
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        return cls(**data)
+
Evidence
SearchCounter/LevelInfo/Promotion/PunchCard 等叶子模型直接用 cls(**data),对输入字段集合要求严格;DashboardClient
会直接把 API/HTML 解析结果交给 DashboardData.from_dict,因此一旦出现额外键就会在解析链路中爆炸。

src/api/models.py[29-34]
src/api/models.py[59-64]
src/api/models.py[75-80]
src/api/dashboard_client.py[84-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
当前模型解析对输入字段集要求过严,容易因 API/HTML 增量字段导致 `TypeError: unexpected keyword argument`。
### Issue Context
代码库里也存在更稳健的 `from_dict` 风格(显式读取需要的键并提供默认值)。该模块建议采用类似策略或统一过滤未知字段。
### Fix Focus Areas
- src/api/models.py[7-20]
- src/api/models.py[23-95]
- src/api/models.py[97-143]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. Inconsistent retry semantics 🐞 Bug ⛯ Reliability
Description
DashboardClient 暴露 max_retries 参数,但超时/网络错误的重试次数在 _call_api() 中被硬编码为“最多再试一次”,与 get_dashboard_data() 对
5xx 的可配置重试形成不一致语义。该不一致会让调用方对 max_retries 的实际效果产生误解,难以通过配置精确控制请求次数/耗时。
Code

src/api/dashboard_client.py[R113-147]

+        last_error: Exception | None = None
+        for attempt in range(2):
+            try:
+                client = await self._get_client()
+                response = await client.get(self._api_url, headers=headers)
+                response.raise_for_status()
+                data = response.json()
+
+                if not isinstance(data, dict):
+                    raise DashboardError("Invalid API response: not a dict")
+                if "dashboard" not in data:
+                    raise DashboardError("Invalid API response: missing 'dashboard' field")
+                if not isinstance(data["dashboard"], dict):
+                    raise DashboardError("Invalid API response: 'dashboard' is not a dict")
+
+                return self._parse_dashboard(data["dashboard"])
+
+            except httpx.HTTPStatusError as e:
+                raise DashboardError(
+                    f"HTTP error: {e.response.status_code}", status_code=e.response.status_code
+                ) from e
+            except httpx.TimeoutException as e:
+                if attempt == 0:
+                    logger.debug("Request timeout, retrying once...")
+                    last_error = e
+                    continue
+                raise DashboardError("API timeout") from e
+            except httpx.RequestError as e:
+                if attempt == 0:
+                    logger.debug(f"Network error, retrying once: {e}")
+                    last_error = e
+                    continue
+                raise DashboardError(f"Network error: {e}") from last_error
+            except (json.JSONDecodeError, TypeError, KeyError, ValueError) as e:
+                raise DashboardError(f"Parse error: {e}") from e
Evidence
类初始化保存了 _max_retries 作为“最大重试次数”,但 _call_api 对 Timeout/RequestError 使用固定 range(2);同时
get_dashboard_data 才使用 _max_retries 处理 5xx。这意味着不同错误类型下的重试策略受不同机制控制。

src/api/dashboard_client.py[37-65]
src/api/dashboard_client.py[113-147]
src/api/dashboard_client.py[204-220]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`max_retries` 的配置语义在不同错误类型下不一致:5xx 由 `get_dashboard_data()` 按 `_max_retries` 控制,但 timeout/network 在 `_call_api()` 内部固定只重试一次,导致调用方无法通过配置统一控制总尝试次数与耗时。
### Issue Context
当前实现存在两套重试机制(`_call_api` vs `get_dashboard_data`),需要统一或显式区分配置项。
### Fix Focus Areas
- src/api/dashboard_client.py[37-65]
- src/api/dashboard_client.py[96-151]
- src/api/dashboard_client.py[194-228]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


19. isort未识别api包 🐞 Bug ⛯ Reliability
Description
新增顶层包 api 并在代码中使用 from api...,但 Ruff isort 的 known-first-party 未包含 api,可能导致 import 分组/排序在 CI
或本地 lint 中不一致甚至失败。
Code

src/account/points_detector.py[R11-12]

+from api.dashboard_client import DashboardClient, DashboardError
+
Evidence
代码已按第一方包方式导入 api,但 isort 配置未把 api 识别为 first-party,后续执行 ruff/isort 可能对 import 进行不期望的重排或报错(取决于规则)。

src/account/points_detector.py[9-13]
pyproject.toml[87-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Ruff isort 未将新包 `api` 标记为 first-party,可能造成 lint/import-order 不稳定。
### Issue Context
当前 pytest 配置将 `src` 加入 pythonpath,因此 `api` 是项目内顶层包。
### Fix Focus Areas
- pyproject.toml[87-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


20. get_current_points() swallows errors 📘 Rule violation ✓ Correctness
Description
get_current_points() suppresses DashboardError and returns None without logging, creating
silent failures and losing debugging context. This makes it difficult to understand why API
retrieval failed when investigating issues.
Code

src/api/dashboard_client.py[R162-166]

+        try:
+            data = await self.get_dashboard_data()
+            return data.user_status.available_points
+        except DashboardError:
+            return None
Evidence
The checklist forbids silent failures; the new method catches DashboardError and returns None
without any log/metrics, removing actionable context about the failure.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/api/dashboard_client.py[155-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DashboardClient.get_current_points()` catches `DashboardError` and returns `None` without logging, causing silent failures.
## Issue Context
Callers need enough context to debug API failures (e.g., which failure occurred, whether fallback was used, etc.).
## Fix Focus Areas
- src/api/dashboard_client.py[155-166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


21. 401/403判断过于脆弱 🐞 Bug ⛯ Reliability
Description
会话过期逻辑通过匹配 DashboardError 文本包含 “HTTP error: 401/403” 来触发 fallback,该判断与错误字符串格式强耦合,未来改动错误信息或包装方式时容易失效。
Code

src/api/dashboard_client.py[R134-140]

+            except DashboardError as e:
+                if "HTTP error: 401" in str(e) or "HTTP error: 403" in str(e):
+                    # 会话过期,直接 fallback
+                    fallback_data = await self._html_fallback()
+                    if fallback_data:
+                        return fallback_data
+                    raise
Evidence
_call_api() 把状态码编码进异常字符串;get_dashboard_data() 再通过 substring 判断 401/403,属于非结构化契约,维护成本高且易回归。

src/api/dashboard_client.py[77-82]
src/api/dashboard_client.py[134-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
401/403 的 fallback 触发条件依赖异常字符串内容,属于脆弱实现。
### Issue Context
当前 `_call_api()` 已经拿到了 `e.response.status_code`,但在包装时丢失了结构化信息。
### Fix Focus Areas
- src/api/dashboard_client.py[66-82]
- src/api/dashboard_client.py[131-141]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


22. dataclass解包易被额外字段击穿 🐞 Bug ⛯ Reliability
Description
多个模型 from_dict 直接 cls(**data),一旦 API/HTML 中包含未在 dataclass 声明的键(后端扩展/字段新增很常见),会抛出 TypeError
导致解析失败,降低“API 优先”的稳定性。
Code

src/api/models.py[R29-34]

+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> 'SearchCounter':
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        return cls(**data)
+
Evidence
SearchCounter/LevelInfo/Promotion/PunchCard 等叶子模型直接用 cls(**data),对输入字段集合要求严格;DashboardClient
会直接把 API/HTML 解析结果交给 DashboardData.from_dict,因此一旦出现额外键就会在解析链路中爆炸。

src/api/models.py[29-34]
src/api/models.py[59-64]
src/api/models.py[75-80]
src/api/dashboard_client.py[84-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
当前模型解析对输入字段集要求过严,容易因 API/HTML 增量字段导致 `TypeError: unexpected keyword argument`。
### Issue Context
代码库里也存在更稳健的 `from_dict` 风格(显式读取需要的键并提供默认值)。该模块建议采用类似策略或统一过滤未知字段。
### Fix Focus Areas
- src/api/models.py[7-20]
- src/api/models.py[23-95]
- src/api/models.py[97-143]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


23. isort未识别api包 🐞 Bug ⛯ Reliability
Description
新增顶层包 api 并在代码中使用 from api...,但 Ruff isort 的 known-first-party 未包含 api,可能导致 import 分组/排序在 CI
或本地 lint 中不一致甚至失败。
Code

src/account/points_detector.py[R11-12]

+from api.dashboard_client import DashboardClient, DashboardError
+
Evidence
代码已按第一方包方式导入 api,但 isort 配置未把 api 识别为 first-party,后续执行 ruff/isort 可能对 import 进行不期望的重排或报错(取决于规则)。

src/account/points_detector.py[9-13]
pyproject.toml[87-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Ruff isort 未将新包 `api` 标记为 first-party,可能造成 lint/import-order 不稳定。
### Issue Context
当前 pytest 配置将 `src` 加入 pythonpath,因此 `api` 是项目内顶层包。
### Fix Focus Areas
- pyproject.toml[87-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

24. 单测因重试真实sleep变慢 🐞 Bug ➹ Performance
Description
重试逻辑使用 await asyncio.sleep(RETRY_DELAY=1.0);多条单测用例会触发 2 次重试,从而真实等待约 2 秒/用例,累计显著拉长单测耗时并增加 CI 波动。
Code

src/api/dashboard_client.py[R142-145]

+                if attempt < self.MAX_RETRIES:
+                    logger.warning(f"API attempt {attempt + 1} failed: {e}, retrying...")
+                    await asyncio.sleep(self.RETRY_DELAY)
+                    continue
Evidence
DashboardClient 的默认 RETRY_DELAY 为 1 秒且在失败时必 sleep;测试中多处构造 500/超时/网络错误场景会走重试分支,因此会产生真实等待。

src/api/dashboard_client.py[25-28]
src/api/dashboard_client.py[142-145]
tests/unit/test_dashboard_client.py[157-169]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
单测触发重试逻辑时会执行真实 `asyncio.sleep(1.0)`,导致测试变慢。
### Issue Context
这些测试场景主要验证分支逻辑,不需要真实等待。
### Fix Focus Areas
- src/api/dashboard_client.py[25-28]
- src/api/dashboard_client.py[131-145]
- tests/unit/test_dashboard_client.py[157-210]
- tests/unit/test_dashboard_client.py[242-292]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


25. 单测因重试真实sleep变慢 🐞 Bug ➹ Performance
Description
重试逻辑使用 await asyncio.sleep(RETRY_DELAY=1.0);多条单测用例会触发 2 次重试,从而真实等待约 2 秒/用例,累计显著拉长单测耗时并增加 CI 波动。
Code

src/api/dashboard_client.py[R142-145]

+                if attempt < self.MAX_RETRIES:
+                    logger.warning(f"API attempt {attempt + 1} failed: {e}, retrying...")
+                    await asyncio.sleep(self.RETRY_DELAY)
+                    continue
Evidence
DashboardClient 的默认 RETRY_DELAY 为 1 秒且在失败时必 sleep;测试中多处构造 500/超时/网络错误场景会走重试分支,因此会产生真实等待。

src/api/dashboard_client.py[25-28]
src/api/dashboard_client.py[142-145]
tests/unit/test_dashboard_client.py[157-169]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
单测触发重试逻辑时会执行真实 `asyncio.sleep(1.0)`,导致测试变慢。
### Issue Context
这些测试场景主要验证分支逻辑,不需要真实等待。
### Fix Focus Areas
- src/api/dashboard_client.py[25-28]
- src/api/dashboard_client.py[131-145]
- tests/unit/test_dashboard_client.py[157-210]
- tests/unit/test_dashboard_client.py[242-292]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Grey Divider

Previous review results

Review updated until commit 156d234

Results up to commit 073fc9d


🐞 Bugs (8) 📘 Rule violations (5) 📎 Requirement gaps (0)

Grey Divider
Action required
1. Unscoped Cookie header 🐞 Bug ⛨ Security ⭐ New
Description
DashboardClient 通过 context.cookies() 获取同一浏览器上下文的所有 Cookie 并手动拼接为单个 Cookie header 发往
rewards.bing.com,这会绕过浏览器的同源 Cookie 约束,存在跨域 Cookie 泄露风险。建议仅发送 rewards.bing.com 域相关 Cookie。
Code

src/api/dashboard_client.py[R86-112]

+    async def _get_cookies_header(self) -> str:
+        """
+        从 Page context 获取 cookies 字符串
+
+        Returns:
+            cookies 字符串
+        """
+        cookies = await self._page.context.cookies()
+        return "; ".join(f"{c['name']}={c['value']}" for c in cookies)
+
+    async def _call_api(self) -> DashboardData:
+        """
+        调用 Dashboard API
+
+        Returns:
+            DashboardData 对象
+
+        Raises:
+            DashboardError: API 调用失败
+        """
+        headers = {
+            "Referer": REWARDS_URLS["dashboard"],
+            "Cookie": await self._get_cookies_header(),
+            "Accept": "application/json",
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+        }
+
Evidence
代码未对 cookies 做任何 domain/path
过滤;而项目常量中包含多个不同域名(bing.com、login.live.com、rewards.microsoft.com、rewards.bing.com)。在同一 Playwright
context 下很可能同时存在多域 Cookie,被拼接后会全部发送到 rewards.bing.com。

src/api/dashboard_client.py[86-111]
src/constants/urls.py[20-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
当前实现把 Playwright context 中的所有 cookie 拼接成 `Cookie` header 发送到 `rewards.bing.com`,会把非目标域 cookie 一并发送,存在跨域 cookie 泄露风险。

### Issue Context
项目会访问多个域(`www.bing.com` / `login.live.com` / `rewards.microsoft.com` / `rewards.bing.com`),同一 context 下可能同时存在这些域的 cookie。

### Fix Focus Areas
- src/api/dashboard_client.py[86-112]
- src/constants/urls.py[20-44]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Broad Exception in points 📘 Rule vi...

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

该 PR 引入 Dashboard API 客户端,优先通过 Rewards Dashboard API 拉取积分/计数器数据,并在失败时回退到 HTML 解析,以提升获取积分数据的稳定性与性能。

Changes:

  • 新增 src/api/ 模块:DashboardClient + 数据模型(dataclass)封装 API/HTML 数据解析
  • PointsDetector.get_current_points 中集成 API 优先读取积分,失败回退到现有 HTML 逻辑
  • 新增单元测试并引入 respx 作为 HTTPX mock 依赖

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
tests/unit/test_dashboard_client.py 增加 DashboardClient 与模型解析的单测覆盖(含重试/fallback)
src/api/models.py 定义 dashboard 响应的 dataclass 模型与 from_dict 解析逻辑
src/api/dashboard_client.py 实现 API 调用 + 重试 + 401/403 处理 + HTML fallback 的客户端
src/api/init.py 暴露 API 模块公共导出
src/account/points_detector.py 积分检测逻辑改为 API 优先,HTML 作为备用
pyproject.toml 新增 respx 测试依赖

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 25, 2026

Reviewer's Guide

在新的 api 包下引入了带类型标注、可复用的 Dashboard API 客户端和数据模型,将其接入积分检测器,采用“优先使用 API、回退到 HTML”的策略,收紧任务状态解析逻辑,并新增 httpx/respx 依赖以及支持该集成所需的测试和配置更新。

Dashboard API 模型和客户端的类图

classDiagram
    class DashboardError {
        +int status_code
        +__init__(message: str, status_code: int)
        +is_auth_error() bool
    }

    class DashboardClient {
        -Page _page
        -int _max_retries
        -float _retry_delay
        -float _timeout
        -AsyncClient _client
        -str _api_url
        +DEFAULT_MAX_RETRIES: int
        +DEFAULT_RETRY_DELAY: float
        +DEFAULT_TIMEOUT: float
        +__init__(page: Page, max_retries: int, retry_delay: float, timeout: float)
        +close() None
        +__aenter__() DashboardClient
        +__aexit__(exc_type: type, exc_val: BaseException, exc_tb: object) None
        -_get_cookies_header() str
        -_call_api() DashboardData
        -_parse_dashboard(data: dict~str, object~) DashboardData
        -_html_fallback() DashboardData
        +get_dashboard_data() DashboardData
        +get_current_points() int
        +get_search_counters() SearchCounters
    }

    class DashboardData {
        +UserStatus user_status
        +dict~str, list~Promotion~~ daily_set_promotions
        +list~Promotion~ more_promotions
        +list~PunchCard~ punch_cards
        +StreakPromotion streak_promotion
        +list~StreakBonusPromotion~ streak_bonus_promotions
        +from_dict(data: dict~str, Any~) DashboardData
    }

    class UserStatus {
        +int available_points
        +LevelInfo level_info
        +SearchCounters counters
        +int bing_star_monthly_bonus_progress
        +int bing_star_monthly_bonus_maximum
        +int default_search_engine_monthly_bonus_progress
        +int default_search_engine_monthly_bonus_maximum
        +str default_search_engine_monthly_bonus_state
        +from_dict(data: dict~str, Any~) UserStatus
    }

    class LevelInfo {
        +str active_level
        +str active_level_name
        +int progress
        +int progress_max
        +from_dict(data: dict~str, Any~) LevelInfo
    }

    class SearchCounter {
        +int progress
        +int max_progress
        +from_dict(data: dict~str, Any~) SearchCounter
    }

    class SearchCounters {
        +list~SearchCounter~ pc_search
        +list~SearchCounter~ mobile_search
        +from_dict(data: dict~str, Any~) SearchCounters
    }

    class Promotion {
        +str promotion_type
        +str title
        +int points
        +str status
        +str url
        +from_dict(data: dict~str, Any~) Promotion
    }

    class PunchCard {
        +str name
        +int progress
        +int max_progress
        +bool completed
        +from_dict(data: dict~str, Any~) PunchCard
    }

    class StreakPromotion {
        +str promotion_type
        +str title
        +int points
        +str status
        +str url
        +int streak_count
        +from_dict(data: dict~str, Any~) StreakPromotion
    }

    class StreakBonusPromotion {
        +str promotion_type
        +str title
        +int points
        +str status
        +str url
        +int streak_day
        +from_dict(data: dict~str, Any~) StreakBonusPromotion
    }

    class PointsDetector {
        +get_current_points(page: Page, skip_navigation: bool) int
        -_check_task_status(page: Page, selectors: list, task_name: str) dict~str, object~
        -_extract_points_from_source(page: Page) int
        -_parse_points(points_text: str) int
    }

    DashboardClient --> DashboardError : raises
    DashboardClient --> DashboardData : returns
    DashboardClient --> SearchCounters : returns
    DashboardData --> UserStatus
    DashboardData --> Promotion
    DashboardData --> PunchCard
    DashboardData --> StreakPromotion
    DashboardData --> StreakBonusPromotion
    UserStatus --> LevelInfo
    UserStatus --> SearchCounters
    SearchCounters --> SearchCounter
    PointsDetector --> DashboardClient : uses
    PointsDetector ..> DashboardError : handles
Loading

文件级变更

Change Details Files
Add typed Dashboard API client with retry, timeout, and HTML fallback and expose it via the new api package.
  • 使用 httpx.AsyncClient 实现 DashboardClient,支持可配置的最大重试次数、重试延迟以及每个请求的超时时间。
  • 基于常量 API_ENDPOINTS/API_PARAMS 构建 API URL,并从 Playwright 的 Page 上下文中构造 Cookie/Referer 头,仅保留 API 域名匹配的 Cookie。
  • 在进行 API 调用时,解析并校验 JSON 结构,将其转换为 DashboardData,并将 HTTP/网络/解析错误映射为 DashboardError,支持鉴别认证错误及相应重试语义。
  • 提供 get_dashboard_dataget_current_pointsget_search_counters 这些高层封装方法,在 API 失败时回退到 HTML 抽取;同时支持异步上下文管理器和显式 close() 调用。
  • api.__init__ 中重新导出 DashboardClientDashboardError 以及主要数据模型,以便于作为一方依赖进行导入。
src/api/dashboard_client.py
src/api/__init__.py
Introduce dataclass-based Dashboard models with camelCase-to-snake_case transformation and robust handling of partial/invalid data.
  • SearchCounterSearchCountersLevelInfoPromotionPunchCardStreakPromotionStreakBonusPromotionUserStatusDashboardData 定义为 dataclass,以表示 dashboard 的模式(schema)。
  • 实现辅助工具,递归地将 camelCase 键转换为 snake_case,并将字典过滤为仅包含 dataclass 声明字段。
  • 使 from_dict 构造函数在处理缺失字段、null 值、非列表值以及多余未知字段时具备鲁棒性,同时在 counters 和 promotions 等嵌套结构上尽量保持类型安全。
src/api/models.py
Integrate DashboardClient into points detection with API‑first strategy and tighten HTML parsing and task status typing.
  • PointsDetector.get_current_points 中,通过 asyncio.wait_for 调用 DashboardClient,当 API 返回非负积分时优先使用该值;在出现 TimeoutErrorDashboardError 时回退到现有的 HTML 解析逻辑。
  • 对 HTML 解析进行小幅加固:在解析积分前先裁剪文本,并在得到一个合理值(≥100)时简化成功日志输出。
  • 强化 _check_task_status 的类型标注,使用带类型的状态字典,并在判断任务完成状态时使用局部变量保存解析后的 progress/max_progress
src/account/points_detector.py
Extend build and tooling configuration to support the new API client and keep import style consistent.
  • pyproject.toml 中添加 httpx 作为运行时依赖、respx 作为开发依赖,用于 HTTP 客户端使用及测试中的 mock。
  • 更新 Ruff isort 的 known_first_party 列表,将新的 apiconstants 包加入其中,以保持导入风格一致。
pyproject.toml
Add comprehensive unit tests for DashboardClient and document the dashboard integration task.
  • DashboardClient 编写大规模单元测试,覆盖成功调用、4xx/5xx 处理、重试行为、超时、网络错误、JSON/结构/字段问题、HTML 回退成功/失败、Cookie 过滤,以及在出错时便利方法返回 None 等场景。
  • 新增 CURRENT_TASK.md,描述 Dashboard API 集成的目标、设计、任务和该功能的验收标准。
tests/unit/test_dashboard_client.py
CURRENT_TASK.md

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

Introduces a typed, reusable Dashboard API client and data models under a new api package, wires it into the points detector with an API‑first, HTML‑fallback strategy, tightens task status parsing, and adds httpx/respx plus tests and configuration updates needed to support the new integration.

Class diagram for Dashboard API models and client

classDiagram
    class DashboardError {
        +int status_code
        +__init__(message: str, status_code: int)
        +is_auth_error() bool
    }

    class DashboardClient {
        -Page _page
        -int _max_retries
        -float _retry_delay
        -float _timeout
        -AsyncClient _client
        -str _api_url
        +DEFAULT_MAX_RETRIES: int
        +DEFAULT_RETRY_DELAY: float
        +DEFAULT_TIMEOUT: float
        +__init__(page: Page, max_retries: int, retry_delay: float, timeout: float)
        +close() None
        +__aenter__() DashboardClient
        +__aexit__(exc_type: type, exc_val: BaseException, exc_tb: object) None
        -_get_cookies_header() str
        -_call_api() DashboardData
        -_parse_dashboard(data: dict~str, object~) DashboardData
        -_html_fallback() DashboardData
        +get_dashboard_data() DashboardData
        +get_current_points() int
        +get_search_counters() SearchCounters
    }

    class DashboardData {
        +UserStatus user_status
        +dict~str, list~Promotion~~ daily_set_promotions
        +list~Promotion~ more_promotions
        +list~PunchCard~ punch_cards
        +StreakPromotion streak_promotion
        +list~StreakBonusPromotion~ streak_bonus_promotions
        +from_dict(data: dict~str, Any~) DashboardData
    }

    class UserStatus {
        +int available_points
        +LevelInfo level_info
        +SearchCounters counters
        +int bing_star_monthly_bonus_progress
        +int bing_star_monthly_bonus_maximum
        +int default_search_engine_monthly_bonus_progress
        +int default_search_engine_monthly_bonus_maximum
        +str default_search_engine_monthly_bonus_state
        +from_dict(data: dict~str, Any~) UserStatus
    }

    class LevelInfo {
        +str active_level
        +str active_level_name
        +int progress
        +int progress_max
        +from_dict(data: dict~str, Any~) LevelInfo
    }

    class SearchCounter {
        +int progress
        +int max_progress
        +from_dict(data: dict~str, Any~) SearchCounter
    }

    class SearchCounters {
        +list~SearchCounter~ pc_search
        +list~SearchCounter~ mobile_search
        +from_dict(data: dict~str, Any~) SearchCounters
    }

    class Promotion {
        +str promotion_type
        +str title
        +int points
        +str status
        +str url
        +from_dict(data: dict~str, Any~) Promotion
    }

    class PunchCard {
        +str name
        +int progress
        +int max_progress
        +bool completed
        +from_dict(data: dict~str, Any~) PunchCard
    }

    class StreakPromotion {
        +str promotion_type
        +str title
        +int points
        +str status
        +str url
        +int streak_count
        +from_dict(data: dict~str, Any~) StreakPromotion
    }

    class StreakBonusPromotion {
        +str promotion_type
        +str title
        +int points
        +str status
        +str url
        +int streak_day
        +from_dict(data: dict~str, Any~) StreakBonusPromotion
    }

    class PointsDetector {
        +get_current_points(page: Page, skip_navigation: bool) int
        -_check_task_status(page: Page, selectors: list, task_name: str) dict~str, object~
        -_extract_points_from_source(page: Page) int
        -_parse_points(points_text: str) int
    }

    DashboardClient --> DashboardError : raises
    DashboardClient --> DashboardData : returns
    DashboardClient --> SearchCounters : returns
    DashboardData --> UserStatus
    DashboardData --> Promotion
    DashboardData --> PunchCard
    DashboardData --> StreakPromotion
    DashboardData --> StreakBonusPromotion
    UserStatus --> LevelInfo
    UserStatus --> SearchCounters
    SearchCounters --> SearchCounter
    PointsDetector --> DashboardClient : uses
    PointsDetector ..> DashboardError : handles
Loading

File-Level Changes

Change Details Files
Add typed Dashboard API client with retry, timeout, and HTML fallback and expose it via the new api package.
  • Implement DashboardClient using httpx.AsyncClient with configurable max retries, retry delay, and per‑request timeout.
  • Build API URL from constants API_ENDPOINTS/API_PARAMS and construct Cookie/Referer headers from the Playwright Page context, filtering cookies by API domain.
  • On API calls, parse and validate JSON structure, convert it to DashboardData, and map HTTP/network/parse errors into DashboardError with auth‑error detection and retry semantics.
  • Provide high‑level helpers get_dashboard_data, get_current_points, and get_search_counters that fall back to HTML extraction on API failures, plus async context manager and explicit close() handling.
  • Re‑export DashboardClient, DashboardError, and the main data models from api.init for convenient first‑party imports.
src/api/dashboard_client.py
src/api/__init__.py
Introduce dataclass-based Dashboard models with camelCase-to-snake_case transformation and robust handling of partial/invalid data.
  • Define SearchCounter, SearchCounters, LevelInfo, Promotion, PunchCard, StreakPromotion, StreakBonusPromotion, UserStatus, and DashboardData as dataclasses representing the dashboard schema.
  • Implement helper utilities to recursively transform camelCase keys to snake_case and filter dictionaries to declared dataclass fields only.
  • Make the from_dict constructors resilient to missing fields, nulls, non‑list values, and unexpected extra keys while preserving type safety for nested structures like counters and promotions.
src/api/models.py
Integrate DashboardClient into points detection with API‑first strategy and tighten HTML parsing and task status typing.
  • In PointsDetector.get_current_points, call DashboardClient with asyncio.wait_for, preferring API points when non‑negative and falling back to existing HTML parsing on TimeoutError or DashboardError.
  • Slightly harden HTML parsing by trimming text before parsing points and simplifying success logging when a plausible value (>=100) is found.
  • Strengthen _check_task_status typing with a typed status dict and use local variables for parsed progress/max_progress when determining completion.
src/account/points_detector.py
Extend build and tooling configuration to support the new API client and keep import style consistent.
  • Add httpx as a runtime dependency and respx as a dev dependency in pyproject.toml for HTTP client usage and mocking in tests.
  • Update Ruff isort known_first_party list to include the new api and constants packages.
pyproject.toml
Add comprehensive unit tests for DashboardClient and document the dashboard integration task.
  • Create a large unit test suite for DashboardClient covering successful API calls, 4xx/5xx handling, retry behavior, timeouts, network errors, JSON/shape/field issues, HTML fallback success/failure, cookie filtering, and convenience methods returning None on error.
  • Add CURRENT_TASK.md describing the Dashboard API integration goals, design, tasks, and acceptance criteria for this feature.
tests/unit/test_dashboard_client.py
CURRENT_TASK.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并给出了一些高层次的反馈:

  • DashboardClient.get_dashboard_data 中,通过字符串匹配(在 str(e) 中查找 "HTTP error: 401" / "403")来检测认证失败比较脆弱;建议从 _call_api 传递状态码(例如通过自定义异常属性或子类),然后基于数值状态码进行分支判断。
  • HTML 回退使用的正则 r"var\s+dashboard\s*=\s*({.*?});" 会在遇到第一个右大括号时停止,如果 dashboard 对象中包含嵌套的大括号或内联脚本,就可能解析失败;建议使用更健壮的提取方式(例如匹配到结束的 ;</script>,或者使用支持括号配对的解析器)以减少解析失败。
  • 若干 from_dict 构造器(例如 SearchCounter.from_dictLevelInfo.from_dictPromotion.from_dict)在 _transform_dict 之后直接调用 cls(**data),一旦 API 添加了未预期的字段就会抛异常;建议通过显式选择已知字段(例如使用 get 或按字段白名单过滤)来构造实例,使这些模型对 schema 变更更具弹性。
给 AI Agent 的提示词
请根据以下代码评审意见进行修改:

## 总体意见
-`DashboardClient.get_dashboard_data` 中,通过字符串匹配(在 `str(e)` 中查找 `"HTTP error: 401"` / `"403"`)来检测认证失败比较脆弱;建议从 `_call_api` 传递状态码(例如通过自定义异常属性或子类),然后基于数值状态码进行分支判断。
- HTML 回退使用的正则 `r"var\s+dashboard\s*=\s*({.*?});"` 会在遇到第一个右大括号时停止,如果 dashboard 对象中包含嵌套的大括号或内联脚本,就可能解析失败;建议使用更健壮的提取方式(例如匹配到结束的 `;</script>`,或者使用支持括号配对的解析器)以减少解析失败。
- 若干 `from_dict` 构造器(例如 `SearchCounter.from_dict``LevelInfo.from_dict``Promotion.from_dict`)在 `_transform_dict` 之后直接调用 `cls(**data)`,一旦 API 添加了未预期的字段就会抛异常;建议通过显式选择已知字段(例如使用 `get` 或按字段白名单过滤)来构造实例,使这些模型对 schema 变更更具弹性。

## 单独评论

### 评论 1
<location path="src/api/dashboard_client.py" line_range="70" />
<code_context>
+            async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
+                response = await client.get(self.API_URL, headers=headers)
+                response.raise_for_status()
+                data = response.json()
+
+                if "dashboard" not in data:
</code_context>
<issue_to_address>
**issue:** 来自 API 响应的 JSON 解析错误没有被包装为 DashboardError,可能会泄漏出非预期的异常。

如果 `response.json()` 失败(例如返回 200 状态但 body 是 HTML,或者 JSON 格式损坏),会抛出 `JSONDecodeError`,并跳过 `DashboardError` 的处理路径,从而导致重试/回退逻辑无法执行。请用 try/except 包裹 `response.json()` 并重新抛出为 `DashboardError`,以保持错误处理和重试逻辑的一致性。
</issue_to_address>

### 评论 2
<location path="src/api/dashboard_client.py" line_range="69-75" />
<code_context>
+                response.raise_for_status()
+                data = response.json()
+
+                if "dashboard" not in data:
+                    raise DashboardError("Invalid API response: missing 'dashboard' field")
+
+                return self._parse_dashboard(data["dashboard"])
</code_context>
<issue_to_address>
**suggestion:**`"dashboard"` 键的检查假设响应一定是字典,如果 JSON 负载不是字典可能会出现异常行为。

如果 API 返回的是非对象类型 JSON(例如 list 或 null),那么执行 `"dashboard" not in data` 会抛出 `TypeError`,而不是 `DashboardError`,从而跳过预期的重试/回退逻辑。建议先用 `isinstance(data, dict)` 做类型判断,如果不是字典,则抛出 `DashboardError`,以保证失败场景都能被一致地处理。

```suggestion
                response.raise_for_status()
                data = response.json()

                if not isinstance(data, dict) or "dashboard" not in data:
                    raise DashboardError("Invalid API response: expected JSON object with 'dashboard' field")

                return self._parse_dashboard(data["dashboard"])
```
</issue_to_address>

### 评论 3
<location path="src/api/models.py" line_range="34-35" />
<code_context>
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SearchCounter":
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        return cls(**data)
+
+
</code_context>
<issue_to_address>
**suggestion:** 直接使用 `cls(**data)` 会让模型对 API schema 变更和额外字段非常脆弱。

由于这里是在解析外部 API 的返回,一旦服务端新增了不在 dataclass 中定义的字段,`cls(**data)` 就会抛出 `TypeError`,导致客户端崩溃。请在实例化前先过滤 `data`,只保留存在于 `cls.__dataclass_fields__` 中的键,同时对其他同样通过 `**data` 解包调用数据类构造函数的 `from_dict` 方法也做相同处理。

建议实现如下:

```python
@dataclass
class SearchCounter:
    """搜索计数器"""

    progress: int = 0
    max_progress: int = 0

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "SearchCounter":
        """从字典创建实例"""
        # 先进行统一的字段转换(例如 camelCase -> snake_case)
        data = _transform_dict(data)

        # 只保留当前数据类中定义的字段,避免外部 API 新增字段导致 TypeError
        allowed_fields = cls.__dataclass_fields__.keys()
        filtered_data = {k: v for k, v in data.items() if k in allowed_fields}

        return cls(**filtered_data)

````src/api/models.py`(或相关的模型文件)中,可能还有其他 `from_dict` 类方法同样是先执行 `data = _transform_dict(data)`,然后直接 `cls(**data)`。  
对于这些方法,请依次应用同样的模式:

1.`_transform_dict(data)` 之后,计算 `allowed_fields = cls.__dataclass_fields__.keys()`2. 构造 `filtered_data = {k: v for k, v in data.items() if k in allowed_fields}`3. 使用 `return cls(**filtered_data)` 实例化,而不是 `cls(**data)`。

这样可以确保所有 dataclass 模型在面对外部 API 的 schema 变更时依然健壮,并且可以安全地忽略未知字段。
</issue_to_address>

Sourcery 对开源项目是免费的 —— 如果你喜欢我们的评审,请考虑分享给他人 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English

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

  • In DashboardClient.get_dashboard_data, relying on string matching ("HTTP error: 401" / "403" in str(e)) to detect auth failures is brittle; consider propagating the status code from _call_api (e.g., via a custom exception attribute or subclass) and branching on the numeric code instead.
  • The HTML fallback regex r"var\s+dashboard\s*=\s*({.*?});" will stop at the first closing brace and can break if the dashboard object contains nested braces or embedded scripts; consider a more robust extraction (e.g., matching until the terminating ;</script> or using a balanced-brace parser) to reduce parse failures.
  • Several from_dict constructors (e.g., SearchCounter.from_dict, LevelInfo.from_dict, Promotion.from_dict) call cls(**data) after _transform_dict, which will raise if the API adds unexpected fields; making these more defensive by explicitly picking known keys (e.g., via get) would make the models more resilient to schema changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DashboardClient.get_dashboard_data`, relying on string matching (`"HTTP error: 401"` / `"403"` in `str(e)`) to detect auth failures is brittle; consider propagating the status code from `_call_api` (e.g., via a custom exception attribute or subclass) and branching on the numeric code instead.
- The HTML fallback regex `r"var\s+dashboard\s*=\s*({.*?});"` will stop at the first closing brace and can break if the dashboard object contains nested braces or embedded scripts; consider a more robust extraction (e.g., matching until the terminating `;</script>` or using a balanced-brace parser) to reduce parse failures.
- Several `from_dict` constructors (e.g., `SearchCounter.from_dict`, `LevelInfo.from_dict`, `Promotion.from_dict`) call `cls(**data)` after `_transform_dict`, which will raise if the API adds unexpected fields; making these more defensive by explicitly picking known keys (e.g., via `get`) would make the models more resilient to schema changes.

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="70" />
<code_context>
+            async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
+                response = await client.get(self.API_URL, headers=headers)
+                response.raise_for_status()
+                data = response.json()
+
+                if "dashboard" not in data:
</code_context>
<issue_to_address>
**issue:** JSON parsing errors from the API response are not wrapped in DashboardError, which can leak unexpected exceptions.

If `response.json()` fails (e.g., HTML error body with 200 or malformed JSON), it will raise `JSONDecodeError` and skip the `DashboardError` path, so retries/fallbacks won’t run. Please wrap `response.json()` in try/except and re-raise as `DashboardError` to keep error handling and retry logic consistent.
</issue_to_address>

### Comment 2
<location path="src/api/dashboard_client.py" line_range="69-75" />
<code_context>
+                response.raise_for_status()
+                data = response.json()
+
+                if "dashboard" not in data:
+                    raise DashboardError("Invalid API response: missing 'dashboard' field")
+
+                return self._parse_dashboard(data["dashboard"])
</code_context>
<issue_to_address>
**suggestion:** The `"dashboard"` key check assumes a dict response and may misbehave if the JSON payload is not a dict.

If the API ever returns a non-object JSON (e.g., list or null), the `"dashboard" not in data` check will raise a `TypeError` instead of a `DashboardError`, skipping the intended retry/fallback path. Consider guarding with `isinstance(data, dict)` and raising `DashboardError` when it’s not, so failures are handled consistently.

```suggestion
                response.raise_for_status()
                data = response.json()

                if not isinstance(data, dict) or "dashboard" not in data:
                    raise DashboardError("Invalid API response: expected JSON object with 'dashboard' field")

                return self._parse_dashboard(data["dashboard"])
```
</issue_to_address>

### Comment 3
<location path="src/api/models.py" line_range="34-35" />
<code_context>
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SearchCounter":
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        return cls(**data)
+
+
</code_context>
<issue_to_address>
**suggestion:** Using `cls(**data)` directly makes the models brittle to API schema changes and extra fields.

Since this parses responses from an external API, any new field added by the server that isn’t in the dataclass will raise a `TypeError` from `cls(**data)` and break the client. Please filter `data` to only include keys present in `cls.__dataclass_fields__` before instantiating, and apply the same approach to other `from_dict` methods that unpack `**data` into dataclass constructors.

Suggested implementation:

```python
@dataclass
class SearchCounter:
    """搜索计数器"""

    progress: int = 0
    max_progress: int = 0

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "SearchCounter":
        """从字典创建实例"""
        # 先进行统一的字段转换(例如 camelCase -> snake_case)
        data = _transform_dict(data)

        # 只保留当前数据类中定义的字段,避免外部 API 新增字段导致 TypeError
        allowed_fields = cls.__dataclass_fields__.keys()
        filtered_data = {k: v for k, v in data.items() if k in allowed_fields}

        return cls(**filtered_data)

```

There may be other `from_dict` classmethods in `src/api/models.py` (or related model files) that similarly do `data = _transform_dict(data)` followed by `cls(**data)`.  
For each of those, the same pattern should be applied:

1. After `_transform_dict(data)`, compute `allowed_fields = cls.__dataclass_fields__.keys()`.
2. Build `filtered_data = {k: v for k, v in data.items() if k in allowed_fields}`.
3. Instantiate with `return cls(**filtered_data)` instead of `cls(**data)`.

This ensures all dataclass models are resilient to external API schema changes and ignore unknown fields safely.
</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.

@Disaster-Terminator
Copy link
Owner Author

@sourcery-ai review

@Disaster-Terminator
Copy link
Owner Author

/agentic_review

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 26, 2026

Persistent review updated to latest commit 073fc9d

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并留下了一些高层反馈:

  • DashboardClient._call_api 中你把重试次数写死为 range(2),而且这套重试逻辑和 get_dashboard_data 中的重试逻辑是分离的;后者已经基于 self._max_retries 处理重试。建议把这些逻辑统一起来,让所有重试行为和重试间隔都由可配置的 max_retries / retry_delay 驱动,以避免令人意外的行为和重复逻辑。
  • HTML 兜底的正则 re.search(r"var\s+dashboard\s*=\s*({.*?});", html, re.DOTALL) 对 JS 的具体形态要求比较严格;你可能需要让它更健壮一些(例如容忍 let/const、可选分号,或额外的空白/注释),从而在页面脚本格式略有变化时减少 fragility。
  • api.models 中,_camel_to_snake 在函数内部导入 re,并被其他构造器间接使用;将 re 的导入移动到模块级可以避免在大量构造对象的热点路径上重复导入。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DashboardClient._call_api` you hard-code `range(2)` and have separate retry logic from `get_dashboard_data`, which already handles retries based on `self._max_retries`; consider unifying these so all retry behavior and delays are driven by the configurable `max_retries`/`retry_delay` to avoid surprising behavior and duplicated logic.
- The HTML fallback regex `re.search(r"var\s+dashboard\s*=\s*({.*?});", html, re.DOTALL)` is quite strict about the exact JS shape; you might want to make this more robust (e.g., tolerate `let/const`, optional semicolon, or additional whitespace/comments) to reduce fragility if the page script format changes slightly.
- In `api.models`, `_camel_to_snake` imports `re` inside the function and is used transitively by other constructors; moving the `re` import to the module level will avoid repeated imports on hot paths where many objects are constructed.

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="113-114" />
<code_context>
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+        }
+
+        last_error: Exception | None = None
+        for attempt in range(2):
+            try:
+                client = await self._get_client()
</code_context>
<issue_to_address>
**suggestion:** Use the configurable retry count instead of the hard-coded `range(2)` in `_call_api`.

`DEFAULT_MAX_RETRIES` and the `max_retries` override aren’t honored here because `_call_api` uses `range(2)`. Please use the configured retry value (e.g., `range(self._max_retries + 1)`) so caller configuration is respected and behavior is consistent.

```suggestion
        last_error: Exception | None = None
        for attempt in range(self._max_retries + 1):
```
</issue_to_address>

### Comment 2
<location path="src/api/models.py" line_range="9-13" />
<code_context>
+_T = TypeVar("_T")
+
+
+def _camel_to_snake(name: str) -> str:
+    """将 camelCase 转换为 snake_case"""
+    import re
+
+    return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
+
+
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid importing `re` inside `_camel_to_snake` on every call.

Because `_camel_to_snake` may be called frequently via `_transform_dict`, importing `re` on each call adds avoidable overhead. Move `import re` to module scope and, if needed, precompile the regex with `re.compile` to keep this hot path efficient and clear.
</issue_to_address>

### Comment 3
<location path="src/api/models.py" line_range="39-48" />
<code_context>
+    progress: int = 0
+    max_progress: int = 0
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SearchCounter":
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        return cls(**_filter_dataclass_fields(data, cls))
+
+
+@dataclass
+class SearchCounters:
+    """搜索计数器集合"""
+
+    pc_search: list[SearchCounter] = field(default_factory=list)
+    mobile_search: list[SearchCounter] = field(default_factory=list)
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SearchCounters":
+        """从字典创建实例"""
+        data = _transform_dict(data)
</code_context>
<issue_to_address>
**suggestion:** Handle non-list values for `pc_search`/`mobile_search` more defensively in `SearchCounters.from_dict`.

This currently assumes `pc_search` and `mobile_search` are always iterable lists. If the API returns `null` or a scalar, iteration will fail. Consider normalizing these keys first, e.g.:

```python
pc_raw = data.get("pc_search") or []
if not isinstance(pc_raw, list):
    pc_raw = []
```

Apply similarly for `mobile_search` so `from_dict` is resilient to schema drift while keeping the same public API.
</issue_to_address>

Hi @Disaster-Terminator! 👋

感谢你通过评论 @sourcery-ai review 来试用 Sourcery!🚀

安装 sourcery-ai bot 来在每个 Pull Request 上获取自动代码审查 ✨

帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的审查。
Original comment in English

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

  • In DashboardClient._call_api you hard-code range(2) and have separate retry logic from get_dashboard_data, which already handles retries based on self._max_retries; consider unifying these so all retry behavior and delays are driven by the configurable max_retries/retry_delay to avoid surprising behavior and duplicated logic.
  • The HTML fallback regex re.search(r"var\s+dashboard\s*=\s*({.*?});", html, re.DOTALL) is quite strict about the exact JS shape; you might want to make this more robust (e.g., tolerate let/const, optional semicolon, or additional whitespace/comments) to reduce fragility if the page script format changes slightly.
  • In api.models, _camel_to_snake imports re inside the function and is used transitively by other constructors; moving the re import to the module level will avoid repeated imports on hot paths where many objects are constructed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DashboardClient._call_api` you hard-code `range(2)` and have separate retry logic from `get_dashboard_data`, which already handles retries based on `self._max_retries`; consider unifying these so all retry behavior and delays are driven by the configurable `max_retries`/`retry_delay` to avoid surprising behavior and duplicated logic.
- The HTML fallback regex `re.search(r"var\s+dashboard\s*=\s*({.*?});", html, re.DOTALL)` is quite strict about the exact JS shape; you might want to make this more robust (e.g., tolerate `let/const`, optional semicolon, or additional whitespace/comments) to reduce fragility if the page script format changes slightly.
- In `api.models`, `_camel_to_snake` imports `re` inside the function and is used transitively by other constructors; moving the `re` import to the module level will avoid repeated imports on hot paths where many objects are constructed.

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="113-114" />
<code_context>
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+        }
+
+        last_error: Exception | None = None
+        for attempt in range(2):
+            try:
+                client = await self._get_client()
</code_context>
<issue_to_address>
**suggestion:** Use the configurable retry count instead of the hard-coded `range(2)` in `_call_api`.

`DEFAULT_MAX_RETRIES` and the `max_retries` override aren’t honored here because `_call_api` uses `range(2)`. Please use the configured retry value (e.g., `range(self._max_retries + 1)`) so caller configuration is respected and behavior is consistent.

```suggestion
        last_error: Exception | None = None
        for attempt in range(self._max_retries + 1):
```
</issue_to_address>

### Comment 2
<location path="src/api/models.py" line_range="9-13" />
<code_context>
+_T = TypeVar("_T")
+
+
+def _camel_to_snake(name: str) -> str:
+    """将 camelCase 转换为 snake_case"""
+    import re
+
+    return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
+
+
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid importing `re` inside `_camel_to_snake` on every call.

Because `_camel_to_snake` may be called frequently via `_transform_dict`, importing `re` on each call adds avoidable overhead. Move `import re` to module scope and, if needed, precompile the regex with `re.compile` to keep this hot path efficient and clear.
</issue_to_address>

### Comment 3
<location path="src/api/models.py" line_range="39-48" />
<code_context>
+    progress: int = 0
+    max_progress: int = 0
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SearchCounter":
+        """从字典创建实例"""
+        data = _transform_dict(data)
+        return cls(**_filter_dataclass_fields(data, cls))
+
+
+@dataclass
+class SearchCounters:
+    """搜索计数器集合"""
+
+    pc_search: list[SearchCounter] = field(default_factory=list)
+    mobile_search: list[SearchCounter] = field(default_factory=list)
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SearchCounters":
+        """从字典创建实例"""
+        data = _transform_dict(data)
</code_context>
<issue_to_address>
**suggestion:** Handle non-list values for `pc_search`/`mobile_search` more defensively in `SearchCounters.from_dict`.

This currently assumes `pc_search` and `mobile_search` are always iterable lists. If the API returns `null` or a scalar, iteration will fail. Consider normalizing these keys first, e.g.:

```python
pc_raw = data.get("pc_search") or []
if not isinstance(pc_raw, list):
    pc_raw = []
```

Apply similarly for `mobile_search` so `from_dict` is resilient to schema drift while keeping the same public API.
</issue_to_address>

Hi @Disaster-Terminator! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并给出了一些整体性的反馈:

  • 当前的重试行为被拆分在 _call_api(硬编码的 range(2),并带有各自的超时/网络重试)和 get_dashboard_data(对 5xx 状态码使用 self._max_retries)之间;建议将其统一起来,让所有重试逻辑都由可配置的 max_retries 驱动,以避免出现令人意外或重复的重试路径。
  • models._camel_to_snake 中,每次调用函数都会在函数内部导入 re;建议将该导入移动到模块级,从而避免重复导入并简化这个辅助函数。
  • 目前的 HTML fallback 正则只匹配 var dashboard = ...;如果站点以后改用 let/const 或改变空格格式,匹配就会静默失败。建议放宽这个模式(例如允许 var|let|const,并对结束符更宽松),让 fallback 更加健壮。
供 AI Agent 使用的提示词
Please address the comments from this code review:

## Overall Comments
- 当前的重试行为被拆分在 `_call_api`(硬编码的 `range(2)`,并带有各自的超时/网络重试)和 `get_dashboard_data`(对 5xx 状态码使用 `self._max_retries`)之间;建议将其统一起来,让所有重试逻辑都由可配置的 `max_retries` 驱动,以避免出现令人意外或重复的重试路径。
-`models._camel_to_snake` 中,每次调用函数都会在函数内部导入 `re`;建议将该导入移动到模块级,从而避免重复导入并简化这个辅助函数。
- 目前的 HTML fallback 正则只匹配 `var dashboard = ...`;如果站点以后改用 `let`/`const` 或改变空格格式,匹配就会静默失败。建议放宽这个模式(例如允许 `var|let|const`,并对结束符更宽松),让 fallback 更加健壮。

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="113-114" />
<code_context>
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+        }
+
+        last_error: Exception | None = None
+        for attempt in range(2):
+            try:
+                client = await self._get_client()
</code_context>
<issue_to_address>
**issue (bug_risk):** _call_api hardcodes retry count instead of using configurable max_retries/retry_delay

This loop (`for attempt in range(2):`, "retrying once") bypasses `self._max_retries` and `self._retry_delay`, so the configured retry settings don’t actually control this behavior and may stack unexpectedly with `get_dashboard_data`’s retries. Please drive this loop from `self._max_retries` with `self._retry_delay` between attempts, or centralize the retry logic so it’s defined in one place.
</issue_to_address>

### Comment 2
<location path="tests/unit/test_dashboard_client.py" line_range="1" />
<code_context>
+"""Dashboard Client 单元测试"""
+
+from unittest.mock import AsyncMock, Mock
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for 401/403 auth errors where the HTML fallback also fails, to assert that `DashboardError` is raised.

Existing tests only cover cases where the HTML fallback succeeds. Please also add a test using `mock_page_no_dashboard` for 401/403 responses, asserting that `get_dashboard_data()` raises `DashboardError` when both the API and HTML fallback fail, to fully exercise the `is_auth_error` path.

Suggested implementation:

```python
from api.models import DashboardData, SearchCounters


@pytest.mark.parametrize("status_code", (401, 403))
@respx.mock
@pytest.mark.asyncio
async def test_get_dashboard_data_auth_error_with_failing_html_fallback(
    status_code,
    dashboard_client: DashboardClient,
    mock_page_no_dashboard: str,
):
    """
    When the API returns an auth error (401/403) and the HTML fallback page
    also does not contain dashboard data, DashboardError should be raised.
    """
    # NOTE: The exact URLs should match those used inside DashboardClient.get_dashboard_data.
    # They may need to be adjusted to the real values in your test suite.
    api_route = respx.get("https://example.com/api/dashboard").mock(
        return_value=httpx.Response(status_code=status_code)
    )

    html_route = respx.get("https://example.com/dashboard").mock(
        return_value=httpx.Response(status_code=200, text=mock_page_no_dashboard)
    )

    with pytest.raises(DashboardError):
        await dashboard_client.get_dashboard_data()

    # Ensure both API and HTML fallback were actually called
    assert api_route.called
    assert html_route.called

```

The above test assumes:
1. `dashboard_client.get_dashboard_data()` calls an API endpoint like `https://example.com/api/dashboard` and a fallback HTML endpoint like `https://example.com/dashboard`. You should update the two `respx.get(...)` URLs to exactly match the URLs used in `DashboardClient.get_dashboard_data()`, or to whatever form your existing tests use (e.g. `respx.get("https://dashboard/api/...")` or with `host="..."`).
2. A `mock_page_no_dashboard` fixture exists and returns HTML without dashboard data. If its name or type differ (e.g. it returns bytes or an `httpx.Response`), adjust the fixture parameter annotation and how it is passed into `httpx.Response` accordingly.
3. If your existing tests wrap `respx` usage differently (for example, using a shared `respx_router` fixture or different decorators), align this new test with that convention so it integrates cleanly with the rest of the test suite.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_dashboard_client.py" line_range="136-145" />
<code_context>
+    assert data.user_status.available_points == 12345
+
+
+@respx.mock
+async def test_get_dashboard_data_api_error_fallback(mock_page):
+    """测试 API 错误 + HTML fallback 场景"""
+    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").respond(
+        status_code=500, text="Internal Server Error"
+    )
+
+    client = DashboardClient(mock_page)
+    data = await client.get_dashboard_data()
+    assert isinstance(data, DashboardData)
+    assert data.user_status.available_points == 12345
+
+
+@respx.mock
+async def test_get_dashboard_data_unauthorized_fallback(mock_page):
+    """测试 401 错误 + HTML fallback 场景"""
+    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").respond(
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for `get_current_points` and `get_search_counters` when they rely on HTML fallback rather than a successful API response.

Right now, only the `get_dashboard_data_*` tests cover HTML fallback; `get_current_points` and `get_search_counters` are only tested for the 200 + valid JSON path. Please add tests where the API returns 5xx or times out and the HTML contains a valid `var dashboard = {...}` (like `mock_page`), asserting that `get_current_points` returns the correct points and `get_search_counters` builds a populated `SearchCounters` from the HTML data.

Suggested implementation:

```python
@respx.mock
async def test_get_current_points_api_error_html_fallback(mock_page):
    """API 5xx 时应从 HTML fallback 中获取当前积分"""
    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").respond(
        status_code=500, text="Internal Server Error"
    )

    client = DashboardClient(mock_page)
    points = await client.get_current_points()
    # HTML 中的 dashboard 数据应与正常场景一致
    assert points == 12345


@respx.mock
async def test_get_search_counters_timeout_html_fallback(mock_page):
    """API 超时时应从 HTML fallback 中构建搜索计数器"""
    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").mock(
        side_effect=httpx.TimeoutException("Request timed out")
    )

    client = DashboardClient(mock_page)
    counters = await client.get_search_counters()

    # 确认从 HTML 中成功构建了 SearchCounters,而不是返回空数据
    # 这里假设 HTML 中提供了 PC 和 Mobile 的搜索计数
    assert counters is not None
    assert getattr(counters, "pc", None) is not None
    assert getattr(counters, "mobile", None) is not None


@respx.mock

```

1. 确保本测试文件顶部已经 `import httpx`,否则需要补充:
   `import httpx`2. 如果在现有测试中已经对 `SearchCounters` 的字段有更精确的断言(例如 `pc.remaining == 10` 等),建议在 `test_get_search_counters_timeout_html_fallback` 中使用相同的期望值来替换当前的通用属性检查,以保证与 HTML mock 数据严格对齐。
</issue_to_address>

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

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

  • The retry behavior is split between _call_api (hard-coded range(2) with its own timeout/network retries) and get_dashboard_data (using self._max_retries for 5xx status codes); consider unifying this so that all retry logic is driven by the configurable max_retries to avoid surprising or duplicated retry paths.
  • In models._camel_to_snake you import re inside the function on every call; moving this import to the module level will avoid repeated imports and simplify the helper.
  • The HTML fallback regex currently only matches var dashboard = ...; if the site ever switches to let/const or changes spacing, this will silently fail, so consider broadening the pattern (e.g. allowing var|let|const and more flexible terminators) to make the fallback more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The retry behavior is split between `_call_api` (hard-coded `range(2)` with its own timeout/network retries) and `get_dashboard_data` (using `self._max_retries` for 5xx status codes); consider unifying this so that all retry logic is driven by the configurable `max_retries` to avoid surprising or duplicated retry paths.
- In `models._camel_to_snake` you import `re` inside the function on every call; moving this import to the module level will avoid repeated imports and simplify the helper.
- The HTML fallback regex currently only matches `var dashboard = ...`; if the site ever switches to `let`/`const` or changes spacing, this will silently fail, so consider broadening the pattern (e.g. allowing `var|let|const` and more flexible terminators) to make the fallback more robust.

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="113-114" />
<code_context>
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+        }
+
+        last_error: Exception | None = None
+        for attempt in range(2):
+            try:
+                client = await self._get_client()
</code_context>
<issue_to_address>
**issue (bug_risk):** _call_api hardcodes retry count instead of using configurable max_retries/retry_delay

This loop (`for attempt in range(2):`, "retrying once") bypasses `self._max_retries` and `self._retry_delay`, so the configured retry settings don’t actually control this behavior and may stack unexpectedly with `get_dashboard_data`’s retries. Please drive this loop from `self._max_retries` with `self._retry_delay` between attempts, or centralize the retry logic so it’s defined in one place.
</issue_to_address>

### Comment 2
<location path="tests/unit/test_dashboard_client.py" line_range="1" />
<code_context>
+"""Dashboard Client 单元测试"""
+
+from unittest.mock import AsyncMock, Mock
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for 401/403 auth errors where the HTML fallback also fails, to assert that `DashboardError` is raised.

Existing tests only cover cases where the HTML fallback succeeds. Please also add a test using `mock_page_no_dashboard` for 401/403 responses, asserting that `get_dashboard_data()` raises `DashboardError` when both the API and HTML fallback fail, to fully exercise the `is_auth_error` path.

Suggested implementation:

```python
from api.models import DashboardData, SearchCounters


@pytest.mark.parametrize("status_code", (401, 403))
@respx.mock
@pytest.mark.asyncio
async def test_get_dashboard_data_auth_error_with_failing_html_fallback(
    status_code,
    dashboard_client: DashboardClient,
    mock_page_no_dashboard: str,
):
    """
    When the API returns an auth error (401/403) and the HTML fallback page
    also does not contain dashboard data, DashboardError should be raised.
    """
    # NOTE: The exact URLs should match those used inside DashboardClient.get_dashboard_data.
    # They may need to be adjusted to the real values in your test suite.
    api_route = respx.get("https://example.com/api/dashboard").mock(
        return_value=httpx.Response(status_code=status_code)
    )

    html_route = respx.get("https://example.com/dashboard").mock(
        return_value=httpx.Response(status_code=200, text=mock_page_no_dashboard)
    )

    with pytest.raises(DashboardError):
        await dashboard_client.get_dashboard_data()

    # Ensure both API and HTML fallback were actually called
    assert api_route.called
    assert html_route.called

```

The above test assumes:
1. `dashboard_client.get_dashboard_data()` calls an API endpoint like `https://example.com/api/dashboard` and a fallback HTML endpoint like `https://example.com/dashboard`. You should update the two `respx.get(...)` URLs to exactly match the URLs used in `DashboardClient.get_dashboard_data()`, or to whatever form your existing tests use (e.g. `respx.get("https://dashboard/api/...")` or with `host="..."`).
2. A `mock_page_no_dashboard` fixture exists and returns HTML without dashboard data. If its name or type differ (e.g. it returns bytes or an `httpx.Response`), adjust the fixture parameter annotation and how it is passed into `httpx.Response` accordingly.
3. If your existing tests wrap `respx` usage differently (for example, using a shared `respx_router` fixture or different decorators), align this new test with that convention so it integrates cleanly with the rest of the test suite.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_dashboard_client.py" line_range="136-145" />
<code_context>
+    assert data.user_status.available_points == 12345
+
+
+@respx.mock
+async def test_get_dashboard_data_api_error_fallback(mock_page):
+    """测试 API 错误 + HTML fallback 场景"""
+    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").respond(
+        status_code=500, text="Internal Server Error"
+    )
+
+    client = DashboardClient(mock_page)
+    data = await client.get_dashboard_data()
+    assert isinstance(data, DashboardData)
+    assert data.user_status.available_points == 12345
+
+
+@respx.mock
+async def test_get_dashboard_data_unauthorized_fallback(mock_page):
+    """测试 401 错误 + HTML fallback 场景"""
+    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").respond(
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for `get_current_points` and `get_search_counters` when they rely on HTML fallback rather than a successful API response.

Right now, only the `get_dashboard_data_*` tests cover HTML fallback; `get_current_points` and `get_search_counters` are only tested for the 200 + valid JSON path. Please add tests where the API returns 5xx or times out and the HTML contains a valid `var dashboard = {...}` (like `mock_page`), asserting that `get_current_points` returns the correct points and `get_search_counters` builds a populated `SearchCounters` from the HTML data.

Suggested implementation:

```python
@respx.mock
async def test_get_current_points_api_error_html_fallback(mock_page):
    """API 5xx 时应从 HTML fallback 中获取当前积分"""
    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").respond(
        status_code=500, text="Internal Server Error"
    )

    client = DashboardClient(mock_page)
    points = await client.get_current_points()
    # HTML 中的 dashboard 数据应与正常场景一致
    assert points == 12345


@respx.mock
async def test_get_search_counters_timeout_html_fallback(mock_page):
    """API 超时时应从 HTML fallback 中构建搜索计数器"""
    respx.get("https://rewards.bing.com/api/getuserinfo?type=1").mock(
        side_effect=httpx.TimeoutException("Request timed out")
    )

    client = DashboardClient(mock_page)
    counters = await client.get_search_counters()

    # 确认从 HTML 中成功构建了 SearchCounters,而不是返回空数据
    # 这里假设 HTML 中提供了 PC 和 Mobile 的搜索计数
    assert counters is not None
    assert getattr(counters, "pc", None) is not None
    assert getattr(counters, "mobile", None) is not None


@respx.mock

```

1. 确保本测试文件顶部已经 `import httpx`,否则需要补充:
   `import httpx`2. 如果在现有测试中已经对 `SearchCounters` 的字段有更精确的断言(例如 `pc.remaining == 10` 等),建议在 `test_get_search_counters_timeout_html_fallback` 中使用相同的期望值来替换当前的通用属性检查,以保证与 HTML mock 数据严格对齐。
</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.

@Disaster-Terminator Disaster-Terminator force-pushed the feature/dashboard-api branch 5 times, most recently from 5a609df to 43a463b Compare February 28, 2026 02:22
- 移除宽松的 bing.com 域名匹配,仅允许 API 域名及其子域名
- 更新测试用例以匹配严格的域名过滤逻辑
- 防止跨域 Cookie 泄露到不相关的子域名
@Disaster-Terminator
Copy link
Owner Author

/agentic_review

@Disaster-Terminator
Copy link
Owner Author

@sourcery-ai review

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 28, 2026

Persistent review updated to latest commit 156d234

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并给出了一些整体性反馈:

  • DashboardClient 目前在 close() 中将 _client 设为 None,但 _call_api 假定它始终是一个存活的 client;建议对 httpx client 进行惰性初始化(或在为 None 时重新创建),这样在 close() 之后或通过上下文管理器多次使用该实例时,都能安全复用。
  • _call_api(处理超时/网络错误)和 get_dashboard_data(处理 5xx 错误)中存在嵌套的重试循环,这会让实际的重试行为更难以推理;建议将重试策略集中到一个地方,从而使控制流和时序特性更清晰。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- DashboardClient 目前在 `close()` 中将 `_client` 设为 `None`,但 `_call_api` 假定它始终是一个存活的 client;建议对 httpx client 进行惰性初始化(或在为 `None` 时重新创建),这样在 `close()` 之后或通过上下文管理器多次使用该实例时,都能安全复用。
- `_call_api`(处理超时/网络错误)和 `get_dashboard_data`(处理 5xx 错误)中存在嵌套的重试循环,这会让实际的重试行为更难以推理;建议将重试策略集中到一个地方,从而使控制流和时序特性更清晰。

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="136" />
<code_context>
+
+        for attempt in range(self._max_retries + 1):
+            try:
+                response = await self._client.get(self._api_url, headers=headers)
+                response.raise_for_status()
+                data = response.json()
</code_context>
<issue_to_address>
**issue:** `_client` 可以被 `close()` 设为 `None`,但 `_call_api` 假定它始终是一个 `AsyncClient`。

如果在调用 `close()` 之后再调用 `get_dashboard_data()` 等方法,目前会在 `self._client.get` 处因 `AttributeError` 而失败。建议要么处理 `self._client is None` 的情况(例如,重新创建 client),要么通过在 `close()` 之后使用这些方法时抛出明确的领域错误来强制执行更严格的生命周期管理。
</issue_to_address>

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

Hey - I've found 1 issue, and left some high level feedback:

  • DashboardClient currently sets _client to None in close() but _call_api assumes it is always a live client; consider lazy-initializing the httpx client (or recreating it if None) so that the instance can be safely reused after being closed or when used via the context manager multiple times.
  • There are nested retry loops in _call_api (for timeout/network errors) and get_dashboard_data (for 5xx errors), which makes the effective retry behaviour harder to reason about; consider centralising the retry policy in one place to keep the control flow and timing characteristics clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- DashboardClient currently sets `_client` to `None` in `close()` but `_call_api` assumes it is always a live client; consider lazy-initializing the httpx client (or recreating it if `None`) so that the instance can be safely reused after being closed or when used via the context manager multiple times.
- There are nested retry loops in `_call_api` (for timeout/network errors) and `get_dashboard_data` (for 5xx errors), which makes the effective retry behaviour harder to reason about; consider centralising the retry policy in one place to keep the control flow and timing characteristics clearer.

## Individual Comments

### Comment 1
<location path="src/api/dashboard_client.py" line_range="136" />
<code_context>
+
+        for attempt in range(self._max_retries + 1):
+            try:
+                response = await self._client.get(self._api_url, headers=headers)
+                response.raise_for_status()
+                data = response.json()
</code_context>
<issue_to_address>
**issue:** `_client` can be set to `None` by `close()`, but `_call_api` assumes it is always an `AsyncClient`.

If methods like `get_dashboard_data()` are called after `close()`, this will currently fail with an `AttributeError` on `self._client.get`. Consider either handling `self._client is None` (e.g., recreating the client) or enforcing a stricter lifecycle by raising a clear domain error when methods are used after `close()`.
</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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- 添加 _client None 检查,防止 close() 后调用
- from_dict 增加列表元素类型校验
- 嵌套对象字段增加类型校验
- Cookie 选择改用 Playwright URL 作用域
- 测试文件添加 sys.path 注入
- pyproject.toml 添加 respx 到 test 依赖
- points_text 添加 None 检查
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants