update at 2025-10-26 10:24:17
This commit is contained in:
192
ARCHITECTURE.md
192
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<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() │
|
||||
│ - 等待滑块出现 │ │ - 检查 Cookie(dbcl2) │
|
||||
│ - 截图到 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 核心创新
|
||||
|
||||
### 简化的距离计算算法
|
||||
|
||||
17
CHANGELOG.md
17
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
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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 个滑块"
|
||||
|
||||
22
README.md
22
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`:关键实现细节记录;
|
||||
|
||||
64
VERSION.md
64
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/<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 短信自动读取,让登录更丝滑 🎉
|
||||
|
||||
BIN
images/debug/template-captcha-1761444176909.png
Normal file
BIN
images/debug/template-captcha-1761444176909.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
images/debug/template-captcha-1761444637479.png
Normal file
BIN
images/debug/template-captcha-1761444637479.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
446
package-lock.json
generated
446
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
35
release.md
35
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` 体验自动读取验证码。
|
||||
|
||||
26
src/login.ts
26
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<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
182
src/sms/douban-code.ts
Normal 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
Reference in New Issue
Block a user