proxyctl 0.1.3__tar.gz → 0.1.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyctl
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Proxy configuration lifecycle management for macOS and Linux
5
5
  Project-URL: Homepage, https://github.com/crhan/proxyctl
6
6
  Project-URL: Issues, https://github.com/crhan/proxyctl/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxyctl"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "Proxy configuration lifecycle management for macOS and Linux"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -36,10 +36,11 @@ def _port_listening(port: int) -> bool:
36
36
  return False
37
37
 
38
38
 
39
- def _test_url(url: str, desc: str, mode: str = "proxy", timeout: int = 8) -> tuple:
39
+ def _test_url(url: str, desc: str, mode: str = "proxy", timeout: int = 8,
40
+ proxy_port: int = 7890) -> tuple:
40
41
  """
41
42
  测试 URL 可达性。
42
- mode=proxy: 走 socks5h://127.0.0.1:7890
43
+ mode=proxy: 走 socks5h://127.0.0.1:{proxy_port}(需配置 mixed-port 或 socks-port)
43
44
  mode=direct: 绕过所有代理 (--noproxy '*')
44
45
  返回 (ok: bool, line: str),调用方负责 print。
45
46
  """
@@ -49,7 +50,7 @@ def _test_url(url: str, desc: str, mode: str = "proxy", timeout: int = 8) -> tup
49
50
  cmd = ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
50
51
  "--max-time", str(timeout)]
51
52
  if mode == "proxy":
52
- cmd += ["--proxy", "socks5h://127.0.0.1:7890"]
53
+ cmd += ["--proxy", f"socks5h://127.0.0.1:{proxy_port}"]
53
54
  else:
54
55
  cmd += ["--noproxy", "*"]
55
56
  cmd.append(url)
@@ -281,7 +282,8 @@ def _proxy_groups_section(api_base: str, api_secret: str,
281
282
  return True
282
283
 
283
284
 
284
- def _ipgeo(ip: str, cache_file: str, api_secret: str) -> str:
285
+ def _ipgeo(ip: str, cache_file: str, api_secret: str,
286
+ proxy_port: int = 7890) -> str:
285
287
  """查询 IP 归属地,带文件缓存。返回 'city,country|org' 格式。"""
286
288
  if not ip:
287
289
  return ""
@@ -293,7 +295,8 @@ def _ipgeo(ip: str, cache_file: str, api_secret: str) -> str:
293
295
  if k not in ("http_proxy", "https_proxy", "all_proxy",
294
296
  "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY")}
