wimotek-edge-agent 1.0.0 → 1.0.4
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.
- package/agent.py +51 -93
- package/dist/wimotek_edge_agent-1.0.4-py3-none-any.whl +0 -0
- package/dist/wimotek_edge_agent-1.0.4.tar.gz +0 -0
- package/package.json +1 -1
- package/pyproject.toml +20 -18
- package/wimotek_edge_agent.egg-info/PKG-INFO +1 -1
- package/dist/wimotek_edge_agent-1.0.0-py3-none-any.whl +0 -0
- package/dist/wimotek_edge_agent-1.0.0.tar.gz +0 -0
package/agent.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Wimotek Universal Edge Agent
|
|
4
|
-
A
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
2. Standard Designated Container Path (/data, default)
|
|
10
|
-
3. User Home Directory Fallback (~/.wimotek/)
|
|
11
|
-
4. System Temp Directory Extreme Fallback (/tmp/wimotek/)
|
|
4
|
+
A lightweight, clean reverse tunnel & service bridge client.
|
|
5
|
+
|
|
6
|
+
State File Logic:
|
|
7
|
+
- User specified path (param / UUID_FILE env) > Default (/data/node_machine.id)
|
|
8
|
+
- If permission denied, logs clear error instructions for the user to resolve.
|
|
12
9
|
"""
|
|
13
10
|
|
|
14
11
|
import os
|
|
@@ -19,63 +16,29 @@ import uuid
|
|
|
19
16
|
import socket
|
|
20
17
|
import asyncio
|
|
21
18
|
import argparse
|
|
22
|
-
import tempfile
|
|
23
19
|
import urllib.parse
|
|
24
20
|
from pathlib import Path
|
|
25
|
-
from typing import Dict, Optional, Callable
|
|
21
|
+
from typing import Dict, Optional, Callable
|
|
26
22
|
import httpx
|
|
27
23
|
|
|
28
24
|
|
|
29
|
-
def
|
|
25
|
+
def resolve_state_file(filename: str = "node_machine.id", custom_path: Optional[str] = None) -> Path:
|
|
30
26
|
"""
|
|
31
|
-
|
|
32
|
-
1.
|
|
33
|
-
2.
|
|
34
|
-
3. 用户目录兜底 (~/.wimotek/,跨平台桌面与普通用户环境)
|
|
35
|
-
4. 临时目录保底 (/tmp/wimotek/,极端无权限环境兜底)
|
|
27
|
+
极简文件路径解析:
|
|
28
|
+
1. 显式入参 / 环境变量 UUID_FILE
|
|
29
|
+
2. 默认 /data/<filename>
|
|
36
30
|
"""
|
|
37
|
-
# 1. 显式指定入参 或 环境变量覆盖
|
|
38
31
|
if custom_path:
|
|
39
32
|
return Path(custom_path).expanduser().resolve()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if
|
|
43
|
-
return Path(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
container_data = Path("/data")
|
|
47
|
-
try:
|
|
48
|
-
if container_data.is_dir() and os.access(container_data, os.W_OK):
|
|
49
|
-
return container_data / filename
|
|
50
|
-
except Exception:
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
# 3. 用户目录兜底 (~/.wimotek/,兼容 Linux / macOS / Windows 普通用户环境)
|
|
54
|
-
try:
|
|
55
|
-
home_dir = Path.home()
|
|
56
|
-
if home_dir and os.access(home_dir, os.W_OK):
|
|
57
|
-
state_dir = home_dir / ".wimotek"
|
|
58
|
-
state_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
if os.access(state_dir, os.W_OK):
|
|
60
|
-
return state_dir / filename
|
|
61
|
-
except Exception:
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
# 4. 极端只读系统保底:操作系统标准临时目录 (/tmp/wimotek/)
|
|
65
|
-
tmp_dir = Path(tempfile.gettempdir()) / "wimotek"
|
|
66
|
-
try:
|
|
67
|
-
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
return tmp_dir / filename
|
|
69
|
-
except Exception:
|
|
70
|
-
return Path(tempfile.gettempdir()) / f"wimotek_{filename}"
|
|
33
|
+
|
|
34
|
+
env_override = os.getenv("UUID_FILE") or os.getenv("STATE_FILE")
|
|
35
|
+
if env_override:
|
|
36
|
+
return Path(env_override).expanduser().resolve()
|
|
37
|
+
|
|
38
|
+
return Path("/data") / filename
|
|
71
39
|
|
|
72
40
|
|
|
73
41
|
class EdgeAgent:
|
|
74
|
-
"""
|
|
75
|
-
通用边缘节点代理客户端
|
|
76
|
-
提供设备身份指纹绑定、NAT4 出站长连接穿透、以及本地任意端口服务的透明反向代理
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
42
|
def __init__(
|
|
80
43
|
self,
|
|
81
44
|
hub_url: Optional[str] = None,
|
|
@@ -91,49 +54,33 @@ class EdgeAgent:
|
|
|
91
54
|
self.hub_url = (hub_url or os.getenv("HUB_URL", "")).rstrip("/")
|
|
92
55
|
self.join_token = (join_token or os.getenv("JOIN_TOKEN", "")).strip()
|
|
93
56
|
self.node_group = (node_group or os.getenv("NODE_GROUP", "默认分组")).strip()
|
|
57
|
+
self.target_host = (target_host or os.getenv("TARGET_HOST") or os.getenv("LOCAL_HOST") or "127.0.0.1").strip()
|
|
94
58
|
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
target_host
|
|
98
|
-
or os.getenv("TARGET_HOST")
|
|
99
|
-
or os.getenv("LOCAL_HOST")
|
|
100
|
-
or os.getenv("LOCAL_CDP_HOST")
|
|
101
|
-
or "127.0.0.1"
|
|
102
|
-
).strip()
|
|
103
|
-
|
|
104
|
-
# 解析主服务端口 (默认 9222)
|
|
105
|
-
raw_port = (
|
|
106
|
-
target_port
|
|
107
|
-
if target_port is not None
|
|
108
|
-
else os.getenv("TARGET_PORT")
|
|
109
|
-
or os.getenv("PORT")
|
|
110
|
-
or os.getenv("CDP_PORT")
|
|
111
|
-
or "9222"
|
|
112
|
-
)
|
|
59
|
+
# 端口默认 9222
|
|
60
|
+
raw_port = target_port if target_port is not None else (os.getenv("TARGET_PORT") or os.getenv("CDP_PORT") or "9222")
|
|
113
61
|
try:
|
|
114
62
|
self.target_port = int(raw_port)
|
|
115
63
|
except ValueError:
|
|
116
64
|
self.target_port = 9222
|
|
117
65
|
|
|
118
|
-
# 多服务端口映射
|
|
119
66
|
self.services: Dict[str, int] = services or self._parse_services()
|
|
120
67
|
self.services["default"] = self.target_port
|
|
121
68
|
|
|
122
|
-
#
|
|
69
|
+
# 默认 false (直连上报),显式为 true 时走反向隧道
|
|
123
70
|
if reverse_tunnel is not None:
|
|
124
71
|
self.reverse_tunnel = bool(reverse_tunnel)
|
|
125
72
|
else:
|
|
126
73
|
self.reverse_tunnel = os.getenv("REVERSE_TUNNEL", "false").lower() in ["true", "1", "yes"]
|
|
127
74
|
|
|
128
|
-
#
|
|
75
|
+
# 节点名默认按出口 IP 或主机名拼接
|
|
129
76
|
env_name = (node_name or os.getenv("NODE_NAME", "")).strip()
|
|
130
77
|
self.node_name = env_name if env_name else self._auto_detect_name()
|
|
131
78
|
|
|
132
|
-
#
|
|
133
|
-
self.uuid_path =
|
|
79
|
+
# 极简指纹路径定位:显式指定 > /data/node_machine.id
|
|
80
|
+
self.uuid_path = resolve_state_file("node_machine.id", custom_path=uuid_file)
|
|
134
81
|
self.node_uuid = self._load_or_create_uuid()
|
|
135
82
|
|
|
136
|
-
#
|
|
83
|
+
# 通信协议端点解析
|
|
137
84
|
self.http_base = ""
|
|
138
85
|
self.ws_base = ""
|
|
139
86
|
if self.hub_url:
|
|
@@ -142,11 +89,10 @@ class EdgeAgent:
|
|
|
142
89
|
ws_scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
143
90
|
self.ws_base = f"{ws_scheme}://{parsed.netloc}"
|
|
144
91
|
|
|
145
|
-
# 动态自定义业务动作处理器表
|
|
146
92
|
self._action_handlers: Dict[str, Callable] = {}
|
|
147
93
|
|
|
148
94
|
def register_action(self, action_name: str, handler: Callable):
|
|
149
|
-
"""
|
|
95
|
+
"""注册自定义业务动作"""
|
|
150
96
|
self._action_handlers[action_name] = handler
|
|
151
97
|
|
|
152
98
|
def _parse_services(self) -> Dict[str, int]:
|
|
@@ -163,7 +109,6 @@ class EdgeAgent:
|
|
|
163
109
|
return services
|
|
164
110
|
|
|
165
111
|
def _auto_detect_name(self) -> str:
|
|
166
|
-
# A. 嗅探公网出口 IP
|
|
167
112
|
try:
|
|
168
113
|
with httpx.Client(timeout=1.5) as client:
|
|
169
114
|
res = client.get("https://api.ipify.org")
|
|
@@ -172,7 +117,6 @@ class EdgeAgent:
|
|
|
172
117
|
except Exception:
|
|
173
118
|
pass
|
|
174
119
|
|
|
175
|
-
# B. 嗅探网卡出口 IP
|
|
176
120
|
try:
|
|
177
121
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
178
122
|
s.connect(("8.8.8.8", 80))
|
|
@@ -186,20 +130,34 @@ class EdgeAgent:
|
|
|
186
130
|
return f"Node-{socket.gethostname()[:12]}"
|
|
187
131
|
|
|
188
132
|
def _load_or_create_uuid(self) -> str:
|
|
189
|
-
|
|
190
|
-
|
|
133
|
+
"""读取或创建指纹,遇无权限/目录不存在直接清晰报错输出指导"""
|
|
134
|
+
# 1. 尝试读取已有文件
|
|
135
|
+
if self.uuid_path.is_file():
|
|
136
|
+
try:
|
|
191
137
|
content = self.uuid_path.read_text().strip()
|
|
192
138
|
if content:
|
|
193
139
|
return content
|
|
194
|
-
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"⚠️ [指纹读取失败] 无法读取已有指纹文件 ({self.uuid_path}): {e}")
|
|
142
|
+
|
|
143
|
+
# 2. 首次写入持久化
|
|
144
|
+
new_id = str(uuid.uuid4())
|
|
145
|
+
try:
|
|
146
|
+
self.uuid_path.parent.mkdir(parents=True, exist_ok=True)
|
|
195
147
|
self.uuid_path.write_text(new_id)
|
|
196
148
|
return new_id
|
|
149
|
+
except PermissionError:
|
|
150
|
+
print(f"❌ [权限错误] 无权限写入指纹文件: {self.uuid_path}")
|
|
151
|
+
print(f"👉 解决方案:请检查容器挂载目录写权限,或指定环境变量 UUID_FILE 改为可写路径,例如: -e UUID_FILE=/my_path/{self.uuid_path.name}")
|
|
197
152
|
except Exception as e:
|
|
198
|
-
print(f"
|
|
199
|
-
|
|
153
|
+
print(f"❌ [目录错误] 无法保存指纹文件 ({self.uuid_path}): {e}")
|
|
154
|
+
print(f"👉 解决方案:请确认目录是否存在并具备写权限,或通过环境变量 UUID_FILE 指定有效路径。")
|
|
155
|
+
|
|
156
|
+
# 仅在严重报错时使用内存 UUID,避免脚本崩溃打断主业务
|
|
157
|
+
return new_id
|
|
200
158
|
|
|
201
159
|
def run_direct_report(self):
|
|
202
|
-
"""直连上报模式:轻量 HTTP 登记 +
|
|
160
|
+
"""直连上报模式:轻量 HTTP 登记 + 心跳"""
|
|
203
161
|
print(f"📡 [Direct-Mode] 正在向主控中心登记: {self.http_base} ...")
|
|
204
162
|
headers = {"Content-Type": "application/json"}
|
|
205
163
|
if self.join_token:
|
|
@@ -253,7 +211,7 @@ class EdgeAgent:
|
|
|
253
211
|
if action in self._action_handlers:
|
|
254
212
|
try:
|
|
255
213
|
handler = self._action_handlers[action]
|
|
256
|
-
if
|
|
214
|
+
if asyncio.iscoroutinefunction(handler):
|
|
257
215
|
resp_data = await handler(payload)
|
|
258
216
|
else:
|
|
259
217
|
resp_data = handler(payload)
|
|
@@ -261,7 +219,7 @@ class EdgeAgent:
|
|
|
261
219
|
except Exception as ex:
|
|
262
220
|
await ws.send(json.dumps({"req_id": req_id, "error": str(ex)}))
|
|
263
221
|
|
|
264
|
-
# 2. 通用透明 HTTP
|
|
222
|
+
# 2. 通用透明 HTTP 反向中继
|
|
265
223
|
elif action in ["http_proxy", "service_proxy"]:
|
|
266
224
|
port = payload.get("port") or self.services.get(payload.get("service", "default"), self.target_port)
|
|
267
225
|
path = payload.get("path", "/")
|
|
@@ -290,7 +248,7 @@ class EdgeAgent:
|
|
|
290
248
|
"error": str(ex),
|
|
291
249
|
}))
|
|
292
250
|
|
|
293
|
-
# 3. 通用透明 WebSocket 管道转发
|
|
251
|
+
# 3. 通用透明 WebSocket 管道转发
|
|
294
252
|
elif action == "ws_proxy":
|
|
295
253
|
port = payload.get("port") or self.services.get("default", self.target_port)
|
|
296
254
|
ws_path = payload.get("path", "")
|
|
@@ -323,12 +281,12 @@ class EdgeAgent:
|
|
|
323
281
|
await asyncio.sleep(5)
|
|
324
282
|
|
|
325
283
|
def run(self):
|
|
326
|
-
"""
|
|
284
|
+
"""运行入口"""
|
|
327
285
|
if not self.hub_url:
|
|
328
286
|
print("ℹ️ 未配置 HUB_URL,Agent 停止运行。")
|
|
329
287
|
return
|
|
330
288
|
|
|
331
|
-
print(f"🚀 [Edge-Agent] 启动节点: {self.node_name} | 模式: {'反向穿透(NAT4)' if self.reverse_tunnel else '
|
|
289
|
+
print(f"🚀 [Edge-Agent] 启动节点: {self.node_name} | 模式: {'反向穿透(NAT4)' if self.reverse_tunnel else '直连上报'} | 主端口: {self.target_port} | 指纹位置: {self.uuid_path}")
|
|
332
290
|
if self.reverse_tunnel:
|
|
333
291
|
asyncio.run(self.run_reverse_tunnel())
|
|
334
292
|
else:
|
|
@@ -344,7 +302,7 @@ def main():
|
|
|
344
302
|
parser.add_argument("--port", type=int, help="要代理的主端口 (默认: 9222)")
|
|
345
303
|
parser.add_argument("--host", type=str, help="本地服务绑定地址 (默认: 127.0.0.1)")
|
|
346
304
|
parser.add_argument("--services", type=str, help="多端口代理定义, 如: cdp:9222,vnc:6080")
|
|
347
|
-
parser.add_argument("--state-file", type=str, help="指纹文件存储路径 (
|
|
305
|
+
parser.add_argument("--state-file", type=str, help="指纹文件存储路径 (默认: /data/node_machine.id)")
|
|
348
306
|
parser.add_argument("--reverse", action="store_true", help="强制开启出站反向长连接隧道")
|
|
349
307
|
parser.add_argument("--direct", action="store_true", help="强制开启直连上报心跳模式")
|
|
350
308
|
args = parser.parse_args()
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = [
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61.0",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "wimotek-edge-agent"
|
|
9
|
+
version = "1.0.4"
|
|
10
|
+
description = "Universal Edge Reverse Proxy & Service Bridge Agent"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.24.0",
|
|
15
|
+
"websockets>=11.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
edge-agent = "wimotek_edge_agent.agent:main"
|
|
20
|
+
wimotek-agent = "wimotek_edge_agent.agent:main"
|
|
Binary file
|
|
Binary file
|