Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6f24c45
feat: 发布v2.0.0版本,新增节点检测重试与批量删除功能
siruoren Jul 20, 2026
9296c7e
feat(index): add button to clear all proxy nodes
siruoren Jul 20, 2026
03eda4f
feat: add config export/import feature and fix timezone display
siruoren Jul 20, 2026
3e68c86
feat(api): 给导出的配置文件名加上时间戳
siruoren Jul 20, 2026
897dff3
refactor(checker): 优化代理检测逻辑,新增响应验证和华为检测源
siruoren Jul 20, 2026
5d083c6
refactor: 调整日志策略与界面文本,添加任务调度随机抖动
siruoren Jul 20, 2026
bf00eb3
refactor: 调整节点失效处理逻辑,优化统计面板布局
siruoren Jul 20, 2026
4a87712
refactor(stat): 更新统计逻辑与展示文案
siruoren Jul 20, 2026
f4806ee
fix(database): 修正get_stats统计逻辑,统计订阅源数量而非总条目
siruoren Jul 20, 2026
cf39e3b
refactor(generator): add proxy link normalization for plain subscription
siruoren Jul 20, 2026
547f17e
fix: 防止订阅验证重复触发并增加异常处理
siruoren Jul 20, 2026
2da3641
fix(scheduler): correct log field for failed instance check count
siruoren Jul 20, 2026
deab3ae
refactor: 重构代理节点订阅关联逻辑,使用subscription_id替代source
siruoren Jul 21, 2026
9538825
style(index template): 简化统计卡片标题文字
siruoren Jul 21, 2026
efd7dba
chore: 移除对 socks4/socks4a 协议的支持
siruoren Jul 21, 2026
a0f16ce
style(app/templates): 修正平均延迟统计卡片的标题文字
siruoren Jul 21, 2026
3d98d50
feat: add client-side pagination for proxy tables
siruoren Jul 21, 2026
ce7c3ab
chore: release 2.0.0 version with full refactor and new features
siruoren Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 94 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,99 @@

All notable changes to this project will be documented in this file.

## [2.0.0] - 2026-07-20

### 核心重构

- **节点-订阅关联重构** - 代理节点使用 `subscription_id`(整数)替代 `source`(字符串)绑定所属订阅源,已存在于其他订阅的节点不重复入库
- **纯文本订阅格式** - 对外订阅输出改为纯文本格式(每行一条原始代理 URI),参照 `subdom.txt` 样式;移除 base64 编码输出
- **内核转发检测** - 新增 Xray 内核转发检测能力,支持 HTTP/SOCKS 代理转发后检测连通性;TCP/TLS 直接检测作为回退
- **检测失败直接删除** - 取消 `fail_count` 累积逻辑,检测不通过的节点直接从数据库删除,不再保留
- **实例源节点不入库** - 服务实例获取的节点仅统计已连接数量,不再写入代理数据库
- **socks4 协议移除** - 不再支持 socks4/socks4a 协议,拉取订阅时自动移除已有的 socks4 节点

### 多协议解析扩展

- **socks5/http 代理支持** - 新增 socks5:// 和 http(s):// 格式的解析、检测和 Clash 配置生成
- **http/https 仅带 #fragment 时视为节点** - 避免 URL 误判为代理节点
- **链接规范化** - socks5/http 代理对外输出确保格式为 `protocol://host:port#host-port`,保留认证信息

### 检测优化

- **多目标 URL 轮询** - 新增 5 个检测目标(Google 204、Gstatic 204、Cloudflare、Apple、华为连通性检测)
- **响应体验证** - 新增 `_validate_check_response` 排除劫持页面和空响应
- **GET + 4KB body 读取** - 替代 HEAD 请求,提升服务器兼容性
- **检测重试机制** - `check_retries` 参数(默认 2),单次检测失败后自动重试
- **ConnectionRefusedError/ResetError** - 不再返回延迟值(视为不可用)
- **TLS 回退检测** - SSL 对象状态验证,`server_hostname` 使用原始域名
- **华为连通性检测** - 新增 `connectivitycheck.platform.hicloud.com/generate_204`