295
297
  r = subprocess.run(
296
- ["curl", "-s", "--max-time", "6", "--proxy", "socks5h://127.0.0.1:7890",
298
+ ["curl", "-s", "--max-time", "6",
299
+ "--proxy", f"socks5h://127.0.0.1:{proxy_port}",
297
300
  f"https://ipinfo.io/{ip}/json"],
298
301
  capture_output=True, text=True, env=env, timeout=10
299
302
  )
@@ -428,11 +431,11 @@ def cmd_bench(api: str, api_secret: str, groups: list = None,
428
431
  _proxy_groups_section(api, api_secret, groups=list(group_members.keys()))
429
432
 
430
433
 
431
- def _fetch_probe(probe, env_clean: dict) -> str:
434
+ def _fetch_probe(probe, env_clean: dict, proxy_port: int = 7890) -> str:
432
435
  """根据 OutboundProbe 配置发起一次 IP 查询,返回提取后的 IP。"""
433
436
  cmd = ["curl", "-s", "--max-time", str(probe.timeout)]
434
437
  if probe.mode == "proxy":
435
- cmd += ["--proxy", "socks5h://127.0.0.1:7890"]
438
+ cmd += ["--proxy", f"socks5h://127.0.0.1:{proxy_port}"]
436
439
  else:
437
440
  cmd += ["--noproxy", "*"]
438
441
  cmd.append(probe.url)
@@ -478,9 +481,13 @@ def cmd_check(engine, api: str, api_secret: str,
478
481
  probes = registry.collect("check_outbound_probes", ctx={})
479
482
  probe_ips: dict[str, str] = {p.name: "" for p in probes}
480
483
 
484
+ # 临时常量:probes 在端口解析之前启动,所以这里用 config 直接拿
485
+ _probe_proxy_port = int(config.get("proxy_port", 7890))
486
+
481
487
  def _make_fetcher(probe):
482
488
  def _run():
483
- probe_ips[probe.name] = _fetch_probe(probe, env_clean)
489
+ probe_ips[probe.name] = _fetch_probe(probe, env_clean,
490
+ proxy_port=_probe_proxy_port)
484
491
  return _run
485
492
 
486
493
  ip_threads = [threading.Thread(target=_make_fetcher(p)) for p in probes]
@@ -521,10 +528,13 @@ def cmd_check(engine, api: str, api_secret: str,
521
528
  print(f" {RED}✗{NC} daemon not running — 执行 proxyctl start")
522
529
  return
523
530
 
524
- # 端口检测(proxy 模式不检查 53
531
+ # 端口检测(proxy 模式不检查 53)— 端口来自 config + api_base
532
+ from urllib.parse import urlparse
533
+ proxy_port = int(config.get("proxy_port", 7890))
534
+ api_port = urlparse(api).port or 9090
525
535
  dns_hijack = mode in ("tun", "mixed")
526
- check_ports = [(53, "dns"), (7890, "proxy"), (9090, "api")] if dns_hijack \
527
- else [(7890, "proxy"), (9090, "api")]
536
+ check_ports = [(53, "dns"), (proxy_port, "proxy"), (api_port, "api")] if dns_hijack \
537
+ else [(proxy_port, "proxy"), (api_port, "api")]
528
538
  ok_ports, fail_ports = [], []
529
539
  for port, desc in check_ports:
530
540
  if _port_listening(port):
@@ -710,7 +720,8 @@ def cmd_check(engine, api: str, api_secret: str,
710
720
  ok, line = _test_tcp(parts[0], int(parts[1]), target.name)
711
721
  else:
712
722
  ok, line = _test_url(target.url, target.name,
713
- target.mode, target.timeout)
723
+ target.mode, target.timeout,
724
+ proxy_port=proxy_port)
714
725
  except Exception as e:
715
726
  ok, line = False, f" {RED}✗{NC} {target.name} error: {e}"
716
727
  results[idx] = (line, ok)
@@ -737,7 +748,7 @@ def cmd_check(engine, api: str, api_secret: str,
737
748
  print(f" {YELLOW}—{NC} 无出口探测项")
738
749
  for probe in probes:
739
750
  ip = probe_ips.get(probe.name, "")
740
- geo = _ipgeo(ip, cache_file, api_secret)
751
+ geo = _ipgeo(ip, cache_file, api_secret, proxy_port=proxy_port)
741
752
  print(f" {probe.name:<7s}{_fmt_ip(ip, geo)}")
742
753
 
743
754
  # 分流校验:当同时有 proxy 出口和 direct 出口时比较
@@ -43,6 +43,11 @@ DEFAULTS = {
43
43
  "api_secret": "", # 必填:Clash API Bearer token
44
44
  "config_dir": os.path.join(HOME, ".config"),
45
45
  "dns_lock_label": "com.proxyctl.dns-lock",
46
+ # 引擎对外暴露的 HTTP/SOCKS mixed-port(应与 mihomo config 的 port/mixed-port 一致)
47
+ "proxy_port": 7890,
48
+ # 个人附加的 NO_PROXY 项(追加到默认 localhost/私网集合之后)
49
+ # 例: ["corp.example.com", "intranet.local"] 或 "corp.example.com,intranet.local"
50
+ "no_proxy_extra": [],
46
51
  }
47
52
 
48
53
  SCRIPTS_DIR = os.path.dirname(os.path.realpath(__file__))
@@ -514,7 +519,8 @@ def _wait_ready(backend: Backend):
514
519
  """
515
520
  if IS_MACOS:
516
521
  wait_port(53, timeout=10)
517
- wait_port(7890, timeout=10)
522
+ wait_port(int(backend.config.get("proxy_port", DEFAULTS["proxy_port"])),
523
+ timeout=10)
518
524
  time.sleep(3)
519
525
 
520
526
 
@@ -1200,10 +1206,16 @@ def cmd_env(config: dict, unset: bool = False):
1200
1206
  print(f"unset {var};")
1201
1207
  return
1202
1208
 
1203
- port = 7890 # mixed-port
1209
+ port = int(config.get("proxy_port", DEFAULTS["proxy_port"])) # mixed-port
1204
1210
  proxy_http = f"http://127.0.0.1:{port}"
1205
1211
  proxy_socks = f"socks5://127.0.0.1:{port}"
1206
1212
  no_proxy = "localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
1213
+ # 用户附加的 NO_PROXY 项(个人域名等);接受 list[str] 或逗号分隔 str
1214
+ extra = config.get("no_proxy_extra") or []
1215
+ if isinstance(extra, str):
1216
+ extra = [s.strip() for s in extra.split(",") if s.strip()]
1217
+ if extra:
1218
+ no_proxy = no_proxy + "," + ",".join(extra)
1207
1219
 
1208
1220
  for var in ("http_proxy", "HTTP_PROXY"):
1209
1221
  print(f"export {var}={proxy_http};")
@@ -1217,7 +1229,7 @@ def cmd_env(config: dict, unset: bool = False):
1217
1229
 
1218
1230
  # ── 帮助 ──────────────────────────────────────────────────────────────────────
1219
1231
 
1220
- VERSION = "0.1.3"
1232
+ VERSION = "0.1.5"
1221
1233
 
1222
1234
  def cmd_help(verbose: bool = False):
1223
1235
  """打印帮助信息
@@ -94,10 +94,17 @@ def _gather_engine(engine) -> dict:
94
94
  return {"pid": pid, "runs": runs, "daemon_up": daemon_up, "etime": etime}
95
95
 
96
96
 
97
- def _gather_ports(claude_proxy_label: str) -> dict:
98
- """采集端口监听状态。"""
99
- ports = [(p, desc, _port_listening(p))
100
- for p, desc in [(7890, "proxy"), (9090, "api")]]
97
+ def _gather_ports(claude_proxy_label: str,
98
+ port_list: list[tuple[int, str]] | None = None) -> dict:
99
+ """采集端口监听状态。
100
+
101
+ Args:
102
+ port_list: [(port, desc), ...],由调用方根据 config/api_base 解析后传入。
103
+ None 时回退到 [(7890,"proxy"), (9090,"api")] 以兼容旧调用。
104
+ """
105
+ if port_list is None:
106
+ port_list = [(7890, "proxy"), (9090, "api")]
107
+ ports = [(p, desc, _port_listening(p)) for p, desc in port_list]
101
108
  cp_running = False
102
109
  cp_pid = ""
103
110
  cp_port = False
@@ -475,10 +482,20 @@ def cmd_status(engine, api: str, api_secret: str,
475
482
  dns_lock_label = config.get("dns_lock_label", "com.proxyctl.dns-lock")
476
483
  claude_proxy_label = config.get("claude_proxy_label", "com.proxyctl.claude-proxy")
477
484
 
485
+ # 端口列表:proxy 取 config.proxy_port,api 从 api_base URL 解析
486
+ proxy_port = int(config.get("proxy_port", 7890))
487
+ api_port = 9090
488
+ try:
489
+ from urllib.parse import urlparse
490
+ api_port = urlparse(api).port or 9090
491
+ except Exception:
492
+ pass
493
+ port_list = [(proxy_port, "proxy"), (api_port, "api")]
494
+
478
495
  # 全部 section 并发采集,拿到一个打印一个
479
496
  with ThreadPoolExecutor(max_workers=8) as pool:
480
497
  f_engine = pool.submit(_gather_engine, engine)
481
- f_ports = pool.submit(_gather_ports, claude_proxy_label)
498
+ f_ports = pool.submit(_gather_ports, claude_proxy_label, port_list)
482
499
  f_tun = pool.submit(_gather_tun, engine, True)
483
500
  f_proxy = pool.submit(_gather_proxy_settings)
484
501
  f_dns = pool.submit(_gather_dns, dns_lock_label)
File without changes
File without changes
File without changes
File without changes
File without changes