nonebot-plugin-proxy-probe 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_proxy_probe/__init__.py +26 -0
- nonebot_plugin_proxy_probe/assets//321/205/320/236/320/257/321/207/320/265/320/256/321/205/320/275/320/247/321/204/342/225/234/320/243.ttf +0 -0
- nonebot_plugin_proxy_probe/cache.py +82 -0
- nonebot_plugin_proxy_probe/commands.py +130 -0
- nonebot_plugin_proxy_probe/config.py +29 -0
- nonebot_plugin_proxy_probe/manager.py +360 -0
- nonebot_plugin_proxy_probe/models.py +145 -0
- nonebot_plugin_proxy_probe/probe.py +1184 -0
- nonebot_plugin_proxy_probe/render.py +266 -0
- nonebot_plugin_proxy_probe-0.2.2.dist-info/METADATA +269 -0
- nonebot_plugin_proxy_probe-0.2.2.dist-info/RECORD +14 -0
- nonebot_plugin_proxy_probe-0.2.2.dist-info/WHEEL +5 -0
- nonebot_plugin_proxy_probe-0.2.2.dist-info/licenses/LICENSE +674 -0
- nonebot_plugin_proxy_probe-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
8
|
+
|
|
9
|
+
from .models import CacheState, ProxyRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
FONT_PATH = Path(__file__).resolve().parent / "assets" / "原神字体.ttf"
|
|
13
|
+
MAX_DISPLAY_RESULTS = 50
|
|
14
|
+
IMAGE_WIDTH = 1900
|
|
15
|
+
MARGIN = 42
|
|
16
|
+
ROW_HEIGHT = 58
|
|
17
|
+
COL_WIDTHS = (380, 180, 380, 876)
|
|
18
|
+
COL_HEADERS = ("IP", "端口", "代理后 IP", "代理后属地")
|
|
19
|
+
FOREIGN_ROW_COLOR = "#C0FF02"
|
|
20
|
+
SINGAPORE_ROW_COLOR = "#FFCE46"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
24
|
+
try:
|
|
25
|
+
return ImageFont.truetype(str(FONT_PATH), size=size)
|
|
26
|
+
except OSError:
|
|
27
|
+
return ImageFont.load_default()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fit_text(
|
|
31
|
+
draw: ImageDraw.ImageDraw,
|
|
32
|
+
text: str,
|
|
33
|
+
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
|
|
34
|
+
max_width: int,
|
|
35
|
+
) -> str:
|
|
36
|
+
text = str(text)
|
|
37
|
+
if draw.textbbox((0, 0), text, font=font)[2] <= max_width:
|
|
38
|
+
return text
|
|
39
|
+
suffix = "…"
|
|
40
|
+
left, right = 0, len(text)
|
|
41
|
+
while left < right:
|
|
42
|
+
middle = (left + right + 1) // 2
|
|
43
|
+
candidate = text[:middle] + suffix
|
|
44
|
+
if draw.textbbox((0, 0), candidate, font=font)[2] <= max_width:
|
|
45
|
+
left = middle
|
|
46
|
+
else:
|
|
47
|
+
right = middle - 1
|
|
48
|
+
return text[:left] + suffix
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sorted_results(results: list[ProxyRecord]) -> list[ProxyRecord]:
|
|
52
|
+
def ip_key(item: ProxyRecord) -> tuple[int, int]:
|
|
53
|
+
parts = item.ip.split(".")
|
|
54
|
+
numeric = 0
|
|
55
|
+
if len(parts) == 4 and all(part.isdigit() for part in parts):
|
|
56
|
+
for part in parts:
|
|
57
|
+
numeric = numeric * 256 + int(part)
|
|
58
|
+
return numeric, item.port
|
|
59
|
+
|
|
60
|
+
return sorted(results, key=ip_key)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def location_row_color(location: str) -> str | None:
|
|
64
|
+
"""新加坡优先高亮;其他非中国属地使用绿色。"""
|
|
65
|
+
normalized = str(location or "").strip()
|
|
66
|
+
if not normalized or normalized in {
|
|
67
|
+
"未知属地",
|
|
68
|
+
"无法探测代理后地址",
|
|
69
|
+
"未探测代理后地址",
|
|
70
|
+
}:
|
|
71
|
+
return None
|
|
72
|
+
if "新加坡" in normalized or "Singapore" in normalized:
|
|
73
|
+
return SINGAPORE_ROW_COLOR
|
|
74
|
+
if any(
|
|
75
|
+
keyword in normalized
|
|
76
|
+
for keyword in ("中国", "香港", "台湾", "澳门")
|
|
77
|
+
):
|
|
78
|
+
return None
|
|
79
|
+
return FOREIGN_ROW_COLOR
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def render_cache_image(state: CacheState) -> bytes:
|
|
83
|
+
visible = sorted_results(state.results)[:MAX_DISPLAY_RESULTS]
|
|
84
|
+
rows = max(1, len(visible))
|
|
85
|
+
header_height = 380
|
|
86
|
+
image_height = header_height + ROW_HEIGHT * (rows + 1) + MARGIN
|
|
87
|
+
image = Image.new("RGB", (IMAGE_WIDTH, image_height), "#f3f6fb")
|
|
88
|
+
draw = ImageDraw.Draw(image)
|
|
89
|
+
|
|
90
|
+
title_font = get_font(38)
|
|
91
|
+
info_font = get_font(25)
|
|
92
|
+
table_header_font = get_font(27)
|
|
93
|
+
table_font = get_font(25)
|
|
94
|
+
|
|
95
|
+
draw.text(
|
|
96
|
+
(MARGIN, 28),
|
|
97
|
+
"Clash 代理扫描结果",
|
|
98
|
+
font=title_font,
|
|
99
|
+
fill="#172033",
|
|
100
|
+
)
|
|
101
|
+
count_text = f"共 {len(state.results)} 条"
|
|
102
|
+
if len(state.results) > MAX_DISPLAY_RESULTS:
|
|
103
|
+
count_text += f"(仅显示前 {MAX_DISPLAY_RESULTS} 条)"
|
|
104
|
+
draw.text(
|
|
105
|
+
(MARGIN, 92),
|
|
106
|
+
f"扫描时间:{state.scan_time} 刷新时间:{state.refresh_time} {count_text}",
|
|
107
|
+
font=info_font,
|
|
108
|
+
fill="#344054",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
operation_names = {"probe": "重新扫描", "refresh": "缓存刷新"}
|
|
112
|
+
operation = operation_names.get(state.operation, "无")
|
|
113
|
+
task_line = f"当前任务:{operation} / {state.task_status}"
|
|
114
|
+
if state.task_total:
|
|
115
|
+
task_line += f" / {state.task_current}/{state.task_total}"
|
|
116
|
+
draw.text(
|
|
117
|
+
(MARGIN, 140),
|
|
118
|
+
f"本机出站 IP:{state.local_ip} 目标参考 IP:{state.target_ip}",
|
|
119
|
+
font=info_font,
|
|
120
|
+
fill="#344054",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
draw.text(
|
|
124
|
+
(MARGIN, 188),
|
|
125
|
+
task_line,
|
|
126
|
+
font=info_font,
|
|
127
|
+
fill="#175cd3" if state.running else "#475467",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
progress = state.progress
|
|
131
|
+
draw.text(
|
|
132
|
+
(MARGIN, 236),
|
|
133
|
+
f"端口扫描:{progress.open_count} open in {progress.scan_completed}",
|
|
134
|
+
font=info_font,
|
|
135
|
+
fill="#344054",
|
|
136
|
+
)
|
|
137
|
+
draw.text(
|
|
138
|
+
(MARGIN + 540, 236),
|
|
139
|
+
f"代理验证:{progress.proxy_count} proxy in {progress.proxy_tested}",
|
|
140
|
+
font=info_font,
|
|
141
|
+
fill="#344054",
|
|
142
|
+
)
|
|
143
|
+
draw.text(
|
|
144
|
+
(MARGIN + 1080, 236),
|
|
145
|
+
f"属地探测:{progress.geo_success} tested in {progress.geo_tested}",
|
|
146
|
+
font=info_font,
|
|
147
|
+
fill="#344054",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
table_left = MARGIN
|
|
151
|
+
table_top = 305
|
|
152
|
+
table_right = IMAGE_WIDTH - MARGIN
|
|
153
|
+
x_positions = [table_left]
|
|
154
|
+
for width in COL_WIDTHS:
|
|
155
|
+
x_positions.append(x_positions[-1] + width)
|
|
156
|
+
x_positions[-1] = table_right
|
|
157
|
+
|
|
158
|
+
draw.rounded_rectangle(
|
|
159
|
+
(
|
|
160
|
+
table_left,
|
|
161
|
+
table_top,
|
|
162
|
+
table_right,
|
|
163
|
+
table_top + ROW_HEIGHT * (rows + 1),
|
|
164
|
+
),
|
|
165
|
+
radius=12,
|
|
166
|
+
fill="white",
|
|
167
|
+
outline="#cfd8e6",
|
|
168
|
+
width=2,
|
|
169
|
+
)
|
|
170
|
+
draw.rectangle(
|
|
171
|
+
(
|
|
172
|
+
table_left,
|
|
173
|
+
table_top,
|
|
174
|
+
table_right,
|
|
175
|
+
table_top + ROW_HEIGHT,
|
|
176
|
+
),
|
|
177
|
+
fill="#dbeafe",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
for index, header in enumerate(COL_HEADERS):
|
|
181
|
+
left = x_positions[index]
|
|
182
|
+
right = x_positions[index + 1]
|
|
183
|
+
bbox = draw.textbbox((0, 0), header, font=table_header_font)
|
|
184
|
+
text_width = bbox[2] - bbox[0]
|
|
185
|
+
text_height = bbox[3] - bbox[1]
|
|
186
|
+
draw.text(
|
|
187
|
+
(
|
|
188
|
+
left + (right - left - text_width) / 2,
|
|
189
|
+
table_top + (ROW_HEIGHT - text_height) / 2 - bbox[1],
|
|
190
|
+
),
|
|
191
|
+
header,
|
|
192
|
+
font=table_header_font,
|
|
193
|
+
fill="#172033",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if visible:
|
|
197
|
+
row_values = [
|
|
198
|
+
(item.ip, str(item.port), item.public_ip, item.location)
|
|
199
|
+
for item in visible
|
|
200
|
+
]
|
|
201
|
+
else:
|
|
202
|
+
row_values = [("暂无缓存代理", "", "", "")]
|
|
203
|
+
|
|
204
|
+
for row_index, values in enumerate(row_values, start=1):
|
|
205
|
+
top = table_top + row_index * ROW_HEIGHT
|
|
206
|
+
row_color = None
|
|
207
|
+
if visible:
|
|
208
|
+
row_color = location_row_color(
|
|
209
|
+
visible[row_index - 1].location
|
|
210
|
+
)
|
|
211
|
+
if row_color is not None:
|
|
212
|
+
draw.rectangle(
|
|
213
|
+
(table_left, top, table_right, top + ROW_HEIGHT),
|
|
214
|
+
fill=row_color,
|
|
215
|
+
)
|
|
216
|
+
elif row_index % 2 == 0:
|
|
217
|
+
draw.rectangle(
|
|
218
|
+
(table_left, top, table_right, top + ROW_HEIGHT),
|
|
219
|
+
fill="#f8fafc",
|
|
220
|
+
)
|
|
221
|
+
for column_index, value in enumerate(values):
|
|
222
|
+
left = x_positions[column_index]
|
|
223
|
+
right = x_positions[column_index + 1]
|
|
224
|
+
padding = 16
|
|
225
|
+
shown = fit_text(
|
|
226
|
+
draw,
|
|
227
|
+
value,
|
|
228
|
+
table_font,
|
|
229
|
+
right - left - padding * 2,
|
|
230
|
+
)
|
|
231
|
+
bbox = draw.textbbox((0, 0), shown, font=table_font)
|
|
232
|
+
text_height = bbox[3] - bbox[1]
|
|
233
|
+
draw.text(
|
|
234
|
+
(
|
|
235
|
+
left + padding,
|
|
236
|
+
top + (ROW_HEIGHT - text_height) / 2 - bbox[1],
|
|
237
|
+
),
|
|
238
|
+
shown,
|
|
239
|
+
font=table_font,
|
|
240
|
+
fill="#172033",
|
|
241
|
+
)
|
|
242
|
+
draw.line(
|
|
243
|
+
(table_left, top, table_right, top),
|
|
244
|
+
fill="#e4e7ec",
|
|
245
|
+
width=1,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
for x in x_positions[1:-1]:
|
|
249
|
+
draw.line(
|
|
250
|
+
(
|
|
251
|
+
x,
|
|
252
|
+
table_top,
|
|
253
|
+
x,
|
|
254
|
+
table_top + ROW_HEIGHT * (rows + 1),
|
|
255
|
+
),
|
|
256
|
+
fill="#cfd8e6",
|
|
257
|
+
width=1,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
output = BytesIO()
|
|
261
|
+
image.save(output, format="PNG", optimize=True)
|
|
262
|
+
return output.getvalue()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def image_to_base64(image_bytes: bytes) -> str:
|
|
266
|
+
return "base64://" + base64.b64encode(image_bytes).decode("ascii")
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonebot-plugin-proxy-probe
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: 扫描内网 Clash 代理并缓存出口 IP 与属地的 NoneBot2 插件
|
|
5
|
+
Author: lhc
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: nonebot2>=2.3.0
|
|
11
|
+
Requires-Dist: nonebot-adapter-onebot>=2.0.0
|
|
12
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.0
|
|
13
|
+
Requires-Dist: requests<3,>=2.31
|
|
14
|
+
Requires-Dist: urllib3<3,>=1.26
|
|
15
|
+
Requires-Dist: Pillow>=10.0.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
<div>
|
|
19
|
+
<a href="https://v2.nonebot.dev/store">
|
|
20
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo"></a>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
## ✨ 代理扫描 ✨
|
|
24
|
+
|
|
25
|
+
[](./LICENSE)[](https://pypi.python.org/pypi/nonebot-plugin-proxy-probe)[](https://www.python.org)[](https://github.com/nonebot/nonebot2)
|
|
26
|
+
|
|
27
|
+
## 📖 介绍
|
|
28
|
+
|
|
29
|
+
扫描目标 IPv4 网段中的 Clash HTTP 代理,验证代理可用性,查询代理出口
|
|
30
|
+
IP 与属地,并将结果持久化缓存、渲染为图片的 NoneBot2 插件。
|
|
31
|
+
|
|
32
|
+
功能特色:
|
|
33
|
+
|
|
34
|
+
- **三级流水线**:端口扫描、代理验证、出口属地探测同时运行。
|
|
35
|
+
- **后台任务**:扫描和刷新不会阻塞 NoneBot,可随时查看缓存或停止任务。
|
|
36
|
+
- **自动识别网络**:可自动获取本机出站网卡 IPv4 和直连公网 IPv4。
|
|
37
|
+
- **跨平台**:兼容 Windows 与 Linux 的网卡和默认路由识别。
|
|
38
|
+
- **多端口支持**:可同时扫描多个 Clash HTTP 代理端口。
|
|
39
|
+
- **本地缓存**:使用 `nonebot-plugin-localstore` 持久化结果与用户设置。
|
|
40
|
+
- **图片输出**:使用插件自带字体绘制表格,以 Base64 图片发送。
|
|
41
|
+
- **属地高亮**:新加坡和其他非中国属地使用不同底色突出显示。
|
|
42
|
+
|
|
43
|
+
## 💿 安装
|
|
44
|
+
|
|
45
|
+
### 使用 nb-cli 安装
|
|
46
|
+
|
|
47
|
+
在 NoneBot2 项目根目录下执行:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
nb plugin install nonebot-plugin-proxy-probe
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 使用 pip 安装
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install nonebot-plugin-proxy-probe
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
使用 `pip` 安装后,在 NoneBot2 项目根目录的 `pyproject.toml` 中加载插件:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[tool.nonebot]
|
|
63
|
+
plugins = ["nonebot_plugin_proxy_probe"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
如果已有其他插件,请将 `nonebot_plugin_proxy_probe` 追加到现有
|
|
67
|
+
`plugins` 列表中。
|
|
68
|
+
|
|
69
|
+
### 本地源码安装
|
|
70
|
+
|
|
71
|
+
也可以在 NoneBot2 项目环境中安装本仓库:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
然后在 `pyproject.toml` 中加载:
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[tool.nonebot]
|
|
81
|
+
plugins = ["nonebot_plugin_proxy_probe"]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## ⚙️ 配置
|
|
85
|
+
|
|
86
|
+
所有配置项均为可选项,**必填:否**。可在 NoneBot2 项目的 `.env` 或
|
|
87
|
+
`.env.prod` 中覆盖默认值。
|
|
88
|
+
|
|
89
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
90
|
+
|:--:|:--:|:--:|:--|
|
|
91
|
+
| `proxy_probe_local_ip` | 否 | (空字符串) | 绑定出站连接的本机 IPv4。留空时自动选择有默认网关的实体网卡。 |
|
|
92
|
+
| `proxy_probe_target_ip` | 否 | (空字符串) | 用于计算扫描网段的参考 IPv4。留空时优先读取 LocalStore 用户设置,否则直连查询自身公网 IPv4。 |
|
|
93
|
+
| `proxy_probe_prefix_length` | 否 | `20` | 扫描网段的 IPv4 前缀长度,默认扫描 `/20`。 |
|
|
94
|
+
| `proxy_probe_ports` | 否 | `[7897,7890]` | 需要扫描和验证的代理端口列表。 |
|
|
95
|
+
| `proxy_probe_connect_timeout` | 否 | `0.35` | TCP 端口连接超时时间,单位秒。 |
|
|
96
|
+
| `proxy_probe_proxy_timeout` | 否 | `5.0` | HTTPS 代理验证超时时间,单位秒。 |
|
|
97
|
+
| `proxy_probe_geo_timeout` | 否 | `5.0` | 出口 IP 与属地接口超时时间,单位秒。 |
|
|
98
|
+
| `proxy_probe_workers` | 否 | `256` | 端口扫描工作线程数。 |
|
|
99
|
+
| `proxy_probe_proxy_workers` | 否 | `256` | 代理验证工作线程数。 |
|
|
100
|
+
| `proxy_probe_geo_workers` | 否 | `256` | 出口 IP 与属地探测工作线程数。 |
|
|
101
|
+
| `proxy_probe_bind_source_ip` | 否 | `true` | 是否将网络连接绑定到选定的本机 IPv4。 |
|
|
102
|
+
| `proxy_probe_test_urls` | 否 | ["https://api.ip.sb/ip","https://cp.cloudflare.com/generate_204","https://www.gstatic.com/generate_204"] | HTTPS 代理验证地址,按列表顺序回退。 |
|
|
103
|
+
| `proxy_probe_exclude_ips` | 否 | `[]` | 扫描时需要排除的 IPv4 地址列表。 |
|
|
104
|
+
|
|
105
|
+
配置示例:
|
|
106
|
+
|
|
107
|
+
```dotenv
|
|
108
|
+
proxy_probe_local_ip=
|
|
109
|
+
proxy_probe_target_ip=
|
|
110
|
+
proxy_probe_prefix_length=20
|
|
111
|
+
proxy_probe_ports=[7897]
|
|
112
|
+
proxy_probe_connect_timeout=0.35
|
|
113
|
+
proxy_probe_proxy_timeout=5.0
|
|
114
|
+
proxy_probe_geo_timeout=5.0
|
|
115
|
+
proxy_probe_workers=256
|
|
116
|
+
proxy_probe_proxy_workers=256
|
|
117
|
+
proxy_probe_geo_workers=256
|
|
118
|
+
proxy_probe_bind_source_ip=true
|
|
119
|
+
proxy_probe_test_urls=["https://api.ip.sb/ip","https://cp.cloudflare.com/generate_204","https://www.gstatic.com/generate_204"]
|
|
120
|
+
proxy_probe_exclude_ips=[]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 自动获取 IP
|
|
124
|
+
|
|
125
|
+
- `proxy_probe_local_ip` 为空时,插件自动选择有默认网关的实体 IPv4
|
|
126
|
+
网卡,并排除 Meta、Tailscale、ZeroTier 等常见虚拟接口。
|
|
127
|
+
- `proxy_probe_target_ip` 为空且没有 LocalStore 用户设置时,插件禁用
|
|
128
|
+
环境代理,绑定选定网卡,直连回退接口查询自身公网 IPv4。
|
|
129
|
+
- 自动成功取得本机内网 IP 与直连公网 IP 后,会给任务发起消息添加
|
|
130
|
+
表情 `4`。
|
|
131
|
+
|
|
132
|
+
### 目标 IP 优先级
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
LocalStore 用户设置 > proxy_probe_target_ip 环境变量 > 自动探测
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
如果环境变量和 LocalStore 设置同时存在,插件启动时会输出 warning,
|
|
139
|
+
并使用 LocalStore 中的值。
|
|
140
|
+
|
|
141
|
+
## 🎉 使用
|
|
142
|
+
|
|
143
|
+
### 指令表
|
|
144
|
+
|
|
145
|
+
| 指令 | 权限 | 说明 |
|
|
146
|
+
|:--:|:--:|:--|
|
|
147
|
+
| `/proxy` | 所有用户 | 显示当前缓存结果图片。 |
|
|
148
|
+
| `/proxy -h`、`/proxy --help` | 所有用户 | 显示命令帮助。 |
|
|
149
|
+
| `/proxy -i <IPv4>`、`/proxy --ip <IPv4>` | 所有用户 | 将目标参考 IPv4 持久化到 LocalStore。也支持 `--ip=<IPv4>`。 |
|
|
150
|
+
| `/proxy -p`、`/proxy --probe` | 所有用户 | 在后台重新扫描目标网段,任务开始时添加表情 `427`。 |
|
|
151
|
+
| `/proxy -r`、`/proxy --refresh` | 所有用户 | 刷新缓存代理的可用性、出口 IP 和属地,任务开始时添加表情 `294`。 |
|
|
152
|
+
| `/proxy -s`、`/proxy --stop` | 所有用户 | 停止当前后台任务,保存并输出已有结果。 |
|
|
153
|
+
|
|
154
|
+
同一时间只允许一个重新扫描或缓存刷新任务运行。任务运行期间仍可使用
|
|
155
|
+
`/proxy` 查看当前缓存和实时进度。
|
|
156
|
+
|
|
157
|
+
## 🔍 扫描与刷新
|
|
158
|
+
|
|
159
|
+
### 重新扫描
|
|
160
|
+
|
|
161
|
+
重新扫描采用动态三级流水线:
|
|
162
|
+
|
|
163
|
+
1. 扫描目标网段和配置端口。
|
|
164
|
+
2. 开放端口立即进入 HTTPS 代理验证队列。
|
|
165
|
+
3. 确认可用的代理立即进入出口 IP 与属地探测队列。
|
|
166
|
+
|
|
167
|
+
重新扫描本身已经包含代理和属地刷新,因此任务结束或停止保存部分结果
|
|
168
|
+
时,扫描时间和刷新时间会同时更新。
|
|
169
|
+
|
|
170
|
+
### 缓存刷新
|
|
171
|
+
|
|
172
|
+
缓存刷新只处理上一次结果列表:
|
|
173
|
+
|
|
174
|
+
- 已经无法代理访问 HTTPS 的项目会从结果中移除。
|
|
175
|
+
- 代理仍可用但属地接口全部失败时,项目仍会保留。
|
|
176
|
+
- 查询失败时,代理后 IP 和属地显示为“无法探测代理后地址”。
|
|
177
|
+
|
|
178
|
+
### 流水线统计
|
|
179
|
+
|
|
180
|
+
结果图片顶部显示:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
端口扫描:开放端口数 open in 已扫描 IP 数
|
|
184
|
+
代理验证:确认代理数 proxy in 已验证开放端口数
|
|
185
|
+
属地探测:查询成功数 tested in 已查询代理数
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
例如:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
端口扫描:500 open in 4096
|
|
192
|
+
代理验证:20 proxy in 500
|
|
193
|
+
属地探测:20 tested in 20
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 🎨 图片输出
|
|
197
|
+
|
|
198
|
+
表格包含四列:
|
|
199
|
+
|
|
200
|
+
| IP | 端口 | 代理后 IP | 代理后属地 |
|
|
201
|
+
|:--:|:--:|:--:|:--|
|
|
202
|
+
|
|
203
|
+
- 每个 `IP:端口` 固定占一行,不折行。
|
|
204
|
+
- 图片最多显示前 50 条,LocalStore 中仍保存全部结果。
|
|
205
|
+
- 新加坡属地整行使用 `#FFCE46`。
|
|
206
|
+
- 其他非中国属地整行使用 `#C0FF02`。
|
|
207
|
+
- 中国大陆、香港、台湾、澳门以及无法判断的属地保持默认底色。
|
|
208
|
+
|
|
209
|
+
## 📦 数据与缓存
|
|
210
|
+
|
|
211
|
+
插件在导入 LocalStore 前执行:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
require("nonebot_plugin_localstore")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
数据保存在 `nonebot-plugin-localstore` 为本插件分配的数据目录:
|
|
218
|
+
|
|
219
|
+
- `proxy_cache.json`:扫描结果、扫描/刷新时间、流水线进度和任务状态。
|
|
220
|
+
- `settings.json`:通过 `/proxy -i` 或 `/proxy --ip` 保存的目标参考 IP。
|
|
221
|
+
|
|
222
|
+
JSON 使用临时文件替换方式原子写入,降低异常退出导致缓存损坏的概率。
|
|
223
|
+
Bot 重启后不会尝试恢复旧线程,未完成任务会标记为因重启中止。
|
|
224
|
+
|
|
225
|
+
## 🧐 图片示例
|
|
226
|
+
|
|
227
|
+
### `/proxy`
|
|
228
|
+
|
|
229
|
+
<img width="486" height="503" alt="proxy command" src="https://github.com/user-attachments/assets/d4d4f917-8c16-4a28-8714-da5e30759c01" />
|
|
230
|
+
|
|
231
|
+
<img width="1280" height="1378" alt="proxy result" src="https://github.com/user-attachments/assets/fe7af0c4-e7cf-4f0c-bcb8-a9181d12b2d2" />
|
|
232
|
+
|
|
233
|
+
### `/proxy -h`
|
|
234
|
+
|
|
235
|
+
<img width="520" height="329" alt="proxy help" src="https://github.com/user-attachments/assets/ce9804da-3c93-4667-9450-48fb7e6d64c9" />
|
|
236
|
+
|
|
237
|
+
### `/proxy -r`
|
|
238
|
+
|
|
239
|
+
<img width="444" height="533" alt="proxy refresh" src="https://github.com/user-attachments/assets/c3888e1a-8fbc-4fcb-b8ea-544c3ed461cc" />
|
|
240
|
+
|
|
241
|
+
<img width="1900" height="1988" alt="proxy refresh result" src="https://github.com/user-attachments/assets/aae2675d-9da3-423d-a707-c6f78023048e" />
|
|
242
|
+
|
|
243
|
+
## 🗂️ 项目结构
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
nonebot_plugin_proxy_probe/
|
|
247
|
+
├── __init__.py # 插件元数据与命令模块加载
|
|
248
|
+
├── cache.py # LocalStore 结果缓存和用户设置持久化
|
|
249
|
+
├── commands.py # /proxy 命令解析与 OneBot 事件处理
|
|
250
|
+
├── config.py # NoneBot/Pydantic 配置模型
|
|
251
|
+
├── manager.py # 后台任务互斥、停止、状态更新与结果发送
|
|
252
|
+
├── models.py # 代理结果、流水线进度和缓存模型
|
|
253
|
+
├── probe.py # 网卡识别、端口扫描、代理验证和属地探测
|
|
254
|
+
├── render.py # Pillow 表格绘制与 Base64 图片转换
|
|
255
|
+
└── assets/
|
|
256
|
+
└── 原神字体.ttf # 图片渲染字体
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 🧩 兼容性
|
|
260
|
+
|
|
261
|
+
- Python `3.10+`
|
|
262
|
+
- NoneBot2 `2.3.0+`
|
|
263
|
+
- OneBot v11 适配器
|
|
264
|
+
- Windows / Linux
|
|
265
|
+
- 依赖 `nonebot-plugin-localstore`
|
|
266
|
+
|
|
267
|
+
## 📄 License
|
|
268
|
+
|
|
269
|
+
本项目遵循仓库中的 [LICENSE](./LICENSE) 文件。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
nonebot_plugin_proxy_probe/__init__.py,sha256=4wZ9U0yka_MtwYAdM9OX5WPjT10-IUv4DHbO3qmMel0,799
|
|
2
|
+
nonebot_plugin_proxy_probe/cache.py,sha256=vXhgtQgcsbbDLiq-VphEQ2bgp4YucxGGtHdgIYKrLp4,2371
|
|
3
|
+
nonebot_plugin_proxy_probe/commands.py,sha256=S9CxRzkMQI3Ndoi_lm2Ey5kWniCW6HxkqU1zRXIsRlY,3627
|
|
4
|
+
nonebot_plugin_proxy_probe/config.py,sha256=NwCEhCqutrlsILv1jTBOkBWMMTW9a6lSD89qXAxceB0,1061
|
|
5
|
+
nonebot_plugin_proxy_probe/manager.py,sha256=OexRt05GlDdmLYlJhZZwVhTwgdfya_mjWPaTx51hzwQ,13753
|
|
6
|
+
nonebot_plugin_proxy_probe/models.py,sha256=VjTNiiG2KOdWdE7jLoj1oBy8xKnTkEQK9jreelP5ouM,5015
|
|
7
|
+
nonebot_plugin_proxy_probe/probe.py,sha256=DO2fR1aS1CYg96PY2ZE7Ysqrx61N5YYGcjimLIG9T4E,36060
|
|
8
|
+
nonebot_plugin_proxy_probe/render.py,sha256=zCk84dtySfKbxYzIQnZFaboP4bTP3AIX2pvnf7JBU-g,7801
|
|
9
|
+
nonebot_plugin_proxy_probe/assets/原神字体.ttf,sha256=SKjDhqZmPjPnqowBym6Es5-dPg-eA9ZGjsF4ROTkjC0,11459780
|
|
10
|
+
nonebot_plugin_proxy_probe-0.2.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
11
|
+
nonebot_plugin_proxy_probe-0.2.2.dist-info/METADATA,sha256=59-uW-oPu7BkTmqy3fwe-dmL8L6ORkjm6dU1MmFyI08,10560
|
|
12
|
+
nonebot_plugin_proxy_probe-0.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
nonebot_plugin_proxy_probe-0.2.2.dist-info/top_level.txt,sha256=AKbV1wHrvmoYggPMRb4ncSBosJuw0kAGGZLWWeG6QwM,27
|
|
14
|
+
nonebot_plugin_proxy_probe-0.2.2.dist-info/RECORD,,
|