first commit

This commit is contained in:
douboer
2025-10-24 20:23:16 +08:00
commit 58dd30f0e3
12 changed files with 3296 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
node_modules/
dist/
coverage/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test
*.test.js
*.spec.js

81
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,81 @@
# 架构说明
本文档描述项目中的主要模块、职责划分以及关键流程,帮助维护者快速理解整体结构。
## 模块概览
```
├── README.md // 使用说明与命令入口
├── ARCHITECTURE.md // 架构与流程说明(本文档)
├── login.md // 需求与步骤原始描述
├── block.md // 滑块破解背景与参考脚本
├── src/
│ ├── login.ts // 豆瓣登录主流程Cookie 复用、短信登录、滑块自动化)
│ └── slider.ts // 滑块模拟工具函数与命令行入口
└── typescript-spec.md // TypeScript 编码规范与示例
```
## 登录流程分层
```
┌─────────────────────────────────────────────┐
│ main() │
│ - 创建浏览器 │
│ - 复用或创建上下文 │
│ - 调用 loginWithSms() │
│ - 保存 Cookies │
└─────────────────────────────────────────────┘
┌────────────────▼────────────────────────────┐
│ loginWithSms() │
│ - 输入手机号 │
│ - 点击“获取验证码” │
│ - trySolveSlider() 自动滑块(可选) │
│ - 提示人工输入短信验证码 │
│ - 提交并校验登录状态 │
└────────────────▲────────────────────────────┘
┌────────────────▼────────────────────────────┐
│ trySolveSlider() │
│ - locateSlider() 识别滑块所在 frame/元素 │
│ - inferSliderDistance() 推测拖动距离 │
│ - performSlide() 实际模拟拖动(来自 slider.ts
│ - 检查滑块成功标记 │
└─────────────────────────────────────────────┘
```
- `prepareContext()`:独立负责 Cookie 复用与上下文创建。
- `isLoggedIn()`:封装登录状态检测逻辑,避免主流程重复判断。
- `slider.ts`:抽离通用滑块控制方法,既服务登录流程,也支持命令行调试。
## 依赖与交互
- Playwright浏览器自动化
- Node.js运行环境、文件/路径操作;
- readline控制台交互输入验证码
- 环境变量:控制手机号、滑块自动化开关与参数;
- `~/cookies.json`:持久化登录态,供下一次运行直接复用。
## 扩展点
- **滑块识别**`sliderHandleSelectors``sliderTrackSelectors` 可按需扩充、覆盖,以支持不同验证码厂商。
- **距离计算**:先截取背景图与拼图块做模板匹配确定缺口位置,必要时退回二值化列分析,再配合多组偏移反复尝试;也可用 `DOUBAN_SLIDER_DISTANCE` / `DOUBAN_SLIDER_OFFSETS` 手动覆盖。
- **距离推断**:默认通过轨道宽度估算,特殊情况可直接设置 `DOUBAN_SLIDER_DISTANCE`
- **验证码输入**:目前依赖人工短信验证码,可接入短信网关或 API 进一步自动化。
- **多账号管理**:现仅支持单账号,可通过配置文件或参数改造为批量登录。
## 数据流
1. 启动脚本读取 `DOUBAN_PHONE`、Cookies 路径等基础配置;
2. 初始化浏览器上下文,必要时进入登录页面;
3. 提交手机号,触发滑块验证;
4. 若启用自动滑块,定位组件并拖动;否则提示人工操作;
5. 读取短信验证码,提交登录表单;
6. 验证成功后将 `storageState` 写入 `cookies.json`
7. 后续执行逻辑复用当前登录态或退出浏览器。
## 日志与错误处理
- 主流程捕获未处理异常并输出错误信息;
- 自动滑块阶段出现异常时仅发出警告并回退到人工操作;
- CLI 提示说明每一步的执行结果,便于排查问题。

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
# douban-crawler
使用 Playwright + TypeScript 实现的豆瓣登录与辅助脚本,主要功能包括:
- 通过短信验证码完成豆瓣登录,并持久化 Cookies
- 自动尝试滑块验证,降低人工干预;
- 可单独运行的滑块模拟脚本,便于在其它场景复用。
## 快速开始
```bash
npm install
npx playwright install chromium
```
首次运行登录脚本需要提供手机号:
```bash
DOUBAN_PHONE=13357108011 npm run login
```
若需启用滑块自动化,可增加 `DOUBAN_AUTO_SLIDER=1`,更多环境变量见下文。
## 脚本说明
| 命令 | 说明 |
| -------------------- | ------------------------------- |
| `npm run login` | 豆瓣短信登录,复用 `~/cookies.json` |
| `npm run slider --` | 手动指定页面/选择器进行滑块模拟 |
## 配置项
登录流程支持以下环境变量:
| 变量名 | 说明 | 默认值 |
| ---------------------------------- | ----------------------------------------------------------------------- | ---------------- |
| `DOUBAN_PHONE` | 登录手机号(必填) | - |
| `DOUBAN_AUTO_SLIDER` | 是否尝试自动完成滑块验证(`1` 表示开启) | `0` |
| `DOUBAN_SLIDER_DISTANCE` | 自定义滑块拖动距离(像素);缺省时脚本根据轨道宽度推测 | 自动推测或 200px |
| `DOUBAN_SLIDER_OFFSETS` | 距离微调列表(逗号分隔,逐个尝试以校正识别误差) | 自动选择 |
| `DOUBAN_SLIDER_HANDLE_SELECTOR` | 覆盖默认滑块按钮选择器 | 内置候选 |
| `DOUBAN_SLIDER_TRACK_SELECTOR` | 覆盖默认滑块轨道选择器 | 内置候选 |
| `DOUBAN_SLIDER_BG_SELECTOR` | 覆盖滑块背景图选择器 | 内置候选 |
| `DOUBAN_SLIDER_PIECE_SELECTOR` | 覆盖滑块拼图块选择器 | 内置候选 |
| `DOUBAN_SLIDER_TIMEOUT` | 等待滑块组件出现的超时(毫秒) | 20000 |
Cookies 将默认保存到用户主目录下的 `~/cookies.json`,可根据需要修改 `src/login.ts` 中的路径。
## 开发脚本
- `src/login.ts`:主登录流程,负责 Cookie 复用、短信登录以及滑块自动化;
- `src/slider.ts`:滑块模拟工具,既提供通用函数,也能独立运行;
- `login.md`:原始业务需求与操作步骤;
- `block.md`滑块破解思路Python 版)与 TypeScript 脚本参考;
- `typescript-spec.md`:团队 TypeScript 编码规范与示例。
## 许可
本项目仅用于功能验证和学习,使用时请遵守目标网站的服务条款。

144
block.md Normal file
View File

@@ -0,0 +1,144 @@
使用Python和Playwright破解滑动验证码
滑动验证码是一种常见的验证码形式通过拖动滑块将缺失的拼图块对准原图中的空缺位置来验证用户操作。本文将介绍如何使用Python中的OpenCV进行模板匹配并结合Playwright实现自动化破解滑动验证码的过程。
所需技术
OpenCV模板匹配用于识别滑块在背景图中的正确位置。
Python主要编程语言。
Playwright用于浏览器自动化模拟用户操作。
破解过程概述
获取验证码图像:
下载背景图和滑块图。
进行必要的图像预处理。
模板匹配:
使用OpenCV的模板匹配算法计算滑块在背景图中的最佳匹配位置。
模拟滑动:
生成模拟人类滑动的轨迹,避免被识别为机器人。
使用Playwright模拟滑动操作。
实现步骤
设置环境
首先我们需要设置Python环境并安装相关的依赖包。
sh
pip install playwright opencv-python-headless numpy
playwright install
2. 获取并预处理验证码图像
接下来编写Python代码下载验证码的背景图和滑块图并对图像进行预处理。
python
import cv2
import numpy as np
import requests
from PIL import Image
from io import BytesIO
def get_images(bg_url, slider_url):
bg_response = requests.get(bg_url)
slider_response = requests.get(slider_url)
bg_image = Image.open(BytesIO(bg_response.content))
slider_image = Image.open(BytesIO(slider_response.content))
bg_image.save("background.png")
slider_image.save("slider.png")
def preprocess_images():
bg_img = cv2.imread('background.png')
slider_img = cv2.imread('slider.png', cv2.IMREAD_GRAYSCALE)
return bg_img, slider_img
在上述代码中,我们下载并保存验证码图像,然后将滑块图转换为灰度图进行处理。
模板匹配
使用OpenCV的模板匹配算法来确定滑块在背景图中的正确位置。
python
def find_slider_position(bg_img, slider_img):
result = cv2.matchTemplate(bg_img, slider_img, cv2.TM_CCOEFF_NORMED)
_, _, _, max_loc = cv2.minMaxLoc(result)
top_left = max_loc
return top_left[0]
bg_url = 'background_image_url'
slider_url = 'slider_image_url'
get_images(bg_url, slider_url)
bg_img, slider_img = preprocess_images()
slider_position = find_slider_position(bg_img, slider_img)
print('Slider Position:', slider_position)
这里我们使用TM_CCOEFF_NORMED算法进行匹配并找到最佳匹配位置的坐标。
模拟滑动操作
通过生成一条模拟人类滑动的轨迹并使用Playwright模拟滑动操作。
python
更多内容联系1436423940
from playwright.sync_api import sync_playwright
import time
import random
def generate_track(distance):
track = []
current = 0
mid = distance * 4 / 5
t = 0.2
v = 0
while current < distance:
if current < mid:
a = 2
else:
a = -3
v0 = v
v = v0 + a * t
move = v0 * t + 0.5 * a * t * t
current += move
track.append(round(move))
return track
def simulate_slider_move(page, slider, track):
page.mouse.move(slider['x'], slider['y'])
page.mouse.down()
for x in track:
page.mouse.move(slider['x'] + x, slider['y'], steps=10)
time.sleep(random.uniform(0.02, 0.03))
page.mouse.up()
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('your_target_website_with_captcha')更多内容访问ttocr.com或联系1436423940
slider = page.query_selector('your_slider_css_selector')
bounding_box = slider.bounding_box()
track = generate_track(slider_position)
simulate_slider_move(page, bounding_box, track)
browser.close()
## TypeScript 实现
项目中提供了等价的 TypeScript/Playwright 脚本 `src/slider.ts`按照以下方式运行
```bash
npm run slider -- <url> <滑块选择器> <拖动距离>
```
示例
```bash
npm run slider -- https://example.com '.slider-handle' 180
```
脚本核心逻辑
- `generateTrack(distance)`生成类似人类滑动加速/减速的位移序列
- `performSlide(page, selector, distance)`定位滑块元素模拟按下移动及释放
- 默认以非无头模式启动浏览器可直接观察效果完成滑动后自动关闭

1259
keyhtml.md Normal file

File diff suppressed because one or more lines are too long

147
login.md Normal file
View File

@@ -0,0 +1,147 @@
## 需求
使用playwrighttypescript实现豆瓣自动登录功能。
登录完成后保存cookies下次登录直接使用cookies自动登录。
## 信息
登录页面https://accounts.douban.com/passport/login?source=main
登录账号13357108011要求可配置
cookies保存在~/cookies.json
## 登录页html
<div class="account-main account-body login-wrap login-start ">
<div class="account-body-tabs">
<ul class="tab-start">
<li class="account-tab-phone on">短信登录/注册</li>
<li class="account-tab-account">密码登录</li>
</ul>
<ul class="tab-quick">
<li class="account-tab-scan">二维码登录</li>
</ul>
<div class="account-tab-switch">
<div class="account-tab-switch-icon">
<a class="quick icon-switch "></a>
<a class="start icon-switch "></a>
</div>
<div class="account-tab-switch-text">
<span class="quick">扫码登录</span>
<span class="start">短信登录/注册</span>
</div>
</div>
</div>
<div class="account-tabcon-start">
<div class="account-form">
<div class="account-form-tips">请仔细阅读 <a target="_blank" href="https://accounts.douban.com/passport/agreement?hide_accept=1">豆瓣使用协议、豆瓣个人信息保护政策</a></div>
<div class="account-form-error"><span class="hide"></span></div>
<div class="account-form-raw">
<label class="account-form-label">手机号:</label>
<div class="account-form-field account-form-field-phone ">
<span class="icon clear-input"></span>
<input type="phone" name="phone" maxlength="13" class="account-form-input" placeholder="手机号" tabindex="1">
<div class="account-form-field-area-code">
<div class="account-form-field-area-code-label js-choose-district">+86</div>
</div>
</div>
</div>
<div class="account-form-raw">
<label class="account-form-label">验证码:</label>
<div class="account-form-field account-form-codes">
<input id="code" type="text" name="code" maxlength="6" class="account-form-input" placeholder="验证码" tabindex="2" autocomplete="off">
<div class="account-form-field-code ">
<a href="javascript:;" class="get-code">获取验证码</a>
</div>
</div>
</div>
<div class="account-form-field-submit ">
<a class="btn btn-phone ">登录豆瓣</a>
</div>
</div>
<div class="account-form-ft">
<p class="account-form-link "><a class="help-link" target="_blank" data-action="login_phone_nocode" href="https://help.douban.com/account?app=1#t1-q5">收不到验证码</a></p>
<!-- 账号安全改进 2020-09-23 -->
<!-- <p class="account-form-remember">
<input name="remember" type="checkbox" id="account-form-remember" tabindex="4"><label for="account-form-remember">下次自动登录</label>
</p> -->
</div>
<div class="captcha-error hide">登录出现问题,<a href="javascript:window.location.reload()" data-action="captch_error">反馈并刷新</a></div>
<div class="account-form-3rd ">
<div class="account-form-3rd-hd">第三方登录: </div>
<div class="account-form-3rd-bd">
<a href="https://www.douban.com/accounts/connect/wechat/?from=main&amp;redir=http%3A//www.douban.com" class="link-3rd-wx link-3rd-wx-on" target="_top" title="用微信登录">wechat</a>
<a href="https://www.douban.com/accounts/connect/sina_weibo/?from=main&amp;redir=http%3A//www.douban.com&amp;fallback=" class="link-3rd-wb link-3rd-wb-on" target="_top" title="用微博登录">weibo</a>
</div>
</div>
</div>
<div class="account-tabcon-quick account-quick">
<div class="account-qr-code ">
<div class="account-qr-tips">
你的账号存在安全隐患,为保证账号安全,请在常用设备上使用 <strong>豆瓣 App App 扫码登录</strong>
</div>
<div class="account-qr-scan">
加载中...
</div>
<div class="account-qr-text ark_cls">
打开 <a href="https://www.douban.com/mobile/">豆瓣 App</a><br>
扫一扫登录
</div>
<div class="account-qr-fail hide">
<span>登录失败</span>
<a href="javascript:;" class="btn btn-refresh account-qr-refresh ">点击刷新</a>
</div>
</div>
<div class="account-qr-success hide">
<div class="account-qr-success-hd">扫描成功!</div>
<div class="account-qr-success-bd">
<div class="account-qr-success-bd-text">请在手机上确认登录</div>
<div class="account-qr-success-bd-pic "></div>
<div class="account-qr-success-bd-link ">
<a href="javascript:;">返回二维码登录</a>
</div>
</div>
</div>
<div class="account-qr-link ">
<a href="javascript:;" class="link-phone">短信验证登录</a>
</div>
</div>
</div>
### step1: "手机号"框输入手机号(账号),账号可配置
### step2: 点击"获取验证码"后,弹出滑块验证浮窗。需要等待人工获取登录完成。
### step3: 获得验证码后,在“验证码”框输入"验证码"
### step4: 点击"登录豆瓣"
## 实现
### 项目初始化
```bash
npm install
npx playwright install chromium
```
### 运行登录脚本
```bash
DOUBAN_PHONE=13357108011 npm run login
```
如需自动尝试滑块验证,可额外开启:
```bash
DOUBAN_PHONE=13357108011 \
DOUBAN_AUTO_SLIDER=1 \
npm run login
```
可选环境变量:
- `DOUBAN_SLIDER_DISTANCE`:手动指定拖动距离(像素),若自动测算失败时使用;
- `DOUBAN_SLIDER_OFFSETS`:自定义距离微调列表(逗号分隔,默认根据是否自定义距离选择一组偏移进行多次尝试);
- `DOUBAN_SLIDER_HANDLE_SELECTOR` / `DOUBAN_SLIDER_TRACK_SELECTOR`:覆盖默认滑块、轨道选择器;
- `DOUBAN_SLIDER_BG_SELECTOR` / `DOUBAN_SLIDER_PIECE_SELECTOR`:覆盖滑块背景图、拼图块选择器;
- `DOUBAN_SLIDER_TIMEOUT`:等待滑块出现的超时时间(毫秒,默认 20000
### 行为说明
- 首次运行会在 `~/cookies.json` 写入登录成功后的 Cookies后续执行会优先复用该文件自动登录。
- 如果 cookies 过期或被服务端清理,脚本会自动回退到短信验证码登录流程,并更新 cookies。
- Playwright 以非无头模式运行,便于观察流程;开启自动滑块时脚本会尝试模拟拖动,失败则仍可手动完成。控制台会提示输入短信验证码。

777
package-lock.json generated Normal file
View File

@@ -0,0 +1,777 @@
{
"name": "douban-crawler",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "douban-crawler",
"version": "1.0.0",
"dependencies": {
"playwright": "^1.41.1",
"sharp": "^0.33.3"
},
"devDependencies": {
"@types/node": "^20.11.30",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
"integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"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",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
}
}
}

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "douban-crawler",
"version": "1.0.0",
"description": "Automation scripts for Douban login and crawling.",
"scripts": {
"login": "ts-node src/login.ts",
"slider": "ts-node src/slider.ts"
},
"dependencies": {
"playwright": "^1.41.1",
"sharp": "^0.33.3"
},
"devDependencies": {
"@types/node": "^20.11.30",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
}
}

