From 06ac359162f2c93977e4a55c6ab55b8ca376d21e Mon Sep 17 00:00:00 2001 From: douboer Date: Sun, 26 Oct 2025 10:24:17 +0800 Subject: [PATCH] update at 2025-10-26 10:24:17 --- ARCHITECTURE.md | 192 ++++---- CHANGELOG.md | 17 + IMPLEMENTATION.md | 71 ++- QUICKSTART.md | 25 +- README.md | 22 +- VERSION.md | 64 ++- .../debug/template-captcha-1761444176909.png | Bin 0 -> 9302 bytes .../debug/template-captcha-1761444637479.png | Bin 0 -> 6817 bytes package-lock.json | 446 +++++++++++++++++- package.json | 4 +- release.md | 35 +- src/login.ts | 26 +- src/sms/douban-code.ts | 182 +++++++ todolist.md | 4 +- 14 files changed, 934 insertions(+), 154 deletions(-) create mode 100644 images/debug/template-captcha-1761444176909.png create mode 100644 images/debug/template-captcha-1761444637479.png create mode 100644 src/sms/douban-code.ts 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 0000000000000000000000000000000000000000..7c770a319542a8b3339ce6202aed7c396c4aef85 GIT binary patch literal 9302 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
|B31{22myjLJ}JNp1??T0tpYV2F+}YCA@^!i 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 P00000NkvXXu0mjfb_j1! literal 0 HcmV?d00001 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生成背景
success
&nbsp;slider