traffic: add persistent app profiles (api+gui)

This commit is contained in:
beckline
2026-02-15 20:56:57 +03:00
parent 70c5eea935
commit b040b9e7d7
7 changed files with 743 additions and 5 deletions

View File

@@ -138,6 +138,26 @@ class TrafficAppMarksResult:
timeout_sec: int = 0
@dataclass(frozen=True)
class TrafficAppProfile:
id: str
name: str
app_key: str
command: str
target: str # vpn|direct
ttl_sec: int
vpn_profile: str
created_at: str
updated_at: str
@dataclass(frozen=True)
class TrafficAppProfileSaveResult:
ok: bool
message: str
profile: Optional[TrafficAppProfile] = None
@dataclass(frozen=True)
class TrafficCandidateSubnet:
@@ -860,6 +880,114 @@ class ApiClient:
timeout_sec=int(data.get("timeout_sec", 0) or 0),
)
def traffic_app_profiles_list(self) -> List[TrafficAppProfile]:
data = cast(
Dict[str, Any],
self._json(self._request("GET", "/api/v1/traffic/app-profiles")) or {},
)
raw = data.get("profiles") or []
if not isinstance(raw, list):
raw = []
out: List[TrafficAppProfile] = []
for it in raw:
if not isinstance(it, dict):
continue
pid = str(it.get("id") or "").strip()
if not pid:
continue
out.append(
TrafficAppProfile(
id=pid,
name=str(it.get("name") or "").strip(),
app_key=str(it.get("app_key") or "").strip(),
command=str(it.get("command") or "").strip(),
target=str(it.get("target") or "").strip().lower(),
ttl_sec=int(it.get("ttl_sec", 0) or 0),
vpn_profile=str(it.get("vpn_profile") or "").strip(),
created_at=str(it.get("created_at") or "").strip(),
updated_at=str(it.get("updated_at") or "").strip(),
)
)
return out
def traffic_app_profile_upsert(
self,
*,
id: str = "",
name: str = "",
app_key: str = "",
command: str,
target: str,
ttl_sec: int = 0,
vpn_profile: str = "",
) -> TrafficAppProfileSaveResult:
payload: Dict[str, Any] = {
"command": str(command or "").strip(),
"target": str(target or "").strip().lower(),
}
if id:
payload["id"] = str(id).strip()
if name:
payload["name"] = str(name).strip()
if app_key:
payload["app_key"] = str(app_key).strip()
if int(ttl_sec or 0) > 0:
payload["ttl_sec"] = int(ttl_sec)
if vpn_profile:
payload["vpn_profile"] = str(vpn_profile).strip()
data = cast(
Dict[str, Any],
self._json(
self._request("POST", "/api/v1/traffic/app-profiles", json_body=payload)
)
or {},
)
msg = str(data.get("message") or "")
raw = data.get("profiles") or []
if not isinstance(raw, list):
raw = []
prof: Optional[TrafficAppProfile] = None
if raw and isinstance(raw[0], dict):
it = cast(Dict[str, Any], raw[0])
pid = str(it.get("id") or "").strip()
if pid:
prof = TrafficAppProfile(
id=pid,
name=str(it.get("name") or "").strip(),
app_key=str(it.get("app_key") or "").strip(),
command=str(it.get("command") or "").strip(),
target=str(it.get("target") or "").strip().lower(),
ttl_sec=int(it.get("ttl_sec", 0) or 0),
vpn_profile=str(it.get("vpn_profile") or "").strip(),
created_at=str(it.get("created_at") or "").strip(),
updated_at=str(it.get("updated_at") or "").strip(),
)
ok = bool(prof) and (msg.strip().lower() in ("saved", "ok"))
if not msg and ok:
msg = "saved"
return TrafficAppProfileSaveResult(ok=ok, message=msg, profile=prof)
def traffic_app_profile_delete(self, id: str) -> CmdResult:
pid = str(id or "").strip()
if not pid:
raise ValueError("missing id")
data = cast(
Dict[str, Any],
self._json(
self._request("DELETE", "/api/v1/traffic/app-profiles", params={"id": pid})
)
or {},
)
return CmdResult(
ok=bool(data.get("ok", False)),
message=str(data.get("message") or ""),
exit_code=None,
stdout="",
stderr="",
)
# DNS / SmartDNS
def dns_upstreams_get(self) -> DnsUpstreams: