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,26 @@
|
|
|
1
|
+
"""NoneBot 代理扫描与缓存插件。"""
|
|
2
|
+
|
|
3
|
+
from nonebot import require
|
|
4
|
+
from nonebot.plugin import PluginMetadata
|
|
5
|
+
|
|
6
|
+
require("nonebot_plugin_localstore")
|
|
7
|
+
|
|
8
|
+
from .config import PluginConfig # noqa: E402
|
|
9
|
+
|
|
10
|
+
__plugin_meta__ = PluginMetadata(
|
|
11
|
+
name="代理扫描",
|
|
12
|
+
description="扫描并缓存 Clash HTTP 代理、出口 IP 与属地",
|
|
13
|
+
usage=(
|
|
14
|
+
"/proxy 显示缓存结果\n"
|
|
15
|
+
"/proxy -h 或 --help 显示帮助\n"
|
|
16
|
+
"/proxy -i <IPv4> 或 --ip <IPv4> 设置目标参考 IP\n"
|
|
17
|
+
"/proxy -p 或 --probe 重新扫描\n"
|
|
18
|
+
"/proxy -r 或 --refresh 刷新缓存\n"
|
|
19
|
+
"/proxy -s 或 --stop 停止后台任务"
|
|
20
|
+
),
|
|
21
|
+
type="application",
|
|
22
|
+
supported_adapters={"~onebot.v11"},
|
|
23
|
+
config=PluginConfig,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from . import commands as commands # noqa: E402,F401
|
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from nonebot import require
|
|
10
|
+
|
|
11
|
+
require("nonebot_plugin_localstore")
|
|
12
|
+
|
|
13
|
+
import nonebot_plugin_localstore as localstore # noqa: E402
|
|
14
|
+
|
|
15
|
+
from .models import CacheState # noqa: E402
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DATA_DIR: Path = localstore.get_plugin_data_dir()
|
|
19
|
+
CACHE_PATH = DATA_DIR / "proxy_cache.json"
|
|
20
|
+
SETTINGS_PATH = DATA_DIR / "settings.json"
|
|
21
|
+
_CACHE_LOCK = threading.RLock()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write_json(path: Path, data: dict[str, object]) -> None:
|
|
25
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
payload = json.dumps(data, ensure_ascii=False, indent=2)
|
|
27
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
28
|
+
temporary.write_text(payload, encoding="utf-8")
|
|
29
|
+
os.replace(temporary, path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_cache() -> CacheState:
|
|
33
|
+
with _CACHE_LOCK:
|
|
34
|
+
try:
|
|
35
|
+
raw = json.loads(CACHE_PATH.read_text(encoding="utf-8"))
|
|
36
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
37
|
+
return CacheState()
|
|
38
|
+
if not isinstance(raw, dict):
|
|
39
|
+
return CacheState()
|
|
40
|
+
try:
|
|
41
|
+
state = CacheState.from_dict(raw)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return CacheState()
|
|
44
|
+
|
|
45
|
+
# Bot 重启后不存在可恢复的工作线程,不能继续显示“运行中”。
|
|
46
|
+
if state.running:
|
|
47
|
+
state.running = False
|
|
48
|
+
state.task_status = "任务因 Bot 重启而中止"
|
|
49
|
+
return state
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_cache(state: CacheState) -> None:
|
|
53
|
+
with _CACHE_LOCK:
|
|
54
|
+
_write_json(CACHE_PATH, state.to_dict())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_target_ip() -> str:
|
|
58
|
+
"""读取用户通过命令持久化设置的目标参考 IPv4。"""
|
|
59
|
+
with _CACHE_LOCK:
|
|
60
|
+
try:
|
|
61
|
+
raw = json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
|
|
62
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
63
|
+
return ""
|
|
64
|
+
if not isinstance(raw, dict):
|
|
65
|
+
return ""
|
|
66
|
+
value = str(raw.get("target_ip") or "").strip()
|
|
67
|
+
try:
|
|
68
|
+
return str(ipaddress.IPv4Address(value)) if value else ""
|
|
69
|
+
except ValueError:
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save_target_ip(target_ip: str) -> None:
|
|
74
|
+
normalized = str(ipaddress.IPv4Address(target_ip))
|
|
75
|
+
with _CACHE_LOCK:
|
|
76
|
+
_write_json(
|
|
77
|
+
SETTINGS_PATH,
|
|
78
|
+
{
|
|
79
|
+
"version": 1,
|
|
80
|
+
"target_ip": normalized,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
|
|
5
|
+
from nonebot import on_command
|
|
6
|
+
from nonebot.adapters.onebot.v11 import (
|
|
7
|
+
Bot,
|
|
8
|
+
GroupMessageEvent,
|
|
9
|
+
Message,
|
|
10
|
+
MessageEvent,
|
|
11
|
+
)
|
|
12
|
+
from nonebot.log import logger
|
|
13
|
+
from nonebot.params import CommandArg
|
|
14
|
+
|
|
15
|
+
from .manager import manager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
USAGE = (
|
|
19
|
+
"用法:\n"
|
|
20
|
+
"/proxy 显示缓存结果\n"
|
|
21
|
+
"/proxy -h 或 --help 显示命令帮助\n"
|
|
22
|
+
"/proxy -i <IPv4> 或 --ip <IPv4> 设置目标参考 IP\n"
|
|
23
|
+
"/proxy -p 或 --probe 重新扫描\n"
|
|
24
|
+
"/proxy -r 或 --refresh 刷新缓存\n"
|
|
25
|
+
"/proxy -s 或 --stop 停止后台任务"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_command(argument: str) -> tuple[str, str] | None:
|
|
30
|
+
normalized = " ".join(argument.strip().split())
|
|
31
|
+
actions = {
|
|
32
|
+
"": ("show", ""),
|
|
33
|
+
"-h": ("help", ""),
|
|
34
|
+
"--help": ("help", ""),
|
|
35
|
+
"-p": ("probe", ""),
|
|
36
|
+
"--probe": ("probe", ""),
|
|
37
|
+
"-r": ("refresh", ""),
|
|
38
|
+
"--refresh": ("refresh", ""),
|
|
39
|
+
"-s": ("stop", ""),
|
|
40
|
+
"--stop": ("stop", ""),
|
|
41
|
+
}
|
|
42
|
+
exact = actions.get(normalized)
|
|
43
|
+
if exact is not None:
|
|
44
|
+
return exact
|
|
45
|
+
|
|
46
|
+
if normalized.startswith("--ip="):
|
|
47
|
+
return "ip", normalized.partition("=")[2].strip()
|
|
48
|
+
fields = normalized.split(" ", 1)
|
|
49
|
+
if fields[0] in {"-i", "--ip"}:
|
|
50
|
+
return "ip", fields[1].strip() if len(fields) == 2 else ""
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_action(argument: str) -> str | None:
|
|
55
|
+
"""保留简单动作解析接口,供旧调用方兼容。"""
|
|
56
|
+
parsed = parse_command(argument)
|
|
57
|
+
return parsed[0] if parsed is not None else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def set_emoji(
|
|
61
|
+
bot: Bot,
|
|
62
|
+
event: MessageEvent,
|
|
63
|
+
emoji_id: str,
|
|
64
|
+
) -> None:
|
|
65
|
+
if not isinstance(event, GroupMessageEvent):
|
|
66
|
+
return
|
|
67
|
+
try:
|
|
68
|
+
await bot.call_api(
|
|
69
|
+
"set_msg_emoji_like",
|
|
70
|
+
group_id=event.group_id,
|
|
71
|
+
message_id=event.message_id,
|
|
72
|
+
emoji_id=emoji_id,
|
|
73
|
+
set=True,
|
|
74
|
+
)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
logger.warning(f"设置消息表情失败: {exc}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
proxy_command = on_command("proxy", priority=5, block=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@proxy_command.handle()
|
|
83
|
+
async def handle_proxy(
|
|
84
|
+
bot: Bot,
|
|
85
|
+
event: MessageEvent,
|
|
86
|
+
args: Message = CommandArg(),
|
|
87
|
+
) -> None:
|
|
88
|
+
parsed = parse_command(args.extract_plain_text())
|
|
89
|
+
if parsed is None:
|
|
90
|
+
await proxy_command.finish(USAGE)
|
|
91
|
+
action, value = parsed
|
|
92
|
+
|
|
93
|
+
if action == "show":
|
|
94
|
+
await proxy_command.finish(manager.get_image_segment())
|
|
95
|
+
|
|
96
|
+
if action == "help":
|
|
97
|
+
await proxy_command.finish(USAGE)
|
|
98
|
+
|
|
99
|
+
if action == "ip":
|
|
100
|
+
if not value:
|
|
101
|
+
await proxy_command.finish(
|
|
102
|
+
"请提供目标参考 IPv4,例如:"
|
|
103
|
+
"/proxy -i 218.194.50.3"
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
target_ip = str(ipaddress.IPv4Address(value))
|
|
107
|
+
except ValueError:
|
|
108
|
+
await proxy_command.finish(f"不是有效的 IPv4 地址:{value}")
|
|
109
|
+
saved, message = await manager.set_target_ip(target_ip)
|
|
110
|
+
await proxy_command.finish(message)
|
|
111
|
+
|
|
112
|
+
if action == "probe":
|
|
113
|
+
await set_emoji(bot, event, "427")
|
|
114
|
+
started, message = await manager.start("probe", bot, event)
|
|
115
|
+
if not started:
|
|
116
|
+
await proxy_command.finish(message)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if action == "refresh":
|
|
120
|
+
await set_emoji(bot, event, "294")
|
|
121
|
+
started, message = await manager.start("refresh", bot, event)
|
|
122
|
+
if not started:
|
|
123
|
+
await proxy_command.finish(message)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if action == "stop":
|
|
127
|
+
stopped, message = await manager.stop(bot, event)
|
|
128
|
+
if not stopped:
|
|
129
|
+
await proxy_command.finish(message)
|
|
130
|
+
return
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from nonebot import get_plugin_config
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PluginConfig(BaseModel):
|
|
8
|
+
proxy_probe_local_ip: str = ""
|
|
9
|
+
proxy_probe_target_ip: str = ""
|
|
10
|
+
proxy_probe_prefix_length: int = 20
|
|
11
|
+
proxy_probe_ports: list[int] = Field(default_factory=lambda: [7897,7890]) # default_factory 可以明确避免多个实例共享同一个可变列表。
|
|
12
|
+
proxy_probe_connect_timeout: float = 0.35
|
|
13
|
+
proxy_probe_proxy_timeout: float = 5.0
|
|
14
|
+
proxy_probe_geo_timeout: float = 5.0
|
|
15
|
+
proxy_probe_workers: int = 256
|
|
16
|
+
proxy_probe_proxy_workers: int = 256
|
|
17
|
+
proxy_probe_geo_workers: int = 256
|
|
18
|
+
proxy_probe_bind_source_ip: bool = True
|
|
19
|
+
proxy_probe_test_urls: list[str] = Field(
|
|
20
|
+
default_factory=lambda: [
|
|
21
|
+
"https://api.ip.sb/ip",
|
|
22
|
+
"https://cp.cloudflare.com/generate_204",
|
|
23
|
+
"https://www.gstatic.com/generate_204",
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
proxy_probe_exclude_ips: list[str] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
plugin_config = get_plugin_config(PluginConfig)
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import copy
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from nonebot.adapters.onebot.v11 import (
|
|
10
|
+
Bot,
|
|
11
|
+
GroupMessageEvent,
|
|
12
|
+
MessageEvent,
|
|
13
|
+
MessageSegment,
|
|
14
|
+
)
|
|
15
|
+
from nonebot.log import logger
|
|
16
|
+
|
|
17
|
+
from .cache import load_cache, load_target_ip, save_cache, save_target_ip
|
|
18
|
+
from .config import PluginConfig, plugin_config
|
|
19
|
+
from .models import CacheState, PipelineProgress, ProxyRecord
|
|
20
|
+
from .probe import (
|
|
21
|
+
ProbeConfig,
|
|
22
|
+
ProbeRunResult,
|
|
23
|
+
RefreshProgress,
|
|
24
|
+
RefreshRunResult,
|
|
25
|
+
detect_direct_public_ip,
|
|
26
|
+
detect_local_network,
|
|
27
|
+
run_probe,
|
|
28
|
+
run_refresh,
|
|
29
|
+
)
|
|
30
|
+
from .render import image_to_base64, render_cache_image
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_time() -> str:
|
|
34
|
+
return datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_probe_config(
|
|
38
|
+
config: PluginConfig,
|
|
39
|
+
persisted_target_ip: str = "",
|
|
40
|
+
) -> ProbeConfig:
|
|
41
|
+
network = detect_local_network(config.proxy_probe_local_ip.strip())
|
|
42
|
+
local_ip = network.local_ip
|
|
43
|
+
target_ip = (
|
|
44
|
+
persisted_target_ip.strip()
|
|
45
|
+
or config.proxy_probe_target_ip.strip()
|
|
46
|
+
)
|
|
47
|
+
if not target_ip:
|
|
48
|
+
target_ip = detect_direct_public_ip(
|
|
49
|
+
local_ip,
|
|
50
|
+
config.proxy_probe_geo_timeout,
|
|
51
|
+
config.proxy_probe_bind_source_ip,
|
|
52
|
+
network.dns_servers,
|
|
53
|
+
)
|
|
54
|
+
logger.info(
|
|
55
|
+
f"代理扫描使用网卡 {network.interface_name or '自动路由'} "
|
|
56
|
+
f"({local_ip}),目标参考 IP {target_ip}"
|
|
57
|
+
)
|
|
58
|
+
return ProbeConfig(
|
|
59
|
+
local_ip=local_ip,
|
|
60
|
+
target_ip=target_ip,
|
|
61
|
+
prefix_length=config.proxy_probe_prefix_length,
|
|
62
|
+
proxy_ports=tuple(config.proxy_probe_ports),
|
|
63
|
+
connect_timeout=config.proxy_probe_connect_timeout,
|
|
64
|
+
proxy_timeout=config.proxy_probe_proxy_timeout,
|
|
65
|
+
geo_timeout=config.proxy_probe_geo_timeout,
|
|
66
|
+
workers=config.proxy_probe_workers,
|
|
67
|
+
proxy_workers=config.proxy_probe_proxy_workers,
|
|
68
|
+
geo_workers=config.proxy_probe_geo_workers,
|
|
69
|
+
bind_source_ip=config.proxy_probe_bind_source_ip,
|
|
70
|
+
proxy_test_urls=tuple(config.proxy_probe_test_urls),
|
|
71
|
+
exclude_ips=tuple(config.proxy_probe_exclude_ips),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ProbeManager:
|
|
76
|
+
def __init__(self) -> None:
|
|
77
|
+
self._state_lock = threading.RLock()
|
|
78
|
+
self._state = load_cache()
|
|
79
|
+
self._persisted_target_ip = load_target_ip()
|
|
80
|
+
self._task_lock = asyncio.Lock()
|
|
81
|
+
self._task: asyncio.Task[None] | None = None
|
|
82
|
+
self._stop_event: threading.Event | None = None
|
|
83
|
+
self._recipient: tuple[Bot, MessageEvent] | None = None
|
|
84
|
+
self._last_save = 0.0
|
|
85
|
+
self._persist_lock = threading.RLock()
|
|
86
|
+
environment_target_ip = plugin_config.proxy_probe_target_ip.strip()
|
|
87
|
+
if self._persisted_target_ip and environment_target_ip:
|
|
88
|
+
logger.warning(
|
|
89
|
+
"proxy_probe_target_ip 环境变量与 LocalStore 持久化设置"
|
|
90
|
+
"同时存在,将使用 LocalStore 缓存值 "
|
|
91
|
+
f"{self._persisted_target_ip},忽略环境变量值 "
|
|
92
|
+
f"{environment_target_ip}"
|
|
93
|
+
)
|
|
94
|
+
if self._state.running:
|
|
95
|
+
self._state.running = False
|
|
96
|
+
self._state.task_status = "任务因 Bot 重启而中止"
|
|
97
|
+
save_cache(self._state)
|
|
98
|
+
|
|
99
|
+
def get_state(self) -> CacheState:
|
|
100
|
+
with self._state_lock:
|
|
101
|
+
return copy.deepcopy(self._state)
|
|
102
|
+
|
|
103
|
+
def get_image_segment(self) -> MessageSegment:
|
|
104
|
+
image = render_cache_image(self.get_state())
|
|
105
|
+
return MessageSegment.image(image_to_base64(image))
|
|
106
|
+
|
|
107
|
+
def running_description(self) -> str:
|
|
108
|
+
state = self.get_state()
|
|
109
|
+
names = {"probe": "重新扫描", "refresh": "缓存刷新"}
|
|
110
|
+
return names.get(state.operation, "后台")
|
|
111
|
+
|
|
112
|
+
def configured_target_ip(self) -> str:
|
|
113
|
+
return (
|
|
114
|
+
self._persisted_target_ip
|
|
115
|
+
or plugin_config.proxy_probe_target_ip.strip()
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def set_target_ip(self, target_ip: str) -> tuple[bool, str]:
|
|
119
|
+
async with self._task_lock:
|
|
120
|
+
if self._task is not None and not self._task.done():
|
|
121
|
+
return (
|
|
122
|
+
False,
|
|
123
|
+
f"已有{self.running_description()}任务正在运行,"
|
|
124
|
+
"请先使用 /proxy -s 停止。",
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
await asyncio.to_thread(save_target_ip, target_ip)
|
|
128
|
+
except (OSError, ValueError) as exc:
|
|
129
|
+
return False, f"保存目标 IP 失败:{exc}"
|
|
130
|
+
self._persisted_target_ip = target_ip
|
|
131
|
+
return (
|
|
132
|
+
True,
|
|
133
|
+
f"目标参考 IP 已持久化为 {target_ip},"
|
|
134
|
+
"后续扫描和刷新将优先使用该值。",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _persist(self, force: bool = False) -> None:
|
|
138
|
+
with self._persist_lock:
|
|
139
|
+
now = time.monotonic()
|
|
140
|
+
if not force and now - self._last_save < 0.5:
|
|
141
|
+
return
|
|
142
|
+
with self._state_lock:
|
|
143
|
+
snapshot = copy.deepcopy(self._state)
|
|
144
|
+
try:
|
|
145
|
+
save_cache(snapshot)
|
|
146
|
+
self._last_save = now
|
|
147
|
+
except OSError as exc:
|
|
148
|
+
logger.warning(f"保存代理缓存失败: {exc}")
|
|
149
|
+
|
|
150
|
+
def _probe_callback(
|
|
151
|
+
self,
|
|
152
|
+
progress: PipelineProgress,
|
|
153
|
+
results: list[ProxyRecord],
|
|
154
|
+
current: int,
|
|
155
|
+
total: int,
|
|
156
|
+
) -> None:
|
|
157
|
+
with self._state_lock:
|
|
158
|
+
self._state.progress = progress
|
|
159
|
+
self._state.results = list(results)
|
|
160
|
+
self._state.task_current = current
|
|
161
|
+
self._state.task_total = total
|
|
162
|
+
self._persist()
|
|
163
|
+
|
|
164
|
+
def _refresh_callback(
|
|
165
|
+
self,
|
|
166
|
+
results: list[ProxyRecord],
|
|
167
|
+
refresh: RefreshProgress,
|
|
168
|
+
) -> None:
|
|
169
|
+
with self._state_lock:
|
|
170
|
+
self._state.results = list(results)
|
|
171
|
+
current = self._state.progress
|
|
172
|
+
self._state.progress = PipelineProgress(
|
|
173
|
+
total=current.total,
|
|
174
|
+
scan_completed=current.scan_completed,
|
|
175
|
+
open_count=current.open_count,
|
|
176
|
+
proxy_tested=refresh.proxy_tested,
|
|
177
|
+
proxy_count=refresh.proxy_count,
|
|
178
|
+
geo_tested=refresh.geo_tested,
|
|
179
|
+
geo_success=refresh.geo_success,
|
|
180
|
+
)
|
|
181
|
+
self._state.task_current = refresh.completed
|
|
182
|
+
self._state.task_total = refresh.total
|
|
183
|
+
self._persist()
|
|
184
|
+
|
|
185
|
+
async def start(
|
|
186
|
+
self,
|
|
187
|
+
operation: str,
|
|
188
|
+
bot: Bot,
|
|
189
|
+
event: MessageEvent,
|
|
190
|
+
) -> tuple[bool, str]:
|
|
191
|
+
async with self._task_lock:
|
|
192
|
+
if self._task is not None and not self._task.done():
|
|
193
|
+
return (
|
|
194
|
+
False,
|
|
195
|
+
f"已有{self.running_description()}任务正在运行,"
|
|
196
|
+
"可使用 /proxy -s 停止。",
|
|
197
|
+
)
|
|
198
|
+
if operation == "refresh" and not self.get_state().results:
|
|
199
|
+
return False, "当前没有缓存代理,请先使用 /proxy -p 扫描。"
|
|
200
|
+
|
|
201
|
+
self._stop_event = threading.Event()
|
|
202
|
+
self._recipient = (bot, event)
|
|
203
|
+
with self._state_lock:
|
|
204
|
+
self._state.running = True
|
|
205
|
+
self._state.operation = operation
|
|
206
|
+
self._state.task_status = "运行中"
|
|
207
|
+
self._state.task_current = 0
|
|
208
|
+
if operation == "probe":
|
|
209
|
+
total = 1 << (32 - plugin_config.proxy_probe_prefix_length)
|
|
210
|
+
self._state.task_total = total
|
|
211
|
+
self._state.progress = PipelineProgress(total=total)
|
|
212
|
+
self._state.results = []
|
|
213
|
+
else:
|
|
214
|
+
self._state.task_total = len(self._state.results)
|
|
215
|
+
current = self._state.progress
|
|
216
|
+
self._state.progress = PipelineProgress(
|
|
217
|
+
total=current.total,
|
|
218
|
+
scan_completed=current.scan_completed,
|
|
219
|
+
open_count=current.open_count,
|
|
220
|
+
)
|
|
221
|
+
self._persist(force=True)
|
|
222
|
+
self._task = asyncio.create_task(
|
|
223
|
+
self._run(operation),
|
|
224
|
+
name=f"nonebot-proxy-{operation}",
|
|
225
|
+
)
|
|
226
|
+
return True, ""
|
|
227
|
+
|
|
228
|
+
async def stop(
|
|
229
|
+
self,
|
|
230
|
+
bot: Bot,
|
|
231
|
+
event: MessageEvent,
|
|
232
|
+
) -> tuple[bool, str]:
|
|
233
|
+
async with self._task_lock:
|
|
234
|
+
task = self._task
|
|
235
|
+
stop_event = self._stop_event
|
|
236
|
+
if task is None or task.done() or stop_event is None:
|
|
237
|
+
return False, "当前没有正在运行的代理任务。"
|
|
238
|
+
self._recipient = (bot, event)
|
|
239
|
+
with self._state_lock:
|
|
240
|
+
self._state.task_status = "正在停止"
|
|
241
|
+
self._persist(force=True)
|
|
242
|
+
stop_event.set()
|
|
243
|
+
await asyncio.shield(task)
|
|
244
|
+
return True, ""
|
|
245
|
+
|
|
246
|
+
async def _run(self, operation: str) -> None:
|
|
247
|
+
stopped = False
|
|
248
|
+
error: Exception | None = None
|
|
249
|
+
origin_recipient = self._recipient
|
|
250
|
+
persisted_target_ip = self._persisted_target_ip
|
|
251
|
+
target_ip_auto_detected = not (
|
|
252
|
+
persisted_target_ip
|
|
253
|
+
or plugin_config.proxy_probe_target_ip.strip()
|
|
254
|
+
)
|
|
255
|
+
try:
|
|
256
|
+
config = await asyncio.to_thread(
|
|
257
|
+
build_probe_config,
|
|
258
|
+
plugin_config,
|
|
259
|
+
persisted_target_ip,
|
|
260
|
+
)
|
|
261
|
+
with self._state_lock:
|
|
262
|
+
self._state.local_ip = config.local_ip
|
|
263
|
+
self._state.target_ip = config.target_ip
|
|
264
|
+
self._persist(force=True)
|
|
265
|
+
if (
|
|
266
|
+
target_ip_auto_detected
|
|
267
|
+
and origin_recipient is not None
|
|
268
|
+
):
|
|
269
|
+
bot, event = origin_recipient
|
|
270
|
+
if isinstance(event, GroupMessageEvent):
|
|
271
|
+
try:
|
|
272
|
+
await bot.call_api(
|
|
273
|
+
"set_msg_emoji_like",
|
|
274
|
+
group_id=event.group_id,
|
|
275
|
+
message_id=event.message_id,
|
|
276
|
+
emoji_id="4",
|
|
277
|
+
set=True,
|
|
278
|
+
)
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
logger.warning(f"设置 IP 探测成功表情失败: {exc}")
|
|
281
|
+
stop_event = self._stop_event
|
|
282
|
+
if stop_event is None:
|
|
283
|
+
raise RuntimeError("停止事件未初始化")
|
|
284
|
+
if operation == "probe":
|
|
285
|
+
result: ProbeRunResult = await asyncio.to_thread(
|
|
286
|
+
run_probe,
|
|
287
|
+
config,
|
|
288
|
+
stop_event,
|
|
289
|
+
self._probe_callback,
|
|
290
|
+
)
|
|
291
|
+
stopped = result.interrupted
|
|
292
|
+
with self._state_lock:
|
|
293
|
+
self._state.progress = result.progress
|
|
294
|
+
self._state.results = result.results
|
|
295
|
+
self._state.task_current = result.progress.scan_completed
|
|
296
|
+
self._state.task_total = result.progress.total
|
|
297
|
+
completed_at = format_time()
|
|
298
|
+
self._state.scan_time = completed_at
|
|
299
|
+
self._state.refresh_time = completed_at
|
|
300
|
+
else:
|
|
301
|
+
cached = self.get_state().results
|
|
302
|
+
refresh_result: RefreshRunResult = await asyncio.to_thread(
|
|
303
|
+
run_refresh,
|
|
304
|
+
config,
|
|
305
|
+
cached,
|
|
306
|
+
stop_event,
|
|
307
|
+
self._refresh_callback,
|
|
308
|
+
)
|
|
309
|
+
stopped = refresh_result.interrupted
|
|
310
|
+
with self._state_lock:
|
|
311
|
+
self._state.results = refresh_result.results
|
|
312
|
+
refresh = refresh_result.progress
|
|
313
|
+
current = self._state.progress
|
|
314
|
+
self._state.progress = PipelineProgress(
|
|
315
|
+
total=current.total,
|
|
316
|
+
scan_completed=current.scan_completed,
|
|
317
|
+
open_count=current.open_count,
|
|
318
|
+
proxy_tested=refresh.proxy_tested,
|
|
319
|
+
proxy_count=refresh.proxy_count,
|
|
320
|
+
geo_tested=refresh.geo_tested,
|
|
321
|
+
geo_success=refresh.geo_success,
|
|
322
|
+
)
|
|
323
|
+
self._state.task_current = refresh.completed
|
|
324
|
+
self._state.task_total = refresh.total
|
|
325
|
+
self._state.refresh_time = format_time()
|
|
326
|
+
except Exception as exc:
|
|
327
|
+
error = exc
|
|
328
|
+
logger.exception("代理探测后台任务失败")
|
|
329
|
+
finally:
|
|
330
|
+
with self._state_lock:
|
|
331
|
+
self._state.running = False
|
|
332
|
+
if error is not None:
|
|
333
|
+
self._state.task_status = f"失败:{error}"
|
|
334
|
+
elif stopped:
|
|
335
|
+
self._state.task_status = "已停止,显示部分结果"
|
|
336
|
+
else:
|
|
337
|
+
self._state.task_status = "已完成"
|
|
338
|
+
snapshot = copy.deepcopy(self._state)
|
|
339
|
+
try:
|
|
340
|
+
save_cache(snapshot)
|
|
341
|
+
except OSError as exc:
|
|
342
|
+
logger.warning(f"保存代理最终缓存失败: {exc}")
|
|
343
|
+
|
|
344
|
+
recipient = self._recipient
|
|
345
|
+
if recipient is not None:
|
|
346
|
+
bot, event = recipient
|
|
347
|
+
try:
|
|
348
|
+
if error is not None:
|
|
349
|
+
await bot.send(event, f"代理任务失败:{error}")
|
|
350
|
+
await bot.send(event, self.get_image_segment())
|
|
351
|
+
except Exception:
|
|
352
|
+
logger.exception("发送代理结果图片失败")
|
|
353
|
+
|
|
354
|
+
async with self._task_lock:
|
|
355
|
+
self._task = None
|
|
356
|
+
self._stop_event = None
|
|
357
|
+
self._recipient = None
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
manager = ProbeManager()
|