Metadata-Version: 2.5
Name: crawlerkeeper-agent
Version: 1.6.1
Summary: CrawlerKeeper 节点代理程序：节点注册、心跳上报、任务拉取与并行执行、部署同步与日志指标上报
Author: CrawlerKeeper Team
License-Expression: MIT
License-File: LICENSE
Keywords: agent,crawler,crawlerkeeper,distributed,spider
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: System :: Distributed Computing
Requires-Python: >=3.10
Requires-Dist: prometheus-client>=0.17
Requires-Dist: psutil>=5.9.0
Requires-Dist: python-daemon>=3.0.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.28.0
Description-Content-Type: text/markdown

# CrawlerKeeper Agent

爬虫节点代理程序，负责与主控系统通信，执行爬虫任务。

> **部署与升级操作手册**：PyPI 发布、服务器部署、版本升级的完整 SOP 属团队内部文档，不随发布包公开。

## 功能特性

- 节点自动注册
- 定时心跳上报（CPU/内存/磁盘）
- 任务自动拉取和执行
- **并行任务执行**：支持跨-Spider 并行，不同 Spider 的任务可同时执行
- **同一 Spider 串行**：通过 Semaphore 机制确保同一 Spider 的任务串行执行，避免冲突
- **容器资源感知**：根据容器 CPU 限制动态计算并发数
- **优雅关闭**：支持 SIGTERM/SIGINT 信号处理，任务排空不丢失
- 支持 Scrapy、Playwright、Selenium 爬虫框架
- 任务结果和日志上报
- YAML 配置文件支持
- 环境变量覆盖

## 目录结构

```
crawlerkeeper-agent/
├── src/
│   ├── crawlerkeeper/
│   │   ├── __init__.py
│   │   ├── __main__.py        # 命令行入口（pip 安装后的 crawlerkeeper-agent 命令）
│   │   ├── agent.py           # 主程序组装根
│   │   ├── cli.py             # 命令行参数解析（配置文件路径与 extra_config）
│   │   ├── task/              # 任务执行域
│   │   │   ├── manager.py     # 任务管理（轮询/抢占/线程池/优雅关闭）
│   │   │   ├── executor.py    # 爬虫执行器
│   │   │   ├── reporter.py    # 任务状态/结果/日志上报
│   │   │   ├── log_monitor.py # 爬虫日志文件监控
│   │   │   └── tmp_sweeper.py # 临时文件兜底清扫（TTL 保留期）
│   │   ├── deploy/            # 部署与代码包域
│   │   │   ├── manager.py     # 部署拉取
│   │   │   ├── spider_pkg.py  # 爬虫代码包下载/校验/解压
│   │   │   └── code_package.py # ZIP 魔数校验工具
│   │   ├── node/              # 节点环境域
│   │   │   ├── heartbeat.py   # 心跳管理
│   │   │   ├── deps.py        # 依赖安装（uv/npm）
│   │   │   ├── nodejs.py      # nvm/node 版本管理
│   │   │   └── runtime_scanner.py # 节点运行时版本扫描
│   │   └── platform/          # 横切底座（不依赖任何业务域）
│   │       ├── client.py      # HTTP 客户端
│   │       ├── config.py      # 配置管理
│   │       ├── context.py     # 参数对象（执行/部署/日志上下文）
│   │       ├── logger.py      # 日志管理
│   │       ├── metrics.py     # Prometheus 指标
│   │       └── workspace.py   # 工作目录布局（爬虫目录/临时目录/运行目录）
│   └── main.py               # 本地源码运行入口
├── config/
│   └── config.yaml           # 配置文件
├── pyproject.toml            # 依赖声明 + PyPI 打包配置
├── publish.sh                # 构建 + 发布脚本
├── bump_version.sh           # 版本号管理
├── run.sh                    # 本地启动脚本
└── sdk/
    └── python/              # 爬虫 SDK（独立发包到 PyPI）
        ├── crawlerkeeper_sdk/
        ├── pyproject.toml
        ├── publish.sh        # 构建 + 发布脚本
        └── migrate_spiders.py  # 爬虫项目迁移脚本
```

## 快速开始

### 1. 安装

```bash
# 方式一：从 PyPI 安装（推荐，服务器部署用）
pip install crawlerkeeper-agent
# 或
uv tool install crawlerkeeper-agent

# 方式二：源码运行（本地开发用）
uv sync
```

### 2. 配置

编辑 `config/config.yaml` 文件，配置主控系统地址：

```yaml
server:
  url: "http://localhost:8080"  # 主控系统地址

agent:
  name: "node-001"              # 节点名称
  ip: ""                        # 留空自动获取
  port: 9000                    # Agent 监听端口
  registration_token: ""        # 节点注册 Token（必填，从管理后台获取）

heartbeat:
  interval: 30                  # 心跳间隔（秒）

task:
  poll_interval: 10             # 任务拉取间隔（秒）
  work_dir: "./data/spiders"     # 工作目录
  # 最大并行任务数无需配置：默认按容器 CPU 自动计算，可用环境变量 AGENT_MAX_WORKERS 覆盖

logging:
  level: "INFO"
  file: "/var/log/agent.log"
```

### 3. 启动

```bash
# PyPI 安装方式（命令行入口）
crawlerkeeper-agent -c config/config.yaml

# 或模块方式
python -m crawlerkeeper -c config/config.yaml

# 源码运行（本地开发）
python src/main.py -c config/config.yaml

# 或使用启动脚本
./run.sh
```

### 3.1 命令行参数

| 参数 | 说明 |
|------|------|
| `-c, --config <path>` | 配置文件路径 |
| `--cleanup_temp_files=on\|off` | 任务结束后是否立即清理临时运行目录，默认 `on` |
| `--tmp_retention_days=N` | 临时文件兜底清扫保留期（天），默认 `7`，`0` 表示关闭定时清扫 |

**临时文件生命周期**：

```
tmp/<taskId>_<runUid前8位>/
├── task.log      # 任务日志（LOG_FILE_PATH）
├── task.data     # 任务数据（DATA_FILE_PATH）
└── <爬虫自写文件>
```

- 任务结束（成功/失败/异常）后立即删除整个运行目录，连同爬虫自写文件一并清理
- 异常退出（kill -9、OOM、容器重启）留下的孤儿目录，由定时兜底清扫按保留期回收
- 兜底清扫在 Agent 启动时执行一轮，此后每 1 小时一轮；正在执行中的任务目录始终跳过
- 升级前遗留的平铺临时文件（`tmp/<uid>.log`、`tmp/<uid>.data`）由同一轮清扫自愈删除

```bash
# 关闭定时兜底清扫（仅保留任务结束即时清理）
./run.sh --tmp_retention_days=0 --daemon=on start

# 保留 3 天
./run.sh --tmp_retention_days=3 --daemon=on start
```

### 4. 环境变量

也可以使用环境变量覆盖配置文件：

| 变量名 | 说明 |
|--------|------|
| AGENT_SERVER_URL | 主控系统地址 |
| AGENT_API_KEY | API 密钥 |
| AGENT_NAME | 节点名称 |
| AGENT_IP | 节点 IP |
| AGENT_PORT | Agent 监听端口 |
| AGENT_REGISTRATION_TOKEN | 节点注册 Token（必填） |
| AGENT_HEARTBEAT_INTERVAL | 心跳间隔（秒） |
| AGENT_TASK_POLL_INTERVAL | 任务拉取间隔（秒） |
| AGENT_TASK_EXECUTE_TIMEOUT | 单个任务执行超时（秒），默认 3600；设为 0 表示不限时 |
| AGENT_WORK_DIR | 工作目录 |
| AGENT_MAX_WORKERS | 最大并行任务数（覆盖自动计算，范围 2-8，优先级最高） |
| CONTAINER_CPU_LIMIT | 容器 CPU 限制（如 "2000m" 或 "4"），自动计算 max_workers |
| AGENT_LOG_LEVEL | 日志级别 |
| AGENT_LOG_FILE | 日志文件 |
| AGENT_UV_INDEX_URL | 爬虫依赖安装使用的 PyPI 索引地址；未设置时自动探测（pypi.org 可达走官方源，不可达回退清华镜像） |
| AGENT_UV_CACHE_DIR | uv 全局缓存目录；容器部署建议指向持久化路径，让重复部署命中缓存 |
| AGENT_UV_SYNC_TIMEOUT | uv sync 安装依赖超时（秒），默认 180 |

