from __future__ import annotations import json from typing import Any from main_window.constants import SINGBOX_EDITOR_PROTOCOL_IDS, SINGBOX_PROTOCOL_SEED_SPEC class SingBoxEditorMixin: def _selected_singbox_profile_id(self) -> str: selected = self._selected_transport_client() if selected is not None: selected_cid = str(getattr(selected, "id", "") or "").strip() if ( selected_cid and self._singbox_editor_profile_id and selected_cid == str(self._singbox_editor_profile_client_id or "").strip() ): return str(self._singbox_editor_profile_id).strip() if selected_cid: # Desktop SingBox tab keeps one deterministic profile per engine card. return selected_cid return self._selected_transport_engine_id() def _set_singbox_editor_enabled(self, enabled: bool) -> None: widgets = [ self.ent_singbox_proto_name, self.chk_singbox_proto_enabled, self.cmb_singbox_proto_protocol, self.ent_singbox_vless_server, self.spn_singbox_vless_port, self.ent_singbox_vless_uuid, self.ent_singbox_proto_password, self.cmb_singbox_vless_flow, self.cmb_singbox_vless_packet_encoding, self.cmb_singbox_ss_method, self.ent_singbox_ss_plugin, self.spn_singbox_hy2_up_mbps, self.spn_singbox_hy2_down_mbps, self.ent_singbox_hy2_obfs, self.ent_singbox_hy2_obfs_password, self.cmb_singbox_tuic_congestion, self.cmb_singbox_tuic_udp_mode, self.chk_singbox_tuic_zero_rtt, self.ent_singbox_wg_private_key, self.ent_singbox_wg_peer_public_key, self.ent_singbox_wg_psk, self.ent_singbox_wg_local_address, self.ent_singbox_wg_reserved, self.spn_singbox_wg_mtu, self.btn_singbox_wg_paste_private, self.btn_singbox_wg_copy_private, self.btn_singbox_wg_paste_peer, self.btn_singbox_wg_copy_peer, self.btn_singbox_wg_paste_psk, self.btn_singbox_wg_copy_psk, self.cmb_singbox_vless_transport, self.ent_singbox_vless_path, self.ent_singbox_vless_grpc_service, self.cmb_singbox_vless_security, self.ent_singbox_vless_sni, self.ent_singbox_tls_alpn, self.cmb_singbox_vless_utls_fp, self.ent_singbox_vless_reality_pk, self.ent_singbox_vless_reality_sid, self.chk_singbox_vless_insecure, self.chk_singbox_vless_sniff, ] for w in widgets: w.setEnabled(bool(enabled)) def _clear_singbox_editor(self) -> None: self._singbox_editor_loading = True try: self._singbox_editor_profile_id = "" self._singbox_editor_profile_client_id = "" self._singbox_editor_protocol = "vless" self._singbox_editor_source_raw = {} self.ent_singbox_proto_name.setText("") self.chk_singbox_proto_enabled.setChecked(True) self.cmb_singbox_proto_protocol.setCurrentIndex(0) self.ent_singbox_vless_server.setText("") self.spn_singbox_vless_port.setValue(443) self.ent_singbox_vless_uuid.setText("") self.ent_singbox_proto_password.setText("") self.cmb_singbox_vless_flow.setCurrentIndex(0) self.cmb_singbox_vless_flow.setEditText("") self.cmb_singbox_vless_packet_encoding.setCurrentIndex(0) self.cmb_singbox_ss_method.setCurrentIndex(0) self.ent_singbox_ss_plugin.setText("") self.spn_singbox_hy2_up_mbps.setValue(0) self.spn_singbox_hy2_down_mbps.setValue(0) self.ent_singbox_hy2_obfs.setText("") self.ent_singbox_hy2_obfs_password.setText("") self.cmb_singbox_tuic_congestion.setCurrentIndex(0) self.cmb_singbox_tuic_udp_mode.setCurrentIndex(0) self.chk_singbox_tuic_zero_rtt.setChecked(False) self.ent_singbox_wg_private_key.setText("") self.ent_singbox_wg_peer_public_key.setText("") self.ent_singbox_wg_psk.setText("") self.ent_singbox_wg_local_address.setText("") self.ent_singbox_wg_reserved.setText("") self.spn_singbox_wg_mtu.setValue(0) self.cmb_singbox_vless_transport.setCurrentIndex(0) self.ent_singbox_vless_path.setText("") self.ent_singbox_vless_grpc_service.setText("") self.cmb_singbox_vless_security.setCurrentIndex(0) self.ent_singbox_vless_sni.setText("") self.ent_singbox_tls_alpn.setText("") self.cmb_singbox_vless_utls_fp.setCurrentIndex(0) self.ent_singbox_vless_reality_pk.setText("") self.ent_singbox_vless_reality_sid.setText("") self.chk_singbox_vless_insecure.setChecked(False) self.chk_singbox_vless_sniff.setChecked(True) finally: self._singbox_editor_loading = False self.on_singbox_vless_editor_changed() def _load_singbox_editor_for_selected(self, *, silent: bool = True) -> None: client = self._selected_transport_client() if client is None: self._clear_singbox_editor() self._set_singbox_editor_enabled(False) return try: cid = str(getattr(client, "id", "") or "").strip() profile = self.ctrl.singbox_profile_get_for_client( client, profile_id=self._selected_singbox_profile_id(), ) self._apply_singbox_editor_profile(profile, fallback_name=str(getattr(client, "name", "") or "").strip()) self._singbox_editor_profile_client_id = cid self._set_singbox_editor_enabled(True) except Exception as e: if not silent: raise self._append_transport_log(f"[profile] editor load failed: {e}") self._clear_singbox_editor() self._set_singbox_editor_enabled(False) def _find_editor_proxy_outbound(self, outbounds: list[Any]) -> dict[str, Any]: proxy = {} for row in outbounds: if not isinstance(row, dict): continue t = str(row.get("type") or "").strip().lower() tag = str(row.get("tag") or "").strip().lower() if self._is_supported_editor_protocol(t): proxy = row break if tag == "proxy": proxy = row return dict(proxy) if isinstance(proxy, dict) else {} def _find_editor_sniff_inbound(self, inbounds: list[Any]) -> dict[str, Any]: inbound = {} for row in inbounds: if not isinstance(row, dict): continue tag = str(row.get("tag") or "").strip().lower() t = str(row.get("type") or "").strip().lower() if tag == "socks-in" or t == "socks": inbound = row break return dict(inbound) if isinstance(inbound, dict) else {} def _apply_singbox_editor_profile(self, profile, *, fallback_name: str = "") -> None: raw = getattr(profile, "raw_config", {}) or {} if not isinstance(raw, dict): raw = {} protocol = str(getattr(profile, "protocol", "") or "").strip().lower() or "vless" outbounds = raw.get("outbounds") or [] if not isinstance(outbounds, list): outbounds = [] inbounds = raw.get("inbounds") or [] if not isinstance(inbounds, list): inbounds = [] proxy = self._find_editor_proxy_outbound(outbounds) inbound = self._find_editor_sniff_inbound(inbounds) proxy_type = str(proxy.get("type") or "").strip().lower() if self._is_supported_editor_protocol(proxy_type): protocol = proxy_type tls = proxy.get("tls") if isinstance(proxy.get("tls"), dict) else {} reality = tls.get("reality") if isinstance(tls.get("reality"), dict) else {} utls = tls.get("utls") if isinstance(tls.get("utls"), dict) else {} transport = proxy.get("transport") if isinstance(proxy.get("transport"), dict) else {} security = "none" if bool(tls.get("enabled", False)): security = "tls" if bool(reality.get("enabled", False)): security = "reality" transport_type = str(transport.get("type") or "").strip().lower() or "tcp" path = str(transport.get("path") or "").strip() grpc_service = str(transport.get("service_name") or "").strip() alpn_vals = tls.get("alpn") or [] if not isinstance(alpn_vals, list): alpn_vals = [] alpn_text = ",".join([str(x).strip() for x in alpn_vals if str(x).strip()]) self._singbox_editor_loading = True try: self._singbox_editor_profile_id = str(getattr(profile, "id", "") or "").strip() self._singbox_editor_protocol = protocol self._singbox_editor_source_raw = json.loads(json.dumps(raw)) self.ent_singbox_proto_name.setText( str(getattr(profile, "name", "") or "").strip() or fallback_name or self._singbox_editor_profile_id ) self.chk_singbox_proto_enabled.setChecked(bool(getattr(profile, "enabled", True))) pidx = self.cmb_singbox_proto_protocol.findData(protocol) self.cmb_singbox_proto_protocol.setCurrentIndex(pidx if pidx >= 0 else 0) self.ent_singbox_vless_server.setText(str(proxy.get("server") or "").strip()) try: self.spn_singbox_vless_port.setValue(int(proxy.get("server_port") or 443)) except Exception: self.spn_singbox_vless_port.setValue(443) self.ent_singbox_vless_uuid.setText(str(proxy.get("uuid") or "").strip()) self.ent_singbox_proto_password.setText(str(proxy.get("password") or "").strip()) flow_value = str(proxy.get("flow") or "").strip() idx = self.cmb_singbox_vless_flow.findData(flow_value) if idx >= 0: self.cmb_singbox_vless_flow.setCurrentIndex(idx) else: self.cmb_singbox_vless_flow.setEditText(flow_value) pe = str(proxy.get("packet_encoding") or "").strip().lower() if pe in ("none", "off", "false"): pe = "" idx = self.cmb_singbox_vless_packet_encoding.findData(pe) self.cmb_singbox_vless_packet_encoding.setCurrentIndex(idx if idx >= 0 else 0) ss_method = str(proxy.get("method") or "").strip().lower() idx = self.cmb_singbox_ss_method.findData(ss_method) if idx >= 0: self.cmb_singbox_ss_method.setCurrentIndex(idx) else: self.cmb_singbox_ss_method.setEditText(ss_method) self.ent_singbox_ss_plugin.setText(str(proxy.get("plugin") or "").strip()) try: self.spn_singbox_hy2_up_mbps.setValue(int(proxy.get("up_mbps") or 0)) except Exception: self.spn_singbox_hy2_up_mbps.setValue(0) try: self.spn_singbox_hy2_down_mbps.setValue(int(proxy.get("down_mbps") or 0)) except Exception: self.spn_singbox_hy2_down_mbps.setValue(0) obfs = proxy.get("obfs") if isinstance(proxy.get("obfs"), dict) else {} self.ent_singbox_hy2_obfs.setText(str(obfs.get("type") or "").strip()) self.ent_singbox_hy2_obfs_password.setText(str(obfs.get("password") or "").strip()) cc = str(proxy.get("congestion_control") or "").strip() idx = self.cmb_singbox_tuic_congestion.findData(cc) if idx >= 0: self.cmb_singbox_tuic_congestion.setCurrentIndex(idx) else: self.cmb_singbox_tuic_congestion.setCurrentIndex(0) udp_mode = str(proxy.get("udp_relay_mode") or "").strip() idx = self.cmb_singbox_tuic_udp_mode.findData(udp_mode) self.cmb_singbox_tuic_udp_mode.setCurrentIndex(idx if idx >= 0 else 0) self.chk_singbox_tuic_zero_rtt.setChecked(bool(proxy.get("zero_rtt_handshake", False))) self.ent_singbox_wg_private_key.setText(str(proxy.get("private_key") or "").strip()) self.ent_singbox_wg_peer_public_key.setText(str(proxy.get("peer_public_key") or "").strip()) self.ent_singbox_wg_psk.setText(str(proxy.get("pre_shared_key") or "").strip()) local_addr = proxy.get("local_address") or [] if not isinstance(local_addr, list): if str(local_addr or "").strip(): local_addr = [str(local_addr).strip()] else: local_addr = [] self.ent_singbox_wg_local_address.setText( ",".join([str(x).strip() for x in local_addr if str(x).strip()]) ) reserved = proxy.get("reserved") or [] if not isinstance(reserved, list): if str(reserved or "").strip(): reserved = [str(reserved).strip()] else: reserved = [] self.ent_singbox_wg_reserved.setText( ",".join([str(x).strip() for x in reserved if str(x).strip()]) ) try: self.spn_singbox_wg_mtu.setValue(int(proxy.get("mtu") or 0)) except Exception: self.spn_singbox_wg_mtu.setValue(0) idx = self.cmb_singbox_vless_transport.findData(transport_type) self.cmb_singbox_vless_transport.setCurrentIndex(idx if idx >= 0 else 0) self.ent_singbox_vless_path.setText(path) self.ent_singbox_vless_grpc_service.setText(grpc_service) idx = self.cmb_singbox_vless_security.findData(security) self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 0) self.ent_singbox_vless_sni.setText(str(tls.get("server_name") or "").strip()) self.ent_singbox_tls_alpn.setText(alpn_text) idx = self.cmb_singbox_vless_utls_fp.findData(str(utls.get("fingerprint") or "").strip()) self.cmb_singbox_vless_utls_fp.setCurrentIndex(idx if idx >= 0 else 0) self.ent_singbox_vless_reality_pk.setText(str(reality.get("public_key") or "").strip()) self.ent_singbox_vless_reality_sid.setText(str(reality.get("short_id") or "").strip()) self.chk_singbox_vless_insecure.setChecked(bool(tls.get("insecure", False))) self.chk_singbox_vless_sniff.setChecked(bool(inbound.get("sniff", True))) finally: self._singbox_editor_loading = False self.on_singbox_vless_editor_changed() def _validate_singbox_editor_form(self) -> None: protocol = self._current_editor_protocol() addr = self.ent_singbox_vless_server.text().strip() if not addr: raise RuntimeError("Address is required") security = str(self.cmb_singbox_vless_security.currentData() or "none").strip().lower() transport = str(self.cmb_singbox_vless_transport.currentData() or "tcp").strip().lower() if protocol == "vless": if not self.ent_singbox_vless_uuid.text().strip(): raise RuntimeError("UUID is required for VLESS") if security == "reality" and not self.ent_singbox_vless_reality_pk.text().strip(): raise RuntimeError("Reality public key is required for Reality security mode") elif protocol == "trojan": if not self.ent_singbox_proto_password.text().strip(): raise RuntimeError("Password is required for Trojan") if security == "reality": raise RuntimeError("Reality security is not supported for Trojan in this editor") elif protocol == "shadowsocks": method = str(self.cmb_singbox_ss_method.currentData() or "").strip() if not method: method = self.cmb_singbox_ss_method.currentText().strip() if not method: raise RuntimeError("SS method is required for Shadowsocks") if not self.ent_singbox_proto_password.text().strip(): raise RuntimeError("Password is required for Shadowsocks") elif protocol == "hysteria2": if not self.ent_singbox_proto_password.text().strip(): raise RuntimeError("Password is required for Hysteria2") elif protocol == "tuic": if not self.ent_singbox_vless_uuid.text().strip(): raise RuntimeError("UUID is required for TUIC") if not self.ent_singbox_proto_password.text().strip(): raise RuntimeError("Password is required for TUIC") elif protocol == "wireguard": if not self.ent_singbox_wg_private_key.text().strip(): raise RuntimeError("WireGuard private key is required") if not self.ent_singbox_wg_peer_public_key.text().strip(): raise RuntimeError("WireGuard peer public key is required") local_addr = [str(x).strip() for x in self.ent_singbox_wg_local_address.text().split(",") if str(x).strip()] if not local_addr: raise RuntimeError("WireGuard local address is required (CIDR list)") self._parse_wg_reserved_values( [str(x).strip() for x in self.ent_singbox_wg_reserved.text().split(",") if str(x).strip()], strict=True, ) if protocol in ("vless", "trojan"): if transport == "grpc" and not self.ent_singbox_vless_grpc_service.text().strip(): raise RuntimeError("gRPC service is required for gRPC transport") if transport in ("ws", "http", "httpupgrade") and not self.ent_singbox_vless_path.text().strip(): raise RuntimeError("Transport path is required for selected transport") def _build_singbox_editor_raw_config(self) -> dict[str, Any]: base = self._singbox_editor_source_raw if not isinstance(base, dict): base = {} raw: dict[str, Any] = json.loads(json.dumps(base)) protocol = self._current_editor_protocol() outbounds = raw.get("outbounds") or [] if not isinstance(outbounds, list): outbounds = [] proxy_idx = -1 for i, row in enumerate(outbounds): if not isinstance(row, dict): continue t = str(row.get("type") or "").strip().lower() tag = str(row.get("tag") or "").strip().lower() if self._is_supported_editor_protocol(t) or tag == "proxy": proxy_idx = i break proxy: dict[str, Any] if proxy_idx >= 0: proxy = dict(outbounds[proxy_idx]) if isinstance(outbounds[proxy_idx], dict) else {} else: proxy = {} proxy["type"] = protocol proxy["tag"] = str(proxy.get("tag") or "proxy") proxy["server"] = self.ent_singbox_vless_server.text().strip() proxy["server_port"] = int(self.spn_singbox_vless_port.value()) # clear protocol-specific keys before repopulating for key in ( "uuid", "password", "method", "plugin", "flow", "packet_encoding", "up_mbps", "down_mbps", "obfs", "congestion_control", "udp_relay_mode", "zero_rtt_handshake", "private_key", "peer_public_key", "pre_shared_key", "local_address", "reserved", "mtu", ): proxy.pop(key, None) if protocol == "vless": proxy["uuid"] = self.ent_singbox_vless_uuid.text().strip() flow = str(self.cmb_singbox_vless_flow.currentData() or "").strip() if not flow: flow = self.cmb_singbox_vless_flow.currentText().strip() if flow: proxy["flow"] = flow packet_encoding = str(self.cmb_singbox_vless_packet_encoding.currentData() or "").strip().lower() if packet_encoding and packet_encoding != "none": proxy["packet_encoding"] = packet_encoding elif protocol == "trojan": proxy["password"] = self.ent_singbox_proto_password.text().strip() elif protocol == "shadowsocks": method = str(self.cmb_singbox_ss_method.currentData() or "").strip() if not method: method = self.cmb_singbox_ss_method.currentText().strip() proxy["method"] = method proxy["password"] = self.ent_singbox_proto_password.text().strip() plugin = self.ent_singbox_ss_plugin.text().strip() if plugin: proxy["plugin"] = plugin elif protocol == "hysteria2": proxy["password"] = self.ent_singbox_proto_password.text().strip() up = int(self.spn_singbox_hy2_up_mbps.value()) down = int(self.spn_singbox_hy2_down_mbps.value()) if up > 0: proxy["up_mbps"] = up if down > 0: proxy["down_mbps"] = down obfs_type = self.ent_singbox_hy2_obfs.text().strip() if obfs_type: obfs: dict[str, Any] = {"type": obfs_type} obfs_password = self.ent_singbox_hy2_obfs_password.text().strip() if obfs_password: obfs["password"] = obfs_password proxy["obfs"] = obfs elif protocol == "tuic": proxy["uuid"] = self.ent_singbox_vless_uuid.text().strip() proxy["password"] = self.ent_singbox_proto_password.text().strip() cc = str(self.cmb_singbox_tuic_congestion.currentData() or "").strip() if not cc: cc = self.cmb_singbox_tuic_congestion.currentText().strip() if cc: proxy["congestion_control"] = cc udp_mode = str(self.cmb_singbox_tuic_udp_mode.currentData() or "").strip() if udp_mode: proxy["udp_relay_mode"] = udp_mode if self.chk_singbox_tuic_zero_rtt.isChecked(): proxy["zero_rtt_handshake"] = True elif protocol == "wireguard": proxy["private_key"] = self.ent_singbox_wg_private_key.text().strip() proxy["peer_public_key"] = self.ent_singbox_wg_peer_public_key.text().strip() psk = self.ent_singbox_wg_psk.text().strip() if psk: proxy["pre_shared_key"] = psk local_addr = [str(x).strip() for x in self.ent_singbox_wg_local_address.text().split(",") if str(x).strip()] if local_addr: proxy["local_address"] = local_addr reserved_vals = self._parse_wg_reserved_values( [str(x).strip() for x in self.ent_singbox_wg_reserved.text().split(",") if str(x).strip()], strict=True, ) if reserved_vals: proxy["reserved"] = reserved_vals mtu = int(self.spn_singbox_wg_mtu.value()) if mtu > 0: proxy["mtu"] = mtu transport_type = str(self.cmb_singbox_vless_transport.currentData() or "tcp").strip().lower() if protocol in ("vless", "trojan"): self._apply_proxy_transport( proxy, transport=transport_type, path=self.ent_singbox_vless_path.text().strip(), grpc_service=self.ent_singbox_vless_grpc_service.text().strip(), ) else: proxy.pop("transport", None) security = str(self.cmb_singbox_vless_security.currentData() or "none").strip().lower() if protocol == "vless": pass elif protocol == "trojan": if security == "reality": security = "tls" elif protocol in ("hysteria2", "tuic"): security = "tls" else: security = "none" alpn = [] for p in self.ent_singbox_tls_alpn.text().split(","): v = str(p or "").strip() if v: alpn.append(v) self._apply_proxy_tls( proxy, security=security, sni=self.ent_singbox_vless_sni.text().strip(), utls_fp=str(self.cmb_singbox_vless_utls_fp.currentData() or "").strip(), tls_insecure=bool(self.chk_singbox_vless_insecure.isChecked()), reality_public_key=self.ent_singbox_vless_reality_pk.text().strip(), reality_short_id=self.ent_singbox_vless_reality_sid.text().strip(), alpn=alpn, ) if proxy_idx >= 0: outbounds[proxy_idx] = proxy else: outbounds.insert(0, proxy) has_direct = any( isinstance(row, dict) and str(row.get("type") or "").strip().lower() == "direct" and str(row.get("tag") or "").strip().lower() == "direct" for row in outbounds ) if not has_direct: outbounds.append({"type": "direct", "tag": "direct"}) raw["outbounds"] = outbounds inbounds = raw.get("inbounds") or [] if not isinstance(inbounds, list): inbounds = [] inbound_idx = -1 for i, row in enumerate(inbounds): if not isinstance(row, dict): continue tag = str(row.get("tag") or "").strip().lower() t = str(row.get("type") or "").strip().lower() if tag == "socks-in" or t == "socks": inbound_idx = i break inbound = ( dict(inbounds[inbound_idx]) if inbound_idx >= 0 and isinstance(inbounds[inbound_idx], dict) else {} ) inbound["type"] = str(inbound.get("type") or "socks") inbound["tag"] = str(inbound.get("tag") or "socks-in") inbound["listen"] = str(inbound.get("listen") or "127.0.0.1") inbound["listen_port"] = int(inbound.get("listen_port") or 10808) sniff = bool(self.chk_singbox_vless_sniff.isChecked()) inbound["sniff"] = sniff inbound["sniff_override_destination"] = sniff if inbound_idx >= 0: inbounds[inbound_idx] = inbound else: inbounds.insert(0, inbound) raw["inbounds"] = inbounds route = raw.get("route") if isinstance(raw.get("route"), dict) else {} route["final"] = str(route.get("final") or "direct") rules = route.get("rules") or [] if not isinstance(rules, list): rules = [] has_proxy_rule = False for row in rules: if not isinstance(row, dict): continue outbound = str(row.get("outbound") or "").strip().lower() inbound_list = row.get("inbound") or [] if not isinstance(inbound_list, list): inbound_list = [] inbound_norm = [str(x).strip().lower() for x in inbound_list if str(x).strip()] if outbound == "proxy" and "socks-in" in inbound_norm: has_proxy_rule = True break if not has_proxy_rule: rules.insert(0, {"inbound": ["socks-in"], "outbound": "proxy"}) route["rules"] = rules raw["route"] = route return raw def _save_singbox_editor_draft(self, client, *, profile_id: str = ""): protocol = self._current_editor_protocol() self._validate_singbox_editor_form() raw_cfg = self._build_singbox_editor_raw_config() name = self.ent_singbox_proto_name.text().strip() enabled = bool(self.chk_singbox_proto_enabled.isChecked()) res = self.ctrl.singbox_profile_save_raw_for_client( client, profile_id=profile_id, name=name, enabled=enabled, protocol=protocol, raw_config=raw_cfg, ) profile = self.ctrl.singbox_profile_get_for_client(client, profile_id=profile_id) self._singbox_editor_profile_id = str(getattr(profile, "id", "") or "").strip() self._singbox_editor_profile_client_id = str(getattr(client, "id", "") or "").strip() self._singbox_editor_protocol = str(getattr(profile, "protocol", "") or protocol).strip().lower() or protocol self._singbox_editor_source_raw = json.loads(json.dumps(getattr(profile, "raw_config", {}) or {})) return res def _sync_selected_singbox_profile_link(self, *, silent: bool = True) -> None: client = self._selected_transport_client() if client is None: return try: preferred_pid = str(getattr(client, "id", "") or "").strip() res = self.ctrl.singbox_profile_ensure_linked( client, preferred_profile_id=preferred_pid, ) except Exception as e: if not silent: raise self._append_transport_log(f"[profile] auto-link skipped: {e}") return line = (res.pretty_text or "").strip() if not line: return # Keep noisy "already linked" messages out of normal flow. if "already linked" in line.lower() and silent: return self._append_transport_log(f"[profile] {line}") self.ctrl.log_gui(f"[singbox-profile] {line}")