joy-cli/scripts/manhuang/clashctl
feibisi 50f5089459 feat(manhuang): scout 号池 health 同步 + e2e --call 接 endpoints
sync-codex-health 拉 health/alive/endpoints(key 文件 600);
e2e --from-endpoints 真调中转;余额不足单独 break;refresh 不覆盖 call 结果。
2026-07-13 09:51:37 +08:00

243 lines
7.7 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# clashctl — 本机 ClashX Meta / Mihomo 控制面薄封装
#
# 通用 API 命令 → vendored clash-api.py
# (upstream: Hsiifu3/clash-verge-skill, MIT)
# 神州/蛮荒缺口ensure-direct · probe · regression · 系统代理铁律
#
# 约定2026-07-13
# - 系统代理必须 ON否则上不了外网
# - 隔离靠 mode=rule + 生产 DIRECT不靠关系统代理
#
# 用法:
# bash scripts/manhuang/clashctl status
# bash scripts/manhuang/clashctl mode rule
# bash scripts/manhuang/clashctl ensure-direct
# bash scripts/manhuang/clashctl probe --deep
# bash scripts/manhuang/clashctl regression
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
API_PY="${ROOT}/scripts/manhuang/clash-api.py"
PROBE_PY="${ROOT}/scripts/manhuang/probe-proxy-path.py"
REGRESS="${ROOT}/scripts/manhuang/check-net-path-regression.sh"
SNIPPET="${ROOT}/configs/manhuang/clash-rules-shenzhou-direct.snippet.yaml"
ENHANCED_CFG="${HOME}/.config/clash.meta/config-shenzhou-direct.yaml"
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
export CLASH_API="${CLASH_API:-http://127.0.0.1:9090}"
export CLASH_PREFER_HTTP="${CLASH_PREFER_HTTP:-1}"
export CLASH_SECRET="${CLASH_SECRET:-}"
API_BASE="${CLASH_API%/}"
# 与 snippet / 探针一致的核心生产后缀
PROD_SUFFIXES=(
weixiaoduo.com
wenpai.org
feibisi.com
feicode.com
cravatar.com
admincdn.com
)
usage() {
cat <<'EOF'
clashctl — ClashX Meta / Mihomo external-controller 封装
通用(转发 clash-api.py:
status 版本 / mode / 端口 / 流量 / 选中节点
mode [rule|global|direct]
groups | nodes <g> | select <g> <node>
delay <node> | delay-group <g>
rules [--limit N] | conns | dns <domain>
flush-dns | restart | upgrade-geo
本仓运维:
ensure-direct mode=rule + 检查生产 DIRECT缺规则时尝试热加载增强配置
probe [--deep] 网络供应链探针
regression [--strict]
sys-proxy macOS scutil 系统代理(期望 ON
chain-breaks 刷新 chain_v2 breaks 缓存(本地,不 SSH
pay-probe [--force] 礼品卡/虚拟卡市价探针 → 刷新 breaks
e2e [--call] [--from-endpoints] 端到端;--call 可用 endpoints 文件
sync-codex [--refresh-quick] scout→Mac 号池 health
board [--dry-run] breaks → Task Board observe
环境:
CLASH_API=http://127.0.0.1:9090 CLASH_SECRET=…
CLASH_PREFER_HTTP=1 默认走 HTTPClashX Meta :9090
EOF
}
api() {
if [[ -n "${CLASH_SECRET}" ]]; then
python3 "$API_PY" --api "$API_BASE" --secret "$CLASH_SECRET" "$@"
else
python3 "$API_PY" --api "$API_BASE" "$@"
fi
}
http_json() {
local method="$1" path="$2" data="${3:-}"
local url="${API_BASE}${path}"
local args=(-sS -m 8 -X "$method" "$url" -H "Content-Type: application/json")
[[ -n "${CLASH_SECRET}" ]] && args+=(-H "Authorization: Bearer ${CLASH_SECRET}")
if [[ -n "$data" ]]; then
curl "${args[@]}" -d "$data"
else
curl "${args[@]}"
fi
}
cmd_sys_proxy() {
if ! command -v scutil >/dev/null 2>&1; then
echo "scutil 不可用(非 macOS" >&2
return 1
fi
local out http_en socks_en http_host http_port on=0
out="$(scutil --proxy 2>/dev/null || true)"
http_en="$(echo "$out" | awk '/HTTPEnable/{print $3; exit}')"
https_en="$(echo "$out" | awk '/HTTPSEnable/{print $3; exit}')"
socks_en="$(echo "$out" | awk '/SOCKSEnable/{print $3; exit}')"
http_host="$(echo "$out" | awk '/^ HTTPProxy /{print $3; exit}')"
http_port="$(echo "$out" | awk '/^ HTTPPort /{print $3; exit}')"
socks_host="$(echo "$out" | awk '/^ SOCKSProxy /{print $3; exit}')"
socks_port="$(echo "$out" | awk '/^ SOCKSPort /{print $3; exit}')"
[[ "${http_en:-0}" == "1" || "${https_en:-0}" == "1" || "${socks_en:-0}" == "1" ]] && on=1
local dest="${http_host:-${socks_host:-?}}:${http_port:-${socks_port:-?}}"
if [[ "$on" -eq 1 ]]; then
echo "系统代理: ON → ${dest}(常态;关了上不了外网)"
return 0
fi
echo "系统代理: OFF ⚠ 本机可能上不了外网;请打开 ClashX Meta「系统代理」" >&2
echo "隔离靠 rule+生产 DIRECT不要用关代理当隔离。" >&2
return 2
}
cmd_ensure_direct() {
echo "[ensure-direct] controller ${API_BASE}"
local mode
mode="$(http_json GET /configs | python3 -c 'import sys,json; print(json.load(sys.stdin).get("mode",""))' 2>/dev/null || true)"
if [[ -z "$mode" ]]; then
echo "FAIL: 无法连接 ${API_BASE}" >&2
return 1
fi
if [[ "$(echo "$mode" | tr '[:upper:]' '[:lower:]')" != "rule" ]]; then
echo "[ensure-direct] mode=${mode} → rule"
http_json PATCH /configs '{"mode":"rule"}' >/dev/null
else
echo "[ensure-direct] mode=rule ok"
fi
local missing
missing="$(
http_json GET /rules | PROD_LIST="${PROD_SUFFIXES[*]}" python3 -c '
import json, os, sys
need = [x for x in os.environ.get("PROD_LIST", "").split() if x]
data = json.load(sys.stdin)
have = set()
for r in data.get("rules") or []:
if str(r.get("proxy") or "") != "DIRECT":
continue
p = str(r.get("payload") or "").lower().strip()
if p:
have.add(p)
miss = []
for n in need:
nl = n.lower()
if nl in have or any(nl == h or nl.endswith("." + h) for h in have):
continue
miss.append(n)
print(" ".join(miss))
'
)"
if [[ -z "${missing// }" ]]; then
echo "[ensure-direct] 生产 DIRECT 已覆盖: ${PROD_SUFFIXES[*]}"
else
echo "[ensure-direct] 缺少 DIRECT: $missing"
if [[ -f "$ENHANCED_CFG" ]]; then
echo "[ensure-direct] 热加载 $ENHANCED_CFG"
local payload
payload="$(python3 -c 'import json,sys; print(json.dumps({"path": sys.argv[1]}))' "$ENHANCED_CFG")"
if http_json PUT "/configs?force=true" "$payload" >/dev/null 2>&1; then
echo "[ensure-direct] force reload 已提交"
http_json PATCH /configs '{"mode":"rule"}' >/dev/null 2>&1 || true
else
echo "[ensure-direct] force reload 失败;请手动合并:" >&2
echo " $SNIPPET" >&2
fi
else
echo "[ensure-direct] 无 $ENHANCED_CFG" >&2
echo " 请合并 $SNIPPET 到 rules 顶部后 reload" >&2
fi
fi
cmd_sys_proxy || true
echo "[ensure-direct] 完成 · 建议: clashctl regression"
}
cmd_probe() {
local args=()
[[ "${1:-}" == "--deep" || "${1:-}" == "deep" ]] && args+=(--deep)
shift || true
python3 "$PROBE_PY" "${args[@]}" "$@"
}
cmd_regression() {
bash "$REGRESS" "$@"
}
cmd_chain_breaks() {
export PYTHONPATH="${ROOT}/src${PYTHONPATH:+:$PYTHONPATH}"
python3 "${ROOT}/scripts/manhuang/refresh-chain-breaks.py" "$@"
}
cmd_board() {
# 先刷新 breaks再推 Board
cmd_chain_breaks >/dev/null 2>&1 || true
bash "${ROOT}/scripts/manhuang/actions-to-board.sh" "$@"
}
cmd_pay_probe() {
bash "${ROOT}/scripts/manhuang/probe-pay-rails.sh" "$@"
}
cmd_e2e() {
export PYTHONPATH="${ROOT}/src${PYTHONPATH:+:$PYTHONPATH}"
python3 "${ROOT}/scripts/manhuang/probe-e2e-capability.py" "$@"
cmd_chain_breaks >/dev/null 2>&1 || true
}
cmd_sync_codex() {
bash "${ROOT}/scripts/manhuang/sync-codex-health.sh" "$@"
}
main() {
local cmd="${1:-}"
shift || true
case "$cmd" in
""|-h|--help|help) usage ;;
status|mode|groups|nodes|select|delay|delay-group|conns|conns-close|rules|dns|flush-dns|restart|upgrade-geo)
api "$cmd" "$@"
;;
ensure-direct|ensure) cmd_ensure_direct "$@" ;;
probe) cmd_probe "$@" ;;
regression|regress) cmd_regression "$@" ;;
sys-proxy|sysproxy|proxy-status) cmd_sys_proxy "$@" ;;
chain-breaks|breaks|chain) cmd_chain_breaks "$@" ;;
pay-probe|pay) cmd_pay_probe "$@" ;;
e2e|capability) cmd_e2e "$@" ;;
sync-codex|codex-sync|sync-health) cmd_sync_codex "$@" ;;
board|to-board) cmd_board "$@" ;;
*)
echo "unknown: $cmd" >&2
usage >&2
exit 1
;;
esac
}
main "$@"