### 调度优化

- **CronTrigger 随机延迟** - 所有 crontab 任务加入 `jitter`(0~600 秒),避免多订阅源同时更新
- **订阅验证防重入** - `_verifying_subs` 集合跟踪正在验证的订阅 ID,防止并发重复验证
- **拉取后自动验证** - `_fetch_single_subscription` 完成后自动触发该订阅的已入库节点验证

### 时间与日志

- **UTC+8 统一** - 所有服务时间统一为东八区(UTC+8),包括数据库时间戳和日志时间
- **单文件日志** - 日志写入 `logs/proxy_pool.log` 单文件,不归档、不保留历史日志
- **内核日志合并** - 内核 stdout/stderr 合并写入 `logs/proxy-core.log`

### Web 界面

- **统计面板重构** - "总节点数"→"总订阅条目数"(显示订阅源数量),"可用数"→"可用节点数",移除"不可用"统计卡片
- **分页显示** - 每个订阅源可用节点列表分页,每页 10 条
- **操作按钮更新** - "验证所有订阅"→"验证所有节点",新增"清除所有节点"、"导出配置"、"导入配置"按钮
- **实例源表头** - "总数"→"已连接",显示已连接节点数量而非数据库条目数

### 配置管理

- **一键导出/导入** - 导出订阅源和实例源配置为 JSON 文件(含时间戳),导入时自动去重
- **导出文件名时间戳** - 格式 `nethub_config_YYYYMMDD_HHMMSS.json`

### API 接口

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/proxies` | 可用节点列表 |
| GET | `/api/proxies/all` | 所有节点 |
| GET | `/api/proxies/grouped` | 按 subscription_id 分组的可用节点 |
| DELETE | `/api/proxies/{id}` | 删除节点 |
| DELETE | `/api/proxies` | 一键清除所有节点 |
| GET | `/api/subscription/plain` | 纯文本格式订阅 |
| GET | `/api/subscription/v2ray` | 纯文本格式订阅(同 plain) |
| GET | `/api/subscription/clash` | Clash 格式订阅(YAML) |
| POST | `/api/fetch` | 拉取所有订阅 |
| POST | `/api/fetch/{sub_id}` | 拉取指定订阅 |
| POST | `/api/verify` | 验证所有节点 |
| POST | `/api/verify/{sub_id}` | 验证指定订阅节点 |
| GET | `/api/stats` | 统计信息 |
| GET | `/api/health` | 健康检查 |
| GET | `/api/subscriptions` | 订阅源列表 |
| POST | `/api/subscriptions` | 添加订阅源 |
| POST | `/api/subscriptions/auto` | 自动添加订阅源(仅 URL 必填) |
| PUT | `/api/subscriptions/{sub_id}` | 更新订阅源 |
| DELETE | `/api/subscriptions/{sub_id}` | 删除订阅源及其下所有节点 |
| GET | `/api/check-urls` | 检测目标 URL 列表 |
| POST | `/api/check-urls` | 添加检测目标 URL |
| DELETE | `/api/check-urls/{url_id}` | 删除检测目标 URL |
| GET | `/api/config/export` | 导出配置(JSON) |
| POST | `/api/config/import` | 导入配置(JSON) |
| GET | `/api/instance-sources` | 服务实例源列表 |
| POST | `/api/instance-sources` | 添加服务实例源 |
| PUT | `/api/instance-sources/{source_id}` | 更新服务实例源 |
| DELETE | `/api/instance-sources/{source_id}` | 删除服务实例源 |
| POST | `/api/instance-sources/{source_id}/fetch` | 获取实例源已连接节点数 |
| POST | `/api/instance-sources/{source_id}/import` | 导入实例源订阅 |

### Docker 构建

- **固定版本下载** - 内核数据文件使用固定版本标签,替代 `latest` 避免超时
- **curl 重试参数** - `--connect-timeout 30 --max-time 180 --retry 3 --retry-delay 5`

---

## [1.0.0] - 2026-07-18

### 核心功能
Expand Down Expand Up @@ -47,7 +140,7 @@ All notable changes to this project will be documented in this file.
| GET | `/api/proxies` | 可用节点列表 |
| GET | `/api/proxies/all` | 所有节点 |
| DELETE | `/api/proxies/{id}` | 删除节点 |
| GET | `/api/subscription/v2ray` | 核心订阅 |
| GET | `/api/subscription/v2ray` | 核心订阅(base64) |
| GET | `/api/subscription/clash` | Clash 订阅 |
| POST | `/api/fetch` | 拉取所有订阅 |
| POST | `/api/fetch/{sub_id}` | 拉取指定订阅 |
Expand Down
94 changes: 63 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
# NetHub v1.0.0
# NetHub v2.0.0

自动获取、检测、维护节点池,提供 Web 管理界面和订阅链接输出。

## 功能特性

- **订阅源管理** - 数据库驱动的增删改查,每个订阅源独立配置 Crontab、延迟阈值、重试次数、并发数
- **Crontab 定时拉取** - 每个订阅源支持 5 位 Crontab 表达式精准调度
- **HTTP 延迟检测** - 模拟通过节点访问目标网站,测量完整请求延迟(DNS + TCP + TLS + HTTP),多目标取最大值
- **检测目标动态配置** - 检测目标 URL 存入数据库,页面可增删,修改即时生效,无需外部文件
- **多协议支持** - vmess / vless / trojan / ss / hysteria2 解析与 Clash 配置生成
- **自动清理** - 连续 3 次验证失败的节点自动移除;连续 30 天无节点的订阅源自动删除
- **单页面管理** - 订阅源管理 + 可用节点列表在同一页面,按订阅源 Tab 切换
- **协议分布图** - 饼状图动态展示各协议节点数量和百分比
- **订阅输出** - 仅输出当前可用节点,支持核心格式(base64)和 Clash(YAML)格式
- **日志归档** - 按天自动归档,自动清理 7 天前的日志
- **订阅源管理** - 数据库驱动的增删改查,每个订阅源独立配置 Crontab、延迟阈值、并发数;删除订阅源时自动清除其下所有节点
- **Crontab 定时拉取** - 每个订阅源支持 5 位 Crontab 表达式,内置随机延迟(0~10 分钟)避免多源同时更新
- **内核转发检测** - Xray 内核转发后检测连通性,TCP/TLS 直接检测作为回退;多目标 URL 轮询 + 响应体验证 + 检测重试
- **检测失败直接删除** - 取消失败计数累积,检测不通过的节点直接从数据库删除
- **节点-订阅绑定** - 每个节点绑定所属 `subscription_id`,已存在于其他订阅的节点不重复入库
- **纯文本订阅输出** - 每行一条原始代理 URI,参照 `subdom.txt` 格式;同时提供 Clash(YAML)格式
- **多协议支持** - vmess / vless / trojan / ss / hysteria2 / socks5 / http(s) 解析、检测与 Clash 配置生成
- **服务实例源** - 获取已连接节点数量统计(不入库),支持手工导入实例源中的订阅地址
- **配置导出/导入** - 一键导出订阅源和实例源配置为 JSON 文件(含时间戳),导入时自动去重
- **UTC+8 时区统一** - 所有服务时间统一为东八区
- **单文件日志** - 不归档、不保留历史日志
- **Docker 部署** - Docker Compose 一键启动

## 快速开始
Expand All @@ -31,7 +32,7 @@ vim config.yaml
docker-compose up -d
```

访问 http://localhost:8080 查看 Web 界面。
访问 http://localhost:2020 查看 Web 界面。

### 本地运行

Expand Down Expand Up @@ -60,15 +61,19 @@ check:
timeout: 5.0 # 检测超时(秒)
max_concurrent: 50 # 全局并发检测数
latency_threshold: 1500.0 # 全局延迟阈值(毫秒)
check_mode: "auto" # 检测模式: auto(优先内核转发回退TCP) / http(仅内核转发) / tcp(仅TCP/TLS)
socks_port: 1080 # 本地 SOCKS 转发端口
http_port: 1081 # 本地 HTTP 转发端口
kernel_path: "xray" # 内核可执行文件路径
check_retries: 2 # 单次检测失败后重试次数

scheduler:
fetch_interval: 3600 # 拉取订阅间隔(秒)
verify_interval: 1800 # 验证节点间隔(秒)
cleanup_interval: 7200 # 清理间隔(秒)
max_fail_count: 3 # 最大连续失败次数
```

> 检测目标 URL 默认为 `https://www.google.com/generate_204` 和 `https://www.gstatic.com/generate_204`,首次启动自动写入数据库,后续在页面「检测目标」中管理,修改即时生效。
> 检测目标 URL 默认包含 Google 204、Gstatic 204、Cloudflare、Apple、华为连通性检测等,首次启动自动写入数据库,后续在页面「检测目标」中管理,修改即时生效。

### 环境变量

Expand All @@ -85,15 +90,17 @@ scheduler:
|------|------|------|
| GET | `/api/proxies` | 可用节点列表 |
| GET | `/api/proxies/all` | 所有节点 |
| GET | `/api/proxies/grouped` | 按订阅来源分组的可用节点 |
| GET | `/api/proxies/grouped` | 按 subscription_id 分组的可用节点 |
| DELETE | `/api/proxies/{id}` | 删除节点 |
| DELETE | `/api/proxies` | 一键清除所有节点 |

### 订阅输出

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/subscription/v2ray` | 核心格式订阅(base64) |
| GET | `/api/subscription/clash` | Clash 格式订阅(YAML) |
| GET | `/api/subscription/plain` | 纯文本格式(每行一条 URI) |
| GET | `/api/subscription/v2ray` | 纯文本格式(同 plain) |
| GET | `/api/subscription/clash` | Clash 格式(YAML) |

### 订阅源管理

Expand All @@ -103,7 +110,7 @@ scheduler:
| POST | `/api/subscriptions` | 添加订阅源 |
| POST | `/api/subscriptions/auto` | 自动添加(仅 URL 必填,自动拉取验证) |
| PUT | `/api/subscriptions/{sub_id}` | 更新订阅源 |
| DELETE | `/api/subscriptions/{sub_id}` | 删除订阅源 |
| DELETE | `/api/subscriptions/{sub_id}` | 删除订阅源及其下所有节点 |

### 拉取与验证

Expand All @@ -114,27 +121,45 @@ scheduler:
| POST | `/api/verify` | 验证所有节点 |
| POST | `/api/verify/{sub_id}` | 验证指定订阅节点 |

### 检测目标
### 服务实例源

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/instance-sources` | 服务实例源列表 |
| POST | `/api/instance-sources` | 添加服务实例源 |
| PUT | `/api/instance-sources/{source_id}` | 更新服务实例源 |
| DELETE | `/api/instance-sources/{source_id}` | 删除服务实例源 |
| POST | `/api/instance-sources/{source_id}/fetch` | 获取实例源已连接节点数 |
| POST | `/api/instance-sources/{source_id}/import` | 导入实例源订阅 |

### 配置管理

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/config/export` | 导出配置(JSON) |
| POST | `/api/config/import` | 导入配置(JSON,自动去重) |

### 检测目标

| 方法 | 路径 | 说明 |
|------|------|--------|
| GET | `/api/check-urls` | 检测目标 URL 列表 |
| POST | `/api/check-urls` | 添加检测目标 URL |
| DELETE | `/api/check-urls/{url_id}` | 删除检测目标 URL |

### 其他

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/stats` | 统计信息 |
|------|------|--------|
| GET | `/api/stats` | 统计信息(总订阅条目数、可用节点数、平均延迟) |
| GET | `/api/health` | 健康检查 |

## 订阅链接使用

在节点客户端中添加以下订阅链接:

- **核心**: `http://your-server:8080/api/subscription/v2ray`
- **Clash**: `http://your-server:8080/api/subscription/clash`
- **纯文本**: `http://your-server:2020/api/subscription/plain`
- **Clash**: `http://your-server:2020/api/subscription/clash`

