proxyctl 0.5.4__tar.gz → 0.5.6__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.
Files changed (33) hide show
  1. {proxyctl-0.5.4 → proxyctl-0.5.6}/PKG-INFO +1 -1
  2. {proxyctl-0.5.4 → proxyctl-0.5.6}/man/proxyctl.1 +27 -0
  3. {proxyctl-0.5.4 → proxyctl-0.5.6}/pyproject.toml +1 -1
  4. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/_io.py +2 -1
  5. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/check.py +42 -17
  6. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/cli.py +34 -9
  7. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/completion.py +7 -6
  8. proxyctl-0.5.6/src/proxyctl/connections.py +908 -0
  9. proxyctl-0.5.6/src/proxyctl/connections_filters.py +317 -0
  10. proxyctl-0.5.6/src/proxyctl/connections_human.py +239 -0
  11. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/explain.py +59 -7
  12. proxyctl-0.5.6/src/proxyctl/traffic.py +984 -0
  13. proxyctl-0.5.6/src/proxyctl/traffic_store.py +257 -0
  14. {proxyctl-0.5.4 → proxyctl-0.5.6}/.gitignore +0 -0
  15. {proxyctl-0.5.4 → proxyctl-0.5.6}/LICENSE +0 -0
  16. {proxyctl-0.5.4 → proxyctl-0.5.6}/README.md +0 -0
  17. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/__init__.py +0 -0
  18. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/audit.py +0 -0
  19. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/autostart.py +0 -0
  20. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/builtin_plugins/__init__.py +0 -0
  21. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
  22. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
  23. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/core/__init__.py +0 -0
  24. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/core/plugin.py +0 -0
  25. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/engine/__init__.py +0 -0
  26. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/engine/base.py +0 -0
  27. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/engine/mihomo.py +0 -0
  28. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/engine/singbox.py +0 -0
  29. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/status.py +0 -0
  30. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/subscription.py +0 -0
  31. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/suggest.py +0 -0
  32. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/suggest_rules.py +0 -0
  33. {proxyctl-0.5.4 → proxyctl-0.5.6}/src/proxyctl/trace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyctl
3
- Version: 0.5.4
3
+ Version: 0.5.6
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
@@ -131,6 +131,33 @@ Linux systemctl --user)。
131
131
  4 阶段健康检查:basic / groups / connectivity / outbound-ip。
132
132
  \fB--plain\fR 输出 TSV(stage / ok / detail)。
133
133
  .TP
134
+ \fBproxyctl connections\fR [\fB--host\fR \fITEXT\fR ...] [\fB--chain\fR \fITEXT\fR ...] [\fB--route\fR \fIKIND\fR ...] [\fB--preset\fR \fINAME\fR ...] [\fB--agent\fR \fINAME\fR ...] [\fB--query\fR \fITEXT\fR ...] [\fB--app\fR \fINAME\fR ...] [\fB--all\fR] [\fB--json\fR]
135
+ 把本机 App/进程的 TCP 连接与 \fIproxy_port\fR、mihomo \fB/connections\fR
136
+ API 的 \fIsourcePort\fR 连接详情关联。默认不做 AI 工具过滤,输出全部可关联
137
+ 目的站点;\fB--host\fR 按 host/rulePayload/IP 过滤,\fB--chain\fR /
138
+ \fB--line\fR 按 Mihomo 链路节点过滤,\fB--route\fR 按 route_kind 过滤
139
+ (proxy/direct/reject/unknown,也接受 代理/直连/拒绝/未知),\fB--preset\fR
140
+ 使用预设(如 ai/openai/anthropic/coding),\fB--agent\fR 按工具过滤(如
141
+ codex/claude/chatgpt/openclaw),\fB--query\fR / \fB--filter\fR 做全文过滤。
142
+ \fB--app\fR 保留兼容,用于进程/App 过滤;\fB--all\fR 为兼容 no-op。
143
+ macOS 上还会用 \fBnetstat -anv\fR 反查被
144
+ Network/System Extension 接管后的 socket owner,并在 JSON 中输出
145
+ \fBproxy_owner_connections\fR 与 \fBproxy_owner_groups\fR。后者按
146
+ rulePayload / host / IP 聚合目的地,输出 app_contexts / candidate_contexts,
147
+ 并标出 mixed_chains / mixed_route_kind。
148
+ 只读;非 mihomo 后端或本地 API 不可用时降级输出 unmatched。
149
+ .TP
150
+ \fBproxyctl traffic\fR [snapshot|sample|watch|report] [\fB--by\fR \fIline,app\fR] [\fB--interval\fR \fISEC\fR] [\fB--count\fR \fIN\fR] [\fB--since\fR \fI1h\fR] [\fB--store\fR \fIDIR\fR] [\fB--host\fR \fITEXT\fR ...] [\fB--chain\fR \fITEXT\fR ...] [\fB--route\fR \fIKIND\fR ...] [\fB--preset\fR \fINAME\fR ...] [\fB--agent\fR \fINAME\fR ...] [\fB--json\fR]
151
+ 按当前活跃 Mihomo \fB/connections\fR 的 upload/download 统计流量快照。
152
+ 默认 \fB--by line\fR 按首个链路节点统计线路;也可用
153
+ \fB--by line,app\fR 进一步按软件归因。软件归因分可信度:
154
+ 可见进程为 process/socket-owner,被 Network/System Extension 遮蔽时按目的域名
155
+ 给出 inferred 候选。\fBsample\fR 记录一次连接计数器增量,
156
+ \fBwatch\fR 按间隔重复记录,\fBreport\fR 读取已记录增量并按
157
+ \fB--since\fR 汇总。人类输出模式下 \fBwatch\fR 每次采样都会实时打印一行进度;
158
+ \fB--json\fR 保持最终单个 envelope。首次看到的连接只建立基线,不回填此前已经
159
+ 产生的字节;未运行 sample/watch 的时间段无法补算。
160
+ .TP
134
161
  \fBproxyctl trace\fR \fIDOMAIN\fR [\fB--json\fR]
135
162
  4 阶段域名链路诊断:DNS 解析 / 规则匹配预测 / 连通性 / 实际连接。
136
163
  .TP
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxyctl"
3
- version = "0.5.4"
3
+ version = "0.5.6"
4
4
  description = "Proxy configuration lifecycle management for macOS and Linux"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -72,6 +72,7 @@ _COLOR_NAMES = ("RED", "GREEN", "YELLOW", "CYAN", "BOLD", "DIM", "NC")
72
72
  _COLOR_MODULES_KNOWN = (
73
73
  "proxyctl.cli", "proxyctl.status", "proxyctl.check",
74
74
  "proxyctl.trace", "proxyctl.audit", "proxyctl.explain",
75
+ "proxyctl.connections", "proxyctl.connections_human", "proxyctl.traffic",
75
76
  )
76
77
 
77
78
 
@@ -398,7 +399,7 @@ def lock_paths(lock_dir: str | None = None) -> dict[str, str]:
398
399
  home = os.path.expanduser("~")
399
400
  base = lock_dir or os.path.join(home, ".config", "proxyctl")
400
401
  # 已知锁名(与 cli._exec_with_lock 一致)
401
- names = ["system", "config", "daemon"]
402
+ names = ["system", "config", "daemon", "traffic"]
402
403
  return {n: os.path.join(base, f".lock.{n}") for n in names}
403
404
 
404
405
 
@@ -251,17 +251,9 @@ def _proxy_groups_section(api_base: str, api_secret: str,
251
251
  print(f" {CYAN}{gname}{NC}({gtype}) → {BOLD}{gnow}{NC} "
252
252
  f"{ds(gnow_d)}ms [{count_str} of {total}]{tested_str}")
253
253
 
254
- # v0.5.4:selector/fallback 类型 + now 指向子组时,
255
- # 跳过顶层成员列表(避免跟子组节点重复),只展开 now 那一个子组。
256
- # now 指向真节点时仍打印顶层成员(用户可能想看其他可选项)。
257
- sub = (proxies.get(gnow) if g.get("type") in ("Selector", "Fallback")
258
- else None)
259
- now_is_subgroup = bool(sub and sub.get("all"))
260
- if g.get("type") in ("Selector", "Fallback") and not now_is_subgroup:
261
- print_members(gmembers, gnow)
262
- elif g.get("type") not in ("Selector", "Fallback"):
263
- print_members(gmembers, gnow)
264
- if now_is_subgroup:
254
+ def print_sub_summary(sub_name: str, sub: dict, is_now: bool) -> bool:
255
+ """打印一行子组 summary(含 now 标记 + 健康度 + tested ago)。
256
+ 返回该子组之前是否已展开过叶子。"""
265
257
  sub_type = "url" if sub.get("type") == "URLTest" else "sel"
266
258
  sub_now = sub.get("now", "?")
267
259
  sub_now_d = get_delay(sub_now)
@@ -275,13 +267,46 @@ def _proxy_groups_section(api_base: str, api_secret: str,
275
267
  if s_nodata: sc.append(f"{YELLOW}{s_nodata}—{NC}")
276
268
  st = group_tested_ago(sub_members)
277
269
  st_str = f" {DIM}{st}{NC}" if st else ""
278
- already = gnow in expanded_subgroups
270
+ already = sub_name in expanded_subgroups
279
271
  tail = f" {DIM}(详见上方){NC}" if already else ""
280
- print(f" {CYAN}{gnow}{NC}({sub_type}) {BOLD}{sub_now}{NC} "
281
- f"{ds(sub_now_d)}ms [{'/'.join(sc)} of {len(sub_members)}]{st_str}{tail}")
282
- if not already:
283
- print_members(sub_members, sub_now)
284
- expanded_subgroups.add(gnow)
272
+ marker = "→" if is_now else " "
273
+ print(f" {marker}{CYAN}{sub_name}{NC}({sub_type}) → "
274
+ f"{BOLD}{sub_now}{NC} "
275
+ f"{ds(sub_now_d)}ms [{'/'.join(sc)} of {len(sub_members)}]"
276
+ f"{st_str}{tail}")
277
+ return already
278
+
279
+ # v0.5.5:selector/fallback 含子组时,每个子组都打一行 summary(让
280
+ # 用户看到兄弟子组存在 + 健康度),但只对 now 子组展开叶子(保留
281
+ # v0.5.3 精简意图)。混在子组里的真节点(如 local-N)按 4 列表格
282
+ # flush。selector/fallback 全为真节点 / 其他类型组:原 print_members
283
+ # 行为不变。
284
+ if g.get("type") in ("Selector", "Fallback"):
285
+ has_subgroup = any(proxies.get(m, {}).get("all") for m in gmembers)
286
+ if not has_subgroup:
287
+ print_members(gmembers, gnow)
288
+ else:
289
+ leaf_buf: list = []
290
+
291
+ def flush_leaves():
292
+ if leaf_buf:
293
+ print_members(leaf_buf, gnow)
294
+ leaf_buf.clear()
295
+
296
+ for m in gmembers:
297
+ sub = proxies.get(m, {})
298
+ if sub.get("all"):
299
+ flush_leaves()
300
+ already = print_sub_summary(m, sub, is_now=(m == gnow))
301
+ if m == gnow and not already:
302
+ print_members(sub.get("all", []),
303
+ sub.get("now", "?"))
304
+ expanded_subgroups.add(m)
305
+ else:
306
+ leaf_buf.append(m)
307
+ flush_leaves()
308
+ else:
309
+ print_members(gmembers, gnow)
285
310
  # 顶层组本身展开完后也标记,避免被下层组重新展开
286
311
  expanded_subgroups.add(gname)
287
312
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  支持 Mihomo 后端(首发)/ Sing-box 后端(预留)
4
4
 
5
- 用法:proxyctl [start|stop|restart|status|log|check|fix|recover|
6
- mode|audit|trace|bench|dns-lock|dns-unlock]
5
+ 用法:proxyctl [start|stop|restart|status|log|check|connections|traffic|
6
+ fix|recover|mode|audit|trace|bench|dns-lock|dns-unlock]
7
7
  """
8
8
 
9
9
  import functools
@@ -70,8 +70,11 @@ def load_config() -> dict:
70
70
  user_cfg = yaml.safe_load(f) or {}
71
71
  cfg.update(user_cfg)
72
72
  except Exception as e:
73
- print(f"{YELLOW}警告:读取配置文件失败:{e}{NC}")
74
- print(f" 使用默认配置,可能需要在 {CONFIG_FILE} 中配置 api_secret")
73
+ print(f"{YELLOW}警告:读取配置文件失败:{e}{NC}", file=sys.stderr)
74
+ print(
75
+ f" 使用默认配置,可能需要在 {CONFIG_FILE} 中配置 api_secret",
76
+ file=sys.stderr,
77
+ )
75
78
  return cfg
76
79
 
77
80
 
@@ -1501,8 +1504,17 @@ def cmd_help():
1501
1504
  f"{DIM}一键 JSON + 关色 + 非交互{NC}")
1502
1505
  print()
1503
1506
 
1507
+ print(f"{BOLD}常用示例{NC}")
1508
+ print(f" {CYAN}proxyctl doctor --json{NC} "
1509
+ f"{DIM}最快健康基线,agent 首选{NC}")
1510
+ print(f" {CYAN}proxyctl mode tun -n --json{NC} "
1511
+ f"{DIM}预览写操作 plan,不执行{NC}")
1512
+ print(f" {CYAN}proxyctl help trace{NC} "
1513
+ f"{DIM}查看单命令用法和示例{NC}")
1514
+ print()
1515
+
1504
1516
  print(f"{BOLD}用法{NC} proxyctl <command> [args] "
1505
- f"[--json|--plain] [--dry-run] [--no-color] [--quiet]")
1517
+ f"[--json|--plain] [--dry-run|-n] [--no-color] [--quiet]")
1506
1518
  print()
1507
1519
 
1508
1520
  # 按 group 元数据驱动渲染
@@ -1533,7 +1545,7 @@ def cmd_help():
1533
1545
  print(f"{BOLD}全局 flag{NC}")
1534
1546
  print(f" --json 输出 envelope JSON(schema v2)")
1535
1547
  print(f" --plain 输出纯 TSV(audit/check 等支持表格的命令)")
1536
- print(f" --dry-run 预演写操作(输出 plan,不真正执行)")
1548
+ print(f" --dry-run/-n 预演写操作(输出 plan,不真正执行)")
1537
1549
  print(f" --no-color 关闭 ANSI(也读 NO_COLOR)")
1538
1550
  print(f" --quiet/-q 压制非关键 stderr")
1539
1551
  print(f" --help/-h 本帮助")
@@ -1647,6 +1659,7 @@ def cmd_version_print() -> None:
1647
1659
  "lock_path_in_error": True,
1648
1660
  "side_effects_enum": True,
1649
1661
  "dry_run": True,
1662
+ "dry_run_short_flag": True,
1650
1663
  "plain": True,
1651
1664
  "flag_position_invariant": True,
1652
1665
  "agents_md": True,
@@ -1667,6 +1680,8 @@ def cmd_version_print() -> None:
1667
1680
  "doctor_since_filter": True, # 0.5.0: --since <version> 屏蔽未来规则
1668
1681
  "suggestions_ignore_file": True, # 0.5.0: ~/.config/proxyctl/suggestions.ignore
1669
1682
  "proxy_group_dead_check": True, # 0.5.0: proxy_group.mostly_dead 规则
1683
+ "traffic_snapshot": True, # 0.5.6+: active /connections traffic grouping
1684
+ "traffic_history": True, # 0.5.6+: sample/watch/report recorded deltas
1670
1685
  },
1671
1686
  }
1672
1687
  _io.emit_json(_io.envelope("version", data=data))
@@ -1751,7 +1766,7 @@ def _extract_global_flags(argv: list) -> tuple:
1751
1766
  """从 argv 中剥离全局 flag,返回 (剩余 argv, flag dict)。
1752
1767
 
1753
1768
  clig.dev 原则:flag 位置无关 — 在任意位置出现的 --json / --no-color /
1754
- --quiet / -q / --dry-run / --plain 都会被识别并剥离,剩余位置参数顺序保持不变。
1769
+ --quiet / -q / --dry-run / -n / --plain 都会被识别并剥离,剩余位置参数顺序保持不变。
1755
1770
  """
1756
1771
  flags = {
1757
1772
  "json": False, "no_color": False, "quiet": False,
@@ -1765,7 +1780,7 @@ def _extract_global_flags(argv: list) -> tuple:
1765
1780
  flags["no_color"] = True
1766
1781
  elif a in ("--quiet", "-q"):
1767
1782
  flags["quiet"] = True
1768
- elif a == "--dry-run":
1783
+ elif a in ("--dry-run", "-n"):
1769
1784
  flags["dry_run"] = True
1770
1785
  elif a == "--plain":
1771
1786
  flags["plain"] = True
@@ -2732,6 +2747,14 @@ def _h_check(ctx):
2732
2747
  cmd_check(ctx["backend"], ctx["api_base"], ctx["api_secret"],
2733
2748
  ctx["config"], mode_str, registry=ctx["registry"])
2734
2749
 
2750
+ def _h_connections(ctx):
2751
+ from proxyctl.connections import cmd_connections
2752
+ cmd_connections(ctx["args"], ctx["backend"], ctx["config"])
2753
+
2754
+ def _h_traffic(ctx):
2755
+ from proxyctl.traffic import cmd_traffic
2756
+ cmd_traffic(ctx["args"], ctx["backend"], ctx["config"])
2757
+
2735
2758
  def _h_bench(ctx):
2736
2759
  from proxyctl.check import cmd_bench
2737
2760
  bench_groups = ctx["args"] or None
@@ -2898,6 +2921,8 @@ DISPATCH: dict = {
2898
2921
  "status": _h_status,
2899
2922
  "log": _h_log,
2900
2923
  "check": _h_check,
2924
+ "connections": _h_connections,
2925
+ "traffic": _h_traffic,
2901
2926
  "bench": _h_bench,
2902
2927
  "fix": _h_fix,
2903
2928
  "recover": _h_recover,
@@ -2965,4 +2990,4 @@ def _suggest_command_and_exit(unknown: str) -> None:
2965
2990
 
2966
2991
 
2967
2992
  if __name__ == "__main__":
2968
- main()
2993
+ main()
@@ -4,7 +4,7 @@
4
4
  0.3.x 起补全:
5
5
 
6
6
  - 全局 flag: --help / --version / --json / --no-color / --quiet
7
- / --dry-run / --plain
7
+ / --dry-run / -n / --plain
8
8
  - explain <topic> → topic 列表
9
9
  - mode <tun|proxy> → 模式
10
10
  - engine <mihomo|singbox> → 引擎
@@ -66,7 +66,7 @@ _proxyctl_complete() {{
66
66
 
67
67
  # 第二个 token = 子命令
68
68
  if [ ${{COMP_CWORD}} -eq 1 ]; then
69
- COMPREPLY=( $(compgen -W "${{cmds}} help --help --version --json --plain --dry-run --no-color --quiet" -- "${{cur}}") )
69
+ COMPREPLY=( $(compgen -W "${{cmds}} help --help --version --json --plain --dry-run -n --no-color --quiet" -- "${{cur}}") )
70
70
  return 0
71
71
  fi
72
72
 
@@ -133,7 +133,7 @@ _proxyctl_complete() {{
133
133
  local dry_run_cmds=({dry_run_cmds})
134
134
  for c in "${{dry_run_cmds[@]}}"; do
135
135
  if [ "$first" = "$c" ]; then
136
- COMPREPLY=( $(compgen -W "--dry-run --json --no-color --quiet --help" -- "${{cur}}") )
136
+ COMPREPLY=( $(compgen -W "--dry-run -n --json --no-color --quiet --help" -- "${{cur}}") )
137
137
  return 0
138
138
  fi
139
139
  done
@@ -142,7 +142,7 @@ _proxyctl_complete() {{
142
142
  local plain_cmds=({plain_cmds})
143
143
  for c in "${{plain_cmds[@]}}"; do
144
144
  if [ "$first" = "$c" ]; then
145
- COMPREPLY=( $(compgen -W "--plain --json --dry-run --help" -- "${{cur}}") )
145
+ COMPREPLY=( $(compgen -W "--plain --json --dry-run -n --help" -- "${{cur}}") )
146
146
  return 0
147
147
  fi
148
148
  done
@@ -160,7 +160,7 @@ _proxyctl_complete() {{
160
160
  fi
161
161
 
162
162
  # 默认 flag 补全
163
- COMPREPLY=( $(compgen -W "--help --json --plain --dry-run --no-color --quiet" -- "${{cur}}") )
163
+ COMPREPLY=( $(compgen -W "--help --json --plain --dry-run -n --no-color --quiet" -- "${{cur}}") )
164
164
  }}
165
165
  complete -F _proxyctl_complete proxyctl
166
166
  """
@@ -204,6 +204,7 @@ _proxyctl() {{
204
204
  '--json[结构化 envelope v2 输出]' \\
205
205
  '--plain[纯 TSV 输出(audit/check);与 --json 互斥]' \\
206
206
  '--dry-run[预演写命令;输出 data.plan]' \\
207
+ '-n[预演写命令;输出 data.plan]' \\
207
208
  '--no-color[关闭 ANSI 颜色]' \\
208
209
  '--quiet[安静模式]' \\
209
210
  '-q[安静模式]'
@@ -258,7 +259,7 @@ def _gen_fish() -> str:
258
259
  "complete -c proxyctl -l version -d '显示版本'",
259
260
  "complete -c proxyctl -l json -d 'envelope v2 输出'",
260
261
  "complete -c proxyctl -l plain -d 'TSV 输出(audit/check)'",
261
- "complete -c proxyctl -l dry-run -d '预演写命令'",
262
+ "complete -c proxyctl -s n -l dry-run -d '预演写命令'",
262
263
  "complete -c proxyctl -l no-color -d '关闭 ANSI'",
263
264
  "complete -c proxyctl -l quiet -d '安静模式'",
264
265
  ""]