# 登录脚本实现笔记(v1.2.0)
本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 引入了完整的滑块验证码自动破解能力,v1.2.0 在此基础上新增 macOS 短信自动读取与回填流程,让整体登录体验更加无感。
## 文件结构
```mermaid
graph TD
src_dir[src/]
src_dir --> login_ts[login.ts
Playwright 入口脚本]
src_dir --> sms_dir[sms/
v1.2.0 新增短信读取模块]
sms_dir --> sms_code[douban-code.ts]
src_dir --> slider_dir[slider/
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`
检查 `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
```
**工作流程**:
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
```
**工作流程**:
1. 使用 Sharp 加载图像
2. 缩放到 800px 宽度(保持宽高比)
3. 调用 `CandidateSearch.findCandidates()` 获取候选框
4. 对每个候选框计算综合评分
5. 按评分排序,选择前 2 个
6. 如果只有 1 个,尝试使用模板匹配找第二个
7. 绘制红框标注并保存到 `outputPath`
8. 返回检测到的滑块位置数组
**评分标准**:
- 形状评分:宽高比、面积合理性
- 色调一致性:内部颜色是否统一
- 边缘密度:边缘特征是否明显
- 梯度平滑度:是否有明确的边界
### `CandidateSearch.findCandidates(rawImage)`
多策略并行检测候选区域:
```typescript
async findCandidates(rawImage: RawImage): Promise
```
**四种策略**:
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
```
**实现细节**:
- 获取滑块按钮的 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 的完整实现细节。滑块自动化已成功集成并经过验证。