proxyctl 0.4.1__tar.gz → 0.4.3__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.
- {proxyctl-0.4.1 → proxyctl-0.4.3}/PKG-INFO +9 -2
- {proxyctl-0.4.1 → proxyctl-0.4.3}/README.md +8 -1
- {proxyctl-0.4.1 → proxyctl-0.4.3}/man/proxyctl.1 +17 -5
- {proxyctl-0.4.1 → proxyctl-0.4.3}/pyproject.toml +1 -1
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/check.py +50 -2
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/cli.py +262 -5
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/completion.py +4 -2
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/explain.py +18 -5
- {proxyctl-0.4.1 → proxyctl-0.4.3}/.gitignore +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/LICENSE +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/_io.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/status.py +0 -0
- {proxyctl-0.4.1 → proxyctl-0.4.3}/src/proxyctl/trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxyctl
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -64,11 +64,18 @@ PROXYCTL_AGENT=1 proxyctl <cmd> # 一键 --json + 关色 + 非交互
|
|
|
64
64
|
- envelope schema v2:`schema_version / cmd / ok / data / error / code / hints[] / warnings[] / doc / meta{{ts,elapsed_ms,proxyctl_version,request_id}}`
|
|
65
65
|
- 退出码分语义:`0 OK / 2 USAGE / 3 NOT_FOUND / 4 PERMISSION / 5 ENGINE_DOWN / 6 CONFIG_ERR / 7 NETWORK_ERR / 8 LOCKED / 9 TIMEOUT / 10 DEPENDENCY_MISSING`
|
|
66
66
|
- 写命令支持 `--dry-run` 输出结构化 plan(`data.plan = [PlanStep, ...]`);自 0.4.0 起
|
|
67
|
-
`plan.target` 全部真实化,`action=="subprocess"` 的 target.split() 可直接当 argv
|
|
67
|
+
`plan.target` 全部真实化,`action=="subprocess"` 的 target.split() 可直接当 argv 复读;
|
|
68
|
+
自 0.4.2 起 `start / stop / restart / restart-clean / recover` 也加入 `--dry-run` 行列
|
|
69
|
+
(之前是文档承诺但实际真跑,已修复):
|
|
68
70
|
```bash
|
|
69
71
|
proxyctl dns-unlock --dry-run --json | jq -r '.data.plan[] | select(.action=="subprocess").target'
|
|
70
72
|
# → launchctl bootout system/com.proxyctl.dns-lock
|
|
71
73
|
# → rm -f /Library/LaunchDaemons/com.proxyctl.dns-lock.plist
|
|
74
|
+
|
|
75
|
+
proxyctl stop --dry-run --json | jq -r '.data.plan[] | select(.action=="subprocess").target'
|
|
76
|
+
# macOS → launchctl bootout system/com.proxyctl.dns-lock
|
|
77
|
+
# → launchctl bootout system/com.mihomo.tun
|
|
78
|
+
# Linux → systemctl --user stop mihomo.service
|
|
72
79
|
```
|
|
73
80
|
PlanStep.action 枚举:`subprocess / system_op / fs_write / fs_copy / fs_write_atomic / fs_remove / edit_yaml / scan_log / http_put`。
|
|
74
81
|
CI 层 contract test(`tests/integration/test_plan_exec_contract.py`)保证 plan ↔ exec 永不漂移。
|
|
@@ -35,11 +35,18 @@ PROXYCTL_AGENT=1 proxyctl <cmd> # 一键 --json + 关色 + 非交互
|
|
|
35
35
|
- envelope schema v2:`schema_version / cmd / ok / data / error / code / hints[] / warnings[] / doc / meta{{ts,elapsed_ms,proxyctl_version,request_id}}`
|
|
36
36
|
- 退出码分语义:`0 OK / 2 USAGE / 3 NOT_FOUND / 4 PERMISSION / 5 ENGINE_DOWN / 6 CONFIG_ERR / 7 NETWORK_ERR / 8 LOCKED / 9 TIMEOUT / 10 DEPENDENCY_MISSING`
|
|
37
37
|
- 写命令支持 `--dry-run` 输出结构化 plan(`data.plan = [PlanStep, ...]`);自 0.4.0 起
|
|
38
|
-
`plan.target` 全部真实化,`action=="subprocess"` 的 target.split() 可直接当 argv
|
|
38
|
+
`plan.target` 全部真实化,`action=="subprocess"` 的 target.split() 可直接当 argv 复读;
|
|
39
|
+
自 0.4.2 起 `start / stop / restart / restart-clean / recover` 也加入 `--dry-run` 行列
|
|
40
|
+
(之前是文档承诺但实际真跑,已修复):
|
|
39
41
|
```bash
|
|
40
42
|
proxyctl dns-unlock --dry-run --json | jq -r '.data.plan[] | select(.action=="subprocess").target'
|
|
41
43
|
# → launchctl bootout system/com.proxyctl.dns-lock
|
|
42
44
|
# → rm -f /Library/LaunchDaemons/com.proxyctl.dns-lock.plist
|
|
45
|
+
|
|
46
|
+
proxyctl stop --dry-run --json | jq -r '.data.plan[] | select(.action=="subprocess").target'
|
|
47
|
+
# macOS → launchctl bootout system/com.proxyctl.dns-lock
|
|
48
|
+
# → launchctl bootout system/com.mihomo.tun
|
|
49
|
+
# Linux → systemctl --user stop mihomo.service
|
|
43
50
|
```
|
|
44
51
|
PlanStep.action 枚举:`subprocess / system_op / fs_write / fs_copy / fs_write_atomic / fs_remove / edit_yaml / scan_log / http_put`。
|
|
45
52
|
CI 层 contract test(`tests/integration/test_plan_exec_contract.py`)保证 plan ↔ exec 永不漂移。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH PROXYCTL 1 "2026-05" "proxyctl 0.4.
|
|
1
|
+
.TH PROXYCTL 1 "2026-05" "proxyctl 0.4.3" "User Commands"
|
|
2
2
|
.SH NAME
|
|
3
3
|
proxyctl \- Proxy configuration lifecycle management for macOS / Linux
|
|
4
4
|
.SH SYNOPSIS
|
|
@@ -33,14 +33,19 @@ audit / check 支持 \fB--plain\fR TSV 输出;
|
|
|
33
33
|
写命令预演:输出 \fIdata.plan = [PlanStep, ...]\fR(不真正执行)。
|
|
34
34
|
PlanStep 字段:step / action / target / reversible / requires_sudo /
|
|
35
35
|
side_effects / summary。
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
适用:\fBstart / stop / restart / restart-clean / recover\fR(自 0.4.2)/
|
|
37
|
+
mode / engine / fix / audit apply / config set / daemon /
|
|
38
|
+
claude-proxy / dns-lock / dns-unlock。
|
|
38
39
|
.IP
|
|
39
40
|
\fB自 0.4.0\fR:plan.target 全部真实化(无 \fI<...>\fR 占位符);
|
|
40
41
|
action=\fBsubprocess\fR 的 target.split() 可直接当 argv(run 时
|
|
41
42
|
prepend sudo)。action 枚举:subprocess / system_op / fs_write /
|
|
42
43
|
fs_copy / fs_write_atomic / fs_remove / edit_yaml / scan_log /
|
|
43
44
|
http_put。CI 层 contract test 保证 plan ↔ exec 永不漂移。
|
|
45
|
+
.IP
|
|
46
|
+
\fB自 0.4.2\fR:原本 dispatcher 未接卫语句、`--dry-run` 被静默吃掉真执行的
|
|
47
|
+
5 个写命令补齐 plan helper;lifecycle plan 按 \fIIS_MACOS\fR 平台分流,
|
|
48
|
+
macOS 列 launchctl + 系统联动,Linux 列 systemctl --user。
|
|
44
49
|
.TP
|
|
45
50
|
.BR --no-color
|
|
46
51
|
关闭 ANSI 颜色(默认按 isatty / NO_COLOR / TERM=dumb / PROXYCTL_NO_COLOR 自动判断)。
|
|
@@ -90,17 +95,24 @@ engine / mode / port / config_path / engine_config_path / lock_held / lock_path
|
|
|
90
95
|
.TP
|
|
91
96
|
\fBproxyctl completion\fR \fIbash\fR | \fIzsh\fR | \fIfish\fR
|
|
92
97
|
生成 shell 补全脚本。
|
|
98
|
+
.TP
|
|
99
|
+
.B proxyctl version [--json]
|
|
100
|
+
显示版本号 + supported_features。等价 \fB--version\fR flag(自 0.4.2 起也提供
|
|
101
|
+
子命令形式,方便 agent 通过 DISPATCH 探测)。
|
|
93
102
|
|
|
94
103
|
.SS 生命周期(写:sudo + process + system)
|
|
95
104
|
.TP
|
|
96
|
-
.B proxyctl start / stop / restart / restart-clean
|
|
105
|
+
.B proxyctl start / stop / restart / restart-clean [\fB--dry-run\fR]
|
|
97
106
|
启停代理引擎(写系统 DNS / 代理 / launchd)。restart-clean 额外清缓存。
|
|
107
|
+
全部支持 \fB--dry-run\fR(自 0.4.2);plan 按平台分流(macOS launchctl /
|
|
108
|
+
Linux systemctl --user)。
|
|
98
109
|
.TP
|
|
99
110
|
.B proxyctl fix [--dry-run]
|
|
100
111
|
修复 DNS / 代理 / 热重载(system + cache)。
|
|
101
112
|
.TP
|
|
102
|
-
.B proxyctl recover
|
|
113
|
+
.B proxyctl recover [\fB--dry-run\fR]
|
|
103
114
|
切网后软恢复:热重载 + 清 fakeip + 重测代理组(不重启进程)。
|
|
115
|
+
\fB--dry-run\fR 自 0.4.2 起列出三个 Clash API endpoint。
|
|
104
116
|
|
|
105
117
|
.SS 模式 / 引擎(写:sudo + config-write)
|
|
106
118
|
.TP
|
|
@@ -532,6 +532,52 @@ def _fetch_probe(probe, env_clean: dict, proxy_port: int = 7890) -> str:
|
|
|
532
532
|
return text
|
|
533
533
|
|
|
534
534
|
|
|
535
|
+
def _collect_fail_hints(collector: dict, *, dns_bad: bool, failed: bool) -> list[str]:
|
|
536
|
+
"""从 check 的 collector 提取每个失败 stage 的简短摘要。
|
|
537
|
+
|
|
538
|
+
设计目标:envelope.ok=False 时 agent 不需要挖 stages.*.ok 自行定位真凶。
|
|
539
|
+
本函数为 cmd_check 在 emit_json 之前调用,输出 hints 列表。
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
collector: cmd_check 沉淀的事实快照(见 cmd_check.collector)
|
|
543
|
+
dns_bad: basic.network.dns 不健康
|
|
544
|
+
failed: 整体是否任一 stage fail(fail 变量真值)
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
摘要 hint 行列表。整体 pass 时返回空列表。
|
|
548
|
+
"""
|
|
549
|
+
hints: list[str] = []
|
|
550
|
+
if not failed:
|
|
551
|
+
return hints
|
|
552
|
+
|
|
553
|
+
stages = collector.get("stages") or {}
|
|
554
|
+
basic = stages.get("basic") or {}
|
|
555
|
+
ports = basic.get("ports") or {}
|
|
556
|
+
fail_ports = ports.get("fail") or []
|
|
557
|
+
if fail_ports:
|
|
558
|
+
hints.append(f"missing ports: {' '.join(fail_ports)}")
|
|
559
|
+
|
|
560
|
+
if basic.get("daemon_up") is False:
|
|
561
|
+
hints.append("engine not running")
|
|
562
|
+
|
|
563
|
+
# connectivity: 列表形式,每项 {name, url, ok, message}
|
|
564
|
+
conn = stages.get("connectivity") or []
|
|
565
|
+
failed_conns = [c.get("name", "?") for c in conn
|
|
566
|
+
if isinstance(c, dict) and not c.get("ok")]
|
|
567
|
+
if failed_conns:
|
|
568
|
+
hints.append(f"connectivity failed: {','.join(failed_conns)}")
|
|
569
|
+
|
|
570
|
+
split = stages.get("split_routing")
|
|
571
|
+
if isinstance(split, dict) and split.get("ok") is False:
|
|
572
|
+
hints.append("split routing inactive (proxy == direct egress)")
|
|
573
|
+
|
|
574
|
+
if dns_bad:
|
|
575
|
+
# 保留旧行为:DNS 异常时引导 proxyctl fix
|
|
576
|
+
hints.append("DNS unhealthy — try `proxyctl fix`")
|
|
577
|
+
|
|
578
|
+
return hints
|
|
579
|
+
|
|
580
|
+
|
|
535
581
|
def cmd_check(engine, api: str, api_secret: str,
|
|
536
582
|
config: dict, mode_str: str = "", registry=None):
|
|
537
583
|
"""proxyctl check — 4 阶段全面健康检查。
|
|
@@ -923,11 +969,13 @@ def cmd_check(engine, api: str, api_secret: str,
|
|
|
923
969
|
if as_json:
|
|
924
970
|
_sys.stdout = _real_stdout
|
|
925
971
|
from proxyctl._io import emit_json, envelope, OK, GENERIC
|
|
926
|
-
|
|
972
|
+
# 失败时聚合每个失败 stage 的简短摘要到 hints。
|
|
973
|
+
# 不让 agent 在 envelope.ok=False 时还要挖 stages.*.ok 才定位真凶。
|
|
974
|
+
hints = _collect_fail_hints(collector, dns_bad=dns_bad, failed=fail)
|
|
927
975
|
emit_json(envelope("check", data=collector,
|
|
928
976
|
ok=(not fail),
|
|
929
977
|
code=OK if not fail else GENERIC,
|
|
930
|
-
|
|
978
|
+
hints=hints))
|
|
931
979
|
_sys.exit(0 if not fail else 1)
|
|
932
980
|
if as_plain:
|
|
933
981
|
_sys.stdout = _real_stdout
|
|
@@ -1581,6 +1581,8 @@ def cmd_version_print() -> None:
|
|
|
1581
1581
|
"doctor_healthy_field": True, # 0.3.3
|
|
1582
1582
|
"agent_guide_sections": True, # 0.3.3
|
|
1583
1583
|
"log_ndjson_v2": True,
|
|
1584
|
+
"lifecycle_dry_run": True, # 0.4.2: start/stop/restart/recover --dry-run
|
|
1585
|
+
"version_subcommand": True, # 0.4.2: `proxyctl version` 子命令
|
|
1584
1586
|
},
|
|
1585
1587
|
}
|
|
1586
1588
|
_io.emit_json(_io.envelope("version", data=data))
|
|
@@ -1814,6 +1816,50 @@ def _dns_unlock_subprocess_argvs(plist_path: str,
|
|
|
1814
1816
|
]
|
|
1815
1817
|
|
|
1816
1818
|
|
|
1819
|
+
def _service_start_argvs(backend: Backend) -> list[list[str]]:
|
|
1820
|
+
"""返回 service_start 的 argv list(不含 sudo),平台分流。
|
|
1821
|
+
|
|
1822
|
+
macOS:[cp <src> <dst>, launchctl bootstrap system <plist>]
|
|
1823
|
+
cp 是 conditional(plist 已存在时跳过),plan 作为上界总展示。
|
|
1824
|
+
Linux:[systemctl --user start <unit>]
|
|
1825
|
+
"""
|
|
1826
|
+
if IS_MACOS:
|
|
1827
|
+
src = os.path.join(DEFAULT_CONFIG_DIR, "launchdaemons",
|
|
1828
|
+
os.path.basename(backend.plist))
|
|
1829
|
+
return [
|
|
1830
|
+
["cp", src, backend.plist],
|
|
1831
|
+
["launchctl", "bootstrap", "system", backend.plist],
|
|
1832
|
+
]
|
|
1833
|
+
return [["systemctl", "--user", "start", backend.unit]]
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def _service_stop_argvs(backend: Backend) -> list[list[str]]:
|
|
1837
|
+
"""返回 service_stop 的 argv list(不含 sudo),平台分流。"""
|
|
1838
|
+
if IS_MACOS:
|
|
1839
|
+
return [["launchctl", "bootout", backend.label]]
|
|
1840
|
+
return [["systemctl", "--user", "stop", backend.unit]]
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
def _service_restart_argvs(backend: Backend) -> list[list[str]]:
|
|
1844
|
+
"""返回 service_restart 的 argv list(不含 sudo),平台分流。"""
|
|
1845
|
+
if IS_MACOS:
|
|
1846
|
+
return [["launchctl", "kickstart", "-k", backend.label]]
|
|
1847
|
+
return [["systemctl", "--user", "restart", backend.unit]]
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
def _recover_curl_endpoints(api_base: str) -> dict:
|
|
1851
|
+
"""返回 cmd_recover 三个 Clash API endpoint 的 URL。
|
|
1852
|
+
|
|
1853
|
+
每个 endpoint 进入 plan 时作为 http_put action 的 target;contract test
|
|
1854
|
+
用宽松断言(actual curl argv 中包含该 URL 子串)验证 plan ↔ exec 一致。
|
|
1855
|
+
"""
|
|
1856
|
+
return {
|
|
1857
|
+
"configs_reload": f"{api_base}/configs?force=true",
|
|
1858
|
+
"fakeip_flush": f"{api_base}/cache/fakeip/flush",
|
|
1859
|
+
"proxies": f"{api_base}/proxies",
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
|
|
1817
1863
|
def _plan_mode(backend, target: str) -> list[dict]:
|
|
1818
1864
|
"""mode tun / mode proxy 的 plan。
|
|
1819
1865
|
|
|
@@ -2048,6 +2094,192 @@ def _plan_dns_unlock(config: dict) -> list[dict]:
|
|
|
2048
2094
|
]
|
|
2049
2095
|
|
|
2050
2096
|
|
|
2097
|
+
# ── start / stop / restart / restart-clean / recover plan helpers ───────────
|
|
2098
|
+
#
|
|
2099
|
+
# 0.4.2 起,原本完全没接 _maybe_dry_run 的 5 个写命令补上 plan:
|
|
2100
|
+
# - 跨平台:plan 按 IS_MACOS 分流,macOS 列 launchctl + 系统联动;
|
|
2101
|
+
# Linux 列 systemctl --user
|
|
2102
|
+
# - actual ⊆ plan:DNS 注入 / proxy 激活等 iterating system_op 用描述性
|
|
2103
|
+
# target;conditional 步骤(首次 cp、dns-lock conditional bootstrap、
|
|
2104
|
+
# proxy_activate 仅 mode==proxy)作为上界总列出,contract test 用
|
|
2105
|
+
# silence_helpers + actual ⊆ plan 方向天然容纳。
|
|
2106
|
+
|
|
2107
|
+
def _plan_start(backend, config) -> list[dict]:
|
|
2108
|
+
"""start 的 plan:启动引擎 + macOS 系统联动(DNS/dns-lock/proxy)。"""
|
|
2109
|
+
argvs = _service_start_argvs(backend)
|
|
2110
|
+
if not IS_MACOS:
|
|
2111
|
+
return [
|
|
2112
|
+
{"action": "subprocess",
|
|
2113
|
+
"target": " ".join(argvs[0]),
|
|
2114
|
+
"summary": f"通过 systemctl --user 启动 {backend.unit}",
|
|
2115
|
+
"reversible": True, "requires_sudo": False,
|
|
2116
|
+
"side_effects": ["process"]},
|
|
2117
|
+
]
|
|
2118
|
+
dns_lock_label = config.get("dns_lock_label", DEFAULTS["dns_lock_label"])
|
|
2119
|
+
dns_lock_plist = f"/Library/LaunchDaemons/{dns_lock_label}.plist"
|
|
2120
|
+
proxy_port = config.get("proxy_port", DEFAULTS["proxy_port"])
|
|
2121
|
+
return [
|
|
2122
|
+
{"action": "subprocess",
|
|
2123
|
+
"target": " ".join(argvs[0]),
|
|
2124
|
+
"summary": f"如 {backend.plist} 不存在则从源拷贝 plist(首次安装)",
|
|
2125
|
+
"reversible": True, "requires_sudo": True,
|
|
2126
|
+
"side_effects": ["config-write"]},
|
|
2127
|
+
{"action": "subprocess",
|
|
2128
|
+
"target": " ".join(argvs[1]),
|
|
2129
|
+
"summary": f"启动引擎 {backend.name}",
|
|
2130
|
+
"reversible": True, "requires_sudo": True,
|
|
2131
|
+
"side_effects": ["process"]},
|
|
2132
|
+
{"action": "system_op",
|
|
2133
|
+
"target": "networksetup -setdnsservers (per service) 127.0.0.1 + "
|
|
2134
|
+
"scutil 三层注入 + dscacheutil -flushcache",
|
|
2135
|
+
"summary": "系统 DNS → 127.0.0.1(三层防线)",
|
|
2136
|
+
"reversible": True, "requires_sudo": True,
|
|
2137
|
+
"side_effects": ["system"]},
|
|
2138
|
+
{"action": "subprocess",
|
|
2139
|
+
"target": " ".join(["launchctl", "bootstrap", "system",
|
|
2140
|
+
dns_lock_plist]),
|
|
2141
|
+
"summary": f"启动 DNS 看门狗 daemon(如已部署但未运行)",
|
|
2142
|
+
"reversible": True, "requires_sudo": True,
|
|
2143
|
+
"side_effects": ["process"]},
|
|
2144
|
+
{"action": "system_op",
|
|
2145
|
+
"target": f"networksetup -setwebproxy* / -setsecurewebproxy* / "
|
|
2146
|
+
f"-setsocksfirewallproxy* (per service) 127.0.0.1:{proxy_port}",
|
|
2147
|
+
"summary": "开启系统代理(仅 mode=proxy 时;mode=tun 时跳过)",
|
|
2148
|
+
"reversible": True, "requires_sudo": True,
|
|
2149
|
+
"side_effects": ["system"]},
|
|
2150
|
+
]
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
def _plan_stop(backend, config) -> list[dict]:
|
|
2154
|
+
"""stop 的 plan:停止引擎 + macOS 还原系统配置。"""
|
|
2155
|
+
argvs = _service_stop_argvs(backend)
|
|
2156
|
+
if not IS_MACOS:
|
|
2157
|
+
return [
|
|
2158
|
+
{"action": "subprocess",
|
|
2159
|
+
"target": " ".join(argvs[0]),
|
|
2160
|
+
"summary": f"通过 systemctl --user 停止 {backend.unit}",
|
|
2161
|
+
"reversible": True, "requires_sudo": False,
|
|
2162
|
+
"side_effects": ["process"]},
|
|
2163
|
+
]
|
|
2164
|
+
dns_lock_label = config.get("dns_lock_label", DEFAULTS["dns_lock_label"])
|
|
2165
|
+
full_lock_label = f"system/{dns_lock_label}"
|
|
2166
|
+
return [
|
|
2167
|
+
{"action": "subprocess",
|
|
2168
|
+
"target": " ".join(["launchctl", "bootout", full_lock_label]),
|
|
2169
|
+
"summary": "停止 DNS 看门狗 daemon(如在跑)",
|
|
2170
|
+
"reversible": True, "requires_sudo": True,
|
|
2171
|
+
"side_effects": ["process"]},
|
|
2172
|
+
{"action": "system_op",
|
|
2173
|
+
"target": "networksetup -setdnsservers (per service) empty + "
|
|
2174
|
+
"scutil remove + dscacheutil -flushcache",
|
|
2175
|
+
"summary": "还原系统 DNS(清除三层注入)",
|
|
2176
|
+
"reversible": True, "requires_sudo": True,
|
|
2177
|
+
"side_effects": ["system"]},
|
|
2178
|
+
{"action": "system_op",
|
|
2179
|
+
"target": "networksetup -setwebproxystate / -setsecurewebproxystate / "
|
|
2180
|
+
"-setsocksfirewallproxystate (per service) off",
|
|
2181
|
+
"summary": "关闭系统代理",
|
|
2182
|
+
"reversible": True, "requires_sudo": True,
|
|
2183
|
+
"side_effects": ["system"]},
|
|
2184
|
+
{"action": "subprocess",
|
|
2185
|
+
"target": " ".join(argvs[0]),
|
|
2186
|
+
"summary": f"停止引擎 {backend.name}",
|
|
2187
|
+
"reversible": True, "requires_sudo": True,
|
|
2188
|
+
"side_effects": ["process"]},
|
|
2189
|
+
]
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
def _plan_restart(backend, config, *, clean: bool = False) -> list[dict]:
|
|
2193
|
+
"""restart / restart-clean 的 plan。
|
|
2194
|
+
|
|
2195
|
+
clean=True 时额外加 fs_remove backend.cache_file。
|
|
2196
|
+
"""
|
|
2197
|
+
steps: list[dict] = []
|
|
2198
|
+
if clean:
|
|
2199
|
+
steps.append({
|
|
2200
|
+
"action": "fs_remove",
|
|
2201
|
+
"target": backend.cache_file,
|
|
2202
|
+
"summary": f"删除引擎缓存 {backend.cache_file}",
|
|
2203
|
+
"reversible": False,
|
|
2204
|
+
"side_effects": ["cache"],
|
|
2205
|
+
})
|
|
2206
|
+
steps.append({
|
|
2207
|
+
"action": "fs_remove",
|
|
2208
|
+
"target": "/tmp/proxyctl-recover-* /tmp/sb-recover-* /tmp/sb-proxy-fail",
|
|
2209
|
+
"summary": "清空 watchdog 失败状态文件(避免误判触顶窗口)",
|
|
2210
|
+
"reversible": False,
|
|
2211
|
+
"side_effects": ["cache"],
|
|
2212
|
+
})
|
|
2213
|
+
argvs = _service_restart_argvs(backend)
|
|
2214
|
+
if not IS_MACOS:
|
|
2215
|
+
steps.append({
|
|
2216
|
+
"action": "subprocess",
|
|
2217
|
+
"target": " ".join(argvs[0]),
|
|
2218
|
+
"summary": f"通过 systemctl --user 重启 {backend.unit}",
|
|
2219
|
+
"reversible": True, "requires_sudo": False,
|
|
2220
|
+
"side_effects": ["process"],
|
|
2221
|
+
})
|
|
2222
|
+
return steps
|
|
2223
|
+
proxy_port = config.get("proxy_port", DEFAULTS["proxy_port"])
|
|
2224
|
+
dns_lock_label = config.get("dns_lock_label", DEFAULTS["dns_lock_label"])
|
|
2225
|
+
dns_lock_plist = f"/Library/LaunchDaemons/{dns_lock_label}.plist"
|
|
2226
|
+
steps.extend([
|
|
2227
|
+
{"action": "subprocess",
|
|
2228
|
+
"target": " ".join(argvs[0]),
|
|
2229
|
+
"summary": f"重启引擎 {backend.name}",
|
|
2230
|
+
"reversible": True, "requires_sudo": True,
|
|
2231
|
+
"side_effects": ["process"]},
|
|
2232
|
+
{"action": "system_op",
|
|
2233
|
+
"target": "networksetup -setdnsservers (per service) 127.0.0.1 + "
|
|
2234
|
+
"scutil 三层注入 + dscacheutil -flushcache",
|
|
2235
|
+
"summary": "系统 DNS → 127.0.0.1(刷新三层防线)",
|
|
2236
|
+
"reversible": True, "requires_sudo": True,
|
|
2237
|
+
"side_effects": ["system"]},
|
|
2238
|
+
{"action": "subprocess",
|
|
2239
|
+
"target": " ".join(["launchctl", "bootstrap", "system",
|
|
2240
|
+
dns_lock_plist]),
|
|
2241
|
+
"summary": "启动 DNS 看门狗 daemon(如已部署但未运行)",
|
|
2242
|
+
"reversible": True, "requires_sudo": True,
|
|
2243
|
+
"side_effects": ["process"]},
|
|
2244
|
+
{"action": "system_op",
|
|
2245
|
+
"target": f"networksetup -setwebproxy* / -setsecurewebproxy* / "
|
|
2246
|
+
f"-setsocksfirewallproxy* (per service) 127.0.0.1:{proxy_port}",
|
|
2247
|
+
"summary": "开启系统代理(mode=proxy 时)/ 关闭(mode=tun 时)",
|
|
2248
|
+
"reversible": True, "requires_sudo": True,
|
|
2249
|
+
"side_effects": ["system"]},
|
|
2250
|
+
])
|
|
2251
|
+
return steps
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
def _plan_recover(backend, config) -> list[dict]:
|
|
2255
|
+
"""recover 的 plan:3 个 Clash API 请求,不重启进程。
|
|
2256
|
+
|
|
2257
|
+
用 http_put action(与 _plan_fix 风格一致),实际 cmd_recover 还会对
|
|
2258
|
+
/group/<name>/delay endpoint 并发请求 N 次做 healthcheck —— plan 把
|
|
2259
|
+
healthcheck 整体折成一个 http_put step,target=描述性 endpoint pattern。
|
|
2260
|
+
"""
|
|
2261
|
+
api_base = config.get("api_base", DEFAULTS["api_base"])
|
|
2262
|
+
eps = _recover_curl_endpoints(api_base)
|
|
2263
|
+
return [
|
|
2264
|
+
{"action": "http_put",
|
|
2265
|
+
"target": eps["configs_reload"],
|
|
2266
|
+
"summary": "热重载配置(PUT /configs?force=true,清 DNS 缓存)",
|
|
2267
|
+
"reversible": True,
|
|
2268
|
+
"side_effects": ["network-io"]},
|
|
2269
|
+
{"action": "http_put",
|
|
2270
|
+
"target": eps["fakeip_flush"],
|
|
2271
|
+
"summary": "清空 fakeip 缓存(POST /cache/fakeip/flush)",
|
|
2272
|
+
"reversible": True,
|
|
2273
|
+
"side_effects": ["cache", "network-io"]},
|
|
2274
|
+
{"action": "http_put",
|
|
2275
|
+
"target": eps["proxies"],
|
|
2276
|
+
"summary": "拉取 proxies 列表 + 对 URLTest/Fallback/LoadBalance 组并发 "
|
|
2277
|
+
"GET /group/<name>/delay 触发 healthcheck",
|
|
2278
|
+
"reversible": True,
|
|
2279
|
+
"side_effects": ["network-io"]},
|
|
2280
|
+
]
|
|
2281
|
+
|
|
2282
|
+
|
|
2051
2283
|
# ── 主入口 ────────────────────────────────────────────────────────────────────
|
|
2052
2284
|
|
|
2053
2285
|
def main():
|
|
@@ -2138,12 +2370,31 @@ def main():
|
|
|
2138
2370
|
|
|
2139
2371
|
# ── Dispatch handlers + 路由表 ────────────────────────────────────────────
|
|
2140
2372
|
|
|
2141
|
-
def _h_start(ctx):
|
|
2142
|
-
|
|
2143
|
-
|
|
2373
|
+
def _h_start(ctx):
|
|
2374
|
+
_maybe_dry_run("start", lambda: _plan_start(ctx["backend"], ctx["config"]))
|
|
2375
|
+
cmd_start(ctx["backend"], ctx["config"], registry=ctx["registry"])
|
|
2376
|
+
|
|
2377
|
+
def _h_stop(ctx):
|
|
2378
|
+
_maybe_dry_run("stop", lambda: _plan_stop(ctx["backend"], ctx["config"]))
|
|
2379
|
+
cmd_stop(ctx["backend"], ctx["config"], registry=ctx["registry"])
|
|
2380
|
+
|
|
2381
|
+
def _h_restart(ctx):
|
|
2382
|
+
_maybe_dry_run(
|
|
2383
|
+
"restart",
|
|
2384
|
+
lambda: _plan_restart(ctx["backend"], ctx["config"], clean=False))
|
|
2385
|
+
cmd_restart(ctx["backend"], ctx["config"], registry=ctx["registry"])
|
|
2386
|
+
|
|
2144
2387
|
def _h_restart_clean(ctx):
|
|
2145
|
-
|
|
2146
|
-
|
|
2388
|
+
_maybe_dry_run(
|
|
2389
|
+
"restart-clean",
|
|
2390
|
+
lambda: _plan_restart(ctx["backend"], ctx["config"], clean=True))
|
|
2391
|
+
cmd_restart(ctx["backend"], ctx["config"], clean=True,
|
|
2392
|
+
registry=ctx["registry"])
|
|
2393
|
+
|
|
2394
|
+
def _h_recover(ctx):
|
|
2395
|
+
_maybe_dry_run("recover",
|
|
2396
|
+
lambda: _plan_recover(ctx["backend"], ctx["config"]))
|
|
2397
|
+
cmd_recover(ctx["backend"], ctx["config"])
|
|
2147
2398
|
def _h_dns_unlock(ctx):
|
|
2148
2399
|
_maybe_dry_run("dns-unlock", lambda: _plan_dns_unlock(ctx["config"]))
|
|
2149
2400
|
_exec_with_lock("daemon", "dns-unlock", cmd_dns_unlock, ctx["config"])
|
|
@@ -2317,6 +2568,11 @@ def _h_help(ctx):
|
|
|
2317
2568
|
cmd_help()
|
|
2318
2569
|
|
|
2319
2570
|
|
|
2571
|
+
def _h_version(ctx):
|
|
2572
|
+
"""proxyctl version — 子命令形式,与 --version flag 输出一致。"""
|
|
2573
|
+
cmd_version_print()
|
|
2574
|
+
|
|
2575
|
+
|
|
2320
2576
|
DISPATCH: dict = {
|
|
2321
2577
|
"start": _h_start,
|
|
2322
2578
|
"stop": _h_stop,
|
|
@@ -2346,6 +2602,7 @@ DISPATCH: dict = {
|
|
|
2346
2602
|
"doctor": _h_doctor,
|
|
2347
2603
|
"completion": _h_completion,
|
|
2348
2604
|
"help": _h_help,
|
|
2605
|
+
"version": _h_version,
|
|
2349
2606
|
}
|
|
2350
2607
|
|
|
2351
2608
|
|
|
@@ -26,8 +26,10 @@ from __future__ import annotations
|
|
|
26
26
|
import sys
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
|
|
29
|
+
# 写命令支持 --dry-run(与 COMMANDS_META.supports_dry_run 同步)
|
|
30
|
+
# 0.4.2 起,lifecycle 4 个 + recover 也补齐 --dry-run。
|
|
31
|
+
_DRY_RUN_CMDS = ("start", "stop", "restart", "restart-clean", "recover",
|
|
32
|
+
"mode", "engine", "fix", "audit", "config",
|
|
31
33
|
"daemon", "claude-proxy", "dns-lock", "dns-unlock")
|
|
32
34
|
# 0.3.x:audit / check 支持 --plain
|
|
33
35
|
_PLAIN_CMDS = ("audit", "check")
|
|
@@ -858,23 +858,30 @@ COMMANDS_META: list[dict] = [
|
|
|
858
858
|
{"name": "start", "group": "lifecycle", "summary": "启动引擎 + 注入 DNS/代理",
|
|
859
859
|
"args": [], "supports_json": False,
|
|
860
860
|
"side_effects": ["process", "system"],
|
|
861
|
+
"supports_dry_run": True,
|
|
861
862
|
"needs_sudo": True, "interactive": False,
|
|
862
|
-
"exit_codes": [0, 1, 5],
|
|
863
|
+
"exit_codes": [0, 1, 5],
|
|
864
|
+
"examples": ["proxyctl start", "proxyctl start --dry-run"]},
|
|
863
865
|
{"name": "stop", "group": "lifecycle", "summary": "停止引擎 + 还原系统配置",
|
|
864
866
|
"args": [], "supports_json": False,
|
|
865
867
|
"side_effects": ["process", "system"],
|
|
868
|
+
"supports_dry_run": True,
|
|
866
869
|
"needs_sudo": True, "interactive": False,
|
|
867
|
-
"exit_codes": [0, 1],
|
|
870
|
+
"exit_codes": [0, 1],
|
|
871
|
+
"examples": ["proxyctl stop", "proxyctl stop --dry-run"]},
|
|
868
872
|
{"name": "restart", "group": "lifecycle", "summary": "重启引擎",
|
|
869
873
|
"args": [], "supports_json": False,
|
|
870
874
|
"side_effects": ["process", "system"],
|
|
875
|
+
"supports_dry_run": True,
|
|
871
876
|
"needs_sudo": True, "interactive": False, "exit_codes": [0, 1],
|
|
872
|
-
"examples": ["proxyctl restart"]},
|
|
877
|
+
"examples": ["proxyctl restart", "proxyctl restart --dry-run"]},
|
|
873
878
|
{"name": "restart-clean", "group": "lifecycle", "summary": "重启并清除缓存",
|
|
874
879
|
"args": [], "supports_json": False,
|
|
875
880
|
"side_effects": ["process", "system", "cache"],
|
|
881
|
+
"supports_dry_run": True,
|
|
876
882
|
"needs_sudo": True, "interactive": False, "exit_codes": [0, 1],
|
|
877
|
-
"examples": ["proxyctl restart-clean"
|
|
883
|
+
"examples": ["proxyctl restart-clean",
|
|
884
|
+
"proxyctl restart-clean --dry-run"]},
|
|
878
885
|
# diagnostic
|
|
879
886
|
{"name": "status", "group": "diagnostic", "summary": "系统状态面板",
|
|
880
887
|
"args": [], "supports_json": True, "side_effects": [],
|
|
@@ -935,8 +942,9 @@ COMMANDS_META: list[dict] = [
|
|
|
935
942
|
{"name": "recover", "group": "maintenance",
|
|
936
943
|
"summary": "切网后软恢复(清 DNS 缓存 + 重测代理组,不重启)",
|
|
937
944
|
"args": [], "supports_json": False, "side_effects": ["cache", "network-io"],
|
|
945
|
+
"supports_dry_run": True,
|
|
938
946
|
"needs_sudo": False, "interactive": False, "exit_codes": [0, 1, 2, 5, 7],
|
|
939
|
-
"examples": ["proxyctl recover"]},
|
|
947
|
+
"examples": ["proxyctl recover", "proxyctl recover --dry-run"]},
|
|
940
948
|
# daemon / dns-lock
|
|
941
949
|
{"name": "daemon", "group": "daemon",
|
|
942
950
|
"summary": "管理 extra_daemons(如 claude-proxy)",
|
|
@@ -1050,6 +1058,11 @@ COMMANDS_META: list[dict] = [
|
|
|
1050
1058
|
"needs_sudo": False, "interactive": False, "exit_codes": [0, 2],
|
|
1051
1059
|
"examples": ["proxyctl help", "proxyctl help mode",
|
|
1052
1060
|
"proxyctl mode --help"]},
|
|
1061
|
+
{"name": "version", "group": "agent",
|
|
1062
|
+
"summary": "版本号 + supported_features(等价 --version;--json 输出 envelope)",
|
|
1063
|
+
"args": [], "supports_json": True, "side_effects": [],
|
|
1064
|
+
"needs_sudo": False, "interactive": False, "exit_codes": [0],
|
|
1065
|
+
"examples": ["proxyctl version", "proxyctl version --json"]},
|
|
1053
1066
|
]
|
|
1054
1067
|
|
|
1055
1068
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|