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 CHANGED
@@ -1,14 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Wimotek Universal Edge Agent
4
- A robust, decoupled, cross-platform reverse tunnel & service bridge client.
5
- Works on Linux, macOS, Windows, inside containers, or on bare metal without root permissions.
6
-
7
- State File Resolution Chain:
8
- 1. Environment Variable / Explicit Param Override (UUID_FILE / STATE_FILE)
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, Any
21
+ from typing import Dict, Optional, Callable
26
22
  import httpx
27
23
 
28
24
 
29
- def resolve_persistent_state_file(filename: str = "node_machine.id", custom_path: Optional[str] = None) -> Path:
25
+ def resolve_state_file(filename: str = "node_machine.id", custom_path: Optional[str] = None) -> Path:
30
26
  """
31
- 持久化状态指纹文件解析链:
32
- 1. 环境变量 / 显式参数覆盖 (UUID_FILE / STATE_FILE)
33
- 2. 制定标准路径 /data 默认 (容器环境标准约定,存在且可写即用)
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
- env_path = os.getenv("UUID_FILE") or os.getenv("STATE_FILE")
42
- if env_path:
43
- return Path(env_path).expanduser().resolve()
44
-
45
- # 2. 制定标准路径 /data 默认 (容器内通过 -v 挂载到 /data 时自动命中)
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
- # 通用绑定主机 (优先读取通用的 TARGET_HOST/LOCAL_HOST,兼容 CDP 历史命名)
96
- self.target_host = (
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
- # 模式判定:默认 false 为直连上报;显式设为 true 则开启出站反向长连接隧道
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
- # 节点名称:支持显式配置,默认自动嗅探出口 IP / 主机名
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 = resolve_persistent_state_file("node_machine.id", custom_path=uuid_file)
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
- """向 Agent 注册自定义业务动作回调"""
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
- try:
190
- if self.uuid_path.is_file():
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
- new_id = str(uuid.uuid4())
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"⚠️ 无法持久化指纹文件 ({self.uuid_path}): {e},使用内存临时指纹。")
199
- return str(uuid.uuid4())
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 登记 + 60s 心跳"""
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 inspect.iscoroutinefunction(handler):
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 管道转发 (如 CDP / VNC 握手)
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 '直接HTTP登记'} | 主端口: {self.target_port} | 指纹位置: {self.uuid_path}")
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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wimotek-edge-agent",
3
- "version": "1.0.0",
3
+ "version": "1.0.4",
4
4
  "description": "Universal Edge Tunnel Agent CLI Runner",
5
5
  "main": "index.js",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,18 +1,20 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "wimotek-edge-agent"
7
- version = "1.0.0"
8
- description = "Universal Edge Reverse Proxy & Service Bridge Agent"
9
- readme = "README.md"
10
- requires-python = ">=3.9"
11
- dependencies = [
12
- "httpx>=0.24.0",
13
- "websockets>=11.0",
14
- ]
15
-
16
- [project.scripts]
17
- edge-agent = "wimotek_edge_agent.agent:main"
18
- wimotek-agent = "wimotek_edge_agent.agent:main"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wimotek-edge-agent
3
- Version: 1.0.0
3
+ Version: 1.0.4
4
4
  Summary: Universal Edge Reverse Proxy & Service Bridge Agent
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown