diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index d5c4693..be9c99b 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -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
使用说明与运行指引]
+ root --> arch[ARCHITECTURE.md
架构概览与流程说明]
+ root --> impl[IMPLEMENTATION.md
实现细节记录]
+ root --> quick[QUICKSTART.md
快速开始指南]
+ root --> changelog[CHANGELOG.md
更新日志]
+ root --> release[release.md
发布说明]
+ root --> login_doc[login.md
早期需求与操作步骤]
+ root --> pkg[package.json
项目配置]
+ root --> src_dir[src/]
+ root --> noflag[noflag/
原始验证码截图]
+ root --> output_dir[output/
标注结果]
+ root --> ts_spec[typescript-spec.md
编码规范]
+
+ src_dir --> login_ts[login.ts
登录脚本入口]
+ src_dir --> sms_dir[sms/]
+ src_dir --> slider_dir[slider/]
+
+ sms_dir --> sms_code[douban-code.ts
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() │
-│ - 等待滑块出现 │ │ - 检查 Cookie(dbcl2) │
-│ - 截图到 noflag/ │ │ - 确认登录表单状态 │
-│ - 调用 detector │ └────────────────────────┘
-│ - 计算距离 │
-│ - 拖动滑块 │
-│ - 验证成功标识 │
-│ - 失败重试(10次) │
-└────────────────────┘
- │
-┌────────▼───────────────┐
-│ SliderDetector │
-│ - 图像缩放(800px) │
-│ - 多策略检测 │
-│ - 候选框评分 │
-│ - 绘制标注到 output/ │
-└────────────────────────┘
- │
-┌────────▼───────────────┐
-│ CandidateSearch │
-│ - 暗区域检测 │
-│ - Canny 边缘检测 │
-│ - 颜色量化 │
-│ - LAB 色彩空间 │
-│ - IoU 去重 │
-└────────────────────────┘
+```mermaid
+flowchart TD
+ main[main()
• 启动 Chromium
• 复用或创建上下文
• 调用 loginWithSms()
• 保存 Cookies] --> login[loginWithSms()
• 输入手机号
• 触发短信验证码
• 自动处理滑块验证
• 自动读取 macOS 短信验证码
• 提交并校验登录结果]
+ login --> slider[SliderController
• 等待滑块出现
• 截图并调用检测器
• 计算距离与拖动
• 失败自动重试]
+ login --> logged[isLoggedIn()
• 检查 Cookie(dbcl2)
• 确认登录表单状态]
+ slider --> detector[SliderDetector
• 图像缩放(800px)
• 多策略检测
• 候选框评分
• 绘制标注]
+ detector --> candidate[CandidateSearch
• 暗区域检测
• Canny 边缘
• 颜色量化
• LAB 色彩
• IoU 去重]
+ login --> sms[waitForDoubanCode()
• 连接 chat.db
• 跟踪最新消息
• 解析验证码
• 超时降级手动输入]
+ sms --> autofill[自动填入验证码
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[原始验证码
(340x191)] --> img_capture[截图保存
noflag/captcha-*.png]
+ img_capture --> img_scale[缩放至 800px
内存处理图像]
+ img_scale --> img_detect[多策略检测]
+ img_detect --> img_boxes[候选框数组
{x,y,w,h,score}]
+ img_boxes --> img_filter[评分排序 + IoU 去重]
+ img_filter --> img_best[最佳滑块位置
[b1, b2]]
+ img_best --> img_draw[绘制标注
output/captcha-*-detected.png]
+ img_draw --> img_distance[计算距离
(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[最新短信记录
(handle/text/date)]
+ sms_record --> sms_parse[parseDoubanSms()
解析验证码]
+ 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 核心创新
### 简化的距离计算算法
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d80e805..08a28e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
### ✨ 新功能
diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md
index 83891a4..68ca6fa 100644
--- a/IMPLEMENTATION.md
+++ b/IMPLEMENTATION.md
@@ -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
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]
```
辅助文档位于项目根目录:
@@ -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)`
diff --git a/QUICKSTART.md b/QUICKSTART.md
index 3fc71ac..b912ddb 100644
--- a/QUICKSTART.md
+++ b/QUICKSTART.md
@@ -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 个滑块"
diff --git a/README.md b/README.md
index 17a3c40..b3d9cc8 100644
--- a/README.md
+++ b/README.md
@@ -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`:关键实现细节记录;
diff --git a/VERSION.md b/VERSION.md
index 2bb3195..93a98a0 100644
--- a/VERSION.md
+++ b/VERSION.md
@@ -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/
滑块验证模块]
+ slider_dir --> slider_detector[detector.ts
主检测器]
+ slider_dir --> slider_self[detector-self-learning.ts
模板匹配]
+ slider_dir --> slider_controller[slider-controller.ts
Playwright 集成]
+ slider_dir --> slider_candidate[candidate-search.ts
多策略检测]
+ slider_dir --> slider_geometry[geometry.ts
IoU 计算]
+ slider_dir --> slider_image[image.ts
Sobel 边缘]
+ root --> sms_dir[sms/
macOS 短信读取模块]
+ sms_dir --> sms_code[douban-code.ts
解析 chat.db]
+ root --> noflag[noflag/
原始验证码截图]
+ root --> output_dir[output/
标注结果]
```
## 依赖项
- **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 短信自动读取,让登录更丝滑 🎉
diff --git a/images/debug/template-captcha-1761444176909.png b/images/debug/template-captcha-1761444176909.png
new file mode 100644
index 0000000..7c770a3
Binary files /dev/null and b/images/debug/template-captcha-1761444176909.png differ
diff --git a/images/debug/template-captcha-1761444637479.png b/images/debug/template-captcha-1761444637479.png
new file mode 100644
index 0000000..adf1253
Binary files /dev/null and b/images/debug/template-captcha-1761444637479.png differ
diff --git a/package-lock.json b/package-lock.json
index e581359..bc96017 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index a5d86f8..3877c12 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/release.md b/release.md
index 3608a0a..1052793 100644
--- a/release.md
+++ b/release.md
@@ -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` 体验自动读取验证码。
diff --git a/src/login.ts b/src/login.ts
index 296c2cd..29747c3 100644
--- a/src/login.ts
+++ b/src/login.ts
@@ -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 {
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=登录豆瓣');
diff --git a/src/sms/douban-code.ts b/src/sms/douban-code.ts
new file mode 100644
index 0000000..471c8ac
--- /dev/null
+++ b/src/sms/douban-code.ts
@@ -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 {
+ 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 {
+ 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('在设定的时间内未检测到新的豆瓣验证码短信');
+}
diff --git a/todolist.md b/todolist.md
index 127dc03..9c14673 100644
--- a/todolist.md
+++ b/todolist.md
@@ -1,9 +1,11 @@
## todo
1. 滑块检测,自动验证
+✅
2. 短信自动提取
+✅
-## html
+## html参考
整个浮窗代码:
AI生成背景
