#!/usr/bin/env python3 """生成 Termux Agent config.json — 连接顺序 ① NAS 局域网 ② 本机 Docker ③ NAS 外网 frp。 与 agent.py BIND-03 / workphone.mdc §零.十 一致。 用法: python3 build_agent_config.py --device-id xgfe65eimrrofyws --out /tmp/config.json python3 build_agent_config.py --print-primary # 只输出当前可达主服 ws 基址 """ from __future__ import annotations import argparse import json import socket import sys import urllib.request from typing import List, Optional, Tuple SDK_PORT = 8899 NAS_LAN_HOSTS = ("192.168.110.101", "192.168.1.201") NAS_FRP_HOST = "open.quwanzhi.com" HEARTBEAT = 10 PROJECT_ID = "cunkebao" def _tcp_ok(host: str, port: int, timeout: float = 2.5) -> bool: try: with socket.create_connection((host, port), timeout=timeout): return True except OSError: return False def _health_ok(host: str, port: int = SDK_PORT, timeout: float = 3.0) -> bool: try: url = f"http://{host}:{port}/health" with urllib.request.urlopen(url, timeout=timeout) as resp: return resp.status == 200 except Exception: return False def pick_mac_lan_ip() -> str: import subprocess try: out = subprocess.check_output(["ifconfig"], text=True, stderr=subprocess.DEVNULL) except Exception: return "127.0.0.1" ips: List[str] = [] for line in out.splitlines(): parts = line.strip().split() if len(parts) >= 2 and parts[0] == "inet": ip = parts[1] if ip.startswith("127.") or ip.startswith("198.18."): continue ips.append(ip) for ip in ips: if ip.startswith("192.168.110."): return ip for ip in ips: if ip.startswith("192.168.") or ip.startswith("10."): return ip return ips[0] if ips else "127.0.0.1" def resolve_primary(for_phone: bool = False) -> Tuple[str, str, List[str]]: """返回 (primary_ws_base, tier, ordered_ws_bases)。 手机/本机统一顺序(BIND-03 · workphone.mdc §零.十): ① NAS 局域网 → ② 本机 Mac Docker 局域网 → ③ NAS 外网 frp; Agent 运行时 TCP 探测,连不上自动切下一候选。 """ mac_ip = pick_mac_lan_ip() nas_bases = [(f"ws://{h}:{SDK_PORT}/ws/device", "nas_lan") for h in NAS_LAN_HOSTS] frp_base = (f"ws://{NAS_FRP_HOST}:{SDK_PORT}/ws/device", "nas_frp") mac_base = (f"ws://{mac_ip}:{SDK_PORT}/ws/device", "mac_docker") if mac_ip != "127.0.0.1" else None ordered: List[Tuple[str, str]] = list(nas_bases) if mac_base: ordered.append(mac_base) ordered.append(frp_base) if for_phone: # 手机 OTA:固定 NAS 为主连接写入 config,public_servers 按序探测(局域网优先于公网) primary_ws, tier = nas_bases[0][0], "nas_lan" bases = [b for b, _ in ordered] seen = set() ordered_bases: List[str] = [] for b in bases: if b not in seen: seen.add(b) ordered_bases.append(b) return primary_ws, tier, ordered_bases primary_ws, tier = ordered[0][0], ordered[0][1] for ws_base, t in ordered: host = ws_base.split("//")[1].split(":")[0] if _health_ok(host, SDK_PORT): primary_ws, tier = ws_base, t break bases: List[str] = [] seen = set() for ws_base, _ in ordered: if ws_base not in seen: seen.add(ws_base) bases.append(ws_base) bases = [primary_ws] + [b for b in bases if b != primary_ws] return primary_ws, tier, bases def build_config(device_id: str, for_phone: bool = False) -> dict: primary_ws, tier, bases = resolve_primary(for_phone=for_phone) return { "device_id": device_id, "server_url": primary_ws, "public_servers": [b for b in bases if b != primary_ws], "heartbeat_interval": HEARTBEAT, "project_id": PROJECT_ID, "connection_priority": ["nas_lan", "mac_docker", "nas_frp"], "_meta": {"primary_tier": tier, "generator": "build_agent_config.py"}, } def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--device-id", default="xgfe65eimrrofyws") ap.add_argument("--out", default="") ap.add_argument("--print-primary", action="store_true") ap.add_argument("--for-phone", action="store_true", help="手机 OTA:NAS→frp→Mac") args = ap.parse_args() primary_ws, tier, _ = resolve_primary(for_phone=args.for_phone) if args.print_primary: print(primary_ws) print(tier, file=sys.stderr) return 0 cfg = build_config(args.device_id, for_phone=args.for_phone) text = json.dumps(cfg, ensure_ascii=False, indent=2) if args.out: with open(args.out, "w", encoding="utf-8") as f: f.write(text) else: print(text) return 0 if __name__ == "__main__": raise SystemExit(main())