Files
douban-login/IMPLEMENTATION.md
2025-10-26 10:24:17 +08:00

410 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 登录脚本实现笔记v1.2.0
本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 引入了完整的滑块验证码自动破解能力v1.2.0 在此基础上新增 macOS 短信自动读取与回填流程,让整体登录体验更加无感。
## 文件结构
```mermaid
graph TD
src_dir[src/]
src_dir --> login_ts[login.ts<br/>Playwright 入口脚本]
src_dir --> sms_dir[sms/<br/>v1.2.0 新增短信读取模块]
sms_dir --> sms_code[douban-code.ts]
src_dir --> slider_dir[slider/<br/>v1.1.0 滑块验证模块]
slider_dir --> slider_index[index.ts]
slider_dir --> slider_types[types.ts]
slider_dir --> slider_detector[detector.ts]
slider_dir --> slider_self[detector-self-learning.ts]
slider_dir --> slider_controller[slider-controller.ts]
slider_dir --> slider_cli[cli.ts]
slider_dir --> slider_validator[validator.ts]
slider_dir --> slider_detection[detection/]
slider_dir --> slider_utils[utils/]
slider_detection --> slider_candidate[candidate-search.ts]
slider_utils --> slider_geometry[geometry.ts]
slider_utils --> slider_image[image.ts]
```
辅助文档位于项目根目录:
- `README.md`:使用说明与常见问题
- `ARCHITECTURE.md`:整体架构与流程拆解
- `QUICKSTART.md`:快速开始指南
- `CHANGELOG.md`:版本更新日志
- `login.md`:早期需求说明,可作为手动操作参考
## 核心流程v1.2.0
1. **读取配置**
- 通过 `process.env.DOUBAN_PHONE` 获取手机号,缺失时直接退出
- 检查 `process.env.DOUBAN_AUTO_SLIDER` 是否启用自动滑块验证
2. **准备浏览器上下文** (`prepareContext`)
- 若存在 `~/douban-cookie.json`,以 `storageState` 形式加载
- 打开登录页并调用 `isLoggedIn` 校验是否仍在登录态
- 失效时关闭旧上下文并创建全新 session
3. **执行短信登录** (`loginWithSms`)
- 输入手机号 → 点击「获取验证码」
- **[v1.1.0]** 自动检测并处理滑块验证码:
- 调用 `SliderController.solveSlider()`
- 等待验证码 iframe 出现
- 截图并保存到 `noflag/` 目录
- 调用检测算法识别滑块位置
- 计算滑动距离并执行拖动
- 验证成功后继续,失败则重试(最多 10 次)
- **[v1.2.0]** 调用 `waitForDoubanCode()` 轮询 `chat.db`,解析验证码
- 若读取超时或权限不足,提示用户通过 `prompt` 手动输入验证码
- 等待 Playwright 检测到页面离开登录地址或抛出超时
4. **确认状态并写入 Cookie 文件**
- `isLoggedIn` 再次判断是否登录成功
- 调用 `context.storageState({ path })` 将状态写入 `~/douban-cookie.json`
- 终端提示成功信息,方便用户确认文件路径
## 关键函数
### `isLoggedIn(page: Page): Promise<boolean>`
检查 `dbcl2` Cookie 是否存在,并确认登录表单元素是否仍可见。该组合可较准确判断是否处于登录态,同时避免依赖豆瓣首页 DOM。
### `prepareContext(browser: Browser)`
负责上下文复用策略:优先尝试加载本地 Cookie 创建上下文,如果判定仍未登录则回退到全新会话并跳转登录页。函数返回 `{ context, page, usedCookies }`,调用方可据此判断是否需要重走登录流程。
### `loginWithSms(page: Page, phone: string)`
串联短信验证码登录的主要逻辑,所有用户交互点都通过控制台提示:
- 页面操作由脚本自动完成(填手机号、点击按钮)
- **[v1.1.0]** 滑块验证自动处理(启用 `DOUBAN_AUTO_SLIDER=1` 时)
- **[v1.2.0]** 优先自动读取短信验证码,失败时降级到命令行输入
- 函数内部对提交过程设置合理的等待时间,避免过早关闭浏览器
### `waitForDoubanCode(options?: WaitForCodeOptions)`
负责从 macOS 信息数据库读取最新的验证码短信:
- 使用 `better-sqlite3` 以只读方式打开 `~/Library/Messages/chat.db`
- 记录初始最新消息的 `ROWID`,避免重复解析旧短信
- 周期性查询包含“豆瓣”“验证码”关键词的消息并解析其中的 4-6 位验证码
- 成功返回 `{ code, message }`,失败在超时后抛出异常供调用方降级处理
- `options` 支持 `timeoutMs``pollIntervalMs` 以及 `logger` 回调,便于定制等待时长和日志输出
### `main()`
作为 CLI 入口,负责整体 orchestrate校验配置 → 启动浏览器 → 调用上述函数 → 捕获异常并设置 `process.exitCode`
## v1.2.0 新增能力
1. **短信自动读取模块**
- 新增 `src/sms/douban-code.ts`,通过 `better-sqlite3` 查询 macOS “信息”数据库;
- 解析满足“豆瓣 + 验证码”关键字的最新短信,返回验证码及原始消息。
2. **验证码自动回填**
- `login.ts` 显式等待 `input#code` 可见后填入验证码;
- 日志输出增加 `[短信读取]` 前缀,便于排查权限或解析问题;
- 超时或数据库不可用时抛出异常,交由上层降级到 `prompt`
3. **依赖与配置更新**
- 新增 `better-sqlite3` 依赖及类型声明;
- 文档统一说明 macOS 完全磁盘访问权限要求。
## v1.1.0 新增核心函数
### `SliderController.solveSlider(page, sliderSelector, captchaSelector)`
滑块验证的主控制器,负责完整的验证流程:
```typescript
async solveSlider(
page: Page,
sliderSelector: string = '.tcaptcha_drag_button',
captchaSelector: string = '#tcaptcha_iframe'
): Promise<SliderSolveResult>
```
**工作流程**
1. 等待验证码 iframe 加载(`waitForSelector`
2. 等待滑块背景图完全加载
3. 进入重试循环(最多 10 次):
- 调用 `captureSliderImage()` 截图到 `noflag/`
- 调用 `SliderDetector.detectSlider()` 检测滑块
- 调用 `calculateDistance()` 计算移动距离
- 调用 `dragSlider()` 拖动滑块
- 调用 `checkSuccess()` 检测是否成功
- 成功则返回,失败则刷新验证码重试
**返回值**
```typescript
interface SliderSolveResult {
success: boolean; // 是否成功
attempts: number; // 尝试次数
distance?: number; // 滑动距离(像素)
}
```
### `SliderDetector.detectSlider(imagePath, outputPath, drawBoxes)`
滑块检测的核心算法实现:
```typescript
async detectSlider(
imagePath: string,
outputPath: string,
drawBoxes: boolean = true
): Promise<BoundingBox[] | null>
```
**工作流程**
1. 使用 Sharp 加载图像
2. 缩放到 800px 宽度(保持宽高比)
3. 调用 `CandidateSearch.findCandidates()` 获取候选框
4. 对每个候选框计算综合评分
5. 按评分排序,选择前 2 个
6. 如果只有 1 个,尝试使用模板匹配找第二个
7. 绘制红框标注并保存到 `outputPath`
8. 返回检测到的滑块位置数组
**评分标准**
- 形状评分:宽高比、面积合理性
- 色调一致性:内部颜色是否统一
- 边缘密度:边缘特征是否明显
- 梯度平滑度:是否有明确的边界
### `CandidateSearch.findCandidates(rawImage)`
多策略并行检测候选区域:
```typescript
async findCandidates(rawImage: RawImage): Promise<BoundingBox[]>
```
**四种策略**
1. **暗区域检测** (`findDarkRegions`)
- 基于亮度阈值(< 100
- 连通组件分析
- 形状过滤宽高比面积
2. **Canny 边缘检测** (`findEdgeDensityRegions`)
- Canny 算法提取边缘
- 滑动窗口统计边缘密度
- 局部最大值抑制
3. **颜色量化** (`findColorQuantizationRegions`)
- K-means 聚类k=5
- 提取少数色块区域
- 形状验证
4. **LAB 色彩空间** (`findLabColorRegions`)
- 转换到 LAB 空间
- 基于 a*、b* 通道的色度检测
- 连通组件分析
**去重策略**
- 计算所有候选框的 IoU交并比
- IoU > 0.3 认为是同一个滑块
- 保留评分最高的
### `calculateDistance(boxes, scaleX)`
**v1.1.0 简化算法**的核心实现:
```typescript
private calculateDistance(
boxes: BoundingBox[],
scaleX: number
): number
```
**逻辑**
```typescript
if (boxes.length >= 2) {
// 双滑块模式(推荐)
// "两只小鸟嘴尖距离"原理
const distance = (boxes[1].x - boxes[0].x) / scaleX;
return Math.round(distance);
} else if (boxes.length === 1) {
// 单滑块模式(兜底)
const distance = boxes[0].x / scaleX;
return Math.round(distance);
} else {
return 0;
}
```
**为什么除以 scaleX**
- 检测在 800px 宽度图像上进行
- 实际显示宽度是 340px
- scaleX = 800 / 340 ≈ 2.35
- 需要将检测坐标转换回显示坐标
### `dragSlider(distance)`
拖动滑块到指定距离:
```typescript
private async dragSlider(distance: number): Promise<void>
```
**实现细节**
- 获取滑块按钮的 bounding box
- 计算起始位置(滑块中心)
- 计算目标位置(起始 + 距离)
- 使用 `page.mouse.move()` 拖动
- `steps` 参数实现平滑移动(默认 20 步)
**拟人化特性**
- 使用 Playwright 的内置缓动函数
- 平滑的加速-减速曲线
- 避免机械化的匀速直线移动
## 错误处理与提示
- 打印清晰的步骤提示,例如"请等待短信验证码…"、"正在提交验证码…"
- **[v1.1.0]** 滑块检测过程的详细日志:
```
[SliderController] 开始滑块验证,最多尝试 10 次
[SliderController] ===== 第 1/10 次尝试 =====
[SliderDetector] 图像已缩放: 340x191 -> 800x449 (scaleX=2.35)
[SliderDetector] 检测到 2 个滑块候选框
[SliderController] 计算距离: (195 - 45) / 2.35 = 63.8px
[SliderController] ✓ 滑块验证成功!
```
- 捕获 Playwright 的超时异常,允许在页面未完全跳转时通过 `isLoggedIn` 再次确认
- 如登录失败会输出明确日志并保持退出码非零,方便在 CI 或脚本中检测
- **[v1.1.0]** 视觉调试:
- `noflag/` 目录保存原始截图
- `output/` 目录保存带红框标注的检测结果
- 便于人工验证检测准确性
## 手动操作注意事项
- Playwright 会以非无头模式启动 Chromium务必保持窗口前台
- **[v1.1.0]** 启用 `DOUBAN_AUTO_SLIDER=1` 时会自动处理滑块
- 如果自动验证失败10 次后),仍可手动完成滑块
- 如短信验证码输入错误,可重新运行脚本
- 保存的 `douban-cookie.json` 与账号强绑定,若切换账号需手动删除或覆盖该文件
- **[v1.1.0]** 可查看 `output/` 目录的标注图验证检测准确性
## v1.1.0 技术细节
### 坐标系统
**两套坐标系**
1. **图像坐标系**800px 宽度,用于检测
2. **显示坐标系**340px 宽度,用于拖动
**转换公式**
```typescript
显示坐标 = 图像坐标 / scaleX
scaleX = 图像宽度 / 显示宽度 ≈ 800 / 340 ≈ 2.35
```
### 距离计算演进
**v1.0.0**:需要人工完成滑块
**v1.1.0 早期**:复杂的坐标转换
```typescript
// 错误的复杂逻辑(已废弃)
const iframeBox = await iframe.boundingBox();
const distance = targetBox.x - sliderBox.x + iframeBox.x - sliderBox.x;
```
**v1.1.0 最终**:简化为几何原理
```typescript
// 正确的简洁逻辑(当前实现)
const distance = (box2.x - box1.x) / scaleX;
```
**为什么简化有效**
- 检测坐标和拖动坐标在同一个相对坐标系中
- iframe 偏移量对两个滑块的影响相同
- 直接计算水平距离差,无需考虑绝对位置
### 图像处理技术
**Sharp 库应用**
1. **图像缩放**
```typescript
const resized = await sharp(imagePath)
.resize(targetWidth, null, { fit: 'inside' })
.raw()
.toBuffer({ resolveWithObject: true });
```
2. **Sobel 边缘检测**
```typescript
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
// 卷积计算边缘强度
```
3. **颜色空间转换**
```typescript
// RGB → LAB
const X = r * 0.4124 + g * 0.3576 + b * 0.1805;
const Y = r * 0.2126 + g * 0.7152 + b * 0.0722;
const Z = r * 0.0193 + g * 0.1192 + b * 0.9505;
```
4. **形态学操作**
```typescript
// 膨胀:扩大白色区域
// 腐蚀:缩小白色区域
// 连通组件分析:查找连续区域
```
### 性能优化
**并行检测**
```typescript
const [darkBoxes, edgeBoxes, colorBoxes, labBoxes] = await Promise.all([
this.findDarkRegions(rawImage),
this.findEdgeDensityRegions(rawImage),
this.findColorQuantizationRegions(rawImage),
this.findLabColorRegions(rawImage),
]);
```
**IoU 去重**
- 避免重复检测同一个滑块
- 减少后续评分计算量
- 提高整体检测速度
**缓存策略**
- 原始截图保存在 `noflag/`,可重复使用
- 标注结果保存在 `output/`,便于批量验证
## 后续拓展建议
1. **多账号支持**:通过配置文件或命令行参数管理多组手机号与存储路径
2. **验证码服务集成**:接入外部短信/验证码平台以减少人工步骤
3. **任务编排**:在登录后追加业务逻辑(例如抓取列表、导出数据),可在 `main` 函数成功分支追加调用
4. **CLI 体验**:封装命令行参数解析,避免频繁依赖环境变量
5. **[v1.1.0+]** 机器学习模型:
- 使用 CNN 替代规则式检测
- 训练分类器识别滑块和缺口
- 提高复杂背景下的准确率
6. **[v1.1.0+]** 更多验证码类型:
- 点选验证码
- 文字识别验证码
- 旋转验证码
7. **[v1.1.0+]** 反爬虫对抗:
- 更自然的鼠标轨迹(贝塞尔曲线)
- 随机延迟和抖动
- 模拟人类思考时间
## v1.1.0 成功的关键因素
1. **用户洞察**"两只小鸟嘴尖距离"的类比帮助简化了距离计算
2. **坐标系统一**:在同一坐标系中计算相对距离,避免复杂转换
3. **多策略并行**:四种检测算法互补,提高鲁棒性
4. **视觉调试**:红框标注便于人工验证和调试
5. **自动重试**10 次重试机制大幅提高成功率
以上内容覆盖 v1.1.0 的完整实现细节。滑块自动化已成功集成并经过验证。