update at 2025-10-26 10:24:17

This commit is contained in:
douboer
2025-10-26 10:24:17 +08:00
parent bd8da1d56a
commit 06ac359162
14 changed files with 934 additions and 154 deletions

View File

@@ -1,93 +1,77 @@
# 架构说明v1.1.0
# 架构说明v1.2.0
本文档梳理项目中的主要模块、职责划分以及核心流程帮助维护者快速了解整体结构。当前版本包含短信验证码登录、Cookie 持久化以及 AI 驱动的滑块验证码自动破解功能。
## 模块概览
```
├── README.md // 使用说明与运行指引
├── ARCHITECTURE.md // 架构概览与流程说明(本文档)
├── IMPLEMENTATION.md // 关键实现细节记录
├── QUICKSTART.md // 快速开始指南
├── CHANGELOG.md // 版本更新日志
├── release.md // 发布说明
├── login.md // 早期需求与操作步骤
├── package.json // 项目配置v1.1.0
├── src/
├── login.ts // 豆瓣登录脚本入口(集成滑块验证)
└── slider/ // 滑块验证模块v1.1.0 新增)
├── index.ts // 模块导出
├── types.ts // 类型定义
├── detector.ts // 主滑块检测器
│ ├── detector-self-learning.ts // 第二滑块检测
├── slider-controller.ts // 滑块控制器
├── cli.ts // CLI 批量工具
├── validator.ts // 结果验证工具
│ ├── detection/
│ └── candidate-search.ts // 多策略检测
│ └── utils/
│ ├── geometry.ts // 几何计算
│ └── image.ts // 图像处理
├── noflag/ // 原始验证码截图输出目录
├── output/ // 标注结果输出目录
└── typescript-spec.md // 团队 TypeScript 编码规范
```mermaid
graph TD
root((项目根目录))
root --> readme[README.md<br/>使用说明与运行指引]
root --> arch[ARCHITECTURE.md<br/>架构概览与流程说明]
root --> impl[IMPLEMENTATION.md<br/>实现细节记录]
root --> quick[QUICKSTART.md<br/>快速开始指南]
root --> changelog[CHANGELOG.md<br/>更新日志]
root --> release[release.md<br/>发布说明]
root --> login_doc[login.md<br/>早期需求与操作步骤]
root --> pkg[package.json<br/>项目配置]
root --> src_dir[src/]
root --> noflag[noflag/<br/>原始验证码截图]
root --> output_dir[output/<br/>标注结果]
root --> ts_spec[typescript-spec.md<br/>编码规范]
src_dir --> login_ts[login.ts<br/>登录脚本入口]
src_dir --> sms_dir[sms/]
src_dir --> slider_dir[slider/]
sms_dir --> sms_code[douban-code.ts<br/>macOS 短信读取]
subgraph slider["slider/ 模块"]
direction TB
slider_index[index.ts]
slider_types[types.ts]
slider_detector[detector.ts]
slider_self[detector-self-learning.ts]
slider_controller[slider-controller.ts]
slider_cli[cli.ts]
slider_validator[validator.ts]
slider_detection_dir[detection/]
slider_utils_dir[utils/]
slider_detection_dir --> slider_candidate[candidate-search.ts]
slider_utils_dir --> slider_geometry[geometry.ts]
slider_utils_dir --> slider_image[image.ts]
end
slider_dir --> slider_index
slider_dir --> slider_types
slider_dir --> slider_detector
slider_dir --> slider_self
slider_dir --> slider_controller
slider_dir --> slider_cli
slider_dir --> slider_validator
slider_dir --> slider_detection_dir
slider_dir --> slider_utils_dir
```
## 登录流程分层v1.1.0
## 登录流程分层v1.2.0
```
┌─────────────────────────────────────────┐
main() │
- 启动 Chromium │
- 复用或创建上下文 │
- 调用 loginWithSms() │
- 保存 Cookies │
└─────────────────────────────────────────┘
┌──────────────────▼────────────────────┐
│ loginWithSms() │
│ - 输入手机号 │
│ - 触发短信验证码 │
│ - [v1.1.0] 自动处理滑块验证 │
│ - 等待并提交短信验证码 │
│ - 校验是否登录成功 │
└───────────────────────────────────────┘
┌────────────┴──────────────┐
│ │
┌─────▼──────────────┐ ┌─────────▼──────────────┐
│ SliderController │ │ isLoggedIn() │
│ - 等待滑块出现 │ │ - 检查 Cookiedbcl2
│ - 截图到 noflag/ │ │ - 确认登录表单状态 │
│ - 调用 detector │ └────────────────────────┘
│ - 计算距离 │
│ - 拖动滑块 │
│ - 验证成功标识 │
│ - 失败重试(10次) │
└────────────────────┘
┌────────▼───────────────┐
│ SliderDetector │
│ - 图像缩放(800px) │
│ - 多策略检测 │
│ - 候选框评分 │
│ - 绘制标注到 output/ │
└────────────────────────┘
┌────────▼───────────────┐
│ CandidateSearch │
│ - 暗区域检测 │
│ - Canny 边缘检测 │
│ - 颜色量化 │
│ - LAB 色彩空间 │
│ - IoU 去重 │
└────────────────────────┘
```mermaid
flowchart TD
main[main()<br/>• 启动 Chromium<br/>• 复用或创建上下文<br/>• 调用 loginWithSms()<br/>• 保存 Cookies] --> login[loginWithSms()<br/>• 输入手机号<br/>• 触发短信验证码<br/>• 自动处理滑块验证<br/>• 自动读取 macOS 短信验证码<br/>• 提交并校验登录结果]
login --> slider[SliderController<br/>• 等待滑块出现<br/>• 截图并调用检测器<br/>• 计算距离与拖动<br/>• 失败自动重试]
login --> logged[isLoggedIn()<br/>• 检查 Cookie(dbcl2)<br/>• 确认登录表单状态]
slider --> detector[SliderDetector<br/>• 图像缩放(800px)<br/>• 多策略检测<br/>• 候选框评分<br/>• 绘制标注]
detector --> candidate[CandidateSearch<br/>• 暗区域检测<br/>• Canny 边缘<br/>• 颜色量化<br/>• LAB 色彩<br/>• IoU 去重]
login --> sms[waitForDoubanCode()<br/>• 连接 chat.db<br/>• 跟踪最新消息<br/>• 解析验证码<br/>• 超时降级手动输入]
sms --> autofill[自动填入验证码<br/>input#code]
sms --> fallback[提示手动输入验证码]
```
**关键模块职责**
- `prepareContext()`:负责加载已有 Cookie、创建新上下文以及兜底跳转登录页
- `loginWithSms()`:串联短信登录流程,涵盖用户输入与滑块自动化
- `waitForDoubanCode()`:从 macOS 信息数据库读取最新验证码,失败时回退到手动输入
- `SliderController`Playwright 集成,控制滑块验证的完整流程
- `SliderDetector`:图像处理和滑块位置检测的核心算法
- `CandidateSearch`:多种图像识别策略的并行执行
@@ -98,7 +82,8 @@
- **Playwright**:启动浏览器、操作页面元素、持久化 `storageState`、控制滑块拖动
- **Sharp**:图像处理(缩放、边缘检测、颜色量化、模板匹配)
- **Node.js**:文件读写、路径与环境变量处理
- **readline**:在控制台等待用户输入短信验证码
- **better-sqlite3**:只读访问 `~/Library/Messages/chat.db`解析最新短信验证码macOS
- **readline**:作为短信读取的降级方案,提示用户手动输入验证码
- **环境变量**
- `DOUBAN_PHONE`:登录手机号(必填)
- `DOUBAN_AUTO_SLIDER`:启用自动滑块验证(可选,值为 1 时启用)
@@ -106,7 +91,7 @@
- **`noflag/`**:原始验证码截图存储目录
- **`output/`**:标注结果(红框)存储目录
## 数据流v1.1.0
## 数据流v1.2.0
1. **初始化阶段**
- 读取 `DOUBAN_PHONE`,未配置则终止
@@ -126,48 +111,57 @@
7. 拖动滑块到计算位置
8. 检测成功标识(`.tc-success`
9. 失败则刷新重试(最多 10 次)
- 控制台输入短信验证码并提交
- **[v1.2.0]** 调用 `waitForDoubanCode()` 轮询 chat.db捕获最新验证码
- 若读取失败或超时,提示用户手动输入验证码
- 将验证码填入页面并提交
3. **状态持久化**
- 登录成功后调用 `context.storageState()` 写入 `~/douban-cookie.json`
- 浏览器关闭,后续脚本可直接复用该文件
4. **图像数据流**
```mermaid
flowchart TD
img_raw[原始验证码<br/>(340x191)] --> img_capture[截图保存<br/>noflag/captcha-*.png]
img_capture --> img_scale[缩放至 800px<br/>内存处理图像]
img_scale --> img_detect[多策略检测]
img_detect --> img_boxes[候选框数组<br/>{x,y,w,h,score}]
img_boxes --> img_filter[评分排序 + IoU 去重]
img_filter --> img_best[最佳滑块位置<br/>[b1, b2]]
img_best --> img_draw[绘制标注<br/>output/captcha-*-detected.png]
img_draw --> img_distance[计算距离<br/>(b2.x - b1.x) / scaleX]
```
原始验证码(340x191)
▼ 截图
noflag/captcha-timestamp.png
▼ 缩放到 800px
内存中的处理图像(800x449)
▼ 多策略检测
候选框数组 [{x,y,w,h,score}]
▼ 评分排序 + IoU去重
最佳滑块位置 [b1, b2]
▼ 绘制红框
output/captcha-timestamp-detected.png
▼ 计算距离
移动距离 = (b2.x - b1.x) / scaleX
5. **短信数据流macOS**
```mermaid
flowchart TD
sms_db[~/Library/Messages/chat.db] --> sms_query[better-sqlite3 查询]
sms_query --> sms_record[最新短信记录<br/>(handle/text/date)]
sms_record --> sms_parse[parseDoubanSms()<br/>解析验证码]
sms_parse --> sms_autofill[自动填入 input#code]
sms_parse --> sms_manual[失败时提示手动输入]
```
## 日志与错误处理
- 关键步骤均在控制台打印提示,便于追踪流程
- **[v1.2.0]** 短信读取阶段输出 `[短信读取]` 前缀日志,包含基线 ID、轮询状态与命中消息
- **[v1.1.0]** 滑块检测过程的详细日志:
- 图像缩放信息(原始尺寸 → 检测尺寸)
- 检测到的滑块数量和位置
- 每个滑块的评分和尺寸
- 距离计算公式和结果
- 成功/失败状态和重试次数
- 验证码相关操作采用提示 + `prompt` 方式等待人工输入
- 默认优先使用自动短信读取,`prompt` 只在超时或读取失败时触发
- 登录失败或异常会设置 `process.exitCode` 并输出详细错误信息
- 视觉调试:`output/` 目录中的红框标注图便于人工验证检测准确性
## v1.2.0 新增能力
- **macOS 短信自动读取**:通过 `better-sqlite3` 直接查询 `chat.db`,仅处理新消息并解析验证码。
- **自动回填验证码**:等待 `input#code` 可见后自动填充,减少人为介入。
- **降级与日志机制**:超时或权限不足时回退到控制台输入,并输出明确的失败原因与排查建议。
## v1.1.0 核心创新
### 简化的距离计算算法

View File

@@ -1,5 +1,22 @@
# 更新日志
## [1.2.0] - 2025-10-26
### ✨ 新功能
- **macOS 短信自动读取**:新增 `src/sms/douban-code.ts` 模块,自动扫描 `~/Library/Messages/chat.db` 获取最新豆瓣验证码
- **自动回填验证码**`login.ts` 会在成功读取后直接填入验证码,无需再手动输入
- **智能降级机制**:读取失败或权限不足时自动回退到命令行提示,保障流程可继续
### 🔧 优化
- **日志输出**:新增短信读取阶段的日志前缀,方便排查权限或数据库占用问题
- **输入等待**:显式等待验证码输入框(`#code`)可见,再执行填充,避免元素未就绪导致的失败
### 📝 文档更新
- 更新 README、VERSION、release、ARCHITECTURE、IMPLEMENTATION、QUICKSTART 等文档到 v1.2.0,并补充 macOS 权限配置说明
## [1.1.0] - 2025-10-25
### ✨ 新功能

View File

@@ -1,25 +1,28 @@
# 登录脚本实现笔记v1.1.0
# 登录脚本实现笔记v1.2.0
本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 版本集成了完整的滑块验证码自动破解功能,大幅提升自动化程度
本文记录当前版本豆瓣登录脚本的实现细节、关键函数以及后续可扩展点。v1.1.0 引入了完整的滑块验证码自动破解能力v1.2.0 在此基础上新增 macOS 短信自动读取与回填流程,让整体登录体验更加无感
## 文件结构
```
src/
├── login.ts # Playwright 入口脚本
└── slider/ # v1.1.0 新增滑块验证模块
├── index.ts
├── types.ts
├── detector.ts
├── detector-self-learning.ts
├── slider-controller.ts
├── cli.ts
├── validator.ts
├── detection/
│ └── candidate-search.ts
└── utils/
├── geometry.ts
└── image.ts
```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]
```
辅助文档位于项目根目录:
@@ -30,7 +33,7 @@ src/
- `CHANGELOG.md`:版本更新日志
- `login.md`:早期需求说明,可作为手动操作参考
## 核心流程v1.1.0
## 核心流程v1.2.0
1. **读取配置**
- 通过 `process.env.DOUBAN_PHONE` 获取手机号,缺失时直接退出
@@ -50,7 +53,8 @@ src/
- 调用检测算法识别滑块位置
- 计算滑动距离并执行拖动
- 验证成功后继续,失败则重试(最多 10 次)
- 通过 `prompt` 等待用户输入短信验证码并提交
- **[v1.2.0]** 调用 `waitForDoubanCode()` 轮询 `chat.db`,解析验证码
- 若读取超时或权限不足,提示用户通过 `prompt` 手动输入验证码
- 等待 Playwright 检测到页面离开登录地址或抛出超时
4. **确认状态并写入 Cookie 文件**
@@ -74,13 +78,38 @@ src/
- 页面操作由脚本自动完成(填手机号、点击按钮)
- **[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)`

