Skip to content

Commit 156d234

Browse files
fix(api): 修复 Cookie 过滤逻辑安全风险
- 移除宽松的 bing.com 域名匹配,仅允许 API 域名及其子域名 - 更新测试用例以匹配严格的域名过滤逻辑 - 防止跨域 Cookie 泄露到不相关的子域名
1 parent acb41eb commit 156d234

8 files changed

Lines changed: 1293 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,6 @@ screenshots/*.html
8787

8888
# Trae 规格和文档
8989
.trae/spec/
90+
.trae/specs/
9091
.trae/documents/
9192
.trae/data/

CURRENT_TASK.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# TASK: Dashboard API 集成
2+
3+
> 分支: `feature/dashboard-api`
4+
> 并行组: 第一组
5+
> 优先级: 🔴 最高
6+
> 预计时间: 3-4 天
7+
> 依赖: 无
8+
9+
***
10+
11+
## 一、目标
12+
13+
利用已验证可用的 Dashboard API,增强积分检测能力,替代现有的 HTML 解析方案。
14+
15+
***
16+
17+
## 二、背景
18+
19+
### 2.1 API 验证结果
20+
21+
| API | 状态 | HTTP 状态码 | 备注 |
22+
|-----|------|-------------|------|
23+
| Dashboard API | ✅ 可用 | 200 | 返回完整用户数据 |
24+
25+
### 2.2 API 端点
26+
27+
```
28+
GET https://rewards.bing.com/api/getuserinfo?type=1
29+
Headers:
30+
Cookie: {session_cookies}
31+
Referer: https://rewards.bing.com/
32+
```
33+
34+
### 2.3 响应示例
35+
36+
```json
37+
{
38+
"dashboard": {
39+
"userStatus": {
40+
"levelInfo": {
41+
"activeLevel": "newLevel3",
42+
"activeLevelName": "Gold Member",
43+
"progress": 1790,
44+
"progressMax": 750
45+
},
46+
"availablePoints": 12345,
47+
"counters": {
48+
"pcSearch": [...],
49+
"mobileSearch": [...]
50+
}
51+
},
52+
"dailySetPromotions": {...},
53+
"morePromotions": [...],
54+
"punchCards": [...]
55+
}
56+
}
57+
```
58+
59+
***
60+
61+
## 三、任务清单
62+
63+
### 3.1 数据结构定义
64+
65+
- [ ] 创建 `src/api/__init__.py`
66+
- [ ] 创建 `src/api/models.py`
67+
- [ ] `DashboardData` dataclass
68+
- [ ] `UserStatus` dataclass
69+
- [ ] `LevelInfo` dataclass
70+
- [ ] `Counters` dataclass
71+
- [ ] `Promotion` dataclass
72+
- [ ] `PunchCard` dataclass
73+
74+
### 3.2 DashboardClient 实现
75+
76+
- [ ] 创建 `src/api/dashboard_client.py`
77+
- [ ] `get_dashboard_data()` - 获取完整 Dashboard 数据
78+
- [ ] `get_search_counters()` - 获取搜索计数器
79+
- [ ] `get_level_info()` - 获取会员等级信息
80+
- [ ] `get_promotions()` - 获取推广任务列表
81+
- [ ] `get_current_points()` - 获取当前积分
82+
83+
### 3.3 HTML Fallback 机制
84+
85+
- [ ] 实现 API 失败时的 HTML 解析 fallback
86+
- [ ] 从页面脚本提取 `var dashboard = {...}`
87+
88+
### 3.4 集成与测试
89+
90+
- [ ] 更新 `PointsDetector` 使用新 API
91+
- [ ] 创建 `tests/unit/test_dashboard_client.py`
92+
- [ ] 验证积分检测准确性
93+
94+
***
95+
96+
## 四、参考资源
97+
98+
### 4.1 TS 项目参考
99+
100+
| 文件 | 路径 |
101+
|------|------|
102+
| Dashboard API 实现 | `Microsoft-Rewards-Script/src/browser/BrowserFunc.ts` |
103+
| 数据结构定义 | `Microsoft-Rewards-Script/src/interface/DashboardData.ts` |
104+
105+
### 4.2 关键代码参考
106+
107+
```python
108+
async def get_dashboard_data(self) -> DashboardData:
109+
try:
110+
response = await self._call_api()
111+
if response.data and response.data.get('dashboard'):
112+
return self._parse_dashboard(response.data['dashboard'])
113+
except Exception as e:
114+
self.logger.warn(f"API failed: {e}, trying HTML fallback")
115+
return await self._html_fallback()
116+
raise DashboardError("Failed to get dashboard data")
117+
```
118+
119+
***
120+
121+
## 五、验收标准
122+
123+
- [ ] DashboardClient 可成功调用 API
124+
- [ ] 返回完整的用户数据(积分、等级、任务)
125+
- [ ] HTML fallback 机制正常工作
126+
- [ ] 单元测试覆盖率 > 80%
127+
- [ ] 无 mypy 类型错误
128+
129+
***
130+
131+
## 六、合并条件
132+
133+
- [ ] 所有测试通过
134+
- [ ] Code Review 通过
135+
- [ ] 文档更新完成

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"lxml>=5.3.0",
1616
"psutil>=6.1.0",
1717
"pyotp>=2.9.0",
18+
"httpx>=0.28.0",
1819
]
1920

2021
[project.optional-dependencies]
@@ -23,6 +24,7 @@ dev = [
2324
"mypy>=1.14.0",
2425
"pydantic>=2.9.0",
2526
"httpx>=0.28.0",
27+
"respx>=0.21.0",
2628
"tinydb>=4.8.0",
2729
"filelock>=3.15.0",
2830
"rich>=13.0.0",
@@ -84,7 +86,7 @@ ignore = [
8486
]
8587

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

8991
[tool.ruff.format]
9092
quote-style = "double"

src/account/points_detector.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
从 Microsoft Rewards Dashboard 抓取积分信息
44
"""
55

6+
import asyncio
67
import logging
78
import re
89

910
from playwright.async_api import Page
1011

12+
from api.dashboard_client import DashboardClient, DashboardError
1113
from constants import REWARDS_URLS
1214

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

95+
# 优先使用 Dashboard API
96+
try:
97+
logger.debug("尝试使用 Dashboard API 获取积分...")
98+
async with DashboardClient(page) as client:
99+
api_points: int | None = await asyncio.wait_for(
100+
client.get_current_points(), timeout=35.0
101+
)
102+
if api_points is not None and api_points >= 0:
103+
logger.info("✓ 从 API 获取积分成功")
104+
return int(api_points)
105+
except asyncio.TimeoutError:
106+
logger.warning("Dashboard API 超时,使用 HTML 解析作为备用")
107+
except DashboardError as e:
108+
logger.warning(f"Dashboard API 失败: {e},使用 HTML 解析作为备用")
109+
110+
# 备用:HTML 解析
93111
logger.debug("尝试从页面源码提取积分...")
94112
points = await self._extract_points_from_source(page)
95113

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

110-
points = self._parse_points(points_text)
128+
if points_text.strip():
129+
points = self._parse_points(points_text.strip())
111130

112-
if points is not None and points >= 100:
113-
logger.info(f"✓ 当前积分: {points:,}")
114-
return points
115-
elif points is not None:
116-
logger.debug(f"积分值太小,可能是误识别: {points}")
131+
if points is not None and points >= 100:
132+
logger.info("✓ 当前积分获取成功")
133+
return points
134+
elif points is not None:
135+
logger.debug(f"积分值太小,可能是误识别: {points}")
117136

118137
except Exception as e:
119138
logger.debug(f"选择器 {selector} 失败: {e}")
@@ -310,7 +329,12 @@ async def _check_task_status(self, page: Page, selectors: list, task_name: str)
310329
Returns:
311330
任务状态字典
312331
"""
313-
status = {"found": False, "completed": False, "progress": None, "max_progress": None}
332+
status: dict[str, bool | int | None] = {
333+
"found": False,
334+
"completed": False,
335+
"progress": None,
336+
"max_progress": None,
337+
}
314338

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

344-
if status["progress"] >= status["max_progress"]:
370+
if progress_val >= max_progress_val:
345371
status["completed"] = True
346372

347373
logger.debug(f"{task_name} 状态: {status}")

src/api/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""API 模块
2+
3+
提供面向业务层的统一 API 访问入口,包括仪表盘相关的客户端封装以及数据模型。
4+
该模块聚合了对外公开的主要类型,方便调用方通过 `api` 包进行导入和使用。
5+
6+
主要组件
7+
-------
8+
- ``DashboardClient``: 仪表盘 API 客户端,对外提供高层封装的请求接口
9+
- ``DashboardError``: 仪表盘相关错误类型,用于封装请求或解析过程中的异常
10+
- ``DashboardData``: 仪表盘整体数据模型
11+
- ``UserStatus``: 用户当前状态信息模型
12+
- ``LevelInfo``: 用户等级与经验值等信息模型
13+
- ``SearchCounter`` / ``SearchCounters``: 搜索计数与统计信息模型
14+
- ``Promotion``: 活动与促销信息模型
15+
- ``PunchCard``: 打卡与活跃度相关的数据模型
16+
"""
17+
18+
from .dashboard_client import DashboardClient, DashboardError
19+
from .models import (
20+
DashboardData,
21+
LevelInfo,
22+
Promotion,
23+
PunchCard,
24+
SearchCounter,
25+
SearchCounters,
26+
UserStatus,
27+
)
28+
29+
__all__ = [
30+
"DashboardClient",
31+
"DashboardError",
32+
"DashboardData",
33+
"UserStatus",
34+
"LevelInfo",
35+
"SearchCounter",
36+
"SearchCounters",
37+
"Promotion",
38+
"PunchCard",
39+
]

0 commit comments

Comments
 (0)