69
research.md Normal file
View File

@@ -0,0 +1,69 @@
# douban crawler
## 工具
Trace Viewer
Codegen
## todo
使用playwrighttypescript实现豆瓣自动登录功能。
1. login
2. intput book name => bookinfo
```json
{
"book": {
"id": "2567698",
"title": "三体",
"author": "刘慈欣",
"publisher": "重庆出版社",
"publication_date": "2008-01-01",
"number_page": "200",
"cover": "https://img.doubanio.com/797979/cover.jpg",
"isbn": "9787536692930",
"rating": {
"average": 4.8,
"max": 5,
"min": 0,
"rating_count": 12834,
"five_star": 9500,
"four_star": 2500,
"three_star": 600,
"two_star": 150,
"one_star": 84
}
},
"reviews": [
{
"user": "有卡里"
"rating": 5,
"title": "宇宙的黑暗森林法则",
"content": "《三体》让我重新思考人类文明在宇宙中的位置。",
"likes": 42,
"comments_count": 3,
"created_at": "2025-10-23T09:00:00Z"
},
{
"user": "等级分"
"rating": 5,
"title": "测试评论",
"content": "《三体》登陆发大水了饭卡。",
"likes": 21,
"comments_count": 1,
"created_at": "2025-10-22T19:00:00Z"
},
]
}
```
## selector
1. login
https://accounts.douban.com/passport/login?source=main
## 问题
1. 文章中的图片保存解决方案?
- 直存数据库
- 存本地,数据库中存位置信息。
文章中的位置信息呢?也就是拿到图片后,如何在文章中组装呢?

180
src/login.ts Normal file
View File

@@ -0,0 +1,180 @@
/**
* 豆瓣登录脚本入口:
* 1. 优先复用本地缓存的 cookies减少重复登录
* 2. 若失效则走短信验证码流程;
* 3. 登录成功后写回 cookies 供后续脚本复用。
*/
import { chromium, Browser, BrowserContext, Page } from 'playwright';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import readline from 'readline';
const LOGIN_URL = 'https://accounts.douban.com/passport/login?source=main';
const HOME_URL = 'https://www.douban.com';
const COOKIES_PATH = path.join(os.homedir(), 'cookies.json');
const PHONE = process.env.DOUBAN_PHONE ?? '';
/**
* 检查指定路径文件是否存在,避免捕获异常污染主流程。
*/
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* 简单的命令行问答工具,用于等待用户输入验证码或确认步骤。
*/
function prompt(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise<string>((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer);
});
});
}
/**
* 根据当前 URL、页面元素和关键 Cookies 判断是否处于登录态。
*/
async function isLoggedIn(page: Page): Promise<boolean> {
const url = page.url();
if (/accounts\.douban\.com/.test(url)) {
return false;
}
const loginButton = page.locator('text=登录豆瓣');
if ((await loginButton.count()) > 0) {
return false;
}
const cookies = await page.context().cookies();
const hasDbcl2 = cookies.some((cookie) => cookie.name === 'dbcl2');
if (!hasDbcl2) {
return false;
}
const navAccount = page.locator('.nav-user-account');
if ((await navAccount.count()) > 0) {
try {
return await navAccount.first().isVisible();
} catch {
return true;
}
}
return true;
}
/**
* 尝试加载本地 cookies 创建上下文,若失效则回退到全新上下文。
*/
async function prepareContext(browser: Browser): Promise<{
context: BrowserContext;
page: Page;
usedCookies: boolean;
}> {
if (await fileExists(COOKIES_PATH)) {
const context = await browser.newContext({ storageState: COOKIES_PATH });
const page = await context.newPage();
await page.goto(HOME_URL, { waitUntil: 'networkidle' });
if (await isLoggedIn(page)) {
return { context, page, usedCookies: true };
}
console.warn('检测到缓存 Cookies 失效,将重新登录。');
await context.close();
}
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(LOGIN_URL, { waitUntil: 'networkidle' });
return { context, page, usedCookies: false };
}
/**
* 短信验证码登录流程:
* - 输入手机号并触发验证码
* - 提示用户完成必要的前置验证
* - 等待用户输入短信验证码并提交
*/
async function loginWithSms(page: Page, phone: string): Promise<void> {
const phoneInput = page.locator('input[name="phone"]');
await phoneInput.waitFor({ state: 'visible', timeout: 15000 });
await phoneInput.fill(phone);
await page.click('text=获取验证码');
console.log('请在浏览器中完成页面上的验证,并等待短信验证码。');
await prompt('完成验证并收到短信后按 Enter 继续...');
const code = (await prompt('请输入短信验证码: ')).trim();
if (!code) {
throw new Error('未输入短信验证码,登录流程终止。');
}
await page.fill('input[name="code"]', code);
await page.click('text=登录豆瓣');
try {
await page.waitForLoadState('networkidle', { timeout: 60000 });
} catch {
// ignore timeout, we will verify via navigation result below
}
await page.waitForTimeout(2000);
await page.goto(HOME_URL, { waitUntil: 'networkidle' });
}
/**
* 程序主入口:协调上下文、执行登录并持久化 cookies。
*/
async function main(): Promise<void> {
if (!PHONE) {
console.error('请通过环境变量 DOUBAN_PHONE 提供登录手机号。');
process.exitCode = 1;
return;
}
const browser = await chromium.launch({ headless: false });
try {
let { context, page, usedCookies } = await prepareContext(browser);
if (usedCookies) {
console.info('已使用缓存 Cookies 自动登录成功。');
} else {
await loginWithSms(page, PHONE);
if (!(await isLoggedIn(page))) {
console.error('登录失败:未能确认登录状态。');
process.exitCode = 1;
return;
}
await context.storageState({ path: COOKIES_PATH });
console.info(`登录成功Cookies 已保存至 ${COOKIES_PATH}`);
}
// 在此处可继续执行需要认证的业务逻辑
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error('执行登录流程时发生错误:', error);
process.exitCode = 1;
});

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "dist",
"types": ["node", "playwright"]
},
"include": ["src/**/*"]
}

526
typescript-spec.md Normal file
View File

@@ -0,0 +1,526 @@
# TypeScript 编码最佳实践(参考 Clean Code TypeScript
> 本规范以 [Clean Code TypeScript](https://github.com/labs42io/clean-code-typescript) 为基础,结合常见业务场景与团队协作经验,为 TypeScript 项目提供可执行的最佳实践。所有规则都配有简短示例,便于在代码生成、评审与重构中快速对照。
## 1. 变量与常量
- **语义化命名**:使用易读且准确的词汇描述变量含义,避免首字母缩写或无意义命名。
示例:
```ts
const remainingTrialDays = totalTrialDays - usedTrialDays;
```
- **统一词汇表述**:同一概念使用一致命名,减少认知成本。
示例:
```ts
type SubscriptionPlan = 'free' | 'pro';
const currentPlan: SubscriptionPlan = 'pro';
```
- **可搜索名称**:对常量、配置使用具备辨识度的名称,方便全局检索。
示例:
```ts
const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
```
- **解释性变量**:将复杂表达式抽取为具名变量,让意图自说明。
示例:
```ts
const hasIncompleteProfile = !user.email || !user.phone;
if (hasIncompleteProfile) {
promptProfileCompletion();
}
```
- **避免多余上下文**:变量名中不要重复所在模块的上下文信息。
示例:
```ts
// 数据存于 billing 模块,无需前缀 billing
const invoiceTotal = calcInvoiceTotal(items);
```
- **默认参数优于短路**:使用函数默认值替代 `||` 等短路写法,让可选参数更清晰。
示例:
```ts
function sendReport(recipient: string, { retries = 3 }: { retries?: number } = {}) {
/* ... */
}
```
## 2. 函数设计
- **函数只做一件事**:将复杂操作拆分为独立函数,保持逻辑内聚。
示例:
```ts
function createUser(dto: CreateUserInput) {
validateUser(dto);
const user = buildUser(dto);
return saveUser(user);
}
```
- **限制参数数量**:理想状态 ≤ 2 个参数;若超出,改用具名对象并定义类型。
示例:
```ts
interface ScheduleOptions {
triggerAt: Date;
retryCount?: number;
}
function scheduleReminder(userId: string, options: ScheduleOptions) { /* ... */ }
```
- **描述性函数名**:函数名应说明行为与结果,避免依赖注释解释。
示例:
```ts
function calculateLoyaltyDiscount(order: Order): number { /* ... */ }
```
- **避免标志参数**:用枚举或拆分函数替代布尔开关,消除逻辑分支迷雾。
示例:
```ts
type ExportMode = 'draft' | 'final';
function exportDocument(mode: ExportMode) { /* ... */ }
```
- **规避副作用**:默认生成纯函数,必要副作用通过返回值或专门模块处理。
示例:
```ts
function formatDisplayName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
```
- **封装条件表达式**:复杂条件抽离成具名函数或变量,降低认知负担。
示例:
```ts
function isVipEligible(order: Order) {
return order.total > 200 && order.customerTier === 'gold';
}
if (isVipEligible(order)) {
applyVipBenefits(order);
}
```
- **提前返回与正向逻辑**:优先正向判断并提前返回,避免深层嵌套与否定条件。
示例:
```ts
function ensureActive(user: User) {
if (!user.isActive) {
throw new Error('User is inactive');
}
return user;
}
```
- **删除死代码**:及时移除无调用、无覆盖的函数或分支,保持代码整洁。
示例:
```ts
// 检测到无引用后移除旧方法
// function legacyPayment() { /* ... */ }
```
## 3. 对象与数据结构
- **使用访问器**:通过 getter/setter 提供受控访问,便于增加校验与日志。
示例:
```ts
class Account {
constructor(private _balance = 0) {}
get balance(): number {
return this._balance;
}
deposit(amount: number) {
this._balance += amount;
}
}
```
- **隐藏实现细节**:使用 `private` 或 `#` 字段,阻止外部直接修改内部状态。
示例:
```ts
class TokenStore {
#tokens = new Map<string, string>();
set(token: string, value: string) {
this.#tokens.set(token, value);
}
}
```
- **偏好不可变数据**:使用 `Readonly<T>` 或解构复制,避免共享状态污染。
示例:
```ts
const updatedOrder: Readonly<Order> = { ...order, status: 'shipped' };
```
- **类型守卫保障安全**:自定义类型守卫封装校验逻辑,提升复用与可读性。
示例:
```ts
function isAppConfig(value: unknown): value is AppConfig {
return typeof value === 'object' && value !== null && 'env' in value;
}
```
- **利用字面量约束取值**:针对有限集合使用联合字面量或 `enum`,防止魔法字符串。
示例:
```ts
type OrderStatus = 'pending' | 'paid' | 'failed';
const status: OrderStatus = 'paid';
```
## 4. 类与面向对象
- **组合优先于继承**:通过组合与接口扩展行为,降低继承层级带来的窜扰。
示例:
```ts
class UserNotifier {
constructor(private readonly emailSender: EmailSender) {}
notifyWelcome(user: User) {
return this.emailSender.sendWelcome(user.email);
}
}
```
- **保持类职责单一**:类只关注一个领域概念,额外能力拆分为协作类。
示例:
```ts
class PasswordHasher {
hash(value: string): Promise<string> {
return bcrypt.hash(value, 12);
}
}
```
- **接口驱动契约**:用接口定义外部依赖,便于替换实现与注入假对象测试。
示例:
```ts
interface EmailSender {
sendWelcome(email: string): Promise<void>;
}
class SmtpEmailSender implements EmailSender { /* ... */ }
```
- **方法链只在自然场景使用**:当返回 `this` 能提升易用性(如构建器)时才启用链式调用。
示例:
```ts
class QueryBuilder {
private filters: string[] = [];
where(condition: string) {
this.filters.push(condition);
return this;
}
build() {
return this.filters.join(' AND ');
}
}
```
- **访问修饰符显式化**:始终标识 `public`/`private`/`protected`,并优先使用 `readonly` 属性。
示例:
```ts
class FeatureFlag {
constructor(public readonly key: string, private readonly enabled: boolean) {}
isEnabled() {
return this.enabled;
}
}
```
## 5. SOLID 原则
- **单一职责SRP**:模块应聚焦唯一变更原因,降低耦合。
示例:
```ts
class InvoicePrinter {
constructor(private readonly formatter: InvoiceFormatter) {}
print(invoice: Invoice) {
return this.formatter.format(invoice);
}
}
```
- **开放-封闭OCP**:通过扩展实现新需求,而非修改已有代码。
示例:
```ts
interface PricingStrategy {
calculate(order: Order): number;
}
class VipPricingStrategy implements PricingStrategy { /* ... */ }
```
- **里氏替换LSP**:子类型必须兼容父类型契约,避免破坏预期。
示例:
```ts
function printTotals(calculator: PricingStrategy, order: Order) {
console.log(calculator.calculate(order));
}
```
- **接口隔离ISP**:提供小而专注的接口,避免强迫消费者实现无关方法。
示例:
```ts
interface ReadonlyRepository<T> {
findById(id: string): Promise<T | null>;
list(): Promise<T[]>;
}
```
- **依赖反转DIP**:高层模块依赖抽象,低层实现通过注入传入。
示例:
```ts
class PaymentService {
constructor(private readonly gateway: PaymentGateway) {}
pay(invoice: Invoice) {
return this.gateway.charge(invoice);
}
}
```
## 6. 异步与并发
- **优先使用 async/await**:将 Promise 链写成同步流程,更易读并易于错误处理。
示例:
```ts
export async function loadUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
```
- **集中处理错误**:在靠近调用边界处统一捕获和记录,保留上下文。
示例:
```ts
try {
await processPayment(orderId);
} catch (error) {
logger.error('Payment failed', { orderId, error });
throw new PaymentFailedError(orderId, error);
}
```
- **并发任务显式管理**:使用 `Promise.allSettled` 或自定义控制,避免静默丢失异常。
示例:
```ts
const results = await Promise.allSettled(tasks.map(executeTask));
const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
```
- **支持超时与取消**:利用 `AbortController` 或第三方库防止长时间挂起。
示例:
```ts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
```
- **避免竞争条件**:在共享资源上使用锁、队列或幂等操作保障一致性。
示例:
```ts
await lock.acquire('invoice:' + invoiceId, async () => {
await issueInvoice(invoiceId);
});
```
## 7. 错误处理
- **不忽略错误**:捕获后要记录、包装或重新抛出,切勿空 catch。
示例:
```ts
try {
await cache.save(key, value);
} catch (error) {
metrics.increment('cache.save.failure');
throw error;
}
```
- **抛得早,捕获得晚**:靠近错误发生点抛出,自上层统一处理。
示例:
```ts
function ensureHasToken(token?: string): asserts token is string {
if (!token) {
throw new AuthError('Missing token');
}
}
```
- **提供上下文信息**:传递自定义错误类型或消息,方便排查。
示例:
```ts
class PaymentFailedError extends Error {
constructor(public readonly orderId: string, cause: unknown) {
super(`Payment failed for order ${orderId}`);
this.name = 'PaymentFailedError';
this.cause = cause;
}
}
```
- **使用 `Result` 或 `Either` 模式(可选)**:在复杂流程中用显式返回承载错误,减少异常穿透。
示例:
```ts
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
```
## 8. 格式化与一致性
- **自动化格式化**:采用 `prettier` 或团队约定,保持一致缩进与换行。
示例:
```json
{
"scripts": {
"format": "prettier --write \"src/**/*.{ts,tsx}\""
}
}
```
- **启用严格 lint**:结合 `eslint` 与 `@typescript-eslint` 推行关键规则。
示例:
```json
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"]
}
```
- **避免深层嵌套**:通过提前返回或提取函数减少缩进层数。
示例:
```ts
if (!user) {
return null;
}
if (!user.isActive) {
return null;
}
return user;
```
- **统一命名风格**:类型与类使用帕斯卡命名,函数与变量使用驼峰命名。
示例:
```ts
class PaymentGateway {}
function processPayment() { /* ... */ }
```
- **集中导出入口**:通过 `index.ts` 组织导出,控制模块暴露面。
示例:
```ts
export { processPayment } from './processPayment';
export type { PaymentRequest } from './types';
```
## 9. 注释与文档
- **注释意图而非实现**:仅在业务规则或约束难以通过代码表达时补充说明。
示例:
```ts
// 使用结算日的下一工作日作为默认扣款日期
const defaultChargeDate = getNextBusinessDay(settlementDate);
```
- **使用 JSDoc/TS Doc**:对外暴露的 API 写明输入、输出与异常。
示例:
```ts
/**
* 创建新的租户并返回租户 ID。
* @throws TenantLimitExceededError 当达到租户上限时抛出。
*/
export async function createTenant(input: CreateTenantInput): Promise<string> { /* ... */ }
```
- **不要注释掉代码**:移除暂时不用的逻辑,依赖版本控制追踪历史。
示例:
```ts
// 删除旧实现,改由 git 历史追溯
```
- **链接示例或测试**:复杂模块在注释中指向参考用例或文档。
示例:
```ts
// 更多用法见 tests/invoice/generateInvoice.test.ts
generateInvoice(orderId);
```
## 10. 测试策略
- **安排-执行-断言 (AAA)**:测试结构清晰,便于阅读与维护。
示例:
```ts
it('calculates total price', () => {
const items = [{ price: 10 }, { price: 5 }]; // Arrange
const result = totalPrice(items); // Act
expect(result).toBe(15); // Assert
});
```
- **一个测试一个概念**:每个测试聚焦单一行为,失败时易于定位原因。
示例:
```ts
it('throws when user is inactive', () => {
expect(() => ensureActive({ isActive: false } as User)).toThrow();
});
```
- **利用假对象隔离依赖**:通过实现接口的假对象或 mock 控制外部交互。
示例:
```ts
class FakeEmailSender implements EmailSender {
public sent: string[] = [];
async sendWelcome(email: string) { this.sent.push(email); }
}
```
- **覆盖边界条件**:针对 null、异常路径与类型缩小时的行为编写测试。
示例:
```ts
it('returns empty list when no orders found', () => {
expect(loadOrders([])).toEqual([]);
});
```
- **类型驱动验证**:关键类型转换或守卫应有对应测试,确保类型假设成立。
示例:
```ts
it('accepts valid config objects', () => {
const config = { env: 'prod' };
expect(isAppConfig(config)).toBe(true);
});
```
## 11. 架构最佳实践
- **清晰分层**:按领域、应用、基础设施划分模块,控制依赖方向仅从外层指向内层。
示例:
```ts
// application/useCases/createInvoice.ts
import { Invoice } from '../domain/invoice';
import type { InvoiceRepository } from '../domain/ports';
```
- **组合根集中依赖注入**:在应用入口集中装配依赖,避免在业务代码中到处 `new` 实现。
示例:
```ts
export function createInvoiceService(): InvoiceService {
const repository = new PrismaInvoiceRepository();
return new InvoiceService(repository);
}
```
- **领域对象显式建模**:使用类型与不可变对象表达核心概念,让业务规则留在领域层。
示例:
```ts
type Invoice = Readonly<{
id: string;
amount: number;
issuedAt: Date;
}>;
```
- **边界 DTO 与映射**:在接口适配层定义 DTO与领域对象之间通过映射转换保持领域纯净。
示例:
```ts
type InvoiceDto = { id: string; amount: number; issued_at: string };
function toInvoice(dto: InvoiceDto): Invoice {
return { id: dto.id, amount: dto.amount, issuedAt: new Date(dto.issued_at) };
}
```
- **横切关注集中处理**:将日志、缓存、鉴权等横切逻辑通过中间件或装饰器统一管理。
示例:
```ts
export function withCaching<TArgs extends unknown[], TResult>(
fn: (...args: TArgs) => Promise<TResult>,
) {
return async (...args: TArgs) => cache.remember(JSON.stringify(args), () => fn(...args));
}
```
- **配置与环境隔离**:统一从配置模块读取环境变量,并提供类型安全的访问接口。
示例:
```ts
export const config = {
payment: {
apiKey: process.env.PAYMENT_API_KEY ?? '',
timeoutMs: Number(process.env.PAYMENT_TIMEOUT_MS ?? 5000),
},
} as const;
```
## 附录AI 辅助生成代码提示
- **在提示中植入约束**:明确声明需使用 `strict` TypeScript、限制参数数量、禁止 `any`。
示例:
```txt
请实现 strict TypeScript 的订单接口,禁止 any函数不超过 3 个参数,并写出必要类型定义。
```
- **要求输出结构化**:提示 AI 同步生成类型、实现与测试用例。
示例:
```txt
输出包含类型定义、主要函数实现,以及 Jest 测试示例。
```
- **生成后立即校验**:运行 `tsc --noEmit`、`eslint` 与测试脚本,验证生成代码质量。
示例:
```bash
npx tsc --noEmit && npm run lint && npm test
```
- **迭代式对话**:先让 AI 生成数据结构与接口,再补实现与异常处理,最后检查测试通过。
示例:
```txt
第一步:请先提供 API 接口类型定义。
第二步:基于上一步补充实现与错误处理。
```
> 按照本规范执行,可以在保证代码整洁的前提下,充分利用 TypeScript 的类型系统与语言特性,降低维护成本并提升交付质量。