View File

@@ -1,4 +1,4 @@
# 快速开始 - 滑块验证自动化v1.1.0
# 快速开始 - 滑块验证自动化v1.2.0
## 🚀 5 分钟上手
@@ -9,7 +9,13 @@ cd /Users/gavin/douban-login
npm install
```
### 2. 启用自动滑块验证登录
### 2. 授予完全磁盘访问权限macOS
- 系统设置 → 隐私与安全性 → 完全磁盘访问权限 → 添加终端Terminal/iTerm2/VS Code
- 勾选开关后重启终端,确保能够读取 `~/Library/Messages/chat.db`
- 想快速验证,可执行 `ls ~/Library/Messages/chat.db` 检查权限
### 3. 启用自动滑块验证登录
```bash
DOUBAN_AUTO_SLIDER=1 DOUBAN_PHONE=你的手机号 npm run login
@@ -21,8 +27,9 @@ DOUBAN_AUTO_SLIDER=1 DOUBAN_PHONE=你的手机号 npm run login
- ✅ 计算精确的滑动距离
- ✅ 模拟真人滑动轨迹
- ✅ 自动重试直到成功(最多 10 次)
- ✅ 在 macOS 上自动读取短信验证码,读取失败会提示手动输入
### 3. 独立测试滑块功能
### 4. 独立测试滑块功能
```bash
npm run slider
@@ -38,7 +45,7 @@ npm run slider
DOUBAN_AUTO_SLIDER=1 DOUBAN_PHONE=13800138000 npm run login
```
脚本会自动完成整个登录流程,包括滑块验证。
脚本会自动完成整个登录流程,包括滑块验证与 macOS 短信验证码读取(授权不足时会提示手动输入)
### 场景 2查看检测过程
@@ -120,6 +127,16 @@ const result = await controller.solveSlider(page);
## 🔧 故障排查
### 问题:短信读取失败或一直等待
**症状**:终端反复打印 `[短信读取] 未检测到新的豆瓣验证码短信`,最终回退到手动输入。
**排查步骤**
1. 确认已为终端授予“完全磁盘访问权限”,并在授权后重新启动终端;
2. 使用 `ls ~/Library/Messages/chat.db` 验证终端是否具备读取权限;
3. 检查短信是否确实到达 Mac 的“信息”应用;
4. 若仍失败,可直接在提示时手动输入验证码,稍后再排查权限问题。
### 问题:检测不到滑块
**症状**:日志显示"未检测到滑块"或"检测到 0 个滑块"

View File

@@ -1,14 +1,15 @@
# douban-crawler
**版本**: v1.1.0
**版本**: v1.2.0
> Playwright + TypeScript 脚本,用于完成豆瓣短信验证码登录,并将登录态持久化到本地 Cookie 文件。**已集成 AI 驱动的滑块验证码自动识别和求解功能**。
> Playwright + TypeScript 脚本,用于完成豆瓣短信验证码登录,并将登录态持久化到本地 Cookie 文件。**已集成 AI 驱动的滑块验证码求解与 macOS 短信自动读取功能**。
## ✨ 核心功能
- 🔐 **自动登录**: 支持短信验证码登录流程
- 🧩 **智能滑块识别**: 基于图像处理算法自动识别和求解滑块验证码
- 🎯 **高成功率**: 采用多策略检测算法暗区检测、边缘检测、颜色量化、LAB色彩空间分析
- 📨 **自动短信读取**: 在 macOS 上读取信息 App 的最新验证码(需开启完全磁盘访问权限)
- 🔄 **自动重试**: 验证失败时自动刷新并重试,最多 10 次
- 📊 **详细日志**: 完整的调试信息和截图保存,便于问题追溯
- 🖼️ **可视化调试**: 自动标注检测到的滑块位置,保存带红框标记的图片
@@ -23,6 +24,15 @@ npx playwright install chromium
需要 Node.js ≥ 18。Playwright 会自动下载 Chromium首次运行请确保网络可访问 Playwright CDN。
> 💡 自动短信读取依赖 macOS 本地 `~/Library/Messages/chat.db`,首次使用请为正在运行脚本的终端授予 **完全磁盘访问权限**。
### macOS 权限配置
1. 打开“系统设置” → “隐私与安全性” → “完全磁盘访问权限”macOS Ventura 及以上Monterey 及更早版本在“系统偏好设置” → “安全性与隐私”)。
2. 点击 `+` 号添加你运行脚本的终端(如 Terminal、iTerm2、VS Code
3. 勾选启用后重新启动该终端,再次运行 `npm run login`
4. 想确认权限是否生效,可在终端执行 `ls ~/Library/Messages/chat.db` 检查是否能够读取。
## 使用方式
1. 设置手机号环境变量并运行登录脚本:
@@ -40,7 +50,7 @@ npx playwright install chromium
3. 浏览器会自动打开豆瓣登录页,脚本完成以下操作:
- 填入手机号并点击「获取验证码」;
- 如果启用了自动滑块验证,会自动检测并滑动;否则等待用户手动完成;
- 控制台等待用户输入短信验证码
- 在 macOS 上轮询“信息”App 的最新短信验证码,成功读取会自动填写;若读取失败则提示手动输入
- 验证码提交成功后,脚本将登录态写入 `~/douban-cookie.json` 并退出。
4. 下次运行会优先尝试加载该 Cookie 文件,若仍在有效期内可直接登录。
@@ -62,12 +72,14 @@ npx playwright install chromium
若需要更改 Cookie 保存位置,可在 `src/login.ts` 中调整 `COOKIES_PATH` 定义。
> 若不希望使用自动短信读取,可在终端手动输入验证码;无需额外配置即可回退。
## 工作流程说明
1. 读取 `DOUBAN_PHONE`,未提供则直接退出;
2. 若存在 `~/douban-cookie.json`,加载后访问登录页并校验登录态;
3. 如未登录,执行短信验证码流程,期间需手动处理页面可能出现的滑块或图形验证码;
4. 用户在终端输入收到的短信验证码
4. 在 macOS 上自动读取短信验证码,读取失败或授权不足时回退到终端输入
5. 验证通过后,将当前浏览器上下文的 `storageState` 写入 `~/douban-cookie.json`。
## 常见问题
@@ -75,6 +87,7 @@ npx playwright install chromium
- **登录后仍提示手机号未填写?** 确认 Playwright 浏览器窗口焦点在页面内,避免浏览器阻止自动填充。
- **自动滑块验证失败?** 系统会提示手动完成,或者尝试不启用自动滑块功能。
- **Cookie 未生成?** 只有当脚本确认登录成功时才会写入 Cookie。若终端未看到 "登录成功Cookies 已保存…" 的日志,请检查短信验证码是否正确。
- **短信读取失败?** 确认已在系统设置中为终端授予“完全磁盘访问权限”,并重新启动终端;或直接在提示后手动输入验证码。
## 滑块验证模块
@@ -126,6 +139,7 @@ npx playwright install chromium
## 开发文档
- `src/login.ts`:主登录流程,负责 Cookie 复用、短信登录以及滑块自动化;
- `src/sms/`macOS 短信读取模块,解析 `chat.db` 自动提取验证码;
- `src/slider/`:滑块验证模块,包含检测、移动等完整功能;
- `ARCHITECTURE.md`:整体架构与流程说明;
- `IMPLEMENTATION.md`:关键实现细节记录;

View File

@@ -1,12 +1,20 @@
# 版本信息
## 当前版本v1.1.0
## 当前版本v1.2.0
发布日期2025-10-25
发布日期2025-10-26
## 主要特性
### 🎯 AI 驱动的滑块验证码自动破解
### 📨 macOS 短信自动读取v1.2.0
- ✅ 基于 `better-sqlite3` 读取 `~/Library/Messages/chat.db` 的最新验证码短信
- ✅ 智能忽略旧消息,仅对新到达的“豆瓣网”验证码进行解析
- ✅ 使用正则解析 `验证码xxxx` 格式,支持 4-6 位验证码
- ✅ 自动回填验证码输入框,失败时即时回退到手动输入
- ⚠️ 仅支持 macOS并需要为终端授予“完全磁盘访问权限”
### 🎯 AI 驱动的滑块验证码自动破解v1.1.0
- ✅ 多策略并行检测暗区域、边缘、颜色量化、LAB 色彩空间)
- ✅ 双滑块精准识别(左侧滑块 + 右侧缺口)
@@ -71,23 +79,27 @@ const distance = box.x / scaleX;
## 文件结构
```
src/slider/ # 滑块验证模块
├── detector.ts # 主检测器
├── detector-self-learning.ts # 模板匹配
├── slider-controller.ts # Playwright 集成
├── candidate-search.ts # 多策略检测
├── geometry.ts # IoU 计算
└── image.ts # Sobel 边缘检测
noflag/ # 原始验证码截图
output/ # 红框标注结果
```mermaid
graph TD
root[src/]
root --> slider_dir[slider/<br/>滑块验证模块]
slider_dir --> slider_detector[detector.ts<br/>主检测器]
slider_dir --> slider_self[detector-self-learning.ts<br/>模板匹配]
slider_dir --> slider_controller[slider-controller.ts<br/>Playwright 集成]
slider_dir --> slider_candidate[candidate-search.ts<br/>多策略检测]
slider_dir --> slider_geometry[geometry.ts<br/>IoU 计算]
slider_dir --> slider_image[image.ts<br/>Sobel 边缘]
root --> sms_dir[sms/<br/>macOS 短信读取模块]
sms_dir --> sms_code[douban-code.ts<br/>解析 chat.db]
root --> noflag[noflag/<br/>原始验证码截图]
root --> output_dir[output/<br/>标注结果]
```
## 依赖项
- **playwright**: ^1.41.1浏览器自动化
- **sharp**: ^0.33.3图像处理
- **better-sqlite3**: ^12.4.1本地 SQLite 查询读取短信
- **typescript**: ^5.4.2
## 环境变量
@@ -97,11 +109,14 @@ DOUBAN_AUTO_SLIDER=1 # 启用自动滑块验证
DOUBAN_PHONE=手机号 # 登录手机号(必填)
```
自动短信读取不需要新增环境变量保持终端前台即可
## 已知限制
1. **图像识别准确率** 70-80%复杂背景或低对比度图片识别率较低
2. **验证成功率** 50%受反爬虫机制影响
3. **仅供学习**请遵守网站服务条款不要用于商业或恶意用途
4. **平台限制**短信自动读取仅适用于 macOS且需为终端授予完全磁盘访问权限
## 相关文档
@@ -114,6 +129,25 @@ DOUBAN_PHONE=手机号 # 登录手机号(必填)
## 升级指南
### 从 v1.1.0 升级到 v1.2.0
**新增依赖**
```bash
npm install
```
**系统配置**
- macOS 隐私与安全性 完全磁盘访问权限”,添加并勾选运行脚本的终端
- 修改后需重新启动终端或 VS Code
**代码变更**
- `src/login.ts` 自动调用 `waitForDoubanCode` 读取短信
- 新增 `src/sms/` 模块负责解析 `chat.db`
- 读取失败会保留手动输入流程无需额外开关
**回退策略**
- 若不希望开启自动读取可在提示出现时直接手动输入验证码无需修改代码
### 从 v1.0.0 升级到 v1.1.0
**新增依赖**
@@ -149,4 +183,4 @@ export DOUBAN_AUTO_SLIDER=1
---
**v1.1.0** - 从手动验证到 AI 自动化的飞跃 🎉
**v1.2.0** - AI 滑块 + macOS 短信自动读取让登录更丝滑 🎉

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

446
package-lock.json generated
View File

@@ -1,17 +1,19 @@
{
"name": "douban-crawler",
"version": "1.0.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "douban-crawler",
"version": "1.0.0",
"version": "1.2.0",
"dependencies": {
"better-sqlite3": "^12.4.1",
"playwright": "^1.41.1",
"sharp": "^0.33.3"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20.11.30",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
@@ -457,6 +459,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/better-sqlite3": {
"version": "7.6.13",
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.19.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
@@ -500,6 +512,90 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "12.4.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -548,6 +644,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -567,6 +687,36 @@
"node": ">=0.3.1"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -581,6 +731,44 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
@@ -594,6 +782,60 @@
"dev": true,
"license": "ISC"
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.78.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
"integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
@@ -624,6 +866,91 @@
"node": ">=18"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -675,6 +1002,51 @@
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
@@ -684,6 +1056,52 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -735,6 +1153,18 @@
"license": "0BSD",
"optional": true
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -756,6 +1186,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -763,6 +1199,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -1,16 +1,18 @@
{
"name": "douban-crawler",
"version": "1.1.0",
"version": "1.2.0",
"description": "Douban login automation with AI-powered slider CAPTCHA solver.",
"scripts": {
"login": "ts-node src/login.ts",
"slider": "ts-node --transpile-only src/slider/cli.ts"
},
"dependencies": {
"better-sqlite3": "^12.4.1",
"playwright": "^1.41.1",
"sharp": "^0.33.3"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20.11.30",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"

View File

@@ -196,4 +196,37 @@ if (result.success) {
- [ ] 优化检测算法,提高复杂场景的准确率
- [ ] 添加机器学习模型,替代规则式检测
- [ ] 支持更多网站的滑块验证码
- [ ] 自动提取MAC收到的短信
- [x] 自动提取 macOS 收到的短信验证码v1.2.0 已上线)
- [ ] 拓展短信自动读取到第三方短信服务或非 macOS 平台
## v1.2.0
新增: **macOS 短信自动读取** **自动回填验证码** **智能降级策略** **日志可观测性**
### 🚀 亮点
1. **macOS 短信自动读取**:新增 `src/sms/douban-code.ts` 模块,基于 `better-sqlite3` 读取 `~/Library/Messages/chat.db`,自动捕获最新“豆瓣网”验证码短信。
2. **自动回填验证码**:登录流程会在成功获取验证码后自动填充 `#code` 输入框,提升一次性登录体验。
3. **智能降级策略**:若未授予完全磁盘访问权限或数据库被占用,脚本会输出原因并回退到命令行输入,保证流程不中断。
4. **日志可观测性**:短信阶段新增 `[短信读取]` 日志前缀,帮助定位权限、解析或读取失败的问题。
### 🔧 兼容性要求
- 仅支持 macOS需为运行脚本的终端Terminal/iTerm2/VS Code授予“完全磁盘访问权限”并重启终端。
- 新增依赖 `better-sqlite3@^12.4.1`(同步 API零依赖运行以及类型声明 `@types/better-sqlite3`。
- 保留手动输入验证码流程Windows/Linux 用户或未授权情况下仍可照常使用。
### 📦 目录与配置变更
- 新增 `src/sms/` 目录存放短信读取模块。
- `src/login.ts` 在滑块验证后自动调用短信读取逻辑,并等待验证码输入框可见。
- `README`, `VERSION`, `ARCHITECTURE`, `IMPLEMENTATION`, `QUICKSTART`, `CHANGELOG` 等文档同步至 v1.2.0,增加权限配置说明。
### ✅ 升级指南
```bash
npm install
```
1. 授权完全磁盘访问:系统设置 → 隐私与安全性 → 完全磁盘访问权限 → 添加终端并勾选;
2. 重启终端或 VS Code
3. 运行 `npm run login` 体验自动读取验证码。

View File

@@ -10,6 +10,7 @@ import path from 'path';
import os from 'os';
import readline from 'readline';
import { SliderController } from './slider';
import { waitForDoubanCode } from './sms/douban-code';
const LOGIN_URL = 'https://accounts.douban.com/passport/login?source=main';
const COOKIES_PATH = path.join(os.homedir(), 'douban-cookie.json');
@@ -276,15 +277,28 @@ async function loginWithSms(page: Page, phone: string): Promise<void> {
console.log('未检测到滑块验证或验证已完成');
}
console.log('请等待短信验证码...');
await prompt('收到短信验证码后按 Enter 继续...');
const code = (await prompt('请输入短信验证码: ')).trim();
console.log('正在尝试自动读取短信验证码...');
let code: string | null = null;
try {
const result = await waitForDoubanCode({
logger: (message) => console.log(`[短信读取] ${message}`),
});
code = result.code;
console.log(`✓ 已自动获取验证码:${code}`);
} catch (error) {
console.warn('自动读取验证码失败或超时,将回退到手动输入。');
console.warn(`原因: ${(error as Error).message}`);
code = (await prompt('请输入短信验证码: ')).trim();
}
if (!code) {
throw new Error('未输入短信验证码,登录流程终止。');
throw new Error('未能获取短信验证码,登录流程终止。');
}
await page.fill('input[name="code"]', code);
const codeInput = page.locator('input#code[name="code"]');
await codeInput.waitFor({ state: 'visible', timeout: 10000 });
await codeInput.fill(code);
console.log('正在提交验证码...');
await page.click('text=登录豆瓣');

182
src/sms/douban-code.ts Normal file
View File

@@ -0,0 +1,182 @@
import Database from 'better-sqlite3';
import os from 'os';
import path from 'path';
const APPLE_EPOCH_MS = Date.UTC(2001, 0, 1);
const DB_PATH = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
export interface SmsMessage {
id: number;
text: string;
handle: string;
service: string;
isFromMe: boolean;
date: Date;
}
export interface WaitForCodeOptions {
timeoutMs?: number;
pollIntervalMs?: number;
logger?: (message: string) => void;
}
export interface WaitForCodeResult {
code: string;
message: SmsMessage;
}
interface RawMessageRow {
id: number;
text: string | null;
handle: string | null;
service: string | null;
is_from_me: number;
date_raw: number | null;
}
const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
const DEFAULT_POLL_INTERVAL_MS = 2500;
function appleTimestampToDate(raw: number | null): Date {
if (!raw) {
return new Date(0);
}
let ms = raw;
if (raw > 1e15) {
ms = raw / 1_000_000;
} else if (raw > 1e12) {
ms = raw / 1_000;
} else {
ms = raw * 1000;
}
return new Date(APPLE_EPOCH_MS + ms);
}
function openDatabase(): Database.Database {
return new Database(DB_PATH, { readonly: true, fileMustExist: true });
}
function toSmsMessage(row: RawMessageRow | undefined): SmsMessage | null {
if (!row) {
return null;
}
const text = (row.text ?? '').trim();
if (!text) {
return null;
}
return {
id: row.id,
text,
handle: row.handle ?? '',
service: row.service ?? '',
isFromMe: row.is_from_me === 1,
date: appleTimestampToDate(row.date_raw),
};
}
function fetchLatestMessage(db: Database.Database): SmsMessage | null {
const stmt = db.prepare<[], RawMessageRow>(`
SELECT
message.ROWID AS id,
message.text AS text,
handle.id AS handle,
message.service AS service,
message.is_from_me AS is_from_me,
COALESCE(message.date, message.date_delivered, message.date_read) AS date_raw
FROM message
LEFT JOIN handle ON handle.ROWID = message.handle_id
WHERE message.text IS NOT NULL AND message.text != ''
ORDER BY date_raw DESC
LIMIT 1
`);
return toSmsMessage(stmt.get());
}
function fetchLatestDoubanMessage(db: Database.Database): SmsMessage | null {
const stmt = db.prepare<[], RawMessageRow>(`
SELECT
message.ROWID AS id,
message.text AS text,
handle.id AS handle,
message.service AS service,
message.is_from_me AS is_from_me,
COALESCE(message.date, message.date_delivered, message.date_read) AS date_raw
FROM message
LEFT JOIN handle ON handle.ROWID = message.handle_id
WHERE message.is_from_me = 0
AND message.text IS NOT NULL
AND message.text != ''
AND message.text LIKE '%豆瓣%'
AND message.text LIKE '%验证码%'
ORDER BY date_raw DESC
LIMIT 1
`);
return toSmsMessage(stmt.get());
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function parseDoubanSms(text: string | null | undefined): string | null {
if (!text) {
return null;
}
const match = text.match(/验证码[:]\s*([0-9]{4,6})/);
return match ? match[1] : null;
}
export async function waitForDoubanCode(options: WaitForCodeOptions = {}): Promise<WaitForCodeResult> {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
const deadline = Date.now() + timeoutMs;
const logger = options.logger;
let db: Database.Database | null = null;
try {
db = openDatabase();
const baselineMessage = fetchLatestMessage(db);
const baselineId = baselineMessage?.id ?? 0;
if (logger) {
logger(`已连接 chat.db起始消息 ID: ${baselineId}`);
}
while (Date.now() <= deadline) {
const doubanMessage = fetchLatestDoubanMessage(db);
if (doubanMessage && doubanMessage.id > baselineId) {
const code = parseDoubanSms(doubanMessage.text);
if (code) {
if (logger) {
logger(`捕获验证码短信,消息 ID: ${doubanMessage.id}`);
}
return {
code,
message: doubanMessage,
};
}
}
if (logger) {
logger('未检测到新的豆瓣验证码短信,等待后重试...');
}
await delay(pollIntervalMs);
}
} finally {
if (db) {
db.close();
}
}
throw new Error('在设定的时间内未检测到新的豆瓣验证码短信');
}

File diff suppressed because one or more lines are too long