> 订阅内容仅包含延迟低于阈值的可用节点,随节点池自动更新。

Expand All @@ -147,22 +172,28 @@ scheduler:
| Trojan | ✅ | ✅ |
| Shadowsocks | ✅ | ✅ |
| Hysteria2 | ✅ | ✅ |
| SOCKS5 | ✅ | ✅ |
| HTTP/HTTPS | ✅ | ✅ |

> socks4/socks4a 协议不再支持,拉取时自动移除。

## 延迟检测原理

延迟检测模拟真实上网场景:通过转发服务访问目标检测 URL,测量完整请求延迟。
延迟检测模拟真实上网场景:通过 Xray 内核转发访问目标检测 URL,测量完整请求延迟。

```
客户端 → 转发服务 → 目标网站
客户端 → Xray 内核转发 → 目标网站
├── DNS 解析
├── TCP 连接建立
├── TLS 握手(HTTPS 目标)
└── HTTP 请求/响应
```

- 检测目标默认为 `https://www.google.com/generate_204` 和 `https://www.gstatic.com/generate_204`
- 检测目标包含 Google 204、Gstatic 204、Cloudflare、Apple、华为连通性检测等
- 多个目标取最大延迟值,确保所有目标均可达
- 相比仅测试 TCP/TLS 连通性,HTTP 请求延迟更贴近真实上网体验
- 响应体验证排除劫持页面和空响应
- 单次检测失败后自动重试(默认 2 次)
- 检测失败节点直接删除,不保留

## 项目结构

Expand All @@ -174,9 +205,9 @@ proxy_pool/
│ ├── config.py # YAML 配置加载
│ ├── database.py # aiosqlite 异步数据库操作
│ ├── models.py # 数据模型(ProxyInfo / ProxyDBRecord / SubscriptionRecord)
│ ├── parser.py # 订阅拉取 & 解析(5 协议)
│ ├── checker.py # HTTP 延迟检测(多目标取最大值)
│ ├── generator.py # 核心 / Clash 订阅生成
│ ├── parser.py # 订阅拉取 & 解析(7 协议)
│ ├── checker.py # 内核转发检测 + TCP/TLS 回退检测
│ ├── generator.py # 纯文本 / Clash 订阅生成
│ ├── scheduler.py # APScheduler 定时任务调度
│ ├── routers/
│ │ ├── api.py # REST API 路由
Expand All @@ -185,7 +216,8 @@ proxy_pool/
│ ├── base.html # 基础模板
│ ├── index.html # 主页面(管理 + 节点列表)
│ └── subscription.html # 订阅链接页
├── logs/ # 日志目录(自动归档)
├── logs/ # 日志目录(单文件,不归档)
├── data/ # 数据库目录
├── config.yaml # 配置文件
├── docker-compose.yaml # Docker 编排
├── Dockerfile # Docker 镜像
Expand Down
10 changes: 3 additions & 7 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,14 @@ def create_app(config_path: str = "config.yaml") -> FastAPI:
console_handler.setFormatter(log_fmt)
root_logger.addHandler(console_handler)

# 文件:按天归档,保留7天
# 文件:只保留当前日志,不归档
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
from logging.handlers import TimedRotatingFileHandler
file_handler = TimedRotatingFileHandler(
file_handler = logging.FileHandler(
filename=str(log_dir / "proxy_pool.log"),
when="midnight",
interval=1,
backupCount=7,
encoding="utf-8",
)
file_handler.setFormatter(log_fmt)
file_handler.suffix = "%Y-%m-%d"
root_logger.addHandler(file_handler)

# 初始化组件
Expand Down Expand Up @@ -120,6 +115,7 @@ async def startup():
http_port=_config.check.http_port,
check_mode=_config.check.check_mode,
kernel_path=_config.check.kernel_path,
check_retries=_config.check.check_retries,
)

# 初始化调度器
Expand Down
Loading
Loading