Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ screenshots/*.html

# Trae 规格和文档
.trae/spec/
.trae/specs/
.trae/documents/
.trae/data/
135 changes: 135 additions & 0 deletions CURRENT_TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# TASK: Dashboard API 集成

> 分支: `feature/dashboard-api`
> 并行组: 第一组
> 优先级: 🔴 最高
> 预计时间: 3-4 天
> 依赖: 无

***

## 一、目标

利用已验证可用的 Dashboard API,增强积分检测能力,替代现有的 HTML 解析方案。

***

## 二、背景

### 2.1 API 验证结果

| API | 状态 | HTTP 状态码 | 备注 |
|-----|------|-------------|------|
| Dashboard API | ✅ 可用 | 200 | 返回完整用户数据 |

### 2.2 API 端点

```
GET https://rewards.bing.com/api/getuserinfo?type=1
Headers:
Cookie: {session_cookies}
Referer: https://rewards.bing.com/
```

### 2.3 响应示例

```json
{
"dashboard": {
"userStatus": {
"levelInfo": {
"activeLevel": "newLevel3",
"activeLevelName": "Gold Member",
"progress": 1790,
"progressMax": 750
},
"availablePoints": 12345,
"counters": {
"pcSearch": [...],
"mobileSearch": [...]
}
},
"dailySetPromotions": {...},
"morePromotions": [...],
"punchCards": [...]
}
}
```

***

## 三、任务清单

### 3.1 数据结构定义

- [ ] 创建 `src/api/__init__.py`
- [ ] 创建 `src/api/models.py`
- [ ] `DashboardData` dataclass
- [ ] `UserStatus` dataclass
- [ ] `LevelInfo` dataclass
- [ ] `Counters` dataclass
- [ ] `Promotion` dataclass
- [ ] `PunchCard` dataclass

### 3.2 DashboardClient 实现

- [ ] 创建 `src/api/dashboard_client.py`
- [ ] `get_dashboard_data()` - 获取完整 Dashboard 数据
- [ ] `get_search_counters()` - 获取搜索计数器
- [ ] `get_level_info()` - 获取会员等级信息
- [ ] `get_promotions()` - 获取推广任务列表
- [ ] `get_current_points()` - 获取当前积分

### 3.3 HTML Fallback 机制

- [ ] 实现 API 失败时的 HTML 解析 fallback
- [ ] 从页面脚本提取 `var dashboard = {...}`

### 3.4 集成与测试

- [ ] 更新 `PointsDetector` 使用新 API
- [ ] 创建 `tests/unit/test_dashboard_client.py`
- [ ] 验证积分检测准确性

***

## 四、参考资源

### 4.1 TS 项目参考

| 文件 | 路径 |
|------|------|
| Dashboard API 实现 | `Microsoft-Rewards-Script/src/browser/BrowserFunc.ts` |
| 数据结构定义 | `Microsoft-Rewards-Script/src/interface/DashboardData.ts` |

### 4.2 关键代码参考

```python
async def get_dashboard_data(self) -> DashboardData:
try:
response = await self._call_api()
if response.data and response.data.get('dashboard'):
return self._parse_dashboard(response.data['dashboard'])
except Exception as e:
self.logger.warn(f"API failed: {e}, trying HTML fallback")
return await self._html_fallback()
raise DashboardError("Failed to get dashboard data")
```

***

## 五、验收标准

- [ ] DashboardClient 可成功调用 API
- [ ] 返回完整的用户数据(积分、等级、任务)
- [ ] HTML fallback 机制正常工作
- [ ] 单元测试覆盖率 > 80%
- [ ] 无 mypy 类型错误

***

## 六、合并条件

- [ ] 所有测试通过
- [ ] Code Review 通过
- [ ] 文档更新完成
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
"lxml>=5.3.0",
"psutil>=6.1.0",
"pyotp>=2.9.0",
"httpx>=0.28.0",
]

[project.optional-dependencies]
Expand All @@ -23,6 +24,7 @@ dev = [
"mypy>=1.14.0",
"pydantic>=2.9.0",
"httpx>=0.28.0",
"respx>=0.21.0",
"tinydb>=4.8.0",
"filelock>=3.15.0",
"rich>=13.0.0",
Expand All @@ -46,6 +48,8 @@ test = [
"pytest-xdist>=3.5.0",
"hypothesis>=6.125.0",
"faker>=35.0.0",
"httpx>=0.28.0",
"respx>=0.21.0",
]
viz = [
"streamlit>=1.41.0",
Expand Down Expand Up @@ -84,7 +88,7 @@ ignore = [
]

[tool.ruff.lint.isort]
known-first-party = ["src", "infrastructure", "browser", "login", "search", "account", "tasks", "ui"]
known-first-party = ["src", "infrastructure", "browser", "login", "search", "account", "tasks", "ui", "api", "constants"]

[tool.ruff.format]
quote-style = "double"
Expand Down
46 changes: 36 additions & 10 deletions src/account/points_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
从 Microsoft Rewards Dashboard 抓取积分信息
"""

import asyncio
import logging
import re

from playwright.async_api import Page

from api.dashboard_client import DashboardClient, DashboardError
from constants import REWARDS_URLS

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -90,6 +92,22 @@ async def get_current_points(self, page: Page, skip_navigation: bool = False) ->
logger.debug("跳过导航,使用当前页面")
await page.wait_for_timeout(1000)

# 优先使用 Dashboard API
try:
logger.debug("尝试使用 Dashboard API 获取积分...")
async with DashboardClient(page) as client:
api_points: int | None = await asyncio.wait_for(
client.get_current_points(), timeout=35.0
)
if api_points is not None and api_points >= 0:
logger.info("✓ 从 API 获取积分成功")
return int(api_points)
except asyncio.TimeoutError:
logger.warning("Dashboard API 超时,使用 HTML 解析作为备用")
except DashboardError as e:
logger.warning(f"Dashboard API 失败: {e},使用 HTML 解析作为备用")

# 备用:HTML 解析
logger.debug("尝试从页面源码提取积分...")
points = await self._extract_points_from_source(page)

Expand All @@ -107,13 +125,14 @@ async def get_current_points(self, page: Page, skip_navigation: bool = False) ->
points_text = await element.text_content()
logger.debug(f"找到积分文本: {points_text}")

points = self._parse_points(points_text)
if points_text and points_text.strip():
points = self._parse_points(points_text.strip())

if points is not None and points >= 100:
logger.info(f"✓ 当前积分: {points:,}")
return points
elif points is not None:
logger.debug(f"积分值太小,可能是误识别: {points}")
if points is not None and points >= 100:
logger.info("✓ 当前积分获取成功")
return points
elif points is not None:
logger.debug(f"积分值太小,可能是误识别: {points}")

except Exception as e:
logger.debug(f"选择器 {selector} 失败: {e}")
Expand Down Expand Up @@ -310,7 +329,12 @@ async def _check_task_status(self, page: Page, selectors: list, task_name: str)
Returns:
任务状态字典
"""
status = {"found": False, "completed": False, "progress": None, "max_progress": None}
status: dict[str, bool | int | None] = {
"found": False,
"completed": False,
"progress": None,
"max_progress": None,
}

try:
for selector in selectors:
Expand Down Expand Up @@ -338,10 +362,12 @@ async def _check_task_status(self, page: Page, selectors: list, task_name: str)
# 查找类似 "15/30" 的进度
progress_match = re.search(r"(\d+)\s*/\s*(\d+)", text)
if progress_match:
status["progress"] = int(progress_match.group(1))
status["max_progress"] = int(progress_match.group(2))
progress_val = int(progress_match.group(1))
max_progress_val = int(progress_match.group(2))
status["progress"] = progress_val
status["max_progress"] = max_progress_val

if status["progress"] >= status["max_progress"]:
if progress_val >= max_progress_val:
status["completed"] = True

logger.debug(f"{task_name} 状态: {status}")
Expand Down
39 changes: 39 additions & 0 deletions src/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""API 模块

提供面向业务层的统一 API 访问入口,包括仪表盘相关的客户端封装以及数据模型。
该模块聚合了对外公开的主要类型,方便调用方通过 `api` 包进行导入和使用。

主要组件
-------
- ``DashboardClient``: 仪表盘 API 客户端,对外提供高层封装的请求接口
- ``DashboardError``: 仪表盘相关错误类型,用于封装请求或解析过程中的异常
- ``DashboardData``: 仪表盘整体数据模型
- ``UserStatus``: 用户当前状态信息模型
- ``LevelInfo``: 用户等级与经验值等信息模型
- ``SearchCounter`` / ``SearchCounters``: 搜索计数与统计信息模型
- ``Promotion``: 活动与促销信息模型
- ``PunchCard``: 打卡与活跃度相关的数据模型
"""

from .dashboard_client import DashboardClient, DashboardError
from .models import (
DashboardData,
LevelInfo,
Promotion,
PunchCard,
SearchCounter,
SearchCounters,
UserStatus,
)

__all__ = [
"DashboardClient",
"DashboardError",
"DashboardData",
"UserStatus",
"LevelInfo",
"SearchCounter",
"SearchCounters",
"Promotion",
"PunchCard",
]
Loading