## 部署到服务器（免手动上传）

服务器上只需要 `run.sh` + `config/config.yaml` 两个东西，无需再手动上传安装包：

```
/opt/crawlerkeeper-agent/
├── run.sh
└── config/
    └── config.yaml
```

启动命令与源码模式完全一致：

```bash
./run.sh --cleanup_temp_files=on --daemon=on start
```

`run.sh` 会自动判断运行模式：

| 模式 | 判定条件 | 行为 |
|------|----------|------|
| 源码模式 | 脚本同目录存在 `src/main.py` | `uv sync` 后跑源码（本地开发） |
| 安装模式 | 脚本同目录没有源码 | 自动从 PyPI 安装 `crawlerkeeper-agent` 后运行（服务器部署） |

安装模式下的安装策略：已安装则直接复用；未安装则依次尝试 `uv tool install` → `pip install --user` → `pip install`，
装到 `~/.local/bin`（或 `uv tool` 的 bin 目录），失败时给出明确的手动安装命令。

**uv 自举安装**：`run.sh` 检测到 uv 缺失时按序安装：本地 `uv-installer.sh`（预放优先）→
`pip install uv`（经多源索引解析，国内节点命中清华/阿里镜像，避免官方安装器从 GitHub releases
下载二进制）→ 官方安装器 `curl astral.sh`（兜底，国内网络较慢）。
节点只有 python3 没有 pip 模块时（Debian/Ubuntu 最小安装的常态），会先自举 pip 再回到主路径：
`ensurepip` 优先（RHEL 系可用），失败则从同一套索引源拉最新 pip 的 wheel，
经 `python3 <pip.whl>/pip`（zipimport）直接安装自身，全程不依赖 GitHub。

**索引优先级分场景**：pip wheel 自举与 uv 本体安装一律**镜像优先**（清华→阿里→中科大→官方兜底），
因为官方索引（pypi.org/simple）在国内虽可达，但其 CDN（files.pythonhosted.org）常超时，
逐个试官方 wheel 会白等数十分钟；而 Agent 包自身（`crawlerkeeper-agent`）的安装/升级仍保持
**官方源优先 + 镜像作附加源**，避免镜像同步滞后导致装不到刚发布的版本。

兜底链路里若官方安装器仍被卡住（GitHub 下载慢/被墙），可用下列环境变量换源（uv 与官方安装器原生支持，
无需改代码）：

| 变量名 | 说明 |
|--------|------|
| `UV_INSTALLER_GITHUB_BASE_URL` | 官方安装器的 GitHub 基址，如 `https://ghfast.top/https://github.com`，使 uv 二进制走代理下载 |
| `UV_DOWNLOAD_URL` | 直接指定 uv 二进制包的完整下载地址（优先级最高） |
| `UV_PYTHON_INSTALL_MIRROR` | uv 托管 Python（`uv tool install` / `uv sync` 需要新版本解释器时）的下载镜像 |

**多源索引**：安装/升级前会并行探测各索引源可达性，首个可达者作主源、其余可达者作附加源
（pip 会合并各源候选版本取最高版本，uv 按优先级顺序优先命中官方源）。
默认列表按优先级排列：官方源 pypi.org → 清华 → 阿里 → 中科大。
官方源排在首位是因为镜像同步有延迟，刚发布的版本在镜像上会缺失。

相关环境变量：

| 变量名 | 说明 |
|--------|------|
| AGENT_PACKAGE | 安装的包名/版本，默认 `crawlerkeeper-agent`；可写 `crawlerkeeper-agent==1.0.1` 锁定版本 |
| AGENT_AUTO_INSTALL | 设为 `off` 关闭自动安装（未安装时直接报错提示） |
| AGENT_PYPI_INDEX_URL | 安装/升级时的 PyPI 索引地址（单源）；未设置时使用默认多源列表 |
| AGENT_PYPI_INDEX_URLS | 安装/升级时的 PyPI 索引地址列表（逗号分隔，按优先级排序）；未设置时用默认列表：官方源 → 清华 → 阿里 → 中科大 |

### 部署目录文件落位

所有运行时文件都在 **run.sh 所在目录**（不会写进 site-packages 或其他位置）：

| 路径 | 说明 | 源码模式 | 安装模式 |
|------|------|----------|----------|
| `config/config.yaml` | 主控地址、节点 Token 等配置（需自行提供） | 手动放置 | 手动放置 |
| `logs/agent.log` | 运行日志 | 自动创建 | 自动创建 |
| `agent.pid` | 进程 PID（供 stop/restart 使用） | 自动创建 | 自动创建 |
| `.last_daemon_args` | 后台启动参数记忆 | 自动创建 | 自动创建 |
| `data/` | 爬虫代码与任务工作目录 | 自动创建 | 自动创建 |
| `.venv/` | 源码模式的 Python 虚拟环境 | 自动创建（`uv sync`） | 不创建 |

安装模式下 Agent 程序本体装在用户目录（`~/.local/bin` 或 `~/.local/share/uv/tools/`），不占部署目录。

### 版本升级

> 完整的发布 SOP、首次部署步骤、常见问题属团队内部文档，不随发布包公开。

发布新版本到 PyPI 后，服务器上一条命令完成升级（查 PyPI 最新版 → 对比本地版本 → 升级 → 自动重启）：

```bash
./run.sh upgrade            # 升级到 PyPI 最新版
./run.sh upgrade 1.0.2      # 升级到指定版本
```

行为说明：

- 已是最新版本时不做任何操作，也不会重启 Agent（避免服务中断）
- 本地版本高于目标版本时跳过，不做降级
- 升级成功后自动重启（仅当 Agent 正在运行）
- `AGENT_UPGRADE_DRY_RUN=on` 可预演：打印将要执行的安装命令但不实际安装、不重启

## 平台化部署运维（机器管理）

主控平台（`crawlerkeeper-admin`）提供「机器管理」，可**从平台侧直接纳管本 Agent 的安装 / 升级**，
无需逐台手工 SSH。**本 Agent 与 `run.sh` 零改动**：平台经 SSH 直推，命令不经 Agent。

### 运维路径

```
录入凭据/跳板机 → 录入机器 → 连通性测试 → 声明实例 → 安装/重新部署 → 探测存量 → 批量升级
```

| 阶段 | 平台动作 | 本 Agent 侧 |
|------|---------|-------------|
| 声明实例 | 端口 / 节点名 / 绑定 WORKER Token / 参数表单（带默认值），平台生成 `config.yaml` | 无（仅声明） |
| 安装 / 重装 | `mkdir` → 停存量进程（systemd `stop`，或按本实例 config 路径精确匹配 nohup 进程先 TERM 后 KILL）→ 备份旧 config → 推送 `run.sh` + `config.yaml` → 按托管模式分流启动（见下） | 自动安装并注册 |
| 探测存量 | 按扫描路径（默认 `/feeyo/crawlerkeeper-agent-*`）找 `config/config.yaml`，按 `agent.port` / `agent.name` / `agent.api_key` 三键签名校验后登记对账 | 无（只读取配置） |
| 批量升级 | 推送平台内置 `run.sh` → `./run.sh upgrade [版本]` → 等心跳恢复并校验版本 | `run.sh upgrade` 负责装包 |

#### 按托管模式分流（`manage_mode`）

实例声明的 `manage_mode` 决定平台下发的命令序列，两种模式都经由本仓库 `run.sh` 拉起：

| 模式 | 平台侧启动序列 | 断电重启 |
|------|----------------|---------|
| `SYSTEMD`（缺省） | 写 `/etc/systemd/system/ck-agent@<port>.service` → `daemon-reload && enable --now` | systemd 自动拉起 |
| `BARE`（裸进程不接管） | 不写 unit（并收敛清理残留 unit）→ `cd <install_dir> && ./run.sh <清理参数> start`（nohup 后台模式） | **不自动拉起**（进程崩溃不自动重启，机器重启后需人工 / 脚本 `run.sh start`） |

- 批量升级同样按模式分流：`SYSTEMD` 走 `systemctl stop` → `./run.sh upgrade` → `systemctl start`；
  `BARE` 走 `./run.sh upgrade` 自带的「优雅停止 → 安装 → nohup 拉起」热重启路径（不碰 systemctl）。
- 目标版本更低时的降级序列也分流：`SYSTEMD` 用 systemd drop-in 钉 `AGENT_PACKAGE==<版本>`（成功后清除）；
  `BARE` 用一次性环境变量 `AGENT_PACKAGE==<版本> ./run.sh start` 钉版（无残留文件）。
- 探测登记的存量实例落 `BARE`（存量多为手工 nohup 裸进程，平台不强行接管）；已声明实例的 `manage_mode` 不被探测改写。

### 落位约定（平台纳管时的目录结构）

```
/feeyo/crawlerkeeper-agent-<port>/     # 实例安装目录（缺省值，可按机器自定义）
├── run.sh                             # 平台推送（与仓库 crawlerkeeper-agent/run.sh 逐字节同源）
└── config/
    └── config.yaml                    # 平台按实例声明生成（api_key = 绑定的 Token 值）
```

- `config.yaml` 固定落在 `<install_dir>/config/config.yaml`：`run.sh` 由脚本目录推导配置路径，
  平台不修改脚本即可支持一机多实例（每个实例一个目录）。
- systemd unit：`/etc/systemd/system/ck-agent@<port>.service`（`Type=simple`、
  `ExecStart=<install_dir>/run.sh --daemon=off --cleanup_temp_files=<on|off> --tmp_retention_days=<N> start`、
  `Restart=on-failure`、`WantedBy=multi-user.target`）。
  注：`--daemon=off` 时 `run.sh` 不从 `.last_daemon_args` 回填参数（仅 `DAEMON_MODE=on` 时回填），
  故这两个清理参数必须由 `ExecStart` 显式携带，否则会以默认值运行清理逻辑。
- 平台推送的 `run.sh` 与本仓库 `run.sh` 逐字节一致（单测比对防漂移），升级时始终由平台驱动最新脚本。

> 平台侧完整运维手册（含 `CK_SSH_SECRET_KEY` 密钥环境变量配置、批次升级 / 取消 / 重试、
> 常见问题）见 `docs/开发文档/机器管理与运维.md`。


## 任务执行流程

1. **注册节点** - 启动时向主控注册，获取节点 ID 和 API Key
2. **心跳上报** - 定时上报节点资源状态
3. **拉取任务** - 定时从主控获取待执行任务
4. **同步代码** - 从主控下载爬虫文件
5. **安装依赖** - 自动安装爬虫所需依赖（Python 爬虫使用 `uv sync`，超时默认 180 秒，可通过 `AGENT_UV_SYNC_TIMEOUT` 调整；安装过程日志实时输出，便于定位慢在哪个包）
6. **执行爬虫** - 在本地执行爬虫脚本（超过 `AGENT_TASK_EXECUTE_TIMEOUT` 秒未结束的进程会被终止：先 SIGTERM 宽限 10 秒，仍存活则 SIGKILL，任务按 FAILED/「执行超时」上报）
7. **上报结果** - 上报任务执行结果和日志

> **存量长任务迁移提示**：默认超时 3600 秒（1 小时）。如有运行超过 1 小时的存量爬虫任务，
> 升级后会被超时终止，可在对应节点设置 `AGENT_TASK_EXECUTE_TIMEOUT=0` 关闭超时。

## 并行任务执行

Agent 支持跨-Spider 并行执行任务，提高多爬虫场景下的资源利用率。

### 三重去重机制

为防止同一 Spider 任务被重复执行，Agent 实现了三重保险：

1. **第一道防线（后端）**：后端根据 `excludeSpiderIds` 参数过滤，不返回已在执行的 Spider 的任务
2. **第二道防线（Agent `_running_spiders`）**：Agent 侧维护运行中 Spider 列表，防止重复提交
3. **第三道防线（Semaphore）**：每个 Spider 有独立的 Semaphore(1)，确保同一 Spider 任务串行执行

### 并发数计算

Agent 根据以下顺序确定最大并发数：

1. **环境变量 `AGENT_MAX_WORKERS`**（优先级最高）：直接指定，范围 [2, 8]
2. **环境变量 `CONTAINER_CPU_LIMIT`**：K8s 注入的容器 CPU 限制
   - 格式 `2000m` 表示 2 CPU
   - 格式 `4` 表示 4 CPU
3. **CPU 核心数**：`min(os.cpu_count(), 8)`，最小为 2

### 容器环境变量注入（Kubernetes）

在 K8s Pod 中通过 downward API 注入容器资源限制：

```yaml
env:
  - name: CONTAINER_CPU_LIMIT
    valueFrom:
      resourceFieldRef:
        resource: limits.cpu
```

## 优雅关闭

Agent 支持 SIGTERM 和 SIGINT 信号，实现优雅关闭。

### 信号处理

- **SIGTERM**：收到后进入 DRAINING 状态，停止接受新任务，等待运行中任务完成（最多 60 秒），然后退出
- **SIGINT (Ctrl+C)**：收到后立即进入 TERMINATING 状态，强制取消所有任务，然后退出

### 关闭状态机

```
RUNNING (正常)
    │
    │ stop(graceful=True) 或 SIGTERM
    ▼
DRAINING (停止接受新任务，等待运行中任务完成)
    │
    │ 超时或 stop(graceful=False) 或 SIGINT
    ▼
TERMINATING (强制取消未完成任务)
    │
    │ 所有任务完成
    ▼
STOPPED (完全停止)
```

## 依赖要求

- Python 3.10+
- requests
- PyYAML
- psutil
- prometheus_client

## 后端 API 对接

Agent 需要主控系统提供以下接口：

| 接口 | 方法 | 说明 |
|------|------|------|
| /api/nodes/register | POST | 节点注册 |
| /api/nodes/{id}/heartbeat | POST | 心跳上报 |
| /api/nodes/{id}/tasks/pending | GET | 拉取待执行任务 |
| /api/tasks/{id}/result | POST | 上报任务结果 |
| /api/tasks/{id}/logs | POST | 上报任务日志 |
| /api/spider-files/spider/{spiderId} | GET | 获取爬虫文件列表 |
| /api/spider-files/{fileId} | GET | 获取文件内容 |

## Python SDK 发布

爬虫业务代码使用的 SDK 位于 `sdk/python/`，以 `crawlerkeeper-sdk` 包名独立发布到公网 PyPI
（<https://pypi.org/project/crawlerkeeper-sdk/>），爬虫项目通过依赖声明引用，不再拷贝源码。

```bash
cd sdk/python

# 仅构建（产物在 dist/）
./publish.sh

# 发布预演（校验凭证与产物，不上传）
./publish.sh --dry-run

# 构建并发布
./publish.sh --publish
```

凭证来源（优先级从高到低）：

1. 环境变量 `PYPI_TOKEN`
2. token 文件 `~/.pypi_token`（可用 `PYPI_TOKEN_FILE` 覆盖路径）

token 申请: <https://pypi.org/manage/account/token/>

- 版本号单一来源：`crawlerkeeper_sdk/__init__.py` 的 `__version__`
- 发布脚本内置品牌词闸门：待发布内容出现公司品牌词时直接中止
- 爬虫项目迁移说明见 `sdk/python/MIGRATION.md`

## Agent 发布

Agent 本体以 `crawlerkeeper-agent` 包名发布到公网 PyPI
（<https://pypi.org/project/crawlerkeeper-agent/>），服务器直接 pip/uv 安装，
无需手动上传安装包。

```bash
# 仅构建（产物在 dist/）
./publish.sh

# 发布预演（校验凭证与产物，不上传）
./publish.sh --dry-run

# 构建并发布
./publish.sh --publish
```

凭证来源（优先级从高到低）：

1. 环境变量 `PYPI_TOKEN`
2. token 文件 `~/.pypi_token`（可用 `PYPI_TOKEN_FILE` 覆盖路径）

token 申请: <https://pypi.org/manage/account/token/>

- 版本号单一来源：`src/crawlerkeeper/__init__.py` 的 `__version__`（用 `bump_version.sh` 管理）
- sdist/wheel 走 include 白名单：只打包 `src/crawlerkeeper`、`tests`、`LICENSE`、`README.md`，
  内部资产（生产配置、Dockerfile、k8s 清单、demo、打包脚本）一律不进发布包
- 发布脚本内置品牌词闸门：待发布内容出现公司品牌词或内部域名时直接中止

## License

MIT
