222 lines
13 KiB
Markdown
222 lines
13 KiB
Markdown
# 新设备注册到指定账号并完成任务 — 设计与缺口文档
|
||
|
||
> 状态:草案 v1(2026-07-23)
|
||
> 范围:把"每次运行生成全新设备 → 注册到指定账号 → 用该新设备跑完整任务链"这件事从**现状**推进到**可闭环**。
|
||
> 关联:`task_plan.md`(阶段 1–12)/ `findings.md` / `progress.md` / `core/README.md`
|
||
|
||
---
|
||
|
||
## 1. 目标与范围
|
||
|
||
### 1.1 目标拆解
|
||
"新设备注册到指定账号,并用这个新设备完成后续任务"实际包含三件事:
|
||
|
||
1. **新设备身份生成**:每次运行产出一套全新、自洽的设备画像(did/oDid/rdid/egid + 硬件 + runtime hints)。
|
||
2. **新设备注册到服务端**:通过 DFP bootstrap 让服务端签发并承认这套设备身份(cloud did + egid)。
|
||
3. **新设备绑定到指定账号**:让服务端把"这套新设备"与"指定账号"关联起来,后续任务请求同时带账号会话 + 新设备身份并被接受。
|
||
|
||
### 1.2 关键解释分叉(需明确,影响工作量量级)
|
||
|
||
| 解释 | 含义 | 是否需要登录流 |
|
||
|------|------|----------------|
|
||
| **A. 账号已登录态绑定** | 账号已通过 `.env` 持有有效 `api_st/h5_st/client_salt`;"注册到账号"= 让新设备成为该已登录账号的活跃设备 | 否(若 token 不绑设备)/ 待验证 |
|
||
| **B. 全新登录绑定** | 仅有账号凭据(手机号+密码/短信),需在新设备上走完整登录流程,拿设备绑定的 token | 是(passport SDK 逆向,量大) |
|
||
|
||
**推荐路线**:先按 A 推进并用真机/在线测试验证 token 是否跨设备存活(成本低);仅当 A 不可行(token 强绑设备)才投入 B。本文档以 A 为主线,B 作为兜底章节。
|
||
|
||
---
|
||
|
||
## 2. 账号–设备绑定模型(基于证据)
|
||
|
||
证据来源:`out/FINDINGS.md`、`out/h5_ticket_direct_20260712_142457.log`(H5 Cookie 完整抓取)、`core/captured_profile.py`、`core/device_cookie.py`。
|
||
|
||
### 2.1 三类身份材料
|
||
| 类别 | 字段 | 绑定对象 | 获取方式 | 当前来源 |
|
||
|------|------|----------|----------|----------|
|
||
| 账号会话 | `kuaishou.api_st`(native,~405B)、`kuaishou.h5_st`(web,~402B)、`userId`/`ud` | 账号 + 登录态 | 登录接口签发(HttpOnly) | `.env` 抓包固化 |
|
||
| 账号密码材料 | `client_salt`(32-hex) | 账号 | `QCurrentUser.getTokenClientSalt()` | `.env` / `captured_profile.py` |
|
||
| 设备身份 | `did`/`oDid`/`rdid`/`egid` + 硬件字段 | 设备 | DFP bootstrap 在线签发 / 本地派生 | 内存设备生成 |
|
||
| 应用常量 | `client_key=2ac2a76d` | 应用 | 静态 | 硬编码 |
|
||
|
||
### 2.2 绑定机制 = 隐式共载
|
||
- **未发现**独立的"bind device to account"接口(HAR 内 grep `passport/login/account/grant` 无命中,仅 `refresh=false` 噪声)。
|
||
- 账号 token 与设备 did/egid **同处一个 Cookie** 共载上报;服务端在"看到有效账号 token 携带某设备 did/egid"时建立关联。
|
||
- `__NStokensig = SHA256(sig + client_salt)`:`client_salt` 账号绑定、**非设备绑定**,理论上可在任意设备复用(`out/FINDINGS.md` 已验证)。
|
||
|
||
### 2.3 两套会话 token(关键)
|
||
- `kuaishou.api_st`:native API 链路(广告拉取/上报、API 签到/宝箱)。
|
||
- `kuaishou.h5_st`:H5 WebView 链路(余额、task_list、签到、宝箱信息)。
|
||
- 两者**各自独立**、均账号绑定、均 HttpOnly。新设备能否跑通,需分别验证。
|
||
|
||
---
|
||
|
||
## 3. 现状评估
|
||
|
||
| 环节 | 状态 | 证据 / 位置 |
|
||
|------|------|-------------|
|
||
| 新设备画像生成(全套随机) | ✅ 完成 | `core/device_profile.py:289-489` `DeviceProfileGenerator.new_profile` |
|
||
| 进程内即用即弃(不落盘) | ✅ 完成 | `main.py:727-747` `register_memory_device` |
|
||
| DFP 在线注册设备(cloud did+egid) | ✅ 完成 | `tools/new_device.py:54-113`、`core/dfp_client.py` |
|
||
| API 链路使用新设备 | ✅ 已验证 200 OK | `progress.md`(换设备后广告拉取 result=1) |
|
||
| 账号 token 复用于新设备(API) | 🟡 实测可用 | 同上:换设备后 API 仍 200 |
|
||
| **H5 链路使用新设备** | 🟡 opt-in(`--rotate-h5-device`,默认关) | D1 已实现;G2 待在线验证 |
|
||
| 账号 token 复用于新设备(H5) | ❓ 未测试 | 因 §4.1 阻塞,h5_st 从未在新设备上跑过 |
|
||
| 登录流(获取设备绑定 token) | 🟡 已抓到+定性(capture/) | 见 `docs/capture_login_chain.md`;运营商一键登录,会话走 libpfl 加密未还原 |
|
||
| 新设备登录风控(短信/通知/审核) | ❌ 未处理 | — |
|
||
| 出口 IP 轮换 | ❌ 缺失 | `core/dfp_client.py:74-113`、`main._request` 裸连 |
|
||
| bootstrap 失败硬断言 | 🟡 opt-in(`--strict-device-online`,默认关) | D1 已实现(`main.py` `register_memory_device`) |
|
||
|
||
---
|
||
|
||
## 4. 缺口清单(按优先级)
|
||
|
||
### G1【P0·已实现 opt-in】H5 cookie 未随设备轮换
|
||
- **位置**:`main.py:662-663`(`h5_cookie_dict/h5_full_cookie` 仅 `__init__` 拷贝一次);`main.py:723-725`(`_apply_device_profile` 只更 `cookie_dict/full_cookie`)。
|
||
- **后果**:`query_balance/final_balance/task_list/sign_in_resource/sign_in/treasure_box_info/open_treasure_box` 的 Cookie 头与 `__NS_sig3`/`kww`(`main.py:1036,1062,1075`)仍用旧账号设备 → **一次运行双设备**(H5 旧 / API 新)→ "新设备注册到账号"在 H5 侧根本没发生。
|
||
- **修法**:
|
||
```python
|
||
def _apply_device_profile(self, profile: DeviceProfile) -> None:
|
||
self.cookie_dict = apply_device_profile_to_cookie(self.cookie_dict, profile)
|
||
self.full_cookie = cookie_to_string(self.cookie_dict)
|
||
self.h5_cookie_dict = apply_device_profile_to_cookie(self.h5_cookie_dict, profile)
|
||
self.h5_full_cookie = cookie_to_string(self.h5_cookie_dict)
|
||
```
|
||
- **验证**:`--memory-device` 在线跑,对比两次 `out/request_replay_*.jsonl` 中 H5 接口 Cookie 的 `did/egid` 是否换新且互异。
|
||
|
||
### G2【P0·验证未知】h5_st / api_st 是否跨设备存活
|
||
- **问题**:账号 token 是在旧设备上签发的,新设备上携带是否被服务端接受?API 侧已观测 200,**H5 侧从未测过**(被 G1 阻塞)。
|
||
- **动作**:先修 G1,再在线用 `--memory-device` 跑 H5 链路,观察 `sign_in_resource`/`treasure_box_info` 是否 `result=1`。
|
||
- **分支**:
|
||
- 若 H5 也 200 → 解释 A 成立,无需登录流,跳到 G4+。
|
||
- 若 H5 返回会话失效/设备校验类错误 → token 强绑设备,需走 G3(登录流)。
|
||
|
||
### G3【P1·大块逆向】登录流(仅 G2 失败时启用)
|
||
- **现状**:HAR 未捕获登录;无 passport/login SDK 逆向产物。
|
||
- **需补**:
|
||
1. 真机抓一次**全新设备首次登录**的完整 HAR(passport 域、登录接口、短信验证、token 签发)。
|
||
2. 逆向登录接口签名(可能复用已还原的 sig/sig3/xfalcon,但登录体有额外字段)。
|
||
3. 还原 `api_st/h5_st/client_salt` 的签发与下发路径(`getTokenClientSalt()` 来源)。
|
||
4. 处理短信验证码 / 滑块 / 设备校验等服务端挑战。
|
||
- **依赖**:账号凭据(手机号+密码 或 接码通道)。
|
||
|
||
### G4【P1·风控前置】出口 IP 轮换
|
||
- **问题**:同 IP 反复注册新设备 + 跑任务 = 经典风控信号。`dfp_client.post_request`、`main._request` 均无 `proxies` 接入。
|
||
- **修法**:给 `KsNebulaClient` / DFP client 加 `proxies` 透传(`KS_HTTP_PROXY` / per-run 代理池),每运行换出口 IP。
|
||
|
||
### G5【P1·已实现 opt-in】bootstrap 失败应硬失败
|
||
- **位置**:`main.py:731-742`。
|
||
- **问题**:bootstrap 异常或无 egid 时 fallback 到本地假 egid(`device_id.py:98-110` sha512,服务端未签发)后照跑 → 必触发风控。
|
||
- **修法**:`device_online` 模式下 bootstrap 失败 → `raise SystemExit`,不 fallback;或加 `--allow-fallback-egid` 显式开关。
|
||
|
||
### G6【P2·用法】设备 seed 不可固定
|
||
- **位置**:`main.py:1368`(`--device-seed`)、`device_profile.py:287`。
|
||
- **问题**:固定 seed → 每次生成**完全相同**设备。文档/示例里 `--device-seed 20260711` 仅供 dry-run 复现。
|
||
- **修法**:默认不传 seed;如需可复现,用运行时间派生唯一 seed 并记录到日志。
|
||
|
||
### G7【P2·质量】设备字段合理性 / 去聚类
|
||
- **位置**:`device_profile.py:291`(`install_time_ms = now - randint(10s,600s)` → 每个新设备都"10 分钟内刚装",弱聚类特征)。
|
||
- **修法**:install_time 分布到几小时~几天;3 个硬件模板可扩充;`keeper_seed/du/manus/ipv6_map` 已随机(确认其确实进入 DFP deviceInfo 且语义合法)。
|
||
|
||
### G8【P2·一致性】STED / native 持久化态随 egid 同步
|
||
- **位置**:`core/device_profile.py:154-169` `sync_egid_cache` / `refresh_persisted_cache_m`、`core/ksse_sted.py`。
|
||
- **现状**:换 egid 后 `sted_cache_json`/`persisted_cache_m` 已重算并进 DFP deviceInfo。
|
||
- **需确认**:这些 native 态在"纯 Python 无真机"下是否仅作为 deviceInfo 字段上报(服务端不读回本地文件)——若是则已足够;若服务端有读回校验则需补持久化模拟。**初判:仅上报字段,已足够。**
|
||
|
||
---
|
||
|
||
## 5. 关键未知与决策点
|
||
|
||
| 编号 | 未知 / 决策 | 解决方式 | 阻塞 |
|
||
|------|------------|----------|------|
|
||
| Q1 | h5_st/api_st 是否跨设备存活? | 修 G1 后在线测 H5(G2) | 决定是否需 G3 |
|
||
| Q2 | 是否有账号凭据可供全新登录? | 用户确认 | 决定 G3 可行性 |
|
||
| Q3 | 新设备登录是否触发短信/通知风控? | 真机抓登录 HAR 观察 | G3 子问题 |
|
||
| Q4 | DFP bootstrap 在高频注册下是否被限流? | 多次注册实测 | G4、G5 |
|
||
| Q5 | 单账号挂多新设备的服务端上限? | 实测 + 观察风控响应 | 影响换设备策略 |
|
||
|
||
---
|
||
|
||
## 6. 实施计划(A 路线优先)
|
||
|
||
### 阶段 D1:打通 H5 新设备(P0,1–2 项改动)
|
||
- [ ] 修 G1(`_apply_device_profile` 同步 h5 cookie)
|
||
- [ ] 修 G5(bootstrap 失败硬失败,或加显式 fallback 开关)
|
||
- [ ] 单测:`tests/test_main_device_profile.py` 增"H5 cookie 随 memory device 更新"用例
|
||
- [ ] dry-run 验证 H5 与 API 一致用新设备
|
||
|
||
### 阶段 D2:在线验证 token 跨设备(P0,解 Q1)
|
||
- [ ] `--memory-device` 在线跑完整链,记录 H5 各接口 result
|
||
- [ ] 重复跑 2 次,确认设备每次不同且任务成功
|
||
- [ ] 结论:A 可行 → 进 D3;不可行 → 转 B 路线(阶段 D5+)
|
||
|
||
### 阶段 D3:风控前置(P1)
|
||
- [ ] G4 接入代理池 / `KS_HTTP_PROXY`
|
||
- [ ] G6 默认随机 seed
|
||
- [ ] G7 install_time 去聚类、扩硬件模板
|
||
|
||
### 阶段 D4:闭环与回归(P1)
|
||
- [ ] 全链在线跑 N 次,统计成功率与风控响应
|
||
- [ ] 回归既有单测 + DFP parity 测试
|
||
- [ ] 更新 `task_plan.md` 新增阶段
|
||
|
||
### 阶段 D5(仅 B 路线):登录流逆向
|
||
- [ ] 真机抓首次登录 HAR
|
||
- [ ] 定位 passport/login 接口与签名
|
||
- [ ] 还原 api_st/h5_st/client_salt 签发
|
||
- [ ] 处理短信/滑块挑战
|
||
|
||
---
|
||
|
||
## 7. 验证计划
|
||
|
||
每阶段"完成"的判定标准(可复现命令):
|
||
|
||
```bash
|
||
# D1 单测
|
||
uv run python -m unittest tests.test_main_device_profile tests.test_device_cookie -v
|
||
|
||
# D1 dry-run(H5+API 一致用新设备)
|
||
uv run python main.py --dry-run --memory-device --no-device-online --out-dir out\d1_dryrun
|
||
|
||
# D2 在线验证(真·每次新设备)
|
||
uv run python main.py --memory-device --out-dir out\d2_online_run1
|
||
uv run python main.py --memory-device --out-dir out\d2_online_run2
|
||
# 比对 run1/run2 的 request_replay_*.jsonl:did/egid 互异且 H5 接口 result=1
|
||
```
|
||
|
||
验收清单:
|
||
- [ ] 两次运行 did/egid 互不相同(无固定 seed 泄漏)
|
||
- [ ] H5 与 API 的 Cookie 中 did/egid **同次运行一致**
|
||
- [ ] H5 接口在线返回 `result=1`(解 Q1)
|
||
- [ ] bootstrap 失败时进程退出而非 fallback 假 egid
|
||
- [ ] 出口 IP 每运行可切换
|
||
|
||
---
|
||
|
||
## 8. 风险
|
||
|
||
| 风险 | 等级 | 缓解 |
|
||
|------|------|------|
|
||
| h5_st 强绑设备 → A 路线失败 | 高 | D2 早验证;失败转 B |
|
||
| 单账号多设备触发风控/封号 | 高 | 限频、IP 轮换、控制换设备次数(已有 `--device-max-switches`) |
|
||
| DFP bootstrap 高频被限流 | 中 | 实测限流阈值;代理 + 退避 |
|
||
| 登录流逆向工作量超预期 | 中 | 仅在 A 失败时启动;先抓 HAR 评估 |
|
||
| 假 egid 静默 fallback 致封号 | 中 | G5 硬失败 |
|
||
| 设备字段聚类特征 | 低 | G7 去聚类 |
|
||
|
||
---
|
||
|
||
## 9. 附录:关键文件索引
|
||
|
||
| 关注点 | 文件 |
|
||
|--------|------|
|
||
| 任务编排 / 设备轮换 | `main.py`(`KsNebulaClient`、`register_memory_device:727`、`maybe_rotate_device_for_record:749`) |
|
||
| 设备画像生成 | `core/device_profile.py` |
|
||
| 设备字段→Cookie | `core/device_cookie.py` |
|
||
| 设备 id 派生 / 假 egid | `core/device_id.py` |
|
||
| DFP 在线注册 | `tools/new_device.py`、`core/dfp_client.py`、`core/dfp_forms.py` |
|
||
| STED 持久化 | `core/ksse_sted.py` |
|
||
| 账号材料样本 | `core/captured_profile.py`、`.env` |
|
||
| H5 会话/ticket 分析 | `out/analyze_h5_ticket_flow.py`、`out/h5_ticket_direct_*.log` |
|
||
| 算法总览 | `core/README.md`、`out/FINDINGS.md` |
|