import csv
from contextlib import contextmanager
import importlib
import ipaddress
import os
from pathlib import Path
import re
import sys
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple

SOURCE_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT_TEXT = str(SOURCE_ROOT)
if SOURCE_ROOT_TEXT not in sys.path:
    sys.path.insert(0, SOURCE_ROOT_TEXT)

from common import shared as _shared
from common.legacy_namespace import import_legacy_names
from common.log_utils import create_task_log_path, credential_scope_label, export_log_to_desktop, safe_log_name, sanitize_log_text
from common.process_utils import close_channel, terminate_processes
from common.connection_command import configured_default_script_command, shell_env_assignments
from common.output_formatting import append_result_footer, extract_plain_socks5_output, migrate_latest_result_files, read_excel_table_rows, write_excel_view_file, write_latest_result_files
from common.realtime_log_filter import (
    RealtimeLogDisplayFilter,
    filter_realtime_log_text,
    format_realtime_log_text_without_filter,
    is_failure_message,
)
from common.runtime_paths import cloud_data_dir, cloud_log_dir
from common.xray_asset import ensure_remote_xray
from common.task_guards import TaskGuard, TaskGuardError
from common.task_runtime import READ_RESOURCE_RISK_GROUP, WRITE_RESOURCE_RISK_GROUP
from common.credential_parser import append_mark_to_output
from common.customer_ownership import CHECKING_MESSAGE, PARTNER_RECHECK_MESSAGE as NON_PARTNER_MESSAGE, READY_MESSAGE, SERVER_ERROR_MESSAGE, WELCOME_MESSAGE, classify_auth_exception
from common.debug_billing import limited_vendor_features_enabled, set_on_demand_debug_enabled
from common.user_messages import friendly_popup_text
from common.ui_helpers import (
    ACTION_BUTTON_WIDTH,
    LONG_ACTION_BUTTON_WIDTH,
    LOG_PANEL_WIDTH,
    PAGE_CONTENT_GAP,
    PANEL_PADDING,
    PURCHASE_BUILD_BUTTON_WIDTH,
    RESOURCE_ACTION_BUTTON_WIDTH,
    SECTION_GAP,
    VOLC_PURCHASE_RESOURCE_TREE_ROWS,
    apply_dialog_icon,
    ask_app_confirm_dialog,
    autosize_treeview_columns,
    build_cloud_page_layout,
    cloud_dialog_title,
    create_fixed_tab_pages,
    build_realtime_log_panel,
    build_result_output_panel,
    dialog_button_style,
    dialog_button_width,
    format_app_dialog_message,
    play_task_completion_sound,
    show_app_message_dialog,
    treeview_click_hits_indicator,
    treeview_display_column_name,
)
from Volcengine.config import (
    VOLC_AUTOBUY_SOURCE_MODES,
    VOLC_CONFIG_BIG_FULL,
    VOLC_CONFIG_FULL,
    VOLC_CONFIGS,
    VOLC_BALANCE_CHECK_FULL,
    VOLC_IDENTITY_TIMEOUT,
    VOLC_IP_QUOTA_PER_REGION,
    VOLC_REGION_PLANS,
    VOLC_RESOURCE_API_TIMEOUT,
    VOLC_RESOURCE_SCAN_TIMEOUT,
    VOLC_RESOURCE_SLOW_API_GRACE,
    VOLC_SPEC_2C2G,
    VOLC_SPEC_2C4G,
    VOLC_SPECS,
)
from Volcengine.api.volcengine.eip_service import (
    EipDailyPurchaseLimitError,
    VOLC_EIP_DAILY_PURCHASE_LIMIT,
    finish_eip_daily_purchase,
    get_eip_daily_purchase_status,
    reserve_eip_daily_purchase,
)
from Volcengine.api.volcengine.billing_service import get_available_balance

import_legacy_names(globals(), _shared)


class DelayedActionPreconfirmAccepted(Exception):
    pass


class DelayedActionPreconfirmCancelled(Exception):
    pass


class DelayedActionPreconfirmCaptured(Exception):
    def __init__(self, message: str) -> None:
        super().__init__(message)
        self.message = message


class _StyledMessageBox:
    @staticmethod
    def _parent(parent: Any | None = None) -> Any:
        if parent is not None:
            return parent
        root = getattr(tk, "_default_root", None)
        if root is not None:
            return root
        root = tk.Tk()
        root.withdraw()
        return root

    @classmethod
    def showinfo(cls, title: str, message: str, **kwargs: Any) -> None:
        show_app_message_dialog(cls._parent(kwargs.get("parent")), title, friendly_popup_text(message), modal=True)

    showwarning = showinfo
    showerror = showinfo

    @classmethod
    def askyesno(cls, title: str, message: str, **kwargs: Any) -> bool:
        return ask_app_confirm_dialog(cls._parent(kwargs.get("parent")), title, friendly_popup_text(message))


messagebox = _StyledMessageBox

# 火山云日志单独存放，避免资源管理和购买搭建日志与其他云混在一起。
APP_TITLE = "火山引擎管理"
APP_LOG_DIR = str(cloud_log_dir("Volcengine"))
APP_RESULT_DIR = cloud_data_dir("Volcengine") / "latest_results"
CHECKED = "☑"
UNCHECKED = "☐"
LEGACY_CHECKED_VALUES = {"✓", "✔", CHECKED}
LEGACY_UNCHECKED_VALUES = {"□", "☐", UNCHECKED, ""}
USED_IP_CSV_HEADERS = ("日期时间", "火山云用户名-简AK-SK", "地区", "IP")
USED_IP_TABLE_COLUMN_WIDTHS = (22.0, 36.0, 14.0, 18.0)
USED_IP_TABLE_NOTE = "可写入已使用的旧IP和地区，更换新IP时会过滤更换（注意：务必准确填写地区和IP，确保地区归属无误，否则换新失败！）"
FAILED_IP_WORKSHEET_NAME = "更换失败IP库"
FAILED_IP_TABLE_NOTE = "自动记录更换失败或尚未完成更换的公网IP；后续更换成功后会自动移除。"
VOLC_NEW_IP_RESULT_REMINDER = "请到购买搭建-结果框内查看新IP的L2TP/Socks5账号配置。"
PURCHASE_REGION_TREE_HEIGHT = 186
VOLC_RESOURCE_CACHE_MAX_AGE_SECONDS = 600
VOLC_PREPAID_RELEASE_TIMEOUT_SECONDS = 420.0
VOLC_PRIMARY_NIC_RELEASE_TIMEOUT_SECONDS = 420.0
VOLC_DELETE_DEPENDENCY_ATTEMPTS = 8
VOLC_DELETE_DEPENDENCY_MAX_SLEEP_SECONDS = 30.0
VOLC_DELETE_POST_VERIFY_DELAY_SECONDS = 8.0
VOLC_EIP_REGION_QUOTA_LIMIT = 20
VOLC_REPLACE_RANDOM_OLD_IP_HIT_LIMIT = 3
VOLC_PURCHASE_BUILD_SSH_READY_DELAY_SECONDS = 5
VOLCENGINE_STANDALONE_LIMITED_ENV = "WAWA_VOLCENGINE_STANDALONE_LIMITED"
MANAGER_AUTH_CODE_ENV = "WAWA_MANAGER_AUTH_CODE"
CUSTOMER_MANAGER_AUTH_PREFIX = "KHJL"
VOLC_BUILD_MODE_DEFAULT = "default"
VOLC_BUILD_MODE_RANDOM = "random"
VOLC_DEFAULT_SCRIPT_URL = "https://www.wawaapi.com/static/scripts/Volc/volcengine-default.sh"
VOLC_DEFAULT_REMOTE_COMMAND = f"bash <(curl -fsSL {VOLC_DEFAULT_SCRIPT_URL})"
VOLC_RANDOM_SCRIPT_URL = "https://www.wawaapi.com/static/scripts/Volc/volcengine.sh"
VOLC_RANDOM_REMOTE_COMMAND = f"bash <(curl -fsSL {VOLC_RANDOM_SCRIPT_URL})"
VOLC_CUSTOM_CONNECTION_DEFAULT_USER = "user"


class CustomerOwnershipGuardError(RuntimeError):
    """客户归属未确认时阻止继续使用云功能。"""


CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE = NON_PARTNER_MESSAGE
CUSTOMER_OWNERSHIP_CHECKING_MESSAGE = CHECKING_MESSAGE
CUSTOMER_OWNERSHIP_WELCOME_MESSAGE = WELCOME_MESSAGE
CUSTOMER_OWNERSHIP_READY_MESSAGE = READY_MESSAGE
CUSTOMER_OWNERSHIP_CHECK_FAILED_MESSAGE = SERVER_ERROR_MESSAGE
CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE = "火山云查询AK/SK中，正在校验合作伙伴归属，请稍等。"


def _customer_ownership_result_log_message(status: str, message: str) -> str:
    if status == "ok":
        return CUSTOMER_OWNERSHIP_WELCOME_MESSAGE
    return message or CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE


def volcengine_standalone_limited_features() -> bool:
    if os.environ.get(VOLCENGINE_STANDALONE_LIMITED_ENV, "").strip() == "1":
        return True
    if limited_vendor_features_enabled("volc"):
        return True
    legacy_vendor = os.environ.get("WAWA_SINGLE_VENDOR", "").strip().lower()
    return legacy_vendor in {"volc", "volcengine", "huoshan"}


class VolcEngineManagerApp(ttk.Frame):
    """火山引擎先接入独立入口和 SSH 搭建，云资源 OpenAPI 不复用华为命令。"""

    def __init__(self, master: tk.Misc, *, embedded: bool = True) -> None:
        super().__init__(master, padding=0)
        self.embedded = embedded
        self.standalone_limited_features = volcengine_standalone_limited_features()
        self.ak_var = tk.StringVar()
        self.sk_var = tk.StringVar()
        self.mark_var = tk.StringVar()
        self.volc_config_var = tk.StringVar(value="")
        self.volc_spec_var = tk.StringVar(value=VOLC_SPEC_2C2G)
        set_on_demand_debug_enabled("volc", False)
        self.volc_on_demand_var = tk.BooleanVar(value=False)
        self.volc_build_mode_var = tk.StringVar(value=VOLC_BUILD_MODE_RANDOM)
        self.default_conn_user_var = tk.StringVar(value=VOLC_CUSTOM_CONNECTION_DEFAULT_USER)
        self.default_conn_password_var = tk.StringVar(value=SOCKS_PASSWORD)
        self.default_conn_socks_port_var = tk.StringVar(value=str(SOCKS_PORT))
        self.default_conn_custom_saved = False
        self.default_conn_once_var = tk.BooleanVar(value=False)
        self.manage_reason_var = tk.StringVar(value="不再使用")
        self.status_var = tk.StringVar(value="就绪")
        self.busy = False
        self.stop_event = threading.Event()
        self.log_text: Optional[tk.Text] = None
        self.purchase_log_text: Optional[tk.Text] = None
        self.manage_log_text: Optional[tk.Text] = None
        self.ssh_channel = None
        self.ssh_channel_lock = threading.Lock()
        self.child_processes: set[subprocess.Popen] = set()
        self.socks_text: Optional[tk.Text] = None
        self.l2tp_text: Optional[tk.Text] = None
        self.manage_tree: Optional[ttk.Treeview] = None
        self.manage_items: Dict[str, Dict[str, Any]] = {}
        self.volc_region_server_ips: Dict[str, List[str]] = {}
        self.volc_region_socks: Dict[str, List[str]] = {}
        self.volc_region_l2tp: Dict[str, List[str]] = {}
        self.volc_rebuilding_output_regions: Set[str] = set()
        self.volc_cached_resource_items: List[Dict[str, Any]] = []
        self.volc_cached_resource_regions: Set[str] = set()
        self.volc_cached_resource_account_scope = ""
        self.volc_cached_resource_updated_at = 0.0
        self.volc_result_account_scope = ""
        self.volc_result_credentials_fingerprint = ""
        self.volc_purchase_failure_reasons: Dict[str, str] = {}
        self.volc_log_file_paths: Dict[str, str] = {}
        self.volc_log_action_names: Dict[str, str] = {}
        self.customer_guard_status = "idle"
        self.customer_guard_fingerprint = ""
        self.customer_guard_message = ""
        self.customer_guard_notice_fingerprint = ""
        self.customer_guard_log_fingerprint = ""
        self.customer_guard_pending_resource_query = None
        self.customer_guard_dialog = None
        self.customer_guard_after_id: Optional[str] = None
        self.customer_guard_token = 0
        self.suspend_customer_guard_trace = False
        self.realtime_log_filter = RealtimeLogDisplayFilter()
        self.current_log_context = "purchase"
        self.active_task_contexts: Dict[str, int] = {}
        self.task_guards: Dict[str, List[TaskGuard]] = {}
        self.manage_last_clicked_iid = ""
        self.active_task_count = 0
        self.blocking_task_count = 0
        self.task_lock = threading.Lock()
        self.used_ip_table_lock = threading.RLock()
        self.delayed_action_queue: list[tuple[str, Callable[[], Any]]] = []
        self.delayed_action_running_label = ""
        self.delayed_current_action_label = ""
        self.delayed_action_max_total = 6
        self.delayed_preconfirm_active = False
        self.delayed_skip_next_confirmation = False
        self.delayed_preconfirm_methods = {
            "purchase_selected_regions",
            "purchase_selected_region_new_ips",
            "start_build_selected_regions",
            "purchase_and_build_selected_regions",
            "reinstall_selected_servers",
            "reinstall_selected_manage_servers",
            "clear_actionable_manage_resources",
            "unsubscribe_selected_resources",
            "replace_selected_region_ips",
        }
        self.delayed_queue_result_active = False
        self.delayed_queue_result_sequence = 0
        self.delayed_queue_result_current_token = ""
        self.delayed_queue_result_current_label = ""
        self.delayed_queue_result_actions: list[tuple[str, str]] = []
        self.delayed_queue_result_entries: list[dict[str, Any]] = []
        self.delayed_queue_result_suppress_dialog = False
        self.volc_sdk_lock = threading.Lock()
        self._last_socks_lines: List[str] = []
        self._last_l2tp_lines: List[str] = []
        self.ak_var.trace_add("write", self._on_customer_guard_credentials_changed)
        self.sk_var.trace_add("write", self._on_customer_guard_credentials_changed)
        self.business_hot_reload_mtimes = self._collect_business_source_mtimes()
        self.business_hot_reload_dispatching = False
        self._build_ui()

    def _standalone_limited_features(self) -> bool:
        return bool(getattr(self, "standalone_limited_features", False))

    def _current_manager_auth_code(self) -> str:
        code = ""
        try:
            code = str(getattr(self._root(), "customer_manager_auth_code", "") or "")
        except (AttributeError, tk.TclError):
            pass
        if not code:
            code = os.environ.get(MANAGER_AUTH_CODE_ENV, "")
        return re.sub(r"\s+", "", code).upper()

    def _customer_manage_eip_only(self) -> bool:
        return self._current_manager_auth_code().startswith(CUSTOMER_MANAGER_AUTH_PREFIX)

    @staticmethod
    def _business_source_dirs() -> list[Path]:
        volc_dir = Path(__file__).resolve().parent
        return [volc_dir, volc_dir.parent / "common"]

    @classmethod
    def _collect_business_source_mtimes(cls) -> dict[str, float]:
        tracked: dict[str, float] = {}
        for source_dir in cls._business_source_dirs():
            if not source_dir.exists():
                continue
            for path in source_dir.rglob("*.py"):
                if "__pycache__" in path.parts:
                    continue
                try:
                    tracked[str(path.resolve())] = path.stat().st_mtime
                except OSError:
                    continue
        return tracked

    @staticmethod
    def _module_source_mtime(module: Any) -> float | None:
        filename = str(getattr(module, "__file__", "") or "")
        if not filename or filename.endswith((".pyc", ".pyo")):
            return None
        if not filename.endswith(".py") or not os.path.exists(filename):
            return None
        try:
            return os.path.getmtime(filename)
        except OSError:
            return None

    @staticmethod
    def _business_module_reload_priority(name: str) -> int:
        if name == "common.shared":
            return -100
        if name == "Volcengine.config":
            return -80
        if name.startswith("Volcengine.api."):
            return -50
        if name == "Volcengine.app":
            return 1000
        return 0

    def _has_active_tasks(self) -> bool:
        with self.task_lock:
            return bool(self.active_task_count or self.busy)

    def _reload_business_modules_if_changed(self) -> bool:
        current = self._collect_business_source_mtimes()
        previous = getattr(self, "business_hot_reload_mtimes", {}) or {}
        if current == previous:
            return False
        if self._has_active_tasks():
            self.status_var.set("检测到源码更新，任务结束后下次操作自动加载")
            return False
        modules: list[tuple[int, int, int, str, Any]] = []
        prefixes = ("common", "Volcengine")
        importlib.invalidate_caches()
        for name, module in list(sys.modules.items()):
            if not module or not any(name == prefix or name.startswith(prefix + ".") for prefix in prefixes):
                continue
            if self._module_source_mtime(module) is None:
                continue
            prefix_index = prefixes.index(name.split(".", 1)[0])
            modules.append((prefix_index, self._business_module_reload_priority(name), name.count("."), name, module))
        # 中文注释：只重载火山云业务代码，不重建界面；控件布局变化仍交给“界面更新”。
        for _prefix_index, _priority, _depth, _name, module in sorted(modules):
            importlib.reload(module)
        refreshed_module = sys.modules.get("Volcengine.app")
        refreshed_class = getattr(refreshed_module, "VolcEngineManagerApp", None)
        if isinstance(refreshed_class, type) and refreshed_class is not self.__class__:
            self.__class__ = refreshed_class
        self.business_hot_reload_mtimes = self._collect_business_source_mtimes()
        self.status_var.set("业务代码已自动加载最新源码")
        return True

    def _dispatch_after_business_hot_reload(self, method_name: str, *args: Any, **kwargs: Any) -> bool:
        if getattr(self, "business_hot_reload_dispatching", False):
            return False
        try:
            reloaded = self._reload_business_modules_if_changed()
        except Exception as exc:
            try:
                messagebox.showerror(APP_TITLE, friendly_popup_text(f"业务代码自动加载失败：{exc}"), parent=self.winfo_toplevel())
            except Exception:
                messagebox.showerror(APP_TITLE, friendly_popup_text(f"业务代码自动加载失败：{exc}"))
            return False
        if not reloaded:
            return False
        method = getattr(self, method_name, None)
        if not callable(method):
            try:
                messagebox.showerror(APP_TITLE, friendly_popup_text(f"业务代码已更新，但未找到入口：{method_name}"), parent=self.winfo_toplevel())
            except Exception:
                messagebox.showerror(APP_TITLE, friendly_popup_text(f"业务代码已更新，但未找到入口：{method_name}"))
            return True
        self.business_hot_reload_dispatching = True
        try:
            method(*args, **kwargs)
        finally:
            self.business_hot_reload_dispatching = False
        return True

    def _hot_command(self, method_name: str, *args: Any, **kwargs: Any):
        def command() -> Any:
            label = self._delayed_action_label(method_name)

            def execute() -> Any:
                if self._dispatch_after_business_hot_reload(method_name, *args, **kwargs):
                    return None
                method = getattr(self, method_name)
                return method(*args, **kwargs)

            if method_name == "scan_resources":
                return execute()
            return self._run_or_delay_action(
                label,
                execute,
                requires_preconfirm=method_name in self.delayed_preconfirm_methods,
            )

        return command

    def _delayed_action_label(self, method_name: str) -> str:
        labels = {
            "scan_resources": "读取账号资源",
            "purchase_selected_regions": "购买选择地区",
            "purchase_selected_region_new_ips": "购买选择地区新IP",
            "start_build_selected_regions": "搭建选择地区",
            "purchase_and_build_selected_regions": "一键购买并搭建选择地区",
            "reinstall_selected_servers": "重装选定云服务器",
            "reinstall_selected_manage_servers": "重装选定云服务器",
            "clear_actionable_manage_resources": "一键清空账号资源",
            "unsubscribe_selected_resources": "退订/删除选定资源",
            "replace_selected_region_ips": "更换新IP",
            "repair_volc_udp_security_group_rules": "补齐UDP安全组规则",
        }
        return labels.get(method_name, method_name)

    def _is_delayed_action_busy(self) -> bool:
        with self.task_lock:
            return bool(self.busy)

    def _delayed_copy_command(self, label: str, action: Callable[[], Any]) -> Callable[[], Any]:
        return lambda: self._run_or_delay_action(label, action)

    def _preconfirmed_delayed_action(self, action: Callable[[], Any]) -> Callable[[], Any]:
        def execute() -> Any:
            self.delayed_skip_next_confirmation = True
            try:
                return action()
            finally:
                self.delayed_skip_next_confirmation = False

        return execute

    @staticmethod
    def _combined_delayed_queue_confirm_message(preconfirm_message: str, queued_summary: str) -> str:
        message = str(preconfirm_message or "").strip()
        queue_summary = str(queued_summary or "").strip()
        queue_tail = f"{queue_summary}\n\n确认后将加入延时执行队列，按顺序自动执行。"
        if not message:
            return f"当前已有任务正在执行。\n\n{queue_tail}"
        merged = re.sub(
            r"(?:请确认是否继续|是否继续)[？?]\s*$",
            "请确认是否继续，另外当前已有任务正在执行。",
            message,
        )
        if merged == message:
            merged = f"{message}\n\n另外当前已有任务正在执行。"
        return f"{merged}\n\n{queue_tail}"

    def _preconfirm_delayed_action_if_needed(
        self,
        action: Callable[[], Any],
        requires_preconfirm: bool,
    ) -> tuple[Callable[[], Any] | None, str]:
        if not requires_preconfirm:
            return action, ""
        self.delayed_preconfirm_active = True
        try:
            action()
        except DelayedActionPreconfirmCaptured as exc:
            return self._preconfirmed_delayed_action(action), str(exc.message or "")
        except DelayedActionPreconfirmAccepted:
            return self._preconfirmed_delayed_action(action), ""
        except DelayedActionPreconfirmCancelled:
            return None, ""
        finally:
            self.delayed_preconfirm_active = False
        return None, ""

    def _current_delayed_action_label(self) -> str:
        if self.delayed_current_action_label:
            return self.delayed_current_action_label
        if self.delayed_action_running_label:
            return self.delayed_action_running_label
        try:
            status = self.status_var.get().strip()
        except Exception:
            status = ""
        if status.startswith("当前任务："):
            status = status[len("当前任务：") :].split("，后续任务", 1)[0].strip()
        if status and status != "就绪":
            return status
        return "当前任务"

    def _delayed_queue_labels(self, extra_label: str = "") -> list[str]:
        labels = [queued_label for queued_label, _ in self.delayed_action_queue]
        if extra_label:
            labels.append(extra_label)
        return labels

    def _delayed_queue_summary(self, extra_label: str = "") -> str:
        pending = self._delayed_queue_labels(extra_label)
        following = "、".join(pending) if pending else "无"
        return f"当前任务：{self._current_delayed_action_label()}，后续任务：{following}"

    def _delayed_action_total(self) -> int:
        return (1 if self._is_delayed_action_busy() else 0) + len(self.delayed_action_queue)

    def _refresh_delayed_queue_status(self) -> None:
        if self._is_delayed_action_busy() or self.delayed_action_queue or self.delayed_action_running_label:
            self.status_var.set(self._delayed_queue_summary())

    def _begin_delayed_queue_result_batch(self, current_label: str = "") -> None:
        if not self.delayed_queue_result_active:
            self.delayed_queue_result_active = True
            self.delayed_queue_result_sequence = 0
            self.delayed_queue_result_current_token = ""
            self.delayed_queue_result_current_label = ""
            self.delayed_queue_result_actions = []
            self.delayed_queue_result_entries = []
        if current_label and not self.delayed_queue_result_current_token:
            self._start_delayed_queue_result_action(current_label)

    def _start_delayed_queue_result_action(self, label: str) -> None:
        if not self.delayed_queue_result_active:
            return
        label = str(label or "当前任务").strip() or "当前任务"
        self.delayed_queue_result_sequence += 1
        token = str(self.delayed_queue_result_sequence)
        self.delayed_queue_result_current_token = token
        self.delayed_queue_result_current_label = label
        self.delayed_queue_result_actions.append((token, label))

    def _record_delayed_queue_result_popup(self, message: str, *, is_error: bool = False) -> bool:
        if not self.delayed_queue_result_active or self.delayed_queue_result_suppress_dialog:
            return False
        if "队列已满" in str(message or ""):
            return False
        token = self.delayed_queue_result_current_token
        label = self.delayed_queue_result_current_label or self._current_delayed_action_label()
        if not token:
            self._start_delayed_queue_result_action(label)
            token = self.delayed_queue_result_current_token
            label = self.delayed_queue_result_current_label
        text = friendly_popup_text(message).strip()
        if not text:
            return True
        self.delayed_queue_result_entries.append(
            {
                "token": token,
                "label": str(label or "当前任务").strip() or "当前任务",
                "message": text,
                "is_error": bool(is_error),
            }
        )
        return True

    def _finish_delayed_queue_result_action(self, label: str = "") -> None:
        if not self.delayed_queue_result_active:
            return
        token = self.delayed_queue_result_current_token
        if not token:
            return
        if not any(entry.get("token") == token for entry in self.delayed_queue_result_entries):
            fallback_label = str(label or self.delayed_queue_result_current_label or "当前任务").strip()
            self.delayed_queue_result_entries.append(
                {
                    "token": token,
                    "label": fallback_label or "当前任务",
                    "message": "已执行完成。",
                    "is_error": False,
                }
            )
        self.delayed_queue_result_current_token = ""
        self.delayed_queue_result_current_label = ""

    def _show_delayed_queue_result_summary_if_done(self) -> None:
        if not self.delayed_queue_result_active:
            return
        if self._is_delayed_action_busy() or self.delayed_action_queue or self.delayed_action_running_label:
            return
        action_order = list(self.delayed_queue_result_actions)
        entries = list(self.delayed_queue_result_entries)
        self.delayed_queue_result_active = False
        self.delayed_queue_result_sequence = 0
        self.delayed_queue_result_current_token = ""
        self.delayed_queue_result_current_label = ""
        self.delayed_queue_result_actions = []
        self.delayed_queue_result_entries = []
        if not action_order and not entries:
            return
        lines = ["火山引擎-队列任务执行完毕，任务结果："]
        seen_tokens: set[str] = set()
        for token, label in action_order:
            group = [entry for entry in entries if entry.get("token") == token]
            if not group:
                group = [{"message": "已执行完成。", "is_error": False}]
            seen_tokens.add(token)
            message = "\n".join(str(entry.get("message") or "").strip() for entry in group if str(entry.get("message") or "").strip())
            prefix = "[失败] " if any(entry.get("is_error") for entry in group) else ""
            lines.append(f"{prefix}{label}：{message or '已执行完成。'}")
        for entry in entries:
            token = str(entry.get("token") or "")
            if token in seen_tokens:
                continue
            label = str(entry.get("label") or "当前任务").strip() or "当前任务"
            prefix = "[失败] " if entry.get("is_error") else ""
            lines.append(f"{prefix}{label}：{str(entry.get('message') or '').strip() or '已执行完成。'}")
        self.show_info("\n\n".join(lines))

    def _run_or_delay_action(
        self,
        label: str,
        action: Callable[[], Any],
        *,
        requires_preconfirm: bool = False,
    ) -> Any:
        if self._is_delayed_action_busy():
            if self._delayed_action_total() >= self.delayed_action_max_total:
                summary = self._delayed_queue_summary()
                self.status_var.set(summary)
                self.show_info(f"当前云延时执行队列已满，最多保留 6 个按序任务。\n\n{summary}\n\n请等待前面的任务完成后再添加。")
                return None
            queued_action, preconfirm_message = self._preconfirm_delayed_action_if_needed(action, requires_preconfirm)
            if queued_action is None:
                return None
            queued_summary = self._delayed_queue_summary(label)
            confirm_message = self._combined_delayed_queue_confirm_message(preconfirm_message, queued_summary)
            if not self.ask_confirm_cancel(confirm_message, ok_text="确认并加入队列任务"):
                return None
            self._begin_delayed_queue_result_batch(self._current_delayed_action_label())
            self.delayed_action_queue.append((label, queued_action))
            self._refresh_delayed_queue_status()
            queue_summary = self._delayed_queue_summary()
            self.log(f"已加入延时执行队列。{queue_summary}\n队列会按顺序执行，前一个任务完成后自动开始下一个。")
            return None
        self.delayed_current_action_label = label
        try:
            result = action()
            if self._is_delayed_action_busy():
                self._refresh_delayed_queue_status()
            return result
        finally:
            if not self._is_delayed_action_busy():
                self.delayed_current_action_label = ""

    def _run_next_delayed_action(self) -> None:
        if self._is_delayed_action_busy() or self.delayed_action_running_label:
            return
        if not self.delayed_action_queue:
            return
        label, action = self.delayed_action_queue.pop(0)
        self.delayed_action_running_label = label
        self.delayed_current_action_label = label
        self._refresh_delayed_queue_status()
        self._start_delayed_queue_result_action(label)
        try:
            action()
        finally:
            if not self._is_delayed_action_busy():
                finished_label = self.delayed_action_running_label
                self.delayed_action_running_label = ""
                self.delayed_current_action_label = ""
                self._finish_delayed_queue_result_action(finished_label)
                if self.delayed_action_queue:
                    self.after(100, self._run_next_delayed_action)
                else:
                    self.after(100, self._show_delayed_queue_result_summary_if_done)
                return

    def _complete_delayed_action_if_idle(self) -> None:
        if self._is_delayed_action_busy():
            return
        finished_label = self.delayed_action_running_label
        self.delayed_action_running_label = ""
        self.delayed_current_action_label = ""
        self._finish_delayed_queue_result_action(finished_label)
        if self.delayed_action_queue:
            self._run_next_delayed_action()
        else:
            self._show_delayed_queue_result_summary_if_done()
        return

    def _cancel_pending_delayed_actions(self, reason: str) -> None:
        queued_labels = [label for label, _action in self.delayed_action_queue]
        if not queued_labels:
            return
        self.delayed_action_queue.clear()
        self._refresh_delayed_queue_status()
        self.log(f"{reason}\n已取消后续队列任务：{'、'.join(queued_labels)}。")

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        pages = create_fixed_tab_pages(self, (("purchase", "购买搭建"), ("manage", "资源管理")))
        purchase_page = pages["purchase"]
        manage_page = pages["manage"]

        self._build_purchase_page(purchase_page)
        self._build_manage_page(manage_page)

    def _build_purchase_page(self, parent: ttk.Frame) -> None:
        content, log_area = build_cloud_page_layout(parent)
        content.rowconfigure(0, weight=0)
        content.rowconfigure(1, weight=0)
        content.rowconfigure(2, weight=1)

        config = ttk.LabelFrame(content, text="任务选项", padding=PANEL_PADDING)
        config.grid(row=0, column=0, sticky="ew", pady=(0, SECTION_GAP))
        config.columnconfigure(0, weight=1, uniform="task_options")
        config.columnconfigure(1, weight=1, uniform="task_options")
        config.columnconfigure(2, weight=1, uniform="task_options")

        spec_section = ttk.Frame(config)
        quantity_section = ttk.Frame(config)
        build_section = ttk.Frame(config)
        spec_section.grid(row=0, column=0, sticky="ew")
        quantity_section.grid(row=0, column=1, sticky="n", padx=(0, 40))
        build_section.grid(row=0, column=2, sticky="e", padx=(0, 20))
        spec_section.columnconfigure(3, weight=1)

        ttk.Label(spec_section, text="购买", width=4, anchor="center").grid(row=0, column=0, sticky="ew", padx=(2, 4), pady=(3, 1))
        ttk.Label(spec_section, text="选项", width=4, anchor="center").grid(row=1, column=0, sticky="ew", padx=(2, 4), pady=(1, 3))
        tk.Frame(spec_section, width=1, bg=BORDER_COLOR).grid(row=0, column=1, rowspan=2, sticky="ns", padx=(4, 8), pady=3)
        spec_row1 = ttk.Frame(spec_section)
        spec_row1.grid(row=0, column=2, sticky="w", pady=(3, 1))
        ttk.Radiobutton(
            spec_row1,
            text=VOLC_SPECS[VOLC_SPEC_2C2G]["label"],
            variable=self.volc_spec_var,
            value=VOLC_SPEC_2C2G,
        ).pack(side=tk.LEFT)
        spec_row2 = ttk.Frame(spec_section)
        spec_row2.grid(row=1, column=2, sticky="w", pady=(1, 3))
        ttk.Radiobutton(
            spec_row2,
            text=VOLC_SPECS[VOLC_SPEC_2C4G]["label"],
            variable=self.volc_spec_var,
            value=VOLC_SPEC_2C4G,
        ).pack(side=tk.LEFT)
        if not self._standalone_limited_features():
            ttk.Checkbutton(
                spec_section,
                text="按需",
                variable=self.volc_on_demand_var,
                command=self._sync_on_demand_billing,
            ).grid(row=1, column=3, sticky="e", padx=(18, 0), pady=(1, 3))

        ttk.Label(quantity_section, text="便捷", width=4, anchor="center").grid(row=0, column=0, sticky="ew", padx=(2, 4), pady=(3, 1))
        ttk.Label(quantity_section, text="数量", width=4, anchor="center").grid(row=1, column=0, sticky="ew", padx=(2, 4), pady=(1, 3))
        tk.Frame(quantity_section, width=1, bg=BORDER_COLOR).grid(row=0, column=1, rowspan=2, sticky="ns", padx=(4, 8), pady=3)
        quantity_row1 = ttk.Frame(quantity_section)
        quantity_row1.grid(row=0, column=2, sticky="w", pady=(3, 1))
        ttk.Checkbutton(
            quantity_row1,
            text=VOLC_CONFIGS[VOLC_CONFIG_FULL]["label"],
            variable=self.volc_config_var,
            onvalue=VOLC_CONFIG_FULL,
            offvalue="",
        ).pack(side=tk.LEFT)
        quantity_row2 = ttk.Frame(quantity_section)
        quantity_row2.grid(row=1, column=2, sticky="w", pady=(1, 3))
        ttk.Checkbutton(
            quantity_row2,
            text=VOLC_CONFIGS[VOLC_CONFIG_BIG_FULL]["label"],
            variable=self.volc_config_var,
            onvalue=VOLC_CONFIG_BIG_FULL,
            offvalue="",
        ).pack(side=tk.LEFT)

        ttk.Label(build_section, text="搭建", width=4, anchor="center").grid(row=0, column=0, sticky="ew", padx=(2, 4), pady=(3, 1))
        ttk.Label(build_section, text="选项", width=4, anchor="center").grid(row=1, column=0, sticky="ew", padx=(2, 4), pady=(1, 3))
        tk.Frame(build_section, width=1, bg=BORDER_COLOR).grid(row=0, column=1, rowspan=2, sticky="ns", padx=(4, 8), pady=3)
        build_row1 = ttk.Frame(build_section)
        build_row1.grid(row=0, column=2, sticky="w", pady=(3, 1))
        ttk.Radiobutton(build_row1, text="随机连接账密", variable=self.volc_build_mode_var, value=VOLC_BUILD_MODE_RANDOM).pack(side=tk.LEFT)
        build_row2 = ttk.Frame(build_section)
        build_row2.grid(row=1, column=2, sticky="w", pady=(1, 3))
        default_radio = ttk.Radiobutton(
            build_row2,
            text="默认连接账密(右键自定义)",
            variable=self.volc_build_mode_var,
            value=VOLC_BUILD_MODE_DEFAULT,
        )
        default_radio.pack(side=tk.LEFT)
        default_radio.bind("<Button-3>", self._show_default_connection_dialog)

        region_panel = ttk.Frame(content, padding=(0, 0, 0, SECTION_GAP))
        region_panel.grid(row=1, column=0, sticky="ew")
        region_panel.columnconfigure(0, weight=1)
        region_panel.rowconfigure(0, weight=0)
        region_panel.rowconfigure(1, weight=0)
        region_panel.rowconfigure(2, weight=0)

        toolbar = ttk.Frame(region_panel)
        toolbar.grid(row=0, column=0, sticky="ew", pady=(0, SECTION_GAP))
        left_actions = ttk.Frame(toolbar)
        left_actions.pack(side=tk.LEFT)
        ttk.Button(left_actions, text="读取账号资源", width=ACTION_BUTTON_WIDTH, command=self._hot_command("scan_resources")).pack(side=tk.LEFT, padx=(0, 6))
        right_actions = ttk.Frame(toolbar)
        right_actions.pack(side=tk.RIGHT)
        ttk.Button(right_actions, text="重装选定云服务器", width=RESOURCE_ACTION_BUTTON_WIDTH, command=self._hot_command("reinstall_selected_servers")).pack(side=tk.LEFT)

        middle = tk.Frame(region_panel, bg=TABLE_BORDER_COLOR, highlightthickness=1, highlightbackground=TABLE_BORDER_COLOR, highlightcolor=TABLE_BORDER_COLOR)
        middle.grid(row=1, column=0, sticky="ew")
        middle.columnconfigure(0, weight=1)
        middle.rowconfigure(0, weight=0)
        self.region_tree = self._make_tree(
            middle,
            columns=("checked", "label", "host_status", "resource_summary", "server_ip", "flavor", "expire_time"),
            headings=("选择", "地区", "状态", "资源", "服务器IP", "规格", "到期时间"),
            widths=(44, 65, 110, 210, 130, 125, 180),
            anchors=("center", "center", "center", "center", "center", "center", "center"),
            height=VOLC_PURCHASE_RESOURCE_TREE_ROWS,
            show_tree=True,
            tree_heading="选择",
            tree_width=54,
        )
        self.region_tree.configure(displaycolumns=("label", "host_status", "resource_summary", "server_ip", "flavor", "expire_time"))
        self.region_tree.grid(row=0, column=0, sticky="nsew", padx=1, pady=1)
        y_scroll = ttk.Scrollbar(middle, command=self.region_tree.yview, style="App.Vertical.TScrollbar")
        y_scroll.grid(row=0, column=1, sticky="ns")
        x_scroll = ttk.Scrollbar(middle, orient=tk.HORIZONTAL, command=self.region_tree.xview, style="App.Horizontal.TScrollbar")
        x_scroll.grid(row=1, column=0, sticky="ew")
        self.region_tree.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
        self.region_tree.bind("<ButtonRelease-1>", self._on_region_click)
        self._ensure_purchase_region_rows(set())

        bottom = ttk.Frame(region_panel)
        bottom.grid(row=2, column=0, sticky="ew", pady=(SECTION_GAP, 0))
        bottom.columnconfigure(5, weight=1)
        ttk.Button(bottom, text="全选地区", width=ACTION_BUTTON_WIDTH, command=self.select_all_regions).grid(row=0, column=0, sticky="w", padx=(0, 6))
        ttk.Button(bottom, text="取消选择", width=ACTION_BUTTON_WIDTH, command=self.clear_region_selection).grid(row=0, column=1, sticky="w", padx=6)
        ttk.Button(bottom, text="购买选择地区", width=ACTION_BUTTON_WIDTH, command=self._hot_command("purchase_selected_regions")).grid(row=0, column=2, sticky="w", padx=6)
        ttk.Button(bottom, text="购买选择地区新IP", width=LONG_ACTION_BUTTON_WIDTH, command=self._hot_command("purchase_selected_region_new_ips")).grid(row=0, column=3, sticky="w", padx=6)
        ttk.Button(bottom, text="搭建选择地区", width=ACTION_BUTTON_WIDTH, command=self._hot_command("start_build_selected_regions")).grid(row=0, column=4, sticky="w", padx=6)
        ttk.Button(bottom, text="一键购买并搭建选择地区", width=PURCHASE_BUILD_BUTTON_WIDTH, command=self._hot_command("purchase_and_build_selected_regions")).grid(row=0, column=6, sticky="e", padx=(6, 0))

        output = ttk.Frame(content)
        output.grid(row=2, column=0, sticky="nsew")
        self.l2tp_text, self.socks_text = build_result_output_panel(
            output,
            copy_l2tp_command=self._delayed_copy_command("复制L2TP结果", self.copy_l2tp_results),
            copy_plain_socks_command=self._delayed_copy_command("复制纯Socks5结果", self.copy_plain_socks_results),
            copy_socks_command=self._delayed_copy_command("复制Socks5结果", self.copy_socks_results),
        )

        self._build_log_panel(log_area, context="purchase")

    def _on_demand_billing_enabled(self) -> bool:
        return not self._standalone_limited_features() and bool(self.volc_on_demand_var.get())

    def _sync_on_demand_billing(self) -> None:
        enabled = self._on_demand_billing_enabled()
        if not enabled and self._standalone_limited_features():
            self.volc_on_demand_var.set(False)
        set_on_demand_debug_enabled("volc", enabled)

    def _build_manage_page(self, parent: ttk.Frame) -> None:
        content, log_area = build_cloud_page_layout(parent)
        content.rowconfigure(1, weight=1)

        top_actions = ttk.Frame(content)
        top_actions.grid(row=0, column=0, sticky="ew", pady=(0, SECTION_GAP))
        top_actions.columnconfigure(2, weight=1)
        ttk.Button(top_actions, text="一键读取账号资源", width=RESOURCE_ACTION_BUTTON_WIDTH, command=self._hot_command("scan_resources", context="manage")).grid(row=0, column=0, padx=(0, 6), pady=4)
        ttk.Label(top_actions, text="(支持Shift键连选)").grid(row=0, column=1, padx=(8, 0), pady=6)
        if not self._standalone_limited_features():
            ttk.Button(top_actions, text="一键清空账号资源", width=RESOURCE_ACTION_BUTTON_WIDTH, command=self._hot_command("clear_actionable_manage_resources")).grid(row=0, column=3, sticky="e", padx=(6, 0), pady=4)
            ttk.Button(top_actions, text="重装选定云服务器", width=RESOURCE_ACTION_BUTTON_WIDTH, command=self._hot_command("reinstall_selected_manage_servers")).grid(row=0, column=4, sticky="e", padx=(6, 0), pady=4)

        table_frame = tk.Frame(content, bg=TABLE_BORDER_COLOR, highlightthickness=1, highlightbackground=TABLE_BORDER_COLOR, highlightcolor=TABLE_BORDER_COLOR)
        table_frame.grid(row=1, column=0, sticky="nsew")
        table_frame.columnconfigure(0, weight=1)
        table_frame.rowconfigure(0, weight=1)
        self.manage_tree = ttk.Treeview(
            table_frame,
            columns=("checked", "region_label", "kind", "name", "status", "billing", "operation", "ip", "server_id", "resource_id"),
            show="tree headings",
            height=18,
            style="Grid.Treeview",
        )
        self.manage_tree.grid(row=0, column=0, sticky="nsew", padx=1, pady=1)
        self.manage_tree.heading("#0", text="选择", anchor="center")
        self.manage_tree.column("#0", width=54, minwidth=54, stretch=False, anchor="center")
        self.manage_tree.configure(displaycolumns=("region_label", "kind", "name", "status", "billing", "operation", "ip", "server_id", "resource_id"))
        columns = [
            ("checked", "✓", 25, "center"),
            ("region_label", "地区", 75, "center"),
            ("kind", "类型", 90, "center"),
            ("name", "名称/IP", 190, "center"),
            ("status", "状态", 200, "center"),
            ("billing", "计费", 80, "center"),
            ("operation", "可执行操作", 150, "center"),
            ("ip", "公网IP", 130, "center"),
            ("server_id", "到期时间", 170, "center"),
            ("resource_id", "规格", 125, "center"),
        ]
        for column, heading, width, anchor in columns:
            self.manage_tree.heading(column, text=heading, anchor=anchor)
            self.manage_tree.column(column, width=width, minwidth=(25 if column == "checked" else 50), stretch=column not in {"checked"}, anchor=anchor)
        self.manage_tree.tag_configure("oddrow", background=TABLE_ALT_BG)
        self.manage_tree.tag_configure("evenrow", background=TEXT_BG)
        self.manage_tree.tag_configure("regiongroup", background=TABLE_GROUP_BG)
        y_scroll = ttk.Scrollbar(table_frame, command=self.manage_tree.yview, style="App.Vertical.TScrollbar")
        y_scroll.grid(row=0, column=1, sticky="ns")
        x_scroll = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.manage_tree.xview, style="App.Horizontal.TScrollbar")
        x_scroll.grid(row=1, column=0, sticky="ew")
        self.manage_tree.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
        self.manage_tree.bind("<ButtonRelease-1>", self._on_manage_click)
        self.manage_tree.bind("<Double-1>", self._on_manage_double_click)
        self.manage_tree.bind("<<TreeviewOpen>>", self._on_manage_tree_open_close)
        self.manage_tree.bind("<<TreeviewClose>>", self._on_manage_tree_open_close)
        self._clear_volc_manage_tree()

        bottom_actions = ttk.Frame(content)
        bottom_actions.grid(row=2, column=0, sticky="ew", pady=(SECTION_GAP, 0))
        ttk.Button(bottom_actions, text="全选资源", width=ACTION_BUTTON_WIDTH, command=lambda: self.set_manage_checked(True)).grid(row=0, column=0, padx=(0, 6), pady=4)
        next_col = 1
        if not self._standalone_limited_features():
            ttk.Button(bottom_actions, text="全选云服务器", width=ACTION_BUTTON_WIDTH, command=self.select_manage_servers).grid(row=0, column=next_col, padx=6, pady=4)
            next_col += 1
        ttk.Button(bottom_actions, text="全选IP", width=ACTION_BUTTON_WIDTH, command=self.select_manage_ips).grid(row=0, column=next_col, padx=6, pady=4)
        next_col += 1
        ttk.Button(bottom_actions, text="选中失败IP", width=ACTION_BUTTON_WIDTH, command=self.select_failed_manage_ips).grid(row=0, column=next_col, padx=6, pady=4)
        next_col += 1
        ttk.Button(bottom_actions, text="全选网络资源", width=LONG_ACTION_BUTTON_WIDTH, command=self.select_manage_vpc_security_groups).grid(row=0, column=next_col, padx=6, pady=4)
        next_col += 1
        ttk.Button(bottom_actions, text="取消选择", width=ACTION_BUTTON_WIDTH, command=lambda: self.set_manage_checked(False)).grid(row=0, column=next_col, padx=6, pady=4)
        next_col += 1
        ttk.Button(bottom_actions, text="补齐UDP规则", width=ACTION_BUTTON_WIDTH, command=self._hot_command("repair_volc_udp_security_group_rules")).grid(row=0, column=next_col, padx=6, pady=4)
        next_col += 1
        bottom_actions.columnconfigure(next_col, weight=1)
        next_col += 1
        replace_ip_button = ttk.Button(bottom_actions, text="更换新IP(右键配置IP库)", width=24, command=self._hot_command("replace_selected_region_ips"))
        replace_ip_button.grid(row=0, column=next_col, sticky="e", padx=6, pady=4)
        replace_ip_button.bind("<Button-3>", self._show_used_ip_table_config_dialog)
        next_col += 1
        ttk.Button(bottom_actions, text="退订/删除选定资源", width=RESOURCE_ACTION_BUTTON_WIDTH, command=self._hot_command("unsubscribe_selected_resources")).grid(row=0, column=next_col, sticky="e", padx=(6, 0), pady=4)

        self._build_log_panel(log_area, context="manage")

    def _build_manage_placeholder(self, parent: ttk.Frame) -> None:
        content, log_area = build_cloud_page_layout(parent)
        ttk.Label(
            content,
            text="火山引擎资源购买、查询、退订接口未接入；当前仅支持已有云服务器 SSH 搭建。",
            anchor="center",
        ).grid(row=0, column=0, sticky="ew", pady=30)
        self._build_log_panel(log_area, context="manage")

    def _make_tree(
        self,
        parent: ttk.Frame,
        *,
        columns: Sequence[str],
        headings: Sequence[str],
        widths: Sequence[int],
        anchors: Optional[Sequence[str]] = None,
        height: int = 10,
        show_tree: bool = False,
        tree_heading: str = "",
        tree_width: int = 75,
    ) -> ttk.Treeview:
        tree = ttk.Treeview(parent, columns=columns, show=("tree headings" if show_tree else "headings"), height=height, style="Grid.Treeview")
        tree.tag_configure("oddrow", background=TABLE_ALT_BG)
        tree.tag_configure("evenrow", background=TEXT_BG)
        if show_tree:
            tree.heading("#0", text=tree_heading, anchor="center")
            tree.column("#0", width=tree_width, minwidth=tree_width, stretch=False, anchor="center")
        for idx, col in enumerate(columns):
            tree.heading(col, text=headings[idx])
            tree.column(
                col,
                width=widths[idx],
                minwidth=(25 if col == "checked" else 50),
                anchor=(anchors[idx] if anchors else "center"),
                stretch=col not in {"checked", "label", "host_status", "server_ip", "expire_time", "flavor"},
            )
        return tree

    def _build_log_panel(self, parent: ttk.Frame, *, context: str) -> None:
        text, _export_button, _stop_button = build_realtime_log_panel(
            parent,
            export_command=lambda c=context: self.export_log_text(c),
            stop_command=self.stop_tasks,
        )
        if context == "manage":
            self.manage_log_text = text
        else:
            self.purchase_log_text = text
        if self.log_text is None:
            self.log_text = text

    def _on_region_click(self, event: tk.Event) -> None:
        if self.region_tree.identify("region", event.x, event.y) not in {"cell", "tree"}:
            return
        item = self.region_tree.identify_row(event.y)
        if not item:
            return
        column_ref = self.region_tree.identify_column(event.x)
        column_name = treeview_display_column_name(self.region_tree, column_ref)
        if column_name == "#0":
            if treeview_click_hits_indicator(self.region_tree, event):
                return
            column_name = "checked"
        if column_name == "server_ip" and self._copy_ip_from_tree_cell(
            self.region_tree,
            event,
            item,
            column_ref,
            self.region_tree.set(item, "server_ip"),
        ):
            return
        if column_name != "checked":
            return
        values = list(self.region_tree.item(item, "values"))
        values[0] = UNCHECKED if values and values[0] == CHECKED else CHECKED
        self.region_tree.item(item, text=values[0], values=values)

    def select_all_regions(self) -> None:
        for iid in self.region_tree.get_children(""):
            values = list(self.region_tree.item(iid, "values"))
            values[0] = CHECKED
            self.region_tree.item(iid, text=values[0], values=values)

    def clear_region_selection(self) -> None:
        for iid in self.region_tree.get_children(""):
            values = list(self.region_tree.item(iid, "values"))
            values[0] = UNCHECKED
            self.region_tree.item(iid, text=values[0], values=values)

    @staticmethod
    def _purchase_region_row_id(plan: RegionPlan, group_index: int = 1) -> str:
        return plan.key if int(group_index or 1) <= 1 else f"{plan.key}-2"

    def _ensure_purchase_region_rows(self, second_regions: Set[str]) -> None:
        if self.region_tree is None:
            return
        existing_values: Dict[str, List[str]] = {}
        for iid in self.region_tree.get_children(""):
            existing_values[iid] = list(self.region_tree.item(iid, "values"))
            self.region_tree.delete(iid)
        order: List[Tuple[RegionPlan, int]] = [(plan, 1) for plan in VOLC_REGION_PLANS]
        order.extend((plan, 2) for plan in VOLC_REGION_PLANS)
        for index, (plan, group_index) in enumerate(order):
            iid = self._purchase_region_row_id(plan, group_index)
            label = self._volc_region_group_label(plan.label, group_index)
            old_values = existing_values.get(iid) or []
            if len(old_values) >= 7:
                values = list(old_values[:7])
                values[1] = label
            elif len(old_values) >= 6:
                values = [old_values[0], label, old_values[1], old_values[2], old_values[3], old_values[4], old_values[5]]
            else:
                values = [UNCHECKED, label, "未读取", "", "", "", ""]
            self.region_tree.insert(
                "",
                tk.END,
                iid=iid,
                text=values[0],
                values=values[:7],
                tags=("evenrow" if index % 2 == 0 else "oddrow",),
            )

    def _refresh_purchase_region_tree_from_items(self, items: Sequence[Dict[str, Any]]) -> None:
        if self.region_tree is None:
            return
        prepared = [dict(item) for item in items]
        second_regions = {
            str(item.get("_region") or "")
            for item in prepared
            if self._volc_manage_item_group_index(item) >= 2
        }
        self._ensure_purchase_region_rows(second_regions)
        grouped: Dict[Tuple[str, int], List[Dict[str, Any]]] = {}
        for item in prepared:
            region = str(item.get("_region") or "")
            group_index = self._volc_manage_item_group_index(item)
            grouped.setdefault((region, group_index), []).append(item)
        for plan in VOLC_REGION_PLANS:
            for group_index in (1, 2):
                group_items = grouped.get((plan.region, group_index)) or []
                counts = {"ecs": 0, "eip": 0, "network_interface": 0, "security_group": 0, "vpc": 0, "subnet": 0}
                for item in group_items:
                    kind = str(item.get("_kind") or "")
                    if kind in counts:
                        counts[kind] += 1
                summary = f"ECS:{counts['ecs']} EIP:{counts['eip']}"
                ecs_items = [item for item in group_items if item.get("_kind") == "ecs"]
                server_ips = [str(item.get("_public_ip") or "") for item in ecs_items if item.get("_public_ip")]
                first_ecs = sorted(
                    ecs_items,
                    key=lambda item: (
                        0 if str(item.get("_name") or "").startswith("wawacloud") else 1,
                        str(item.get("_name") or ""),
                    ),
                )[0] if ecs_items else {}
                self._set_region_values(
                    plan,
                    group_index=group_index,
                    host_status=str(first_ecs.get("_status") or ("已读取" if group_items else "未发现ECS")),
                    resource_summary=summary,
                    server_ips=server_ips[:2],
                    flavor=str(first_ecs.get("_flavor") or ""),
                    expire_time=str(first_ecs.get("_expire_time") or ""),
                )

    def _clear_volc_manage_tree(self) -> None:
        if self.manage_tree is not None:
            for iid in self.manage_tree.get_children(""):
                self.manage_tree.delete(iid)
        self.manage_items = {}
        self.manage_last_clicked_iid = ""

    def _populate_volc_manage_regions(self) -> None:
        self._clear_volc_manage_tree()

    def prepare_for_new_credentials(self) -> None:
        self.volc_config_var.set("")
        self.volc_spec_var.set(VOLC_SPEC_2C2G)
        self.volc_region_server_ips.clear()
        self.volc_region_socks.clear()
        self.volc_region_l2tp.clear()
        self.volc_cached_resource_items = []
        self.volc_cached_resource_regions.clear()
        self.volc_cached_resource_account_scope = ""
        self.volc_cached_resource_updated_at = 0.0
        self.volc_result_account_scope = ""
        self.volc_result_credentials_fingerprint = ""
        self._last_socks_lines = []
        self._last_l2tp_lines = []
        if self.socks_text is not None:
            self.socks_text.delete("1.0", tk.END)
        if self.l2tp_text is not None:
            self.l2tp_text.delete("1.0", tk.END)
        self.volc_build_mode_var.set(VOLC_BUILD_MODE_RANDOM)
        for widget in (self.purchase_log_text, self.manage_log_text):
            if widget is not None:
                widget.delete("1.0", tk.END)
        for iid in self.region_tree.get_children(""):
            values = list(self.region_tree.item(iid, "values"))
            while len(values) < 7:
                values.append("")
            values[1] = str(self.region_tree.set(iid, "label") or "")
            values[2] = ""
            values[3] = ""
            values[4] = ""
            values[5] = ""
            values[6] = ""
            self.region_tree.item(iid, text=values[0], values=values)
        self._clear_volc_manage_tree()
        self.status_var.set("已切换账号，正在读取资源")

    def set_credentials_silently(self, ak: str, sk: str, mark: str = "") -> None:
        self.suspend_customer_guard_trace = True
        try:
            if self.ak_var.get() != (ak or ""):
                self.ak_var.set(ak or "")
            if self.sk_var.get() != (sk or ""):
                self.sk_var.set(sk or "")
            if self.mark_var.get() != (mark or ""):
                self.mark_var.set(mark or "")
        finally:
            self.suspend_customer_guard_trace = False

    def _allow_sensitive_config_dialog(self) -> bool:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        fingerprint = self._customer_guard_fingerprint(ak, sk) if ak and sk else ""
        if fingerprint and self.customer_guard_status == "ok" and self.customer_guard_fingerprint == fingerprint:
            return True
        message = self._customer_guard_block_message(ak, sk)
        self.show_info(self._connection_config_block_message(message))
        return False

    @staticmethod
    def _connection_config_block_message(message: str = "") -> str:
        return "请先填写并通过K/SK合作伙伴校验后再打开配置。"

    def _show_default_connection_dialog(self, _event: Optional[tk.Event] = None) -> str:
        if not self._allow_sensitive_config_dialog():
            return "break"
        win = tk.Toplevel(self.winfo_toplevel())
        win.title(f"{APP_TITLE}-自定义连接配置")
        win.resizable(False, False)
        win.transient(self.winfo_toplevel())
        win.grab_set()
        apply_dialog_icon(win)

        body = ttk.Frame(win, padding=12)
        body.grid(row=0, column=0, sticky="nsew")
        body.columnconfigure(1, weight=1, minsize=520)

        username_var = tk.StringVar(value=self.default_conn_user_var.get().strip() or VOLC_CUSTOM_CONNECTION_DEFAULT_USER)
        password_var = tk.StringVar(value=self.default_conn_password_var.get().strip() or SOCKS_PASSWORD)
        port_var = tk.StringVar(value=self.default_conn_socks_port_var.get().strip() or str(SOCKS_PORT))
        once_var = tk.BooleanVar(value=bool(self.default_conn_once_var.get()))
        rows = (
            ("用户名", username_var),
            ("密码", password_var),
            ("Socks5端口", port_var),
        )
        for row, (label, variable) in enumerate(rows):
            ttk.Label(body, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
            ttk.Entry(body, textvariable=variable, width=28).grid(row=row, column=1, sticky="ew", pady=4)

        actions = ttk.Frame(body)
        actions.grid(row=len(rows), column=0, columnspan=2, sticky="ew", pady=(10, 0))
        actions.columnconfigure(0, weight=1)

        def save() -> None:
            username = username_var.get().strip() or VOLC_CUSTOM_CONNECTION_DEFAULT_USER
            password = password_var.get().strip() or SOCKS_PASSWORD
            port_text = port_var.get().strip() or str(SOCKS_PORT)
            if not port_text.isdigit() or not (1 <= int(port_text) <= 65535):
                messagebox.showinfo(APP_TITLE, friendly_popup_text("Socks5端口请输入 1-65535 之间的数字。"))
                return
            self.default_conn_user_var.set(username)
            self.default_conn_password_var.set(password)
            self.default_conn_socks_port_var.set(port_text)
            self.default_conn_custom_saved = True
            self.default_conn_once_var.set(bool(once_var.get()))
            win.destroy()

        ttk.Checkbutton(actions, text="生效1次", variable=once_var).grid(row=0, column=0, sticky="w")
        button_bar = ttk.Frame(actions)
        button_bar.grid(row=0, column=1, sticky="e")
        ttk.Button(button_bar, text="应用", width=10, command=save).pack(side=tk.LEFT, padx=(0, 8))
        ttk.Button(button_bar, text="取消", width=10, command=win.destroy).pack(side=tk.LEFT)
        self._center_dialog(win, min_width=360, min_height=180)
        return "break"

    @staticmethod
    def _shell_env_assignments(values: Dict[str, str]) -> str:
        return shell_env_assignments(values)

    def _default_connection_env(self) -> Dict[str, str]:
        username = self.default_conn_user_var.get().strip() or VOLC_CUSTOM_CONNECTION_DEFAULT_USER
        password = self.default_conn_password_var.get().strip() or SOCKS_PASSWORD
        socks_port = self.default_conn_socks_port_var.get().strip() or str(SOCKS_PORT)
        return {
            "VPN_USER_PREFIX": L2TP_USER_PREFIX,
            "VPN_PASSWORD": password,
            "VPN_IPSEC_PSK": password,
            "SOCKS_USER": username,
            "SOCKS_PASSWORD": password,
            "SOCKS_PORT": socks_port,
            "XRAY_USER": username,
            "XRAY_PASS": password,
            "XRAY_PORT": socks_port,
            "PSK_KEY": password,
            "VPN_PASS": password,
        }

    def _default_connection_confirm_details(self) -> List[str]:
        if not self.default_conn_custom_saved or self.volc_build_mode_var.get().strip() != VOLC_BUILD_MODE_DEFAULT:
            return []
        env = self._default_connection_env()
        effect_label = "生效1次" if self.default_conn_once_var.get() else "本次软件运行期间保持"
        return [
            f"自定义连接配置：{effect_label}",
            f"用户名：{env['SOCKS_USER']}",
            f"密码：{env['SOCKS_PASSWORD']}（L2TP PSK密钥相同）",
            f"Socks5端口：{env['SOCKS_PORT']}",
        ]

    def _reset_default_connection_config(self) -> None:
        self.default_conn_user_var.set(VOLC_CUSTOM_CONNECTION_DEFAULT_USER)
        self.default_conn_password_var.set(SOCKS_PASSWORD)
        self.default_conn_socks_port_var.set(str(SOCKS_PORT))
        self.default_conn_custom_saved = False
        self.default_conn_once_var.set(False)

    def _reset_one_time_build_config_after_task(self) -> None:
        if self.volc_build_mode_var.get().strip() != VOLC_BUILD_MODE_DEFAULT:
            return
        if self.default_conn_custom_saved and self.default_conn_once_var.get():
            self._reset_default_connection_config()

    def _volc_build_spec_label(self, *, build_enabled: bool) -> str:
        if not build_enabled:
            return "未搭建"
        mode = self.volc_build_mode_var.get().strip() or VOLC_BUILD_MODE_RANDOM
        return "默认连接账密(右键自定义)" if mode == VOLC_BUILD_MODE_DEFAULT else "随机连接账密"

    def _volc_build_confirm_details(self, *, build_enabled: bool) -> List[str]:
        if not build_enabled:
            return ["搭建信息：未搭建"]
        details = [
            "搭建信息：",
            f"搭建选项：{self._volc_build_spec_label(build_enabled=True)}",
        ]
        details.extend(self._default_connection_confirm_details())
        return details

    def _volc_task_popup_details(
        self,
        plans: Sequence[RegionPlan],
        *,
        purchase_spec: str,
        build_enabled: bool,
    ) -> str:
        build_spec = self._volc_build_spec_label(build_enabled=build_enabled)
        lines = [
            f"{plan.label}|购买规格:{purchase_spec}|搭建规格:{build_spec}"
            for plan in plans
        ]
        return "\n\n明细:\n" + "\n".join(lines) if lines else ""

    def _volc_remote_command(self) -> str:
        if self.volc_build_mode_var.get().strip() == VOLC_BUILD_MODE_RANDOM:
            return VOLC_RANDOM_REMOTE_COMMAND
        if not self.default_conn_custom_saved:
            return VOLC_DEFAULT_REMOTE_COMMAND
        return configured_default_script_command(VOLC_DEFAULT_SCRIPT_URL, self._default_connection_env())

    @staticmethod
    def _credentials_ready_for_customer_guard(ak: str, sk: str) -> bool:
        return len((ak or "").strip()) >= 16 and len((sk or "").strip()) >= 16

    def _set_customer_guard_controls_enabled(self, enabled: bool) -> None:
        excluded_texts = {"粘贴", "保存", "取消", "确认"}
        state_value = "normal" if enabled else "disabled"
        ttk_state = ["!disabled"] if enabled else ["disabled"]

        def walk(widget: tk.Misc) -> None:
            for child in widget.winfo_children():
                class_name = str(child.winfo_class())
                if class_name in {"TButton", "TRadiobutton", "TCheckbutton", "Button", "Radiobutton", "Checkbutton"}:
                    try:
                        text = str(child.cget("text") or "")
                    except Exception:
                        text = ""
                    if text not in excluded_texts and "注册绑定链接" not in text and "获取Ak/Sk教程" not in text and "获取AK/SK教程" not in text:
                        try:
                            child.state(ttk_state)  # type: ignore[attr-defined]
                        except Exception:
                            try:
                                child.configure(state=state_value)
                            except Exception:
                                pass
                walk(child)

        walk(self)

    @staticmethod
    def _customer_guard_fingerprint(ak: str, sk: str) -> str:
        ak_value = (ak or "").strip()
        sk_value = (sk or "").strip()
        if not ak_value or not sk_value:
            return ""
        return f"{ak_value}\n{sk_value}"

    def _reset_customer_guard_state(self) -> None:
        old_job = self.customer_guard_after_id
        self.customer_guard_after_id = None
        if old_job:
            try:
                self.after_cancel(old_job)
            except tk.TclError:
                pass
        self.customer_guard_status = "idle"
        self.customer_guard_fingerprint = ""
        self.customer_guard_message = ""
        self.customer_guard_notice_fingerprint = ""
        self.customer_guard_log_fingerprint = ""
        self.customer_guard_pending_resource_query = None
        self._close_customer_guard_dialog()
        self._set_customer_guard_controls_enabled(True)

    def _on_customer_guard_credentials_changed(self, *_args: Any) -> None:
        if self.suspend_customer_guard_trace:
            return
        self._schedule_customer_guard_check()

    def _schedule_customer_guard_check(self, delay_ms: int = 900, force: bool = False) -> None:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        fingerprint = self._customer_guard_fingerprint(ak, sk)
        if not fingerprint or not self._credentials_ready_for_customer_guard(ak, sk):
            self._reset_customer_guard_state()
            return
        if (
            not force
            and self.customer_guard_status in {"ok", "blocked", "checking"}
            and self.customer_guard_fingerprint == fingerprint
        ):
            return
        old_job = self.customer_guard_after_id
        self.customer_guard_after_id = None
        if old_job:
            try:
                self.after_cancel(old_job)
            except tk.TclError:
                pass
        self.customer_guard_token += 1
        token = self.customer_guard_token
        self.customer_guard_status = "checking"
        self.customer_guard_fingerprint = fingerprint
        self.customer_guard_message = CUSTOMER_OWNERSHIP_CHECKING_MESSAGE
        if self.active_task_count == 0:
            self.status_var.set(CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE)
        if self.customer_guard_log_fingerprint != fingerprint:
            self.customer_guard_log_fingerprint = fingerprint
            self._log_customer_guard_message(CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE)

        def start_check() -> None:
            self.customer_guard_after_id = None

            def worker() -> None:
                status = "blocked"
                message = CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
                customer_guard_module = None
                try:
                    from Volcengine.api.volcengine import customer_guard as customer_guard_module

                    ok, guard_message, _detail = customer_guard_module.validate_customer_ownership(ak, sk)
                    if ok:
                        status = "ok"
                    message = guard_message or CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
                except Exception as exc:
                    status = "blocked"
                    message = classify_auth_exception(exc)
                    if customer_guard_module is not None:
                        detailed_message = customer_guard_module.classify_customer_guard_error(exc)
                        if detailed_message != SERVER_ERROR_MESSAGE:
                            message = detailed_message
                self.after(0, lambda: self._apply_customer_guard_result(token, fingerprint, status, message))

            threading.Thread(target=worker, daemon=True).start()

        self.customer_guard_after_id = self.after(delay_ms, start_check)

    def _apply_customer_guard_result(self, token: int, fingerprint: str, status: str, message: str) -> None:
        if token != self.customer_guard_token:
            return
        if self._customer_guard_fingerprint(self.ak_var.get(), self.sk_var.get()) != fingerprint:
            return
        self.customer_guard_status = status
        self.customer_guard_fingerprint = fingerprint
        self.customer_guard_message = message or CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
        if status == "ok":
            self.status_var.set(CUSTOMER_OWNERSHIP_WELCOME_MESSAGE)
            self._log_customer_guard_message(_customer_ownership_result_log_message(status, message))
            self._set_customer_guard_controls_enabled(True)
            self._close_customer_guard_dialog()
            pending = self.customer_guard_pending_resource_query
            self.customer_guard_pending_resource_query = None
            if pending:
                self.after(100, lambda args=pending: self.scan_resources(*args[0], **args[1]))
            return
        failure_message = message or CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
        self.status_var.set(failure_message)
        self._log_customer_guard_message(_customer_ownership_result_log_message(status, failure_message))
        self._set_customer_guard_controls_enabled(False)
        if self.customer_guard_notice_fingerprint != fingerprint:
            self.customer_guard_notice_fingerprint = fingerprint
            self._show_customer_guard_dialog(failure_message)

    def _customer_guard_block_message(self, ak: str = "", sk: str = "") -> str:
        ak_value = (ak or self.ak_var.get()).strip()
        sk_value = (sk or self.sk_var.get()).strip()
        if not ak_value or not sk_value:
            return "请先填写或粘贴火山引擎 AK/SK。"
        if not self._credentials_ready_for_customer_guard(ak_value, sk_value):
            return "请填写完整火山引擎 AK/SK。"
        fingerprint = self._customer_guard_fingerprint(ak_value, sk_value)
        if self.customer_guard_status == "ok" and self.customer_guard_fingerprint == fingerprint:
            return ""
        if self.customer_guard_status == "checking" and self.customer_guard_fingerprint == fingerprint:
            return CUSTOMER_OWNERSHIP_CHECKING_MESSAGE
        if self.customer_guard_fingerprint == fingerprint and self.customer_guard_message:
            return self.customer_guard_message
        self._schedule_customer_guard_check(delay_ms=100)
        return CUSTOMER_OWNERSHIP_CHECKING_MESSAGE

    def _show_customer_guard_block(self, message: str, context: str = "purchase") -> None:
        if not message:
            return
        log_message = CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE if message == CUSTOMER_OWNERSHIP_CHECKING_MESSAGE else message
        fingerprint = self._customer_guard_fingerprint(self.ak_var.get(), self.sk_var.get())
        checking_already_logged = (
            message == CUSTOMER_OWNERSHIP_CHECKING_MESSAGE
            and bool(fingerprint)
            and self.customer_guard_log_fingerprint == fingerprint
        )
        if not checking_already_logged:
            self._log_customer_guard_message(log_message)
        if message == CUSTOMER_OWNERSHIP_CHECKING_MESSAGE and self.active_task_count == 0:
            self.status_var.set(CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE)
            return
        failure_message = message or CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
        self.status_var.set(failure_message)
        self._set_customer_guard_controls_enabled(False)
        self._show_customer_guard_dialog(failure_message)

    def _append_customer_guard_log_widget(self, widget: Optional[tk.Text], text: str) -> None:
        if widget is None:
            return
        try:
            previous = widget.get("end-2l", "end-1c").strip()
        except Exception:
            previous = ""
        if previous == text:
            return
        line = text if text.endswith("\n") else text + "\n"
        widget.insert(tk.END, line)
        try:
            line_count = int(str(widget.index("end-1c")).split(".")[0])
            if line_count > 2500:
                widget.delete("1.0", f"{line_count - 2200}.0")
        except Exception:
            pass
        widget.see(tk.END)

    def _log_customer_guard_message(self, message: str) -> None:
        text = str(message or "").strip()
        if not text:
            return
        line = text if text.endswith("\n") else text + "\n"
        for widget in (self.purchase_log_text, self.manage_log_text):
            self._append_customer_guard_log_widget(widget, text)
        for context in ("purchase", "manage"):
            self._append_volc_log_file(line, context)

    def _mark_customer_resource_query_ready(self) -> None:
        if self.customer_guard_status != "ok":
            return
        if self.customer_guard_fingerprint != self._customer_guard_fingerprint(self.ak_var.get(), self.sk_var.get()):
            return
        self.status_var.set(CUSTOMER_OWNERSHIP_READY_MESSAGE)
        self._log_customer_guard_message(CUSTOMER_OWNERSHIP_READY_MESSAGE)

    def _copy_ip_from_tree_cell(
        self,
        tree: ttk.Treeview,
        event: tk.Event,
        row_id: str,
        column_ref: str,
        value: Any,
    ) -> bool:
        ips = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(value or ""))
        if not ips:
            return False
        selected_ip = ips[0]
        if len(ips) > 1:
            try:
                bbox = tree.bbox(row_id, column_ref)
                if bbox and bbox[2] > 0:
                    relative_x = max(0, min(int(event.x) - int(bbox[0]), int(bbox[2]) - 1))
                    selected_ip = ips[min(len(ips) - 1, int(relative_x / max(1, int(bbox[2])) * len(ips)))]
            except Exception:
                selected_ip = ips[0]
        self.clipboard_clear()
        self.clipboard_append(selected_ip)
        self.status_var.set(f"已复制服务器IP：{selected_ip}")
        return True

    def _on_manage_click(self, event: tk.Event) -> None:
        if self.manage_tree is None:
            return
        region = self.manage_tree.identify("region", event.x, event.y)
        if region not in {"cell", "tree"}:
            return
        row_id = self.manage_tree.identify_row(event.y)
        if not row_id:
            return
        item = self.manage_items.get(row_id) or {}
        column_ref = self.manage_tree.identify_column(event.x)
        column_name = treeview_display_column_name(self.manage_tree, column_ref)
        if column_name == "#0":
            if treeview_click_hits_indicator(self.manage_tree, event):
                return
            column_name = "checked"
        if column_name == "ip" and self._copy_ip_from_tree_cell(
            self.manage_tree,
            event,
            row_id,
            column_ref,
            self.manage_tree.set(row_id, "ip"),
        ):
            return
        if item.get("_kind") == "region_group" and column_name == "operation":
            self._toggle_volc_manage_group_open(row_id)
            return
        if column_name != "checked":
            return
        values = list(self.manage_tree.item(row_id, "values"))
        if not values:
            return
        target = CHECKED if values[0] == UNCHECKED else UNCHECKED
        shift_pressed = bool(int(getattr(event, "state", 0)) & 0x0001)
        if shift_pressed and self.manage_last_clicked_iid:
            ordered = self._volc_manage_visible_iids(include_groups=False)
            if row_id in ordered and self.manage_last_clicked_iid in ordered:
                start = ordered.index(self.manage_last_clicked_iid)
                end = ordered.index(row_id)
                if start > end:
                    start, end = end, start
                for iid in ordered[start : end + 1]:
                    self._set_volc_manage_row_checked(iid, target)
                self._refresh_volc_manage_group_checks()
                return
        if item.get("_kind") == "region_group":
            self._set_volc_manage_group_checked(row_id, target)
        else:
            self._set_volc_manage_row_checked(row_id, target)
            self.manage_last_clicked_iid = row_id
        self._refresh_volc_manage_group_checks()

    def _on_manage_double_click(self, event: tk.Event) -> str:
        if self.manage_tree is None:
            return "break"
        row_id = self.manage_tree.identify_row(event.y)
        if not row_id:
            return "break"
        item = self.manage_items.get(row_id) or {}
        if item.get("_kind") == "region_group":
            self._toggle_volc_manage_group_open(row_id)
        return "break"

    def _on_manage_tree_open_close(self, _event: tk.Event) -> None:
        if self.manage_tree is None:
            return
        row_id = self.manage_tree.focus()
        if row_id and (self.manage_items.get(row_id) or {}).get("_kind") == "region_group":
            self._sync_volc_manage_group_toggle_text(row_id)

    def _volc_manage_visible_iids(self, *, include_groups: bool = True) -> List[str]:
        if self.manage_tree is None:
            return []
        ordered: List[str] = []

        def walk(parent: str = "") -> None:
            for iid in self.manage_tree.get_children(parent):
                item = self.manage_items.get(iid) or {}
                if include_groups or item.get("_kind") != "region_group":
                    ordered.append(iid)
                if self.manage_tree.item(iid, "open"):
                    walk(iid)

        walk("")
        return ordered

    def _volc_all_manage_iids(self, parent: str = "") -> List[str]:
        if self.manage_tree is None:
            return []
        ordered: List[str] = []
        for iid in self.manage_tree.get_children(parent):
            ordered.append(iid)
            ordered.extend(self._volc_all_manage_iids(iid))
        return ordered

    def _set_volc_manage_row_checked(self, iid: str, value: str) -> None:
        if self.manage_tree is None:
            return
        values = list(self.manage_tree.item(iid, "values"))
        if values:
            values[0] = value
            self.manage_tree.item(iid, text=value, values=values)

    def _set_volc_manage_group_checked(self, group_iid: str, value: str) -> None:
        self._set_volc_manage_row_checked(group_iid, value)
        for child in self._volc_all_manage_iids(group_iid):
            self._set_volc_manage_row_checked(child, value)

    def _toggle_volc_manage_group_open(self, group_iid: str) -> None:
        if self.manage_tree is None:
            return
        self._set_volc_manage_group_open(group_iid, not bool(self.manage_tree.item(group_iid, "open")))

    def _set_volc_manage_group_open(self, group_iid: str, opened: bool) -> None:
        if self.manage_tree is None:
            return
        self.manage_tree.item(group_iid, open=opened)
        self._sync_volc_manage_group_toggle_text(group_iid)

    def _sync_volc_manage_group_toggle_text(self, group_iid: str) -> None:
        if self.manage_tree is None:
            return
        values = list(self.manage_tree.item(group_iid, "values"))
        while len(values) <= 6:
            values.append("")
        values[6] = "－" if bool(self.manage_tree.item(group_iid, "open")) else "＋"
        self.manage_tree.item(group_iid, values=values)

    def _refresh_volc_manage_group_checks(self) -> None:
        if self.manage_tree is None:
            return
        for group_iid in self.manage_tree.get_children(""):
            item = self.manage_items.get(group_iid) or {}
            if item.get("_kind") != "region_group":
                continue
            children = [
                iid
                for iid in self._volc_all_manage_iids(group_iid)
                if (self.manage_items.get(iid) or {}).get("_kind") != "region_group"
            ]
            if not children:
                self._set_volc_manage_row_checked(group_iid, UNCHECKED)
                continue
            child_values = [self.manage_tree.item(iid, "values") for iid in children]
            checked_count = sum(1 for values in child_values if values and values[0] == CHECKED)
            self._set_volc_manage_row_checked(group_iid, CHECKED if checked_count == len(children) else UNCHECKED)

    def set_manage_checked(self, checked: bool) -> None:
        if self.manage_tree is None:
            return
        mark = CHECKED if checked else UNCHECKED
        for iid in self._volc_all_manage_iids():
            self._set_volc_manage_row_checked(iid, mark)
        self._refresh_volc_manage_group_checks()

    def select_manage_vpc_security_groups(self) -> None:
        if self.manage_tree is None:
            return
        selected = 0
        for iid in self._volc_all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            kind = str(item.get("_kind") or "")
            # 网络资源统一进入删除队列；队列会按 VPC 依赖关系先处理网卡、安全组、子网，最后删除 VPC。
            is_target = (
                kind == "network_interface"
                or kind == "security_group"
                or kind == "subnet"
                or kind == "vpc"
            )
            if values:
                values[0] = CHECKED if is_target else UNCHECKED
                self.manage_tree.item(iid, text=values[0], values=values)
            if is_target:
                selected += 1
        self._refresh_volc_manage_group_checks()
        if not selected:
            self.log("当前没有可选择的火山引擎网卡/安全组/子网/VPC。")

    def select_manage_servers(self) -> None:
        if self._standalone_limited_features():
            return
        if self.manage_tree is None:
            return
        selected = 0
        for iid in self._volc_all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            is_server = item.get("_kind") == "ecs"
            if values:
                values[0] = CHECKED if is_server else UNCHECKED
                self.manage_tree.item(iid, text=values[0], values=values)
            if is_server:
                selected += 1
        self._refresh_volc_manage_group_checks()
        if not selected:
            self.log("当前没有读取到可选择的火山引擎云服务器。")

    def select_manage_ips(self) -> None:
        if self.manage_tree is None:
            return
        selected = 0
        for iid in self._volc_all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            is_ip = item.get("_kind") in {"eip", "ip"}
            if values:
                values[0] = CHECKED if is_ip else UNCHECKED
                self.manage_tree.item(iid, text=values[0], values=values)
            if is_ip:
                selected += 1
        self._refresh_volc_manage_group_checks()
        if not selected:
            self.log("当前没有读取到可选择的火山引擎 IP。")

    def select_failed_manage_ips(self) -> Tuple[int, int, int]:
        if self.manage_tree is None:
            return 0, 0, 0
        path = self._used_ip_table_path()
        if path.suffix.lower() != ".xlsx":
            migration_error = str(getattr(self, "_used_ip_table_migration_error", "") or "").strip()
            detail = f"；CSV自动升级失败：{migration_error}" if migration_error else ""
            self.log(
                f"更换失败IP库需要使用 .xlsx 旧IP库{detail}。请先在右键配置中保存或链接 Excel 文件。",
                "manage",
            )
            return 0, 0, 0
        with self._used_ip_table_file_guard(path):
            failed_rows = self._read_failed_ip_rows(path)
        failed_ips = {
            self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]))
            for row in failed_rows
        }
        failed_ips.discard("")
        matched_ips: Set[str] = set()
        for iid in self._volc_all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            public_ip = self._normalize_used_ip_value(item.get("_public_ip"))
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            is_bound = bool(item.get("_associated_id") or self._volc_eip_associated_instance_id(raw))
            is_target = item.get("_kind") == "eip" and is_bound and public_ip in failed_ips
            if values:
                values[0] = CHECKED if is_target else UNCHECKED
                self.manage_tree.item(iid, text=values[0], values=values)
            if is_target:
                matched_ips.add(public_ip)
        self._refresh_volc_manage_group_checks()
        missing_count = len(failed_ips - matched_ips)
        if matched_ips:
            self.log(
                f"已选择更换失败IP {len(matched_ips)} 条"
                + (f"，另有 {missing_count} 条已不在当前资源中。" if missing_count else "。"),
                "manage",
            )
        elif failed_ips:
            self.log("更换失败IP库中的IP均不在当前已读取资源中。", "manage")
        else:
            self.log("更换失败IP库为空。", "manage")
        return len(matched_ips), missing_count, len(failed_ips)

    def clear_actionable_manage_resources(self) -> None:
        if self._standalone_limited_features():
            return
        items = self._volc_actionable_clear_items()
        if not items:
            self.log("当前没有读取到可退订/删除/释放的火山引擎资源。", "manage")
            return
        release_ip_groups = self._volc_release_ip_groups(items)
        release_action = self._ask_cleanup_release_action(
            self._volc_manage_confirm_message("清空账号资源", items),
            allow_used_ip_import=bool(release_ip_groups),
        )
        if release_action == "cancel":
            self.log("已取消火山引擎一键清空账号资源。", "manage")
            return
        if release_action == "confirm_and_import":
            try:
                self._import_volc_release_ip_groups(release_ip_groups)
            except Exception as exc:
                message = f"导入本次待释放IP失败，已停止一键清空账号资源：{exc}"
                self.log(message, "manage")
                self.show_info(message)
                return
        self._run_volc_delete_items("清空账号资源", items)

    def _selected_purchase_region_groups(self) -> Dict[str, Set[int]]:
        selected: Dict[str, Set[int]] = {}
        by_key = {plan.key: plan for plan in VOLC_REGION_PLANS}
        for iid in self.region_tree.get_children(""):
            values = list(self.region_tree.item(iid, "values"))
            base_iid = iid[:-2] if iid.endswith("-2") else iid
            if values and values[0] == CHECKED and base_iid in by_key:
                plan = by_key[base_iid]
                group_index = 2 if iid.endswith("-2") else 1
                selected.setdefault(plan.region, set()).add(group_index)
        return selected

    def _selected_region_plans(self) -> List[RegionPlan]:
        selected_groups = self._selected_purchase_region_groups()
        return [plan for plan in VOLC_REGION_PLANS if plan.region in selected_groups]

    def _volc_config_summary(self) -> str:
        config_key = self.volc_config_var.get().strip()
        config = VOLC_CONFIGS.get(config_key, VOLC_CONFIGS[VOLC_CONFIG_FULL])
        if config_key not in VOLC_CONFIGS:
            config = dict(config)
            config["label"] = self._volc_config_choice_label(config_key)
        spec_key = self.volc_spec_var.get().strip()
        spec = VOLC_SPECS.get(spec_key, {})
        spec_label = spec.get("label", "未选择规格")
        return (
            f"{spec_label}；{config['label']}；单地区IP配额={VOLC_IP_QUOTA_PER_REGION}条；"
            f"选择地区目标IP={config['ip_per_region']}条"
        )

    def _volc_config_choice_label(self, config_key: Optional[str] = None) -> str:
        selected_key = self.volc_config_var.get().strip() if config_key is None else config_key
        if selected_key in VOLC_CONFIGS:
            config = VOLC_CONFIGS[selected_key]
            return VOLC_AUTOBUY_SOURCE_MODES.get(selected_key, config["label"])
        config = VOLC_CONFIGS[VOLC_CONFIG_FULL]
        return f"按已选择地区执行（每地区{config['ip_per_region']}条）"

    @staticmethod
    def _purchase_confirm_message(action_name: str, plans: Sequence[RegionPlan], details: Sequence[str]) -> str:
        lines = [
            f"开始{action_name}{len(plans)}个地区",
            "",
            f"选择地区:{'、'.join(plan.label for plan in plans)}",
        ]
        clean_details = [str(item).strip() for item in details if str(item).strip()]
        if clean_details:
            lines.extend(["", *clean_details])
        lines.extend(["", "该操作会创建云资源并产生费用。", "", "请确认是否继续?"])
        return "\n".join(lines)

    @staticmethod
    def _volc_new_ip_result_popup_message(message: str, *, success: int, warning: str = "") -> str:
        details: List[str] = []
        if success > 0:
            details.append(VOLC_NEW_IP_RESULT_REMINDER)
        warning_text = str(warning or "").strip()
        if warning_text:
            details.append(warning_text)
        return f"{message}\n\n" + "\n\n".join(details) if details else message

    def _volc_purchase_billing_label(self) -> str:
        if self._on_demand_billing_enabled():
            return "ECS按需计费调试模式，仅供测试"
        return "ECS包年包月1个月，自动续费"

    def _selected_volc_config_or_warn(self) -> str:
        config_key = self.volc_config_var.get().strip()
        if self.volc_spec_var.get().strip() not in VOLC_SPECS:
            self.log("请先选择火山引擎任务选项规格。")
            return ""
        return config_key if config_key in VOLC_CONFIGS else "__selected_regions__"

    def _start_volc_log_file(self, name: str, context: str) -> None:
        try:
            section = self._volc_log_section(context)
            action = self._volc_log_action(name, context)
            prefix = "volc-manage" if context == "manage" else "volc-purchase"
            self.volc_log_action_names[context] = action
            self.volc_log_file_paths[context] = str(create_task_log_path("Volcengine", section, action, prefix=prefix, account_key=self._account_log_scope()))
            self._append_volc_log_file(f"===== {name} 开始 {time.strftime('%Y-%m-%d %H:%M:%S')} =====\n", context)
            self._append_volc_log_file(f"日志文件: {self.volc_log_file_paths[context]}\n", context)
        except Exception:
            self.volc_log_file_paths[context] = ""

    def _account_log_scope(self) -> str:
        """用当前火山 AK/SK 区分日志目录，避免多个软件实例同时运行时串日志。"""
        return credential_scope_label(self.ak_var.get(), self.sk_var.get())

    def _volc_log_section(self, context: str) -> str:
        return "资源管理" if context == "manage" else "购买搭建"

    def _volc_log_action(self, name: str, context: str) -> str:
        if context == "manage":
            return name
        if "购买" in name and "搭建" in name:
            return "一键购买搭建"
        if "购买" in name:
            return "购买地区"
        if "搭建" in name:
            return "搭建地区"
        return name

    def _append_volc_log_file(self, text: str, context: str = "purchase") -> None:
        path = self.volc_log_file_paths.get(context, "")
        if not path:
            return
        try:
            text = sanitize_log_text(text)
            encoding = "utf-8-sig" if not os.path.exists(path) or os.path.getsize(path) == 0 else "utf-8"
            with open(path, "a", encoding=encoding, errors="replace") as fh:
                fh.write(text)
        except Exception:
            pass

    def _log_widget_for_context(self, context: str) -> Optional[tk.Text]:
        if context == "manage":
            return self.manage_log_text
        return self.purchase_log_text

    def _is_build_log_context(self, context: str) -> bool:
        if context == "manage":
            return False
        return "搭建" in self.volc_log_action_names.get(context, "")

    def log(self, message: str, context: Optional[str] = None) -> None:
        message = sanitize_log_text(message)
        line = message.rstrip() + "\n"
        filtered_line = filter_realtime_log_text(line)
        if context:
            contexts = [context]
        elif self.active_task_contexts:
            contexts = list(self.active_task_contexts.keys())
        else:
            contexts = [self.current_log_context or "purchase"]
        display_line = ""
        wrote = False
        for item_context in contexts:
            target = self._log_widget_for_context(item_context)
            if target is None:
                continue
            is_resource_query = (
                self.active_task_contexts.get(item_context, 0) > 0
                and "读取账号资源" in self.volc_log_action_names.get(item_context, "")
            )
            if is_resource_query:
                display_line = self.realtime_log_filter.limit_notice(filtered_line, item_context) if is_failure_message(line) else ""
            elif self._is_build_log_context(item_context):
                display_line = format_realtime_log_text_without_filter(line)
            else:
                display_line = self.realtime_log_filter.limit_notice(filtered_line, item_context)
            if display_line:
                target.insert(tk.END, display_line)
                try:
                    line_count = int(str(target.index("end-1c")).split(".")[0])
                    if line_count > 2500:
                        target.delete("1.0", f"{line_count - 2200}.0")
                except Exception:
                    pass
                target.see(tk.END)
            self._append_volc_log_file(line, item_context)
            wrote = True
        if not wrote and self.log_text is not None:
            display_line = self.realtime_log_filter.limit_notice(filtered_line, contexts[0] if contexts else "default")
        if not wrote and self.log_text is not None and display_line:
            self.log_text.insert(tk.END, display_line)
            self.log_text.see(tk.END)

    def _export_log_entries(self) -> List[Tuple[str, str, str]]:
        entries: List[Tuple[str, str, str]] = []
        for log_context, target in (("purchase", self.purchase_log_text), ("manage", self.manage_log_text)):
            if target is None:
                continue
            text = target.get("1.0", tk.END).strip()
            if not text:
                continue
            entries.append((self._volc_log_section(log_context), self.volc_log_action_names.get(log_context, "界面导出日志"), text))
        return entries

    def export_log_text(self, context: Optional[str] = None) -> None:
        entries = self._export_log_entries()
        if not entries:
            target = self._log_widget_for_context(context or self.current_log_context or "purchase")
            if target is None:
                target = self.log_text
            text = target.get("1.0", tk.END).strip() if target is not None else ""
            if text:
                log_context = context or self.current_log_context or "purchase"
                entries.append((self._volc_log_section(log_context), self.volc_log_action_names.get(log_context, "界面导出日志"), text))
        if not entries:
            messagebox.showinfo(APP_TITLE, friendly_popup_text("当前没有实时日志可导出"))
            return
        path = export_log_to_desktop(
            "Volcengine",
            "全部日志",
            "导出日志",
            "",
            account_key=self._account_log_scope(),
            current_logs=entries,
            recent_sections=[self._volc_log_section("purchase"), self._volc_log_section("manage")],
        )
        messagebox.showinfo(APP_TITLE, friendly_popup_text(f"日志压缩包已导出到桌面：\n{path}\n\n请把日志压缩包发给维护人员，简要说明情况即可。"))

    def copy_log_text(self, context: Optional[str] = None) -> None:
        self.export_log_text(context)

    def _result_account_scope(self) -> str:
        fingerprint = self._customer_guard_fingerprint(
            self.ak_var.get().strip(),
            self.sk_var.get().strip(),
        )
        if not fingerprint or fingerprint != self.volc_result_credentials_fingerprint:
            return ""
        return self.volc_result_account_scope

    def _legacy_latest_result_dir(self) -> Path:
        return APP_RESULT_DIR / self._account_log_scope()

    def _latest_result_dir(self) -> Path:
        return APP_RESULT_DIR / (self._result_account_scope() or "账号-未确认")

    def _ensure_result_account_cache(self) -> bool:
        if not self._result_account_scope():
            return False
        return migrate_latest_result_files(
            self._legacy_latest_result_dir(),
            self._latest_result_dir(),
        )

    def _read_latest_result_file(self, filename: str) -> str:
        if not self._result_account_scope():
            return ""
        self._ensure_result_account_cache()
        try:
            return (self._latest_result_dir() / filename).read_text(encoding="utf-8").strip()
        except Exception:
            return ""

    def _save_latest_result_outputs(self, l2tp_output: str = "", socks_output: str = "", *, allow_empty: bool = False) -> None:
        if not self._result_account_scope():
            return
        self._ensure_result_account_cache()
        if not allow_empty and not str(l2tp_output or "").strip() and not str(socks_output or "").strip():
            return
        write_latest_result_files(self._latest_result_dir(), l2tp=l2tp_output, socks5=socks_output)

    def copy_l2tp_results(self) -> None:
        if self.l2tp_text is None:
            return
        text = append_mark_to_output(self.l2tp_text.get("1.0", tk.END), self.mark_var.get())
        if text:
            self.clipboard_clear()
            self.clipboard_append(text)
            self.show_info("L2TP 结果已复制。")

    def copy_plain_socks_results(self) -> None:
        if self.socks_text is None:
            return
        text = extract_plain_socks5_output(self.socks_text.get("1.0", tk.END)).strip()
        if text:
            self.clipboard_clear()
            self.clipboard_append(text)
            self.show_info("纯 Socks5 结果已复制。")

    def copy_socks_results(self) -> None:
        if self.socks_text is None:
            return
        text = append_mark_to_output(self.socks_text.get("1.0", tk.END), self.mark_var.get())
        if text:
            self.clipboard_clear()
            self.clipboard_append(text)
            self.show_info("Socks5 结果已复制。")

    def _center_dialog(self, dialog: tk.Toplevel, min_width: int = 420, min_height: int = 180) -> None:
        dialog.update_idletasks()
        parent = self.winfo_toplevel()
        width = max(min_width, dialog.winfo_reqwidth())
        height = max(min_height, dialog.winfo_reqheight())
        x = parent.winfo_rootx() + max(0, (parent.winfo_width() - width) // 2)
        y = parent.winfo_rooty() + max(0, (parent.winfo_height() - height) // 2)
        dialog.geometry(f"{width}x{height}+{x}+{y}")

    def ask_confirm_cancel(self, message: str, *, ok_text: str = "确认", cancel_text: str = "取消") -> bool:
        if self.delayed_skip_next_confirmation:
            self.delayed_skip_next_confirmation = False
            return True
        if self.delayed_preconfirm_active:
            raise DelayedActionPreconfirmCaptured(str(message or ""))
        display_message = format_app_dialog_message(message)
        dialog = tk.Toplevel(self.winfo_toplevel())
        dialog.title(cloud_dialog_title(APP_TITLE, display_message, default_suffix="确认"))
        dialog.resizable(False, False)
        dialog.transient(self.winfo_toplevel())
        apply_dialog_icon(dialog)
        result = {"ok": False}
        frame = ttk.Frame(dialog, padding=(22, 22, 22, 30))
        frame.pack(fill=tk.BOTH, expand=True)
        ttk.Label(frame, text=display_message, wraplength=560, justify=tk.LEFT).pack(fill=tk.X, pady=(0, 30))
        buttons = ttk.Frame(frame)
        buttons.pack(anchor="center", pady=(10, 0))
        button_style = dialog_button_style(buttons)

        def confirm() -> None:
            result["ok"] = True
            dialog.destroy()

        def cancel() -> None:
            dialog.destroy()

        ttk.Button(buttons, text=ok_text, command=confirm, width=dialog_button_width(ok_text), style=button_style).pack(side=tk.LEFT, padx=(0, 14), ipady=2)
        ttk.Button(buttons, text=cancel_text, command=cancel, width=dialog_button_width(cancel_text), style=button_style).pack(side=tk.LEFT, ipady=2)
        dialog.protocol("WM_DELETE_WINDOW", cancel)
        self._center_dialog(dialog, min_width=440, min_height=200)
        dialog.grab_set()
        dialog.focus_force()
        self.wait_window(dialog)
        confirmed = bool(result["ok"])
        if self.delayed_preconfirm_active:
            if confirmed:
                raise DelayedActionPreconfirmAccepted()
            raise DelayedActionPreconfirmCancelled()
        return confirmed

    def _ask_cleanup_release_action(self, message: str, *, allow_used_ip_import: bool) -> str:
        if not allow_used_ip_import or self.delayed_skip_next_confirmation or self.delayed_preconfirm_active:
            return "confirm" if self.ask_confirm_cancel(message) else "cancel"
        result = {"action": "cancel"}

        def choose(action: str) -> None:
            result["action"] = action

        show_app_message_dialog(
            self.winfo_toplevel(),
            APP_TITLE,
            message,
            modal=True,
            actions=(
                ("确认", lambda: choose("confirm")),
                ("确认并导入旧IP库", lambda: choose("confirm_and_import")),
                ("取消", lambda: choose("cancel")),
            ),
        )
        return result["action"]

    def show_info(self, message: str) -> None:
        """统一在主线程弹出火山任务完成提示，避免后台线程直接操作 Tk。"""
        text = friendly_popup_text(message)
        if self._record_delayed_queue_result_popup(text, is_error=False):
            return
        if self._is_task_completion_message(text):
            play_task_completion_sound()
        show_app_message_dialog(self.winfo_toplevel(), APP_TITLE, text)

    @staticmethod
    def _is_task_completion_message(message: str) -> bool:
        text = str(message or "")
        return any(
            token in text
            for token in (
                "任务结束",
                "任务完成",
                "任务已停止",
                "处理完成",
                "已停止",
                "资源处理完成",
                "一键清空账号资源完成",
                "更换选择地区IP完成",
                "队列任务执行完毕",
            )
        )

    def _show_customer_guard_dialog(self, message: str) -> None:
        self.customer_guard_dialog = show_app_message_dialog(
            self.winfo_toplevel(),
            APP_TITLE,
            friendly_popup_text(message),
            existing_dialog=getattr(self, "customer_guard_dialog", None),
            actions=(
                ("联系客户经理", self._contact_customer_manager_from_guard),
                ("重新校验", self._retry_customer_guard_from_dialog),
            ),
        )

    def _contact_customer_manager_from_guard(self) -> None:
        self._close_customer_guard_dialog()
        top = self.winfo_toplevel()
        callback = getattr(top, "_show_customer_manager_contact_dialog", None)
        if callable(callback):
            callback()
        else:
            show_app_message_dialog(
                top,
                "联系客户经理",
                "请通过多云管理主界面左侧“联系客户经理”打开客户经理联系方式。",
                modal=True,
            )
        fingerprint = self._customer_guard_fingerprint(self.ak_var.get(), self.sk_var.get())
        if self.customer_guard_status == "blocked" and self.customer_guard_fingerprint == fingerprint:
            self.after(0, lambda: self._show_customer_guard_dialog(CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE))

    def _retry_customer_guard_from_dialog(self) -> None:
        self._close_customer_guard_dialog()
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        if not self._credentials_ready_for_customer_guard(ak, sk):
            show_app_message_dialog(self.winfo_toplevel(), APP_TITLE, "请先填写完整 AK/SK。")
            return
        self.customer_guard_notice_fingerprint = ""
        self.customer_guard_log_fingerprint = ""
        self.customer_guard_message = ""
        self._log_customer_guard_message("火山引擎AK/SK合作伙伴重新校验已启动。")
        self._schedule_customer_guard_check(delay_ms=0, force=True)

    def _close_customer_guard_dialog(self) -> None:
        dialog = getattr(self, "customer_guard_dialog", None)
        self.customer_guard_dialog = None
        if dialog is None:
            return
        try:
            if dialog.winfo_exists():
                dialog.destroy()
        except tk.TclError:
            pass

    def stop_tasks(self) -> None:
        self.stop_event.set()
        self.log("已请求停止火山引擎任务。")

    def shutdown(self) -> None:
        """主窗口关闭时释放子进程和 SSH 通道，避免 _MEI 临时目录被占用。"""
        self.stop_event.set()
        self.busy = False
        self.blocking_task_count = 0
        try:
            with self.ssh_channel_lock:
                channel = self.ssh_channel
                self.ssh_channel = None
            close_channel(channel)
        except Exception:
            pass
        terminate_processes(self.child_processes)
        self.child_processes.clear()

    def _begin_volc_task(
        self,
        status: str,
        context: str = "purchase",
        *,
        affects_global_busy: bool = True,
        regions: Optional[Sequence[str]] = None,
        risk_group: str = WRITE_RESOURCE_RISK_GROUP,
    ) -> bool:
        try:
            task_guard = TaskGuard(
                cloud="Volcengine",
                ak=self.ak_var.get(),
                sk=self.sk_var.get(),
                action=status,
                regions=list(regions or ["global"]),
                risk_group=risk_group,
            ).acquire()
        except TaskGuardError as exc:
            self.status_var.set("任务被并发保护拦截")
            self.log(str(exc), context)
            messagebox.showinfo(APP_TITLE, friendly_popup_text(str(exc)))
            return False
        with self.task_lock:
            was_idle = self.active_task_count == 0
            self.active_task_count += 1
            if affects_global_busy:
                self.blocking_task_count += 1
            self.busy = self.blocking_task_count > 0
            self.active_task_contexts[context] = self.active_task_contexts.get(context, 0) + 1
            self.task_guards.setdefault(context, []).append(task_guard)
        if was_idle:
            self.stop_event.clear()
        self.current_log_context = context
        self.realtime_log_filter.reset(context)
        self._start_volc_log_file(status, context)
        self.status_var.set(self._volc_running_status(status))
        self.log(f"{status}已异步启动", context)
        return True

    def _finish_volc_task(
        self,
        status: str = "就绪",
        context: str = "purchase",
        affects_global_busy: bool = True,
    ) -> None:
        task_guard = None
        with self.task_lock:
            self.active_task_count = max(0, self.active_task_count - 1)
            if affects_global_busy:
                self.blocking_task_count = max(0, self.blocking_task_count - 1)
            current = self.active_task_contexts.get(context, 0)
            if current <= 1:
                self.active_task_contexts.pop(context, None)
            else:
                self.active_task_contexts[context] = current - 1
            self.busy = self.blocking_task_count > 0
            remaining = self.active_task_count
            guards = self.task_guards.get(context) or []
            if guards:
                task_guard = guards.pop()
            if not guards:
                self.task_guards.pop(context, None)
        if task_guard is not None:
            task_guard.release()
        self.status_var.set("任务运行中" if remaining else self._volc_finished_status(status))
        self._append_volc_log_file(f"===== 任务结束 {time.strftime('%Y-%m-%d %H:%M:%S')} =====\n", context)
        if not remaining:
            self.after(800, self._complete_delayed_action_if_idle)

    @staticmethod
    def _volc_running_status(status: str) -> str:
        if "读取账号资源" in status:
            return "正在读取账号资源"
        return status

    @staticmethod
    def _volc_finished_status(status: str) -> str:
        if "资源读取失败" in status:
            return "账号资源读取失败"
        if "资源读取完成" in status or "读取账号资源" in status:
            return "账号资源读取完成"
        return status

    def _volc_worker_command(self) -> List[str]:
        if getattr(sys, "frozen", False):
            return [sys.executable, "--volc-worker"]
        return [sys.executable, "-u", os.path.abspath(__file__), "--volc-worker"]

    def _volc_group_count(self) -> int:
        config_key = self.volc_config_var.get().strip()
        config = VOLC_CONFIGS.get(config_key, VOLC_CONFIGS[VOLC_CONFIG_FULL])
        return 2 if int(config.get("ecs_per_region") or 1) >= 2 else 1

    @staticmethod
    def _volc_balance_threshold_for_ip_count(total_ips: int) -> int:
        total_ips = max(0, int(total_ips or 0))
        if total_ips <= 0:
            return 0
        return max(100, VOLC_BALANCE_CHECK_FULL)

    @staticmethod
    def _volc_to_dict(obj: Any) -> Dict[str, Any]:
        if obj is None:
            return {}
        if isinstance(obj, dict):
            return obj
        to_dict = getattr(obj, "to_dict", None)
        if callable(to_dict):
            data = to_dict()
            return data if isinstance(data, dict) else {}
        return {}

    @staticmethod
    def _volc_result(data: Dict[str, Any]) -> Dict[str, Any]:
        result = data.get("Result") or data.get("result") or data
        return result if isinstance(result, dict) else {}

    @staticmethod
    def _volc_first(data: Dict[str, Any], *keys: str) -> str:
        wanted = {str(key).lower() for key in keys if key}
        for key in keys:
            value = data.get(key) if isinstance(data, dict) else None
            if value not in (None, ""):
                return str(value).strip()
        raw = data.get("_raw") if isinstance(data, dict) else None
        if isinstance(raw, dict):
            for key in keys:
                value = raw.get(key)
                if value not in (None, ""):
                    return str(value).strip()

        def walk(value: Any) -> str:
            if isinstance(value, dict):
                for item_key, item_value in value.items():
                    if str(item_key).lower() in wanted and item_value not in (None, ""):
                        if isinstance(item_value, (dict, list)):
                            continue
                        return str(item_value).strip()
                for item_value in value.values():
                    found = walk(item_value)
                    if found:
                        return found
            elif isinstance(value, list):
                for item_value in value:
                    found = walk(item_value)
                    if found:
                        return found
            return ""

        if wanted:
            return walk(data)
        return ""

    @staticmethod
    def _volc_clean_account_mark(value: Any) -> str:
        text = str(value or "").strip().strip(" ,;:/\\|，；：、\"'<>[](){}")
        text = re.sub(r"\s+", "", text)
        return text[:80]

    @staticmethod
    def _volc_is_low_quality_mark(value: Any) -> bool:
        # 中文注释：火山 STS 常只返回数字账号 ID，这不是用户要看的主账号名，不能自动填入 User(可编辑)。
        text = str(value or "").strip()
        if not text:
            return True
        if text.isdigit() and len(text) >= 6:
            return True
        if re.fullmatch(r"[0-9a-fA-F-]{16,}", text):
            return True
        return text.lower() in {"root", "account", "unknown"}

    def _volc_normalized_identity(self, identity: Dict[str, Any]) -> Dict[str, Any]:
        result = self._volc_result(identity)
        return {
            "provider": "volc",
            "account_id": self._volc_first(result, "AccountId", "account_id"),
            "identity_id": self._volc_first(result, "IdentityId", "identity_id"),
            "identity_type": self._volc_first(result, "IdentityType", "identity_type"),
            "trn": self._volc_first(result, "Trn", "trn", "ResourceName", "resource_name"),
            "raw": result,
        }

    def _volc_customer_mark_from_identity(self, identity: Dict[str, Any]) -> str:
        try:
            from Volcengine.api.volcengine import customer_guard

            mark, _detail = customer_guard.resolve_customer_mark_for_identity(self._volc_normalized_identity(identity))
            cleaned = self._volc_clean_account_mark(mark)
            return "" if self._volc_is_low_quality_mark(cleaned) else cleaned
        except Exception:
            return ""

    def _volc_account_mark_from_identity(self, identity: Dict[str, Any]) -> str:
        result = self._volc_result(identity)
        # STS 返回字段随主账号/IAM用户形态不同而变化，先取明确名称，再从 TRN 里兜底解析用户名。
        for key in (
            "AccountName",
            "account_name",
            "UserName",
            "user_name",
            "IdentityName",
            "identity_name",
            "DisplayName",
            "display_name",
            "Name",
            "name",
        ):
            value = self._volc_clean_account_mark(self._volc_first(result, key))
            if value and not self._volc_is_low_quality_mark(value):
                return value
        trn = self._volc_first(result, "Trn", "trn", "ResourceName", "resource_name")
        if trn:
            normalized = trn.replace("\\", "/")
            for separator in ("/", ":"):
                if separator not in normalized:
                    continue
                candidate = self._volc_clean_account_mark(normalized.rsplit(separator, 1)[-1])
                lower_candidate = candidate.lower()
                if candidate and not self._volc_is_low_quality_mark(candidate) and not lower_candidate.startswith("trn"):
                    return candidate
        return ""

    def _volc_iam_user_mark_from_access_key(self, ak: str, sk: str, identity: Dict[str, Any]) -> str:
        try:
            import volcenginesdkiam

            api = volcenginesdkiam.IAMApi(self._volc_api_client("cn-beijing", ak, sk))
            request = volcenginesdkiam.GetUserRequest(access_key_id=ak, pii_redaction=0)
            response = self._volc_call_with_retry(lambda: api.get_user(request), "IAM GetUser", attempts=1)
            data = self._volc_result(self._volc_to_dict(response))
            user = data.get("User") or data.get("user")
            if isinstance(user, dict):
                for key in ("DisplayName", "display_name", "UserName", "user_name", "Name", "name"):
                    mark = self._volc_clean_account_mark(self._volc_first(user, key))
                    if mark and not self._volc_is_low_quality_mark(mark):
                        return mark
        except Exception:
            return ""
        return ""

    def _resolve_volc_account_mark(self, ak: str, sk: str) -> str:
        import volcenginesdksts

        api = volcenginesdksts.STSApi(self._volc_api_client("cn-beijing", ak, sk))
        try:
            request_cls = getattr(volcenginesdksts, "GetCallerIdentityRequest")
            request = request_cls()
        except Exception:
            from volcenginesdksts.models.get_caller_identity_request import GetCallerIdentityRequest

            request = GetCallerIdentityRequest()
        response = self._volc_call_with_retry(lambda: api.get_caller_identity(request), "STS GetCallerIdentity", attempts=2)
        identity = self._volc_to_dict(response)
        # 主账号 STS 只会返回 root TRN，真实可识别名称需要从代理客户资料反查。
        customer_mark = self._volc_customer_mark_from_identity(identity)
        if customer_mark:
            return customer_mark
        iam_mark = self._volc_iam_user_mark_from_access_key(ak, sk, identity)
        if iam_mark:
            return iam_mark
        account_mark = self._volc_account_mark_from_identity(identity)
        return "" if self._volc_is_low_quality_mark(account_mark) else account_mark

    def resolve_mark_from_credentials(self, ak: str, sk: str) -> str:
        """根据火山引擎 AK/SK 自动读取账号用户名，用于填充 User(可编辑)。"""
        return self._resolve_volc_account_mark(ak, sk)

    def populate_mark_from_credentials_after_paste(
        self,
        ak: str,
        sk: str,
        *,
        mark_var: Optional[tk.StringVar] = None,
        on_mark: Optional[Callable[[str], None]] = None,
    ) -> None:
        ak_value = (ak or "").strip()
        sk_value = (sk or "").strip()
        if not ak_value or not sk_value:
            return

        def worker() -> None:
            try:
                mark = self._resolve_volc_account_mark(ak_value, sk_value)
            except Exception as exc:
                self.after(0, self.log, f"读取火山账号名称失败，已保留当前 Mark: {self._volc_exception_text(exc)}")
                return
            if not mark:
                self.after(0, self.log, "未读取到可展示的火山账号用户名，User(可编辑)保持为空。")
                return

            def apply_mark() -> None:
                if self.ak_var.get().strip() != ak_value or self.sk_var.get().strip() != sk_value:
                    return
                target_var = mark_var if isinstance(mark_var, tk.StringVar) else self.mark_var
                current_mark = target_var.get().strip()
                if current_mark and mark.isdigit() and current_mark != mark:
                    self.log(f"火山账号名称只读取到账号ID，已保留当前 Mark: {current_mark}")
                    return
                target_var.set(mark)
                self.mark_var.set(mark)
                if callable(on_mark):
                    on_mark(mark)
                self.log(f"已读取火山账号名称到 Mark: {mark}")

            self.after(0, apply_mark)

        threading.Thread(target=worker, daemon=True).start()

    @staticmethod
    def _volc_list(data: Dict[str, Any], *keys: str) -> List[Dict[str, Any]]:
        result = VolcEngineManagerApp._volc_result(data)
        for key in keys:
            value = result.get(key)
            if isinstance(value, list):
                return [item for item in value if isinstance(item, dict)]
        return []

    def _volc_api_client(self, region: str, ak: str, sk: str) -> Any:
        import volcenginesdkcore

        configuration = volcenginesdkcore.Configuration()
        configuration.ak = ak
        configuration.sk = sk
        configuration.region = region
        try:
            configuration.timeout = int(VOLC_RESOURCE_API_TIMEOUT)
        except Exception:
            pass
        return volcenginesdkcore.ApiClient(configuration)

    @staticmethod
    def _volc_exception_text(exc: Exception) -> str:
        parts: List[str] = []
        for attr, label in (("status", "HTTP"), ("reason", "原因"), ("body", "返回")):
            value = getattr(exc, attr, None)
            if value not in (None, ""):
                parts.append(f"{label}={value}")
        headers = getattr(exc, "headers", None)
        request_id = ""
        try:
            request_id = headers.get("X-Tt-Logid") or headers.get("X-Request-Id") or headers.get("Request-Id")
        except Exception:
            request_id = ""
        if request_id:
            parts.append(f"请求ID={request_id}")
        return "；".join(parts) if parts else str(exc)

    @staticmethod
    def _volc_auth_error(exc: Exception) -> bool:
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return any(token in text for token in ("invalidaccesskey", "signaturedoesnotmatch", "invalidsecret", "missingcredential"))

    @staticmethod
    def _volc_absent_error(exc: Exception) -> bool:
        """判断火山资源是否已经不存在；删除类操作遇到这类错误可视为完成。"""
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return any(
            token in text
            for token in (
                "notfound",
                "not found",
                "not exist",
                "notexist",
                "invalidinstance.notfound",
                "invalidallocationid.notfound",
                "invalidvpc.notfound",
                "invalidsubnet.notfound",
                "invalidsecuritygroup.notfound",
                "invalidnetworkinterface.notfound",
                "invalidnetworkinterfaceid.notfound",
            )
        )

    @staticmethod
    def _volc_dependency_error(exc: Exception) -> bool:
        """判断删除失败是否大概率因为云端依赖尚未释放，适合等待后重试。"""
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return any(
            token in text
            for token in (
                "dependency",
                "inuse",
                "in use",
                "invalidstatus",
                "invalid.status",
                "operationdenied",
                "operation denied",
                "not stopped",
                "notstop",
                "has resource",
                "used by",
                "attached",
                "associated",
            )
        )

    @staticmethod
    def _volc_security_rule_exists_error(exc: Exception) -> bool:
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return any(
            token in text
            for token in (
                "duplicate",
                "alreadyexist",
                "already exists",
                "invalidsecurityrule.conflict",
                "security rule conflicts",
                "conflicts with that of another security rule",
                "已存在",
            )
        )

    @staticmethod
    def _volc_instance_status_sync_error(exc: Exception) -> bool:
        """ECS 退订/删除后可能短时间返回状态不支持；继续等待通常可恢复。"""
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return any(
            token in text
            for token in (
                "invalidinstancestatus",
                "invalid instance status",
                "the status of the specified instance does not support this request",
            )
        )

    @staticmethod
    def _volc_delete_retry_sleep(attempt: int) -> float:
        return min(5.0 * max(1, attempt), VOLC_DELETE_DEPENDENCY_MAX_SLEEP_SECONDS)

    @staticmethod
    def _volc_redact_resource_ids(text: str, *resource_ids: Any) -> str:
        value = str(text or "")
        for resource_id in resource_ids:
            current = str(resource_id or "").strip()
            if current:
                value = value.replace(current, "该资源")
        return value

    def _volc_resource_exception_text(self, exc: Exception, *resource_ids: Any) -> str:
        return self._volc_redact_resource_ids(self._volc_exception_text(exc), *resource_ids)

    def _volc_manage_log(self, message: str) -> None:
        self.after(0, self.log, message, "manage")

    def _volc_check_identity(self, ak: str, sk: str) -> str:
        import volcenginesdksts

        api = volcenginesdksts.STSApi(self._volc_api_client("cn-beijing", ak, sk))
        try:
            request_cls = getattr(volcenginesdksts, "GetCallerIdentityRequest")
            request = request_cls()
        except Exception:
            from volcenginesdksts.models.get_caller_identity_request import GetCallerIdentityRequest

            request = GetCallerIdentityRequest()
        response = self._volc_call_with_retry(lambda: api.get_caller_identity(request), "STS GetCallerIdentity", attempts=2)
        data = self._volc_to_dict(response)
        result = self._volc_result(data)
        account_id = self._volc_first(result, "AccountId", "account_id")
        identity_id = self._volc_first(result, "IdentityId", "identity_id")
        self.volc_result_credentials_fingerprint = self._customer_guard_fingerprint(ak, sk)
        self.volc_result_account_scope = (
            credential_scope_label(account_id, "volc-result")
            if account_id
            else credential_scope_label(ak, sk)
        )
        identity_note = account_id or identity_id
        self.after(0, self.log, f"火山引擎身份校验通过{('，账号ID=' + identity_note) if identity_note else ''}。")
        return account_id

    def _volc_call_with_retry(self, action: Callable[[], Any], label: str, attempts: int = 3) -> Any:
        last_exc: Optional[Exception] = None
        for attempt in range(1, max(1, attempts) + 1):
            try:
                return action()
            except Exception as exc:
                last_exc = exc
                if attempt >= attempts or not is_network_transient_error(exc):
                    raise
                self.after(0, self.log, f"[火山引擎] {label} 连接中断，重试 {attempt}/{attempts}: {self._volc_exception_text(exc)}")
                time.sleep(min(1.2 * attempt, 4.0))
        if last_exc:
            raise last_exc
        raise RuntimeError(f"{label} 调用失败")

    @staticmethod
    def _volc_request(request_cls: Any, **kwargs: Any) -> Any:
        try:
            return request_cls(**kwargs)
        except TypeError:
            request = request_cls()
            for key, value in kwargs.items():
                try:
                    setattr(request, key, value)
                except Exception:
                    pass
            return request

    def _volc_describe_instances(self, region: str, ak: str, sk: str) -> List[Dict[str, Any]]:
        import volcenginesdkecs

        api = volcenginesdkecs.ECSApi(self._volc_api_client(region, ak, sk))
        try:
            request = volcenginesdkecs.DescribeInstancesRequest(max_results=100)
        except TypeError:
            request = volcenginesdkecs.DescribeInstancesRequest()
        response = self._volc_call_with_retry(lambda: api.describe_instances(request), f"{region} DescribeInstances")
        return self._volc_list(self._volc_to_dict(response), "Instances", "instances")

    def _volc_find_instance(self, region: str, instance_id: str, ak: str, sk: str) -> Optional[Dict[str, Any]]:
        """按实例 ID 精确读取 ECS；删除和退订前先确认云端真实状态，避免按旧表格数据误判。"""
        import volcenginesdkecs

        if not instance_id:
            return None
        api = volcenginesdkecs.ECSApi(self._volc_api_client(region, ak, sk))
        try:
            request = volcenginesdkecs.DescribeInstancesRequest(instance_ids=[instance_id], max_results=100)
        except TypeError:
            request = volcenginesdkecs.DescribeInstancesRequest()
            try:
                request.instance_ids = [instance_id]
                request.max_results = 100
            except Exception:
                pass
        response = self._volc_call_with_retry(lambda: api.describe_instances(request), f"{region} DescribeInstancesById", attempts=2)
        instances = self._volc_list(self._volc_to_dict(response), "Instances", "instances")
        for instance in instances:
            current_id = self._volc_first(instance, "InstanceId", "instance_id", "id")
            if current_id == instance_id:
                return instance
        return None

    @staticmethod
    def _volc_instance_charge_type(instance: Dict[str, Any]) -> str:
        """提取火山 ECS 原始计费类型；后续判断用英文枚举，界面显示仍走中文文案。"""
        return VolcEngineManagerApp._volc_first(
            instance,
            "InstanceChargeType",
            "instance_charge_type",
            "ChargeType",
            "BillingType",
            "billing_type",
        )

    @staticmethod
    def _volc_is_prepaid_charge_type(charge_type: str) -> bool:
        """判断是否包年包月；这类 ECS 未退订前不能直接调用 DeleteInstance。"""
        value = str(charge_type or "").strip().lower()
        return value in {"prepaid", "pre_pay", "prepay", "subscription"} or value in {"1", "2"}

    @staticmethod
    def _volc_invalid_charge_type_error(exc: Exception) -> bool:
        """识别火山返回的 InvalidChargeType，用来从删除流程切换到退订流程。"""
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return "invalidchargetype" in text or "chargetype is not valid" in text or "charge type is not valid" in text

    @staticmethod
    def _volc_status_wrong_error(exc: Exception) -> bool:
        """识别费用中心退订返回的 StatusWrong；已停止 ECS 可能需要走 ECS 删除接口释放。"""
        text = VolcEngineManagerApp._volc_exception_text(exc).lower()
        return "statuswrong" in text or "状态不符合预期" in text or "status is not expected" in text

    def _volc_unsubscribe_prepaid_instance(self, region: str, instance_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        """退订包年包月 ECS；清空账号资源遇到 PrePaid ECS 时必须先走费用中心退订。"""
        import volcenginesdkbilling

        label = log_label or f"{region}/云服务器"
        request = self._volc_request(
            volcenginesdkbilling.UnsubscribeInstanceRequest,
            client_token=str(uuid.uuid4()),
            product="ECS",
            instance_id=instance_id,
            unsubscribe_related_instance=True,
        )
        api = volcenginesdkbilling.BILLINGApi(self._volc_api_client("cn-beijing", ak, sk))
        self._volc_manage_log(f"[{label}] 检测到包年包月 ECS，开始调用费用中心退订接口。")
        response = self._volc_call_with_retry(
            lambda: api.unsubscribe_instance(request),
            f"{region} BillingUnsubscribeInstance",
            attempts=2,
        )
        result = self._volc_result(self._volc_to_dict(response))
        if result:
            self._volc_manage_log(f"[{label}] 包年包月 ECS 退订已提交，等待云端同步释放状态。")
        else:
            self._volc_manage_log(f"[{label}] 包年包月 ECS 退订已提交，等待云端同步释放状态。")

    def _volc_wait_instance_absent(
        self,
        region: str,
        instance_id: str,
        ak: str,
        sk: str,
        *,
        timeout: float = 150.0,
        interval: float = 6.0,
        log_label: str = "",
    ) -> bool:
        """等待 ECS 从资源列表消失；云端删除/退订是异步的，过早刷新会看到残留 ECS。"""
        label = log_label or f"{region}/云服务器"
        deadline = time.monotonic() + timeout
        last_status = ""
        while time.monotonic() < deadline:
            if self.stop_event.is_set():
                raise RuntimeError("用户已停止，等待 ECS 释放中断")
            try:
                instance = self._volc_find_instance(region, instance_id, ak, sk)
            except Exception as exc:
                if self._volc_absent_error(exc):
                    return True
                self._volc_manage_log(f"[{label}] 等待释放时读取 ECS 状态失败，稍后重试: {self._volc_resource_exception_text(exc, instance_id)}")
                time.sleep(interval)
                continue
            if not instance:
                self._volc_manage_log(f"[{label}] 云端已查不到 ECS，释放确认完成。")
                return True
            status = self._volc_first(instance, "Status", "status") or "-"
            if status != last_status:
                self._volc_manage_log(f"[{label}] ECS 仍在云端，当前状态={status}，继续等待释放。")
                last_status = status
            time.sleep(interval)
        return False

    def _volc_describe_eips(self, region: str, ak: str, sk: str) -> List[Dict[str, Any]]:
        import volcenginesdkvpc

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        try:
            request = volcenginesdkvpc.DescribeEipAddressesRequest(page_size=100)
        except TypeError:
            request = volcenginesdkvpc.DescribeEipAddressesRequest()
        response = self._volc_call_with_retry(lambda: api.describe_eip_addresses(request), f"{region} DescribeEipAddresses")
        return self._volc_list(self._volc_to_dict(response), "EipAddresses", "eip_addresses", "EipAddressSet")

    def _volc_describe_vpcs(self, region: str, ak: str, sk: str) -> List[Dict[str, Any]]:
        import volcenginesdkvpc

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        try:
            request = volcenginesdkvpc.DescribeVpcsRequest(page_size=100)
        except TypeError:
            request = volcenginesdkvpc.DescribeVpcsRequest()
        response = self._volc_call_with_retry(lambda: api.describe_vpcs(request), f"{region} DescribeVpcs")
        return self._volc_list(self._volc_to_dict(response), "Vpcs", "vpcs", "VpcSet")

    def _volc_describe_subnets(self, region: str, ak: str, sk: str) -> List[Dict[str, Any]]:
        import volcenginesdkvpc

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        try:
            request = volcenginesdkvpc.DescribeSubnetsRequest(page_size=100)
        except TypeError:
            request = volcenginesdkvpc.DescribeSubnetsRequest()
        response = self._volc_call_with_retry(lambda: api.describe_subnets(request), f"{region} DescribeSubnets")
        return self._volc_list(self._volc_to_dict(response), "Subnets", "subnets", "SubnetSet")

    def _volc_describe_security_groups(self, region: str, ak: str, sk: str) -> List[Dict[str, Any]]:
        import volcenginesdkvpc

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        try:
            request = volcenginesdkvpc.DescribeSecurityGroupsRequest(page_size=100)
        except TypeError:
            request = volcenginesdkvpc.DescribeSecurityGroupsRequest()
        response = self._volc_call_with_retry(lambda: api.describe_security_groups(request), f"{region} DescribeSecurityGroups")
        return self._volc_list(self._volc_to_dict(response), "SecurityGroups", "security_groups", "SecurityGroupSet")

    def _volc_describe_network_interfaces(self, region: str, ak: str, sk: str) -> List[Dict[str, Any]]:
        import volcenginesdkvpc

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        request = self._volc_request(volcenginesdkvpc.DescribeNetworkInterfacesRequest, page_size=100)
        response = self._volc_call_with_retry(lambda: api.describe_network_interfaces(request), f"{region} DescribeNetworkInterfaces")
        return self._volc_list(
            self._volc_to_dict(response),
            "NetworkInterfaceSets",
            "network_interface_sets",
            "NetworkInterfaceSet",
            "NetworkInterfaces",
            "network_interfaces",
        )

    def _volc_delete_instance(self, region: str, instance_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        """清空/删除 ECS 的总入口：按量实例直接删，包年包月实例先退订再等待云端释放。"""
        if not instance_id:
            raise ValueError("缺少云服务器ID")
        label = log_label or f"{region}/云服务器"
        already_unsubscribed = False
        try:
            instance = self._volc_find_instance(region, instance_id, ak, sk)
        except Exception as exc:
            if self._volc_absent_error(exc):
                self._volc_manage_log(f"[{label}] 云服务器已不存在，按删除完成处理。")
                return
            self._volc_manage_log(f"[{label}] 删除前读取计费类型失败，继续按删除接口尝试: {self._volc_resource_exception_text(exc, instance_id)}")
            instance = None
        if not instance:
            self._volc_manage_log(f"[{label}] 云端已查不到 ECS，按删除完成处理。")
            return

        charge_type = self._volc_instance_charge_type(instance)
        unsubscribe_status_wrong = False
        if self._volc_is_prepaid_charge_type(charge_type):
            try:
                already_unsubscribed = True
                self._volc_unsubscribe_prepaid_instance(region, instance_id, ak, sk, log_label=label)
                time.sleep(8.0)
                if self._volc_wait_instance_absent(
                    region,
                    instance_id,
                    ak,
                    sk,
                    timeout=VOLC_PREPAID_RELEASE_TIMEOUT_SECONDS,
                    interval=10.0,
                    log_label=label,
                ):
                    return
                self._volc_manage_log(f"[{label}] 退订已提交但云服务器仍在资源列表，继续尝试删除接口。")
            except Exception as exc:
                if not self._volc_status_wrong_error(exc):
                    raise
                unsubscribe_status_wrong = True
                already_unsubscribed = False
                status = self._volc_first(instance, "Status", "status") or "-"
                self._volc_manage_log(
                    f"[{label}] 费用中心退订返回状态不符合预期，当前ECS状态={self._volc_status_text(status)}；"
                    "尝试按已停止云服务器直接删除并等待主网卡自动释放。"
                )

        last_exc: Optional[Exception] = None
        for attempt in range(1, VOLC_DELETE_DEPENDENCY_ATTEMPTS + 1):
            try:
                if already_unsubscribed:
                    self._volc_manage_log(f"[{label}] 退订后确认 ECS 释放/删除 {attempt}/{VOLC_DELETE_DEPENDENCY_ATTEMPTS}。")
                self._volc_delete_instance_direct(region, instance_id, ak, sk, log_label=label)
                if self._volc_wait_instance_absent(region, instance_id, ak, sk, timeout=120.0, log_label=label):
                    return
                last_exc = RuntimeError("删除已提交，但等待 ECS 从云端资源列表消失超时")
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] 云服务器已不存在，按删除完成处理。")
                    return
                last_exc = exc
                if self._volc_invalid_charge_type_error(exc):
                    if not already_unsubscribed:
                        if unsubscribe_status_wrong:
                            self._volc_manage_log(
                                f"[{label}] 直接删除仍提示计费类型不支持，继续等待云端退订/删除状态同步。"
                            )
                        else:
                            already_unsubscribed = True
                            self._volc_manage_log(f"[{label}] 云端提示计费类型不支持直接删除，切换为包年包月退订流程。")
                            try:
                                self._volc_unsubscribe_prepaid_instance(region, instance_id, ak, sk, log_label=label)
                            except Exception as unsubscribe_exc:
                                if self._volc_status_wrong_error(unsubscribe_exc):
                                    unsubscribe_status_wrong = True
                                    already_unsubscribed = False
                                    self._volc_manage_log(
                                        f"[{label}] 退订接口仍返回状态不符合预期，后续继续尝试直接删除/等待释放。"
                                    )
                                else:
                                    raise
                    else:
                        self._volc_manage_log(f"[{label}] 退订已提交但计费状态仍在同步，稍后继续确认: {self._volc_resource_exception_text(exc, instance_id)}")
                elif self._volc_dependency_error(exc) or self._volc_instance_status_sync_error(exc) or is_network_transient_error(exc):
                    self._volc_manage_log(f"[{label}] 删除暂未完成，等待后重试: {self._volc_resource_exception_text(exc, instance_id)}")
                else:
                    raise
            if attempt < VOLC_DELETE_DEPENDENCY_ATTEMPTS:
                time.sleep(self._volc_delete_retry_sleep(attempt))
                if already_unsubscribed and self._volc_wait_instance_absent(region, instance_id, ak, sk, timeout=30.0, log_label=label):
                    return
        if last_exc:
            raise RuntimeError(
                f"[{label}] ECS 未能自动清空；如果这是包年包月实例，请检查火山费用中心退订状态后重新读取资源。"
            ) from last_exc

    def _volc_delete_instance_direct(self, region: str, instance_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        import volcenginesdkecs

        if not instance_id:
            raise ValueError("缺少云服务器ID")
        label = log_label or f"{region}/云服务器"
        api = volcenginesdkecs.ECSApi(self._volc_api_client(region, ak, sk))
        stop_request = self._volc_request(volcenginesdkecs.StopInstanceRequest, instance_id=instance_id, force_stop=True)
        try:
            self._volc_manage_log(f"[{label}] 删除前尝试关机，避免运行中实例删除失败。")
            self._volc_call_with_retry(lambda: api.stop_instance(stop_request), f"{region} StopInstanceBeforeDelete", attempts=2)
            time.sleep(2.0)
        except Exception as exc:
            if self._volc_absent_error(exc):
                self._volc_manage_log(f"[{label}] 云服务器已不存在，按删除完成处理。")
                return
            self._volc_manage_log(f"[{label}] 关机提示，继续尝试删除: {self._volc_resource_exception_text(exc, instance_id)}")
        last_exc: Optional[Exception] = None
        for attempt in range(1, 6):
            request = self._volc_request(volcenginesdkecs.DeleteInstanceRequest, instance_id=instance_id, client_token=str(uuid.uuid4()))
            try:
                self._volc_manage_log(f"[{label}] 删除云服务器尝试 {attempt}/5。")
                self._volc_call_with_retry(lambda: api.delete_instance(request), f"{region} DeleteInstance", attempts=2)
                return
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] 云服务器已不存在，按删除完成处理。")
                    return
                last_exc = exc
                detail = self._volc_resource_exception_text(exc, instance_id)
                if attempt >= 5 or not (
                    self._volc_dependency_error(exc)
                    or self._volc_instance_status_sync_error(exc)
                    or is_network_transient_error(exc)
                ):
                    raise
                self._volc_manage_log(f"[{label}] 删除暂未完成，等待后重试: {detail}")
                time.sleep(min(3.0 * attempt, 12.0))
        if last_exc:
            raise last_exc

    def _open_volc_ssh_client(self, ip: str) -> Any:
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        try:
            ssh.connect(
                ip,
                port=22,
                username=DEFAULT_SSH_USER,
                password=DEFAULT_PASSWORD,
                timeout=SSH_CONNECT_TIMEOUT,
                banner_timeout=SSH_BANNER_TIMEOUT,
                auth_timeout=SSH_AUTH_TIMEOUT,
                look_for_keys=False,
                allow_agent=False,
            )
            return ssh
        except Exception:
            ssh.close()
            raise

    def _wait_volc_ssh_login_ready(self, ip: str, label: str, *, timeout: float = 240.0) -> None:
        if not is_public_ip(ip):
            raise RuntimeError("未识别到云服务器公网IP，无法确认SSH登录就绪")
        deadline = time.monotonic() + max(1.0, float(timeout))
        attempt = 0
        last_exc: Optional[Exception] = None
        while time.monotonic() < deadline:
            if self.stop_event.is_set():
                raise RuntimeError("任务已停止，未继续等待SSH登录就绪")
            attempt += 1
            try:
                ssh = self._open_volc_ssh_client(ip)
            except Exception as exc:
                last_exc = exc
                if attempt == 1 or attempt % 3 == 0:
                    self.after(
                        0,
                        self.log,
                        f"[{label}] 云服务器已运行，SSH暂未就绪，5秒后继续确认: {self._volc_exception_text(exc)}",
                    )
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    break
                if self.stop_event.wait(min(5.0, remaining)):
                    raise RuntimeError("任务已停止，未继续等待SSH登录就绪")
                continue
            ssh.close()
            self.after(0, self.log, f"[{label}] SSH登录已就绪。")
            return
        detail = self._volc_exception_text(last_exc) if last_exc is not None else "未返回SSH状态"
        raise RuntimeError(f"云服务器已运行，但SSH登录在{int(timeout)}秒内未就绪: {detail}")

    def _volc_reinstall_instance(self, region: str, item: Dict[str, Any], ak: str, sk: str) -> None:
        import volcenginesdkecs

        instance_id = str(item.get("_server_id") or item.get("_resource_id") or "").strip()
        instance_name = str(item.get("_name") or instance_id)
        region_label = str(item.get("_region_label") or region).strip() or region
        log_label = f"{region_label}/{instance_name}"
        raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
        image_id = self._volc_first(raw, "ImageId", "image_id")
        if not instance_id:
            raise ValueError("缺少云服务器ID")
        if not image_id:
            raise ValueError("缺少原镜像ID，无法安全重装")
        api = volcenginesdkecs.ECSApi(self._volc_api_client(region, ak, sk))
        stop_request = self._volc_request(volcenginesdkecs.StopInstanceRequest, instance_id=instance_id, force_stop=True)
        start_request = self._volc_request(volcenginesdkecs.StartInstanceRequest, instance_id=instance_id)

        def read_status() -> Tuple[str, str]:
            instance = self._volc_find_instance(region, instance_id, ak, sk)
            status = self._volc_first(instance or {}, "Status", "status")
            return status.upper(), status

        def recover_running() -> None:
            self.after(0, self.log, f"[{log_label}] 重装未完成，开始自动尝试开机恢复。")
            start_submitted = False
            last_status = ""
            last_error: Optional[Exception] = None
            for attempt in range(1, 11):
                try:
                    upper_status, status = read_status()
                    last_status = status
                    last_error = None
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 开机恢复状态检测 {attempt}/10: {self._volc_status_text(status)}。",
                    )
                    if upper_status == "RUNNING":
                        self.after(0, self.log, f"[{log_label}] 自动开机恢复完成，云服务器已运行。")
                        return
                    if upper_status == "STOPPED" and not start_submitted:
                        try:
                            self._volc_call_with_retry(
                                lambda: api.start_instance(start_request),
                                f"{region} StartInstanceRecovery",
                                attempts=2,
                            )
                            start_submitted = True
                            self.after(0, self.log, f"[{log_label}] 自动开机请求已提交。")
                        except Exception as exc:
                            last_error = exc
                            self.after(
                                0,
                                self.log,
                                f"[{log_label}] 自动开机暂未成功: {self._volc_resource_exception_text(exc, instance_id)}。",
                            )
                except Exception as exc:
                    last_error = exc
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 开机恢复状态检测 {attempt}/10 失败: {self._volc_resource_exception_text(exc, instance_id)}。",
                    )
                if attempt < 10:
                    time.sleep(2.0)
            detail = self._volc_status_text(last_status) if last_status else "未读取到状态"
            if last_error is not None:
                detail = self._volc_resource_exception_text(last_error, instance_id)
            self.after(0, self.log, f"[{log_label}] 自动开机恢复未完成，最后状态: {detail}。")

        try:
            try:
                self.after(0, self.log, f"[{log_label}] 开始停止云服务器，准备重装系统盘。")
                self._volc_call_with_retry(lambda: api.stop_instance(stop_request), f"{region} StopInstance", attempts=2)
                self.after(0, self.log, f"[{log_label}] 停止云服务器请求已提交，开始检测停止状态。")
            except Exception as exc:
                self.after(
                    0,
                    self.log,
                    f"[{log_label}] 停止云服务器提示: {self._volc_resource_exception_text(exc, instance_id)}，继续检测实际状态。",
                )

            stopped = False
            last_stop_status = ""
            last_stop_error: Optional[Exception] = None
            for attempt in range(1, 11):
                if self.stop_event.wait(2.0):
                    raise RuntimeError("任务已停止，未继续等待云服务器停止")
                try:
                    upper_status, status = read_status()
                    last_stop_status = status
                    last_stop_error = None
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 停止状态检测 {attempt}/10: {self._volc_status_text(status)}。",
                    )
                    if upper_status == "STOPPED":
                        stopped = True
                        break
                except Exception as exc:
                    last_stop_error = exc
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 停止状态检测 {attempt}/10 失败: {self._volc_resource_exception_text(exc, instance_id)}。",
                    )
            if not stopped:
                detail = self._volc_status_text(last_stop_status) if last_stop_status else "未读取到状态"
                if last_stop_error is not None:
                    detail = self._volc_resource_exception_text(last_stop_error, instance_id)
                raise RuntimeError(f"等待云服务器停止超时，每2秒检测一次，共10轮，最后状态: {detail}")

            self.after(0, self.log, f"[{log_label}] 已确认云服务器停止，开始提交重装系统盘。")
            replace_request = self._volc_request(
                volcenginesdkecs.ReplaceSystemVolumeRequest,
                instance_id=instance_id,
                image_id=image_id,
                password=DEFAULT_PASSWORD,
                keep_image_credential=False,
            )
            last_exc: Optional[Exception] = None
            for attempt in range(1, 4):
                try:
                    self.after(0, self.log, f"[{log_label}] 提交重装系统盘尝试 {attempt}/3。")
                    self._volc_call_with_retry(
                        lambda: api.replace_system_volume(replace_request),
                        f"{region} ReplaceSystemVolume",
                        attempts=1,
                    )
                    last_exc = None
                    self.after(0, self.log, f"[{log_label}] 重装系统盘请求已提交，开始检测运行状态。")
                    break
                except Exception as exc:
                    last_exc = exc
                    if attempt >= 3:
                        break
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 重装请求暂未成功，等待后重试: {self._volc_resource_exception_text(exc, instance_id)}",
                    )
                    time.sleep(3.0)
            if last_exc:
                raise last_exc

            start_submitted = False
            last_running_status = ""
            last_running_error: Optional[Exception] = None
            for attempt in range(1, 11):
                if self.stop_event.wait(2.0):
                    raise RuntimeError("任务已停止，未继续等待云服务器运行")
                try:
                    upper_status, status = read_status()
                    last_running_status = status
                    last_running_error = None
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 运行状态检测 {attempt}/10: {self._volc_status_text(status)}。",
                    )
                    if upper_status == "RUNNING":
                        self.after(0, self.log, f"[{log_label}] 重装完成，云服务器已运行。")
                        return
                    if upper_status == "STOPPED" and not start_submitted:
                        try:
                            self._volc_call_with_retry(
                                lambda: api.start_instance(start_request),
                                f"{region} StartInstance",
                                attempts=2,
                            )
                            start_submitted = True
                            self.after(0, self.log, f"[{log_label}] 重装后开机请求已提交。")
                        except Exception as exc:
                            last_running_error = exc
                            self.after(
                                0,
                                self.log,
                                f"[{log_label}] 重装后开机暂未成功: {self._volc_resource_exception_text(exc, instance_id)}。",
                            )
                except Exception as exc:
                    last_running_error = exc
                    self.after(
                        0,
                        self.log,
                        f"[{log_label}] 运行状态检测 {attempt}/10 失败: {self._volc_resource_exception_text(exc, instance_id)}。",
                    )
            detail = self._volc_status_text(last_running_status) if last_running_status else "未读取到状态"
            if last_running_error is not None:
                detail = self._volc_resource_exception_text(last_running_error, instance_id)
            raise RuntimeError(f"等待云服务器运行超时，每2秒检测一次，共10轮，最后状态: {detail}")
        except Exception:
            recover_running()
            raise


    def _volc_release_eip(self, region: str, item: Dict[str, Any], ak: str, sk: str) -> None:
        import volcenginesdkvpc

        allocation_id = str(item.get("_resource_id") or "").strip()
        if not allocation_id:
            raise ValueError("缺少EIP资源ID")
        label = self._volc_item_label(item)
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
        associated_id = self._volc_first(raw, "AssociatedInstanceId", "associated_instance_id", "InstanceId", "instance_id")
        release_request = self._volc_request(volcenginesdkvpc.ReleaseEipAddressRequest, allocation_id=allocation_id, client_token=str(uuid.uuid4()))
        disassociate_request = self._volc_request(volcenginesdkvpc.DisassociateEipAddressRequest, allocation_id=allocation_id, client_token=str(uuid.uuid4()))

        def is_in_use(exc: Exception) -> bool:
            lowered = self._volc_exception_text(exc).lower()
            return "inuse" in lowered or "in use" in lowered or "invalidip.inuse" in lowered or "in.use" in lowered

        last_exc: Optional[Exception] = None
        for attempt in range(1, 7):
            if associated_id or attempt > 1:
                try:
                    self._volc_manage_log(f"[{label}] 解绑EIP尝试 {attempt}/6。")
                    self._volc_call_with_retry(
                        lambda: api.disassociate_eip_address(disassociate_request),
                        f"{region} DisassociateEipAddress",
                        attempts=1,
                    )
                except Exception as exc:
                    if self._volc_absent_error(exc):
                        self._volc_manage_log(f"[{label}] EIP已不存在，按释放完成处理。")
                        return
                    text = self._volc_exception_text(exc).lower()
                    if not any(token in text for token in ("not associated", "notassociate", "invalidassociation", "not found")):
                        last_exc = exc
                        self._volc_manage_log(f"[{label}] 解绑提示: {self._volc_resource_exception_text(exc, allocation_id, associated_id)}")
                time.sleep(1.5 if attempt < 3 else 2.5)
            try:
                self._volc_manage_log(f"[{label}] 释放EIP尝试 {attempt}/6。")
                self._volc_call_with_retry(lambda: api.release_eip_address(release_request), f"{region} ReleaseEipAddress", attempts=1)
                return
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] EIP已不存在，按释放完成处理。")
                    return
                last_exc = exc
                if not is_in_use(exc):
                    raise
                time.sleep(1.5 if attempt < 3 else 2.5)
        if last_exc:
            raise last_exc

    def _volc_allocate_eip(self, region: str, ak: str, sk: str, name: str = "") -> Dict[str, Any]:
        import volcenginesdkvpc

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        params: Dict[str, Any] = {
            "bandwidth": 200,
            "billing_type": 3,
            "isp": "BGP",
            "client_token": str(uuid.uuid4()),
        }
        if name:
            params["name"] = name
        request = self._volc_request(volcenginesdkvpc.AllocateEipAddressRequest, **params)
        response: Any = None
        last_exc: Optional[Exception] = None
        for attempt in range(1, 3):
            reservation = reserve_eip_daily_purchase(ak, sk, region, name=name)
            token = str(reservation.get("token") or "")
            try:
                response = api.allocate_eip_address(request)
            except Exception as exc:
                last_exc = exc
                finish_eip_daily_purchase(ak, sk, region, token, success=False, error=self._volc_exception_text(exc))
                if attempt >= 2 or not is_network_transient_error(exc):
                    raise
                self.after(
                    0,
                    self.log,
                    f"[火山引擎] {region} AllocateEipAddress 连接中断，重试 {attempt}/2: {self._volc_exception_text(exc)}",
                    "manage",
                )
                time.sleep(min(1.2 * attempt, 4.0))
                continue
            break
        if response is None:
            if last_exc:
                raise last_exc
            raise RuntimeError(f"{region} AllocateEipAddress 调用失败")
        data = self._volc_result(self._volc_to_dict(response))
        allocation_id = self._volc_first(data, "AllocationId", "allocation_id", "EipId", "eip_id", "id")
        eip_address = self._volc_first(data, "EipAddress", "eip_address", "IpAddress", "ip_address")
        try:
            daily_status = finish_eip_daily_purchase(ak, sk, region, token, success=True, ip=eip_address)
        except Exception as exc:
            # 预占记录仍保留并计数，不能因写入完成状态失败而遗留已经购买成功的EIP。
            daily_status = reservation
            self.after(0, self.log, f"[火山引擎] 公网IP已购买，但每日记录完成状态写入失败: {exc}", "manage")
        if not allocation_id:
            raise RuntimeError(f"申请EIP后未返回AllocationId: {data}")
        data.setdefault("AllocationId", allocation_id)
        data["_daily_purchase_count"] = daily_status["count"]
        data["_daily_purchase_remaining"] = daily_status["remaining"]
        return data

    def _volc_associate_eip(
        self,
        region: str,
        allocation_id: str,
        target_id: str,
        private_ip: str,
        instance_type: str,
        ak: str,
        sk: str,
    ) -> None:
        import volcenginesdkvpc

        if not allocation_id or not target_id:
            raise ValueError("缺少EIP或绑定目标ID")
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        params: Dict[str, Any] = {
            "allocation_id": allocation_id,
            "instance_id": target_id,
            "instance_type": instance_type or "EcsInstance",
            "client_token": str(uuid.uuid4()),
        }
        if private_ip:
            params["private_ip_address"] = private_ip
        request = self._volc_request(volcenginesdkvpc.AssociateEipAddressRequest, **params)
        self._volc_call_with_retry(lambda: api.associate_eip_address(request), f"{region} AssociateEipAddress", attempts=2)

    def _volc_disassociate_eip(self, region: str, allocation_id: str, ak: str, sk: str) -> None:
        import volcenginesdkvpc

        if not allocation_id:
            return
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        request = self._volc_request(volcenginesdkvpc.DisassociateEipAddressRequest, allocation_id=allocation_id)
        self._volc_call_with_retry(lambda: api.disassociate_eip_address(request), f"{region} DisassociateEipAddress", attempts=2)

    def _volc_delete_vpc(self, region: str, vpc_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        import volcenginesdkvpc

        label = log_label or f"{region}/VPC"
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        last_exc: Optional[Exception] = None
        for attempt in range(1, VOLC_DELETE_DEPENDENCY_ATTEMPTS + 1):
            request = self._volc_request(volcenginesdkvpc.DeleteVpcRequest, vpc_id=vpc_id)
            try:
                self._volc_manage_log(f"[{label}] 删除VPC尝试 {attempt}/{VOLC_DELETE_DEPENDENCY_ATTEMPTS}。")
                self._volc_call_with_retry(lambda: api.delete_vpc(request), f"{region} DeleteVpc", attempts=2)
                return
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] VPC已不存在，按删除完成处理。")
                    return
                last_exc = exc
                if attempt >= VOLC_DELETE_DEPENDENCY_ATTEMPTS or not (self._volc_dependency_error(exc) or is_network_transient_error(exc)):
                    raise
                self._volc_manage_log(f"[{label}] VPC仍有依赖或状态未同步，等待后重试: {self._volc_resource_exception_text(exc, vpc_id)}")
                time.sleep(self._volc_delete_retry_sleep(attempt))
        if last_exc:
            raise last_exc

    def _volc_delete_security_group(self, region: str, security_group_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        import volcenginesdkvpc

        label = log_label or f"{region}/安全组"
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        last_exc: Optional[Exception] = None
        for attempt in range(1, VOLC_DELETE_DEPENDENCY_ATTEMPTS + 1):
            request = self._volc_request(volcenginesdkvpc.DeleteSecurityGroupRequest, security_group_id=security_group_id)
            try:
                self._volc_manage_log(f"[{label}] 删除安全组尝试 {attempt}/{VOLC_DELETE_DEPENDENCY_ATTEMPTS}。")
                self._volc_call_with_retry(lambda: api.delete_security_group(request), f"{region} DeleteSecurityGroup", attempts=2)
                return
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] 安全组已不存在，按删除完成处理。")
                    return
                last_exc = exc
                if attempt >= VOLC_DELETE_DEPENDENCY_ATTEMPTS or not (self._volc_dependency_error(exc) or is_network_transient_error(exc)):
                    raise
                self._volc_manage_log(f"[{label}] 安全组仍有依赖或状态未同步，等待后重试: {self._volc_resource_exception_text(exc, security_group_id)}")
                time.sleep(self._volc_delete_retry_sleep(attempt))
        if last_exc:
            raise last_exc

    def _volc_delete_subnet(self, region: str, subnet_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        import volcenginesdkvpc

        if not subnet_id:
            raise ValueError("缺少子网ID")
        label = log_label or f"{region}/子网"
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        last_exc: Optional[Exception] = None
        for attempt in range(1, VOLC_DELETE_DEPENDENCY_ATTEMPTS + 1):
            request = self._volc_request(volcenginesdkvpc.DeleteSubnetRequest, subnet_id=subnet_id)
            try:
                self._volc_manage_log(f"[{label}] 删除子网尝试 {attempt}/{VOLC_DELETE_DEPENDENCY_ATTEMPTS}。")
                self._volc_call_with_retry(lambda: api.delete_subnet(request), f"{region} DeleteSubnet", attempts=2)
                return
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] 子网已不存在，按删除完成处理。")
                    return
                last_exc = exc
                if attempt >= VOLC_DELETE_DEPENDENCY_ATTEMPTS or not (self._volc_dependency_error(exc) or is_network_transient_error(exc)):
                    raise
                self._volc_manage_log(f"[{label}] 子网仍有依赖或状态未同步，等待后重试: {self._volc_resource_exception_text(exc, subnet_id)}")
                time.sleep(self._volc_delete_retry_sleep(attempt))
        if last_exc:
            raise last_exc

    def _volc_delete_network_interface(self, region: str, network_interface_id: str, attached_instance_id: str, ak: str, sk: str, *, log_label: str = "") -> None:
        """删除火山网卡；如果仍挂在 ECS 上，先按云端返回的实例 ID 解绑再删除。"""
        import volcenginesdkvpc

        if not network_interface_id:
            raise ValueError("缺少网卡ID")
        label = log_label or f"{region}/网卡"
        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        if attached_instance_id:
            detach_request = self._volc_request(
                volcenginesdkvpc.DetachNetworkInterfaceRequest,
                network_interface_id=network_interface_id,
                instance_id=attached_instance_id,
            )
            for attempt in range(1, 5):
                try:
                    self._volc_manage_log(f"[{label}] 解绑网卡尝试 {attempt}/4。")
                    self._volc_call_with_retry(lambda: api.detach_network_interface(detach_request), f"{region} DetachNetworkInterface", attempts=1)
                    time.sleep(2.0)
                    break
                except Exception as exc:
                    text = self._volc_exception_text(exc).lower()
                    if self._volc_absent_error(exc):
                        self._volc_manage_log(f"[{label}] 网卡已不存在，按删除完成处理。")
                        return
                    if any(token in text for token in ("not attached", "notattach", "invalidattachment", "not associated", "notassociate")):
                        break
                    if attempt >= 4 or not (self._volc_dependency_error(exc) or is_network_transient_error(exc)):
                        raise
                    self._volc_manage_log(f"[{label}] 网卡解绑暂未完成，等待后重试: {self._volc_resource_exception_text(exc, network_interface_id, attached_instance_id)}")
                    time.sleep(min(2.0 * attempt, 8.0))

        last_exc: Optional[Exception] = None
        for attempt in range(1, VOLC_DELETE_DEPENDENCY_ATTEMPTS + 1):
            request = self._volc_request(volcenginesdkvpc.DeleteNetworkInterfaceRequest, network_interface_id=network_interface_id)
            try:
                self._volc_manage_log(f"[{label}] 删除网卡尝试 {attempt}/{VOLC_DELETE_DEPENDENCY_ATTEMPTS}。")
                self._volc_call_with_retry(lambda: api.delete_network_interface(request), f"{region} DeleteNetworkInterface", attempts=2)
                return
            except Exception as exc:
                if self._volc_absent_error(exc):
                    self._volc_manage_log(f"[{label}] 网卡已不存在，按删除完成处理。")
                    return
                last_exc = exc
                if attempt >= VOLC_DELETE_DEPENDENCY_ATTEMPTS or not (self._volc_dependency_error(exc) or is_network_transient_error(exc)):
                    raise
                self._volc_manage_log(f"[{label}] 网卡仍有依赖或状态未同步，等待后重试: {self._volc_resource_exception_text(exc, network_interface_id, attached_instance_id)}")
                time.sleep(self._volc_delete_retry_sleep(attempt))
        if last_exc:
            raise last_exc

    @staticmethod
    def _volc_public_ips_from_resource(resource: Dict[str, Any]) -> List[str]:
        try:
            return extract_public_ips(json.dumps(resource, ensure_ascii=False))
        except Exception:
            return []

    @staticmethod
    def _volc_status_text(status: str) -> str:
        value = (status or "").strip()
        upper = value.upper()
        mapping = {
            "RUNNING": "运行中",
            "STOPPED": "已停止",
            "STOPPING": "停止中",
            "STARTING": "启动中",
            "REBUILDING": "重装中",
            "REBOOTING": "重启中",
            "CREATING": "创建中",
            "DELETING": "删除中",
            "ERROR": "异常",
            "AVAILABLE": "可用",
            "ATTACHED": "已绑定",
            "UNATTACHED": "未绑定",
            "ASSOCIATED": "已绑定",
            "ALLOCATED": "已分配",
        }
        return mapping.get(upper, value or "-")

    @staticmethod
    def _volc_billing_text(resource: Dict[str, Any]) -> str:
        raw = str(
            resource.get("InstanceChargeType")
            or resource.get("instance_charge_type")
            or resource.get("ChargeType")
            or resource.get("BillingType")
            or resource.get("billing_type")
            or ""
        ).strip()
        lowered = raw.lower()
        if lowered in {"prepaid", "pre_pay", "prepay", "subscription"} or raw in {"1", "2"}:
            return "包周期"
        if lowered in {"postpaid", "post_pay", "postpay", "payasyougo"} or raw == "3":
            return "按需"
        return raw or "-"

    @staticmethod
    def _volc_display_expire_time(resource: Dict[str, Any], billing_text: str = "") -> str:
        return display_expire_time_for_billing(resource, billing_text=billing_text)

    @staticmethod
    def _volc_flavor_label(raw_flavor: str) -> str:
        value = str(raw_flavor or "").strip()
        for spec in VOLC_SPECS.values():
            if value == spec.get("instance_type"):
                return str(spec.get("label") or value)
        return value

    @staticmethod
    def _volc_eip_associated_instance_id(eip: Dict[str, Any]) -> str:
        return VolcEngineManagerApp._volc_first(
            eip,
            "AssociatedInstanceId",
            "associated_instance_id",
            "InstanceId",
            "instance_id",
            "AssociatedInstanceID",
            "NetworkInterfaceId",
            "network_interface_id",
            "EniId",
            "eni_id",
        )

    @staticmethod
    def _volc_eip_associated_type(eip: Dict[str, Any]) -> str:
        associated_type = VolcEngineManagerApp._volc_first(
            eip,
            "AssociatedInstanceType",
            "associated_instance_type",
            "InstanceType",
            "instance_type",
        )
        if associated_type:
            return associated_type
        if VolcEngineManagerApp._volc_first(eip, "NetworkInterfaceId", "network_interface_id", "EniId", "eni_id"):
            return "NetworkInterface"
        return "EcsInstance"

    @staticmethod
    def _volc_eip_private_ip(eip: Dict[str, Any]) -> str:
        return VolcEngineManagerApp._volc_first(
            eip,
            "PrivateIpAddress",
            "private_ip_address",
            "PrivateIp",
            "private_ip",
            "AssociatedPrivateIpAddress",
            "associated_private_ip_address",
        )

    @staticmethod
    def _volc_entry_private_ip_order() -> List[str]:
        """返回火山脚本约定的入口私网 IP 顺序；资源库服务器IP必须优先取 192.168.0.10。"""
        try:
            from Volcengine.api.volcengine import data as volc_data

            ordered_ips: List[str] = []
            primary_ip = str(getattr(volc_data, "ECS_PRIMARY_IP", "") or "").strip()
            secondary_ip = str(getattr(volc_data, "ECS_SECONDARY_IP", "") or "").strip()
            ordered_ips.extend(ip for ip in [primary_ip, secondary_ip] if ip)
            ordered_ips.extend(str(ip).strip() for ip in getattr(volc_data, "ECS_PRIMARY_EXTRA_IPS", []) if str(ip).strip())
            ordered_ips.extend(str(ip).strip() for ip in getattr(volc_data, "ECS_SECONDARY_EXTRA_IPS", []) if str(ip).strip())
            return list(dict.fromkeys(ordered_ips))
        except Exception:
            return ["192.168.0.10", "192.168.0.11", "192.168.0.12", "192.168.0.13", "192.168.0.14", "192.168.0.15", "192.168.0.16", "192.168.0.17"]

    @classmethod
    def _volc_sort_public_ips_by_private_ip(cls, pairs: Sequence[Tuple[str, str]]) -> List[str]:
        """按私网 IP 规划排序公网 IP，避免把 192.168.0.17 的出口 IP 显示成云服务器入口 IP。"""
        order_map = {ip: index for index, ip in enumerate(cls._volc_entry_private_ip_order())}
        indexed: List[Tuple[int, int, str]] = []
        seen: Set[str] = set()
        for raw_index, (private_ip, public_ip) in enumerate(pairs):
            public_value = str(public_ip or "").strip()
            if not public_value or public_value in seen:
                continue
            seen.add(public_value)
            private_value = str(private_ip or "").strip()
            indexed.append((order_map.get(private_value, len(order_map) + raw_index), raw_index, public_value))
        return [public_ip for _sort_index, _raw_index, public_ip in sorted(indexed)]

    @classmethod
    def _volc_public_to_private_ip_map_from_nic(cls, nic: Dict[str, Any]) -> Dict[str, str]:
        """从网卡详情提取公网 IP 到私网 IP 的映射，弥补 EIP 列表不返回 PrivateIpAddress 的情况。"""
        result: Dict[str, str] = {}
        planned_private_ips = set(cls._volc_entry_private_ip_order())
        public_ip_keys = {
            "eipaddress",
            "ipaddress",
            "publicipaddress",
            "associatedpublicip",
            "associatedpublicipaddress",
            "natipaddress",
        }
        private_ip_keys = {
            "privateipaddress",
            "privateip",
            "primaryipaddress",
            "associatedprivateipaddress",
            "ipaddress",
        }

        def collect_public_ips(value: Any) -> List[str]:
            ips = extract_public_ips(json.dumps(value, ensure_ascii=False)) if isinstance(value, (dict, list)) else extract_public_ips(str(value or ""))
            return [ip for ip in ips if ip not in planned_private_ips]

        def collect_private_ips(value: Any) -> List[str]:
            text = json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value or "")
            found = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)
            return [ip for ip in found if ip in planned_private_ips]

        def walk(value: Any, inherited_private_ip: str = "") -> None:
            if isinstance(value, dict):
                current_private_ip = inherited_private_ip
                for key, item_value in value.items():
                    key_norm = str(key).replace("_", "").replace("-", "").lower()
                    if key_norm in private_ip_keys:
                        for private_ip in collect_private_ips(item_value):
                            current_private_ip = private_ip
                            break
                    if key_norm in public_ip_keys:
                        for public_ip in collect_public_ips(item_value):
                            if current_private_ip:
                                result.setdefault(public_ip, current_private_ip)
                nested_private_ip = current_private_ip
                for item_value in value.values():
                    if isinstance(item_value, (dict, list)):
                        walk(item_value, nested_private_ip)
                return
            if isinstance(value, list):
                for item_value in value:
                    walk(item_value, inherited_private_ip)

        walk(nic)
        return result

    @staticmethod
    def _volc_network_interface_attached_instance_id(nic: Dict[str, Any]) -> str:
        return VolcEngineManagerApp._volc_first(
            nic,
            "InstanceId",
            "instance_id",
            "AttachedInstanceId",
            "attached_instance_id",
            "AttachedInstanceID",
        )

    @staticmethod
    def _volc_network_interface_security_group_ids(nic: Dict[str, Any]) -> Set[str]:
        ids: Set[str] = set()
        for key in ("SecurityGroupIds", "security_group_ids", "SecurityGroupIDs"):
            value = nic.get(key)
            if isinstance(value, list):
                ids.update(str(item) for item in value if item)
        for key in ("SecurityGroups", "security_groups", "SecurityGroupSet", "security_group_set"):
            groups = nic.get(key)
            if not isinstance(groups, list):
                continue
            for group in groups:
                if isinstance(group, dict):
                    group_id = VolcEngineManagerApp._volc_first(group, "SecurityGroupId", "security_group_id", "id")
                    if group_id:
                        ids.add(group_id)
                elif group:
                    ids.add(str(group))
        return ids

    @staticmethod
    def _volc_known_resource_group_index_from_name(name: str) -> int:
        text = str(name or "").strip().lower()
        if not text:
            return 0
        group1_tokens = (
            "wawacloud_ecs1",
            "wawacloud_vpc1",
            "wawacloud_sub1",
            "wawacloud_sg1",
            "wawacloud_one",
        )
        group2_tokens = (
            "wawacloud_ecs2",
            "wawacloud_vpc2",
            "wawacloud_sub2",
            "wawacloud_sg2",
            "wawacloud_two",
        )
        if any(token in text for token in group2_tokens):
            return 2
        if any(token in text for token in group1_tokens):
            return 1
        return 0

    @staticmethod
    def _volc_resource_group_index_from_name(name: str) -> int:
        known_group = VolcEngineManagerApp._volc_known_resource_group_index_from_name(name)
        if known_group:
            return known_group
        return 1

    @staticmethod
    def _volc_region_group_label(region_label: str, group_index: int) -> str:
        label = str(region_label or "").strip()
        if int(group_index or 1) >= 2 and label:
            return f"{label}{int(group_index)}"
        return label

    def _volc_apply_group_index(self, item: Dict[str, Any], plan: RegionPlan, group_index: int) -> Dict[str, Any]:
        group_index = 2 if int(group_index or 1) >= 2 else 1
        item["_group_index"] = group_index
        item["_region_label"] = self._volc_region_group_label(plan.label, group_index)
        return item

    def _volc_manage_item_group_index(self, item: Dict[str, Any]) -> int:
        binding_group = item.get("_binding_group_index")
        try:
            binding_group_index = int(binding_group)
        except (TypeError, ValueError):
            binding_group_index = 0
        if binding_group_index > 0:
            return 2 if binding_group_index >= 2 else 1
        known_name_group = self._volc_known_resource_group_index_from_name(str(item.get("_name") or ""))
        if known_name_group:
            return 2 if known_name_group >= 2 else 1
        raw_group = item.get("_group_index")
        try:
            group_index = int(raw_group)
        except (TypeError, ValueError):
            group_index = 0
        if group_index >= 2:
            return 2
        name_group = self._volc_resource_group_index_from_name(str(item.get("_name") or ""))
        return 2 if name_group >= 2 else 1

    @staticmethod
    def _volc_purchase_exit_message(code: int) -> str:
        messages = {
            1: "流程初始化或云接口调用失败",
            2: "账户可用余额不足100元，已停止购买公网IP资源，请先充值至100元以上后重试",
            3: "VPC 创建或复用失败",
            4: "子网创建或 VPC 状态等待失败",
            5: "安全组创建或规则配置失败",
            7: "云服务器创建或复用失败",
            8: "云服务器启动、网卡读取或私网 IP 绑定失败",
            9: "公网 IP 申请/复用/绑定前检查失败，通常是 EIP 没买齐或可用未绑定 EIP 不足",
            10: "公网 IP 绑定到云服务器/网卡失败",
            11: "所选地区没有可复用的云服务器；购买选择地区新IP不会创建新云服务器",
        }
        return messages.get(int(code or 0), "购买搭建子进程异常退出")

    @staticmethod
    def _volc_stopped_old_ecs_reason(ecs_name: str = "", instance_id: str = "") -> str:
        target = str(ecs_name or "旧ECS").strip()
        instance_text = f"（{instance_id}）" if instance_id else ""
        return f"检测到旧 ECS {target}{instance_text} 处于已停止状态，无法继续复用；请先在资源管理清理该旧 ECS 后重新执行，已购买的 EIP 会继续复用。"

    def _volc_instance_item(
        self,
        plan: RegionPlan,
        instance: Dict[str, Any],
        public_ips_override: Optional[Sequence[str]] = None,
    ) -> Dict[str, Any]:
        instance_id = self._volc_first(instance, "InstanceId", "instance_id", "id")
        name = self._volc_first(instance, "InstanceName", "instance_name", "Name", "name") or instance_id
        status = self._volc_status_text(self._volc_first(instance, "Status", "status"))
        public_ips = list(public_ips_override or []) or self._volc_public_ips_from_resource(instance)
        flavor = self._volc_flavor_label(self._volc_first(instance, "InstanceTypeId", "instance_type_id", "InstanceType", "instance_type"))
        billing_text = self._volc_billing_text(instance)
        return {
            "_kind": "ecs",
            "_kind_label": "ECS",
            "_region": plan.region,
            "_region_label": plan.label,
            "_name": name,
            "_status": status,
            "_billing": billing_text,
            "_operation_text": "可重装/可删除",
            "_operation_kind": "ecs",
            "_public_ip": public_ips[0] if public_ips else "",
            "_server_id": instance_id,
            "_resource_id": instance_id,
            "_expire_time": self._volc_display_expire_time(instance, billing_text),
            "_flavor": flavor,
            "_raw": instance,
        }

    def _volc_eip_item(self, plan: RegionPlan, eip: Dict[str, Any], private_ip_override: str = "") -> Dict[str, Any]:
        allocation_id = self._volc_first(eip, "AllocationId", "allocation_id", "EipId", "eip_id", "id")
        address = self._volc_first(eip, "EipAddress", "eip_address", "IpAddress", "ip_address")
        if not address:
            ips = self._volc_public_ips_from_resource(eip)
            address = ips[0] if ips else ""
        name = self._volc_first(eip, "Name", "name") or address or allocation_id
        associated_id = self._volc_eip_associated_instance_id(eip)
        private_ip = str(private_ip_override or self._volc_eip_private_ip(eip) or "").strip()
        status_text = f"私网IP{private_ip}" if associated_id and private_ip else self._volc_status_text(self._volc_first(eip, "Status", "status"))
        operation_text = "EIP已绑定，可释放" if associated_id else "未绑定EIP可释放"
        billing_text = self._volc_billing_text(eip)
        return {
            "_kind": "eip",
            "_kind_label": "EIP",
            "_region": plan.region,
            "_region_label": plan.label,
            "_name": name,
            "_status": status_text,
            "_billing": billing_text,
            "_operation_text": operation_text,
            "_operation_kind": "eip_release" if allocation_id else "none",
            "_public_ip": address,
            "_private_ip": private_ip,
            "_associated_id": associated_id,
            "_associated_type": self._volc_eip_associated_type(eip) if associated_id else "",
            "_server_id": "",
            "_resource_id": allocation_id,
            "_expire_time": self._volc_display_expire_time(eip, billing_text),
            "_flavor": str(eip.get("Bandwidth") or eip.get("bandwidth") or ""),
            "_raw": eip,
        }

    def _volc_vpc_item(self, plan: RegionPlan, vpc: Dict[str, Any], bound_vpc_ids: Set[str]) -> Dict[str, Any]:
        vpc_id = self._volc_first(vpc, "VpcId", "vpc_id", "id")
        name = self._volc_first(vpc, "VpcName", "vpc_name", "Name", "name") or vpc_id
        can_delete = bool(vpc_id and vpc_id not in bound_vpc_ids)
        return {
            "_kind": "vpc",
            "_kind_label": "VPC",
            "_region": plan.region,
            "_region_label": plan.label,
            "_name": name,
            "_status": self._volc_status_text(self._volc_first(vpc, "Status", "status")),
            "_billing": "-",
            "_operation_text": "空VPC可删除" if can_delete else "VPC有绑定",
            "_operation_kind": "vpc_delete" if can_delete else "none",
            "_public_ip": "",
            "_server_id": "",
            "_resource_id": vpc_id,
            "_vpc_id": vpc_id,
            "_expire_time": "",
            "_flavor": "",
            "_raw": vpc,
        }

    def _volc_security_group_item(
        self,
        plan: RegionPlan,
        group: Dict[str, Any],
        referenced_sg_ids: Set[str],
    ) -> Optional[Dict[str, Any]]:
        group_id = self._volc_first(group, "SecurityGroupId", "security_group_id", "id")
        name = self._volc_first(group, "SecurityGroupName", "security_group_name", "Name", "name") or group_id
        if name.lower() == "default":
            return None
        can_delete = bool(group_id and group_id not in referenced_sg_ids)
        return {
            "_kind": "security_group",
            "_kind_label": "安全组",
            "_region": plan.region,
            "_region_label": plan.label,
            "_name": name,
            "_status": self._volc_status_text(self._volc_first(group, "Status", "status")),
            "_billing": "-",
            "_operation_text": "安全组可删除" if can_delete else "安全组有绑定",
            "_operation_kind": "security_group_delete" if can_delete else "none",
            "_public_ip": "",
            "_server_id": "",
            "_resource_id": group_id,
            "_vpc_id": self._volc_first(group, "VpcId", "vpc_id"),
            "_expire_time": "",
            "_flavor": "",
            "_raw": group,
        }

    def _volc_network_interface_item(self, plan: RegionPlan, nic: Dict[str, Any]) -> Dict[str, Any]:
        nic_id = self._volc_first(nic, "NetworkInterfaceId", "network_interface_id", "EniId", "eni_id", "id")
        name = self._volc_first(nic, "NetworkInterfaceName", "network_interface_name", "Name", "name") or nic_id
        status = self._volc_first(nic, "Status", "status")
        attached_instance_id = self._volc_network_interface_attached_instance_id(nic)
        nic_type = self._volc_first(nic, "Type", "type", "NetworkInterfaceType", "network_interface_type")
        primary_value = self._volc_first(nic, "Primary", "primary", "IsPrimary", "is_primary")
        is_primary = str(nic_type).lower() in {"primary", "primaryeni", "primary_network_interface"} or str(primary_value).lower() == "true"
        # VPC/子网删除会被残留 ENI 阻塞；主网卡若已经没有绑定 ECS，也应作为孤立网卡进入删除队列。
        can_delete = bool(nic_id and (not is_primary or not attached_instance_id))
        if is_primary and attached_instance_id:
            operation_text = "主网卡随ECS删除"
        elif is_primary:
            operation_text = "残留主网卡可删除"
        elif attached_instance_id:
            operation_text = "网卡已绑定，先解绑再删除"
        else:
            operation_text = "网卡可删除"
        return {
            "_kind": "network_interface",
            "_kind_label": "网卡",
            "_region": plan.region,
            "_region_label": plan.label,
            "_name": name,
            "_status": self._volc_status_text(status),
            "_billing": "-",
            "_operation_text": operation_text,
            "_operation_kind": "network_interface_delete" if can_delete else "none",
            "_public_ip": self._volc_first(nic, "PrivateIpAddress", "private_ip_address", "PrimaryIpAddress", "primary_ip_address"),
            "_server_id": attached_instance_id,
            "_resource_id": nic_id,
            "_vpc_id": self._volc_first(nic, "VpcId", "vpc_id"),
            "_security_group_ids": sorted(self._volc_network_interface_security_group_ids(nic)),
            "_is_primary_network_interface": is_primary,
            "_expire_time": "",
            "_flavor": nic_type,
            "_raw": nic,
        }

    def _volc_subnet_item(self, plan: RegionPlan, subnet: Dict[str, Any]) -> Dict[str, Any]:
        subnet_id = self._volc_first(subnet, "SubnetId", "subnet_id", "id")
        name = self._volc_first(subnet, "SubnetName", "subnet_name", "Name", "name") or subnet_id
        return {
            "_kind": "subnet",
            "_kind_label": "子网",
            "_region": plan.region,
            "_region_label": plan.label,
            "_name": name,
            "_status": self._volc_status_text(self._volc_first(subnet, "Status", "status")),
            "_billing": "-",
            "_operation_text": "子网可删除" if subnet_id else "缺少子网ID",
            "_operation_kind": "subnet_delete" if subnet_id else "none",
            "_public_ip": self._volc_first(subnet, "CidrBlock", "cidr_block"),
            "_server_id": "",
            "_resource_id": subnet_id,
            "_vpc_id": self._volc_first(subnet, "VpcId", "vpc_id"),
            "_expire_time": "",
            "_flavor": "",
            "_raw": subnet,
        }

    def _volc_resource_summary_log_lines(
        self,
        plan: RegionPlan,
        items: Sequence[Dict[str, Any]],
    ) -> List[str]:
        count_keys = ("ecs", "eip", "network_interface", "vpc", "security_group", "subnet")
        counts = {group_index: {key: 0 for key in count_keys} for group_index in (1, 2)}
        has_group_two = False
        for item in items:
            kind = str(item.get("_kind") or "").strip()
            if kind not in count_keys:
                continue
            group_index = 2 if self._volc_manage_item_group_index(dict(item)) >= 2 else 1
            counts[group_index][kind] += 1
            has_group_two = has_group_two or group_index == 2

        lines: List[str] = []
        for group_index in ((1, 2) if has_group_two else (1,)):
            group_counts = counts[group_index]
            label = self._volc_region_group_label(plan.label, group_index)
            lines.append(
                f"[{label}] 资源汇总: "
                f"ECS={group_counts['ecs']} EIP={group_counts['eip']} "
                f"网卡={group_counts['network_interface']} VPC={group_counts['vpc']} "
                f"安全组={group_counts['security_group']} 子网={group_counts['subnet']}"
            )
        return lines

    def _query_volc_region_resources(
        self,
        plan: RegionPlan,
        ak: str,
        sk: str,
        *,
        log_progress: bool = True,
    ) -> List[Dict[str, Any]]:
        started = time.monotonic()
        fetched: Dict[str, Any] = {}
        if log_progress:
            self.after(0, self.log, f"[{plan.label}] 开始读取火山引擎资源。")
        executor = ThreadPoolExecutor(max_workers=6)
        try:
            futures = {
                "ecs": executor.submit(self._volc_describe_instances, plan.region, ak, sk),
                "eip": executor.submit(self._volc_describe_eips, plan.region, ak, sk),
                "vpc": executor.submit(self._volc_describe_vpcs, plan.region, ak, sk),
                "subnet": executor.submit(self._volc_describe_subnets, plan.region, ak, sk),
                "security_group": executor.submit(self._volc_describe_security_groups, plan.region, ak, sk),
                "network_interface": executor.submit(self._volc_describe_network_interfaces, plan.region, ak, sk),
            }
            future_to_name = {future: name for name, future in futures.items()}
            done, not_done = concurrent.futures.wait(list(future_to_name.keys()), timeout=VOLC_RESOURCE_API_TIMEOUT)
            if not_done:
                slow_names = "、".join(future_to_name[future] for future in not_done)
                if log_progress:
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] {slow_names} 接口较慢，继续等待 {VOLC_RESOURCE_SLOW_API_GRACE:.0f} 秒，避免误判资源为0。",
                    )
                more_done, not_done = concurrent.futures.wait(list(not_done), timeout=VOLC_RESOURCE_SLOW_API_GRACE)
                done = set(done) | set(more_done)
            for future in done:
                name = future_to_name[future]
                try:
                    fetched[name] = future.result()
                except Exception as exc:
                    fetched[name] = []
                    if log_progress:
                        self.after(0, self.log, f"[{plan.label}] {name} 查询失败: {self._volc_exception_text(exc)}")
            for future in not_done:
                name = future_to_name[future]
                future.cancel()
                fetched[name] = []
                if log_progress:
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] {name} 查询超过{VOLC_RESOURCE_API_TIMEOUT + VOLC_RESOURCE_SLOW_API_GRACE:.0f}秒仍未返回，已跳过本次慢接口。",
                    )
        finally:
            executor.shutdown(wait=False, cancel_futures=True)

        retry_actions: Dict[str, Callable[[], List[Dict[str, Any]]]] = {
            "ecs": lambda: self._volc_describe_instances(plan.region, ak, sk),
            "eip": lambda: self._volc_describe_eips(plan.region, ak, sk),
            "vpc": lambda: self._volc_describe_vpcs(plan.region, ak, sk),
            "subnet": lambda: self._volc_describe_subnets(plan.region, ak, sk),
            "security_group": lambda: self._volc_describe_security_groups(plan.region, ak, sk),
            "network_interface": lambda: self._volc_describe_network_interfaces(plan.region, ak, sk),
        }
        for name, action in retry_actions.items():
            if fetched.get(name):
                continue
            try:
                time.sleep(0.25)
                fetched[name] = action()
                if log_progress:
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] {name} 首次读取为空，快速复查已补齐: {len(fetched.get(name) or [])} 条",
                    )
            except Exception as exc:
                fetched[name] = fetched.get(name) or []
                if log_progress:
                    self.after(0, self.log, f"[{plan.label}] {name} 快速复查仍失败: {self._volc_exception_text(exc)}")

        instances = list(fetched.get("ecs") or [])
        eips = list(fetched.get("eip") or [])
        vpcs = list(fetched.get("vpc") or [])
        subnets = list(fetched.get("subnet") or [])
        security_groups = list(fetched.get("security_group") or [])
        network_interfaces = list(fetched.get("network_interface") or [])

        instance_group_by_id: Dict[str, int] = {}
        for instance in instances:
            instance_id = self._volc_first(instance, "InstanceId", "instance_id", "id")
            name = self._volc_first(instance, "InstanceName", "instance_name", "Name", "name")
            if instance_id:
                instance_group_by_id[instance_id] = self._volc_resource_group_index_from_name(name)
        vpc_group_by_id: Dict[str, int] = {}
        for vpc in vpcs:
            vpc_id = self._volc_first(vpc, "VpcId", "vpc_id", "id")
            name = self._volc_first(vpc, "VpcName", "vpc_name", "Name", "name")
            if vpc_id:
                vpc_group_by_id[vpc_id] = self._volc_resource_group_index_from_name(name)
        subnet_group_by_id: Dict[str, int] = {}
        for subnet in subnets:
            subnet_id = self._volc_first(subnet, "SubnetId", "subnet_id", "id")
            name = self._volc_first(subnet, "SubnetName", "subnet_name", "Name", "name")
            vpc_id = self._volc_first(subnet, "VpcId", "vpc_id")
            if subnet_id:
                name_group = self._volc_resource_group_index_from_name(name)
                subnet_group_by_id[subnet_id] = name_group if name_group >= 2 else vpc_group_by_id.get(vpc_id, name_group)
        sg_group_by_id: Dict[str, int] = {}
        for group in security_groups:
            group_id = self._volc_first(group, "SecurityGroupId", "security_group_id", "id")
            name = self._volc_first(group, "SecurityGroupName", "security_group_name", "Name", "name")
            vpc_id = self._volc_first(group, "VpcId", "vpc_id")
            if group_id:
                name_group = self._volc_resource_group_index_from_name(name)
                sg_group_by_id[group_id] = name_group if name_group >= 2 else vpc_group_by_id.get(vpc_id, name_group)

        bound_vpc_ids: Set[str] = set()
        for subnet in subnets:
            vpc_id = self._volc_first(subnet, "VpcId", "vpc_id")
            if vpc_id:
                bound_vpc_ids.add(vpc_id)
        referenced_sg_ids: Set[str] = set()
        for instance in instances:
            vpc_id = self._volc_first(instance, "VpcId", "vpc_id")
            if vpc_id:
                bound_vpc_ids.add(vpc_id)
            for key in ("SecurityGroupIds", "security_group_ids"):
                groups = instance.get(key)
                if isinstance(groups, list):
                    referenced_sg_ids.update(str(item) for item in groups if item)
            for key in ("NetworkInterfaces", "network_interfaces"):
                interfaces = instance.get(key)
                if isinstance(interfaces, list):
                    for iface in interfaces:
                        if not isinstance(iface, dict):
                            continue
                        groups = iface.get("SecurityGroupIds") or iface.get("security_group_ids") or []
                        if isinstance(groups, list):
                            referenced_sg_ids.update(str(item) for item in groups if item)
        for nic in network_interfaces:
            vpc_id = self._volc_first(nic, "VpcId", "vpc_id")
            if vpc_id:
                bound_vpc_ids.add(vpc_id)
            referenced_sg_ids.update(self._volc_network_interface_security_group_ids(nic))

        target_to_instance: Dict[str, str] = {}
        for instance in instances:
            instance_id = self._volc_first(instance, "InstanceId", "instance_id", "id")
            if not instance_id:
                continue
            target_to_instance[instance_id] = instance_id
            for key in ("NetworkInterfaceId", "network_interface_id", "PrimaryNetworkInterfaceId", "primary_network_interface_id"):
                nic_id = str(instance.get(key) or "").strip()
                if nic_id:
                    target_to_instance[nic_id] = instance_id
            for key in ("NetworkInterfaceIds", "network_interface_ids"):
                nic_ids = instance.get(key)
                if isinstance(nic_ids, list):
                    for nic_id in nic_ids:
                        if nic_id:
                            target_to_instance[str(nic_id)] = instance_id
            for key in ("NetworkInterfaces", "network_interfaces"):
                interfaces = instance.get(key)
                if not isinstance(interfaces, list):
                    continue
                for iface in interfaces:
                    if not isinstance(iface, dict):
                        continue
                    nic_id = self._volc_first(iface, "NetworkInterfaceId", "network_interface_id", "EniId", "eni_id", "id")
                    if nic_id:
                        target_to_instance[nic_id] = instance_id
        for nic in network_interfaces:
            nic_id = self._volc_first(nic, "NetworkInterfaceId", "network_interface_id", "EniId", "eni_id", "id")
            attached_instance_id = self._volc_network_interface_attached_instance_id(nic)
            if nic_id and attached_instance_id:
                target_to_instance[nic_id] = attached_instance_id
        nic_group_by_id: Dict[str, int] = {}
        for nic in network_interfaces:
            nic_id = self._volc_first(nic, "NetworkInterfaceId", "network_interface_id", "EniId", "eni_id", "id")
            if not nic_id:
                continue
            attached_instance_id = self._volc_network_interface_attached_instance_id(nic)
            vpc_id = self._volc_first(nic, "VpcId", "vpc_id")
            sg_ids = self._volc_network_interface_security_group_ids(nic)
            group_index = instance_group_by_id.get(attached_instance_id) or vpc_group_by_id.get(vpc_id, 1)
            for sg_id in sg_ids:
                group_index = max(group_index, sg_group_by_id.get(sg_id, 1))
            nic_group_by_id[nic_id] = 2 if int(group_index or 1) >= 2 else 1

        public_to_private_ip: Dict[str, str] = {}
        for nic in network_interfaces:
            # 火山 EIP 查询有时只给绑定网卡/实例，不给绑定私网 IP；
            # 这里从网卡详情反查公网 IP 对应的私网 IP，保证服务器IP优先取 192.168.0.10。
            public_to_private_ip.update(self._volc_public_to_private_ip_map_from_nic(nic))

        eips_by_instance: Dict[str, List[Tuple[str, str]]] = {}
        for eip in eips:
            associated_id = self._volc_eip_associated_instance_id(eip)
            if not associated_id:
                continue
            instance_id = target_to_instance.get(associated_id, associated_id)
            address = self._volc_first(eip, "EipAddress", "eip_address", "IpAddress", "ip_address")
            if not address:
                ips = self._volc_public_ips_from_resource(eip)
                address = ips[0] if ips else ""
            if address:
                private_ip = self._volc_eip_private_ip(eip) or public_to_private_ip.get(address, "")
                eips_by_instance.setdefault(instance_id, []).append((private_ip, address))

        items: List[Dict[str, Any]] = []
        for instance in instances:
            status = self._volc_first(instance, "Status", "status").upper()
            if status == "DELETED":
                continue
            instance_id = self._volc_first(instance, "InstanceId", "instance_id", "id")
            item = self._volc_instance_item(plan, instance, self._volc_sort_public_ips_by_private_ip(eips_by_instance.get(instance_id, [])))
            items.append(self._volc_apply_group_index(item, plan, instance_group_by_id.get(instance_id, 1)))
        for eip in eips:
            eip_address = self._volc_first(eip, "EipAddress", "eip_address", "IpAddress", "ip_address")
            if not eip_address:
                eip_ips = self._volc_public_ips_from_resource(eip)
                eip_address = eip_ips[0] if eip_ips else ""
            eip_item = self._volc_eip_item(plan, eip, public_to_private_ip.get(eip_address, ""))
            eip_name_group = self._volc_known_resource_group_index_from_name(str(eip_item.get("_name") or ""))
            associated_id = self._volc_eip_associated_instance_id(eip)
            associated_instance_id = target_to_instance.get(associated_id, associated_id)
            target_group = instance_group_by_id.get(associated_instance_id) or nic_group_by_id.get(associated_id, 0)
            # 已绑定 EIP 以实际 ECS/网卡归属为准；未绑定 EIP 才按 one/two 名称分组。
            eip_group = target_group or eip_name_group or 1
            if associated_id and target_group:
                eip_item["_binding_group_index"] = 2 if int(target_group) >= 2 else 1
                eip_item["_name_group_index"] = 2 if int(eip_name_group) >= 2 else (1 if eip_name_group else 0)
                eip_item["_group_binding_mismatch"] = bool(eip_name_group and eip_name_group != target_group)
            items.append(self._volc_apply_group_index(eip_item, plan, eip_group))
        for nic in network_interfaces:
            nic_item = self._volc_network_interface_item(plan, nic)
            nic_id = str(nic_item.get("_resource_id") or "")
            items.append(self._volc_apply_group_index(nic_item, plan, nic_group_by_id.get(nic_id, 1)))
        for vpc in vpcs:
            vpc_item = self._volc_vpc_item(plan, vpc, bound_vpc_ids)
            vpc_id = str(vpc_item.get("_resource_id") or "")
            items.append(self._volc_apply_group_index(vpc_item, plan, vpc_group_by_id.get(vpc_id, 1)))
        for group in security_groups:
            item = self._volc_security_group_item(plan, group, referenced_sg_ids)
            if item:
                group_id = str(item.get("_resource_id") or "")
                items.append(self._volc_apply_group_index(item, plan, sg_group_by_id.get(group_id, 1)))
        for subnet in subnets:
            subnet_item = self._volc_subnet_item(plan, subnet)
            subnet_id = str(subnet_item.get("_resource_id") or "")
            items.append(self._volc_apply_group_index(subnet_item, plan, subnet_group_by_id.get(subnet_id, 1)))

        if log_progress:
            summary_lines = self._volc_resource_summary_log_lines(plan, items)
            self.after(0, self.log, "\n".join(summary_lines))

        resource_summary = f"ECS:{len(instances)} EIP:{len(eips)}"
        server_ips = [str(item.get("_public_ip") or "") for item in items if item.get("_kind") == "ecs" and item.get("_public_ip")]
        ecs_items = [item for item in items if item.get("_kind") == "ecs"]
        if ecs_items:
            first_ecs = sorted(ecs_items, key=lambda item: (0 if str(item.get("_name") or "").startswith("wawacloud") else 1, str(item.get("_name") or "")))[0]
            self.after(
                0,
                lambda p=plan, row=first_ecs, ip_list=list(server_ips): self._set_region_values(
                    p,
                    host_status=str(row.get("_status") or "已读取"),
                    resource_summary=resource_summary,
                    server_ips=ip_list[:2],
                    flavor=str(row.get("_flavor") or ""),
                    expire_time=str(row.get("_expire_time") or ""),
                ),
            )
            self.volc_region_server_ips[plan.region] = server_ips
        else:
            self.after(0, lambda p=plan, s=resource_summary: self._set_region_values(p, host_status="未发现ECS", resource_summary=s, server_ips=[]))

        elapsed = time.monotonic() - started
        if log_progress:
            self.after(
                0,
                self.log,
                f"[{plan.label}] 资源读取完成，用时={elapsed:.1f}s",
            )
        return items

    def populate_volc_manage_tree(self, items: Sequence[Dict[str, Any]]) -> None:
        if self.manage_tree is None:
            return
        for iid in self.manage_tree.get_children(""):
            self.manage_tree.delete(iid)
        self.manage_items = {}
        self.manage_last_clicked_iid = ""
        self.volc_cached_resource_items = [dict(item) for item in items]
        display_items = self._manage_display_items(items)
        grouped: Dict[Tuple[str, int], List[Dict[str, Any]]] = {}
        for item in display_items:
            region = str(item.get("_region") or "unknown")
            group_index = self._volc_manage_item_group_index(dict(item))
            prepared = dict(item)
            prepared["_group_index"] = group_index
            grouped.setdefault((region, group_index), []).append(prepared)
        index = 0
        kind_order = {"ecs": 0, "eip": 1, "vpc": 2, "subnet": 3, "network_interface": 4, "security_group": 5}
        display_groups: List[Tuple[RegionPlan, int]] = [(plan, 1) for plan in VOLC_REGION_PLANS]
        display_groups.extend((plan, 2) for plan in VOLC_REGION_PLANS if grouped.get((plan.region, 2)))
        for plan, group_index in display_groups:
            region_items = sorted(
                grouped.get((plan.region, group_index)) or [],
                key=lambda item: (kind_order.get(str(item.get("_kind") or ""), 99), str(item.get("_name") or "")),
            )
            counts = {"ecs": 0, "eip": 0, "network_interface": 0, "security_group": 0, "vpc": 0, "subnet": 0}
            for item in region_items:
                kind = str(item.get("_kind") or "")
                if kind in counts:
                    counts[kind] += 1
            if self._customer_manage_eip_only():
                summary = f"EIP:{counts['eip']}"
            elif self._standalone_limited_features():
                summary = (
                    f"EIP:{counts['eip']}/VPC:{counts['vpc']}/"
                    f"子网:{counts['subnet']}/网卡:{counts['network_interface']}/安全组:{counts['security_group']}"
                )
            else:
                summary = (
                    f"ECS:{counts['ecs']}/EIP:{counts['eip']}/VPC:{counts['vpc']}/"
                    f"子网:{counts['subnet']}/网卡:{counts['network_interface']}/安全组:{counts['security_group']}"
                )
            group_iid = f"volc-manage-group-{plan.key}-{group_index}"
            region_label = self._volc_region_group_label(plan.label, group_index)
            self.manage_tree.insert(
                "",
                tk.END,
                iid=group_iid,
                text=UNCHECKED,
                values=(UNCHECKED, region_label, "地区汇总", "", summary, "", "＋", "", "", ""),
                tags=("regiongroup",),
                open=False,
            )
            self.manage_items[group_iid] = {
                "_kind": "region_group",
                "_region": plan.region,
                "_region_label": region_label,
                "_group_index": group_index,
                "_operation_kind": "none",
            }
            for item in region_items:
                index += 1
                iid = f"volc-manage-{index}"
                flavor = item.get("_flavor", "")
                if str(item.get("_kind") or "") == "eip" and flavor:
                    flavor = f"带宽{flavor}M"
                values = (
                    UNCHECKED,
                    "",
                    item.get("_kind_label", ""),
                    item.get("_name", ""),
                    item.get("_status", ""),
                    item.get("_billing", ""),
                    item.get("_operation_text", ""),
                    item.get("_public_ip", ""),
                    item.get("_expire_time", ""),
                    flavor,
                )
                self.manage_items[iid] = dict(item)
                self.manage_tree.insert(
                    group_iid,
                    tk.END,
                    iid=iid,
                    text=UNCHECKED,
                    values=values,
                    tags=("evenrow" if index % 2 == 0 else "oddrow",),
                )
        self._refresh_volc_manage_group_checks()
        self.after_idle(
            lambda: autosize_treeview_columns(
                self.manage_tree,
                max_widths={"#0": 60, "region_label": 110, "name": 260, "status": 260, "operation": 260, "ip": 220, "server_id": 220, "resource_id": 180},
            )
        )
        self._refresh_purchase_region_tree_from_items(items)

    def _manage_display_items(self, items: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
        if self._customer_manage_eip_only():
            return [dict(item) for item in items if str(item.get("_kind") or "").lower() == "eip"]
        if not self._standalone_limited_features():
            return [dict(item) for item in items]
        return [dict(item) for item in items if str(item.get("_kind") or "").lower() != "ecs"]

    def apply_volc_resource_refresh(
        self,
        items: Sequence[Dict[str, Any]],
        refreshed_regions: Optional[Sequence[str]] = None,
        cache_valid: bool = True,
    ) -> None:
        """把指定地区的新查询结果合并进火山资源缓存，未刷新地区不重复查询也不清空。"""
        region_set = {str(region or "").strip() for region in (refreshed_regions or []) if str(region or "").strip()}
        if not region_set:
            self.populate_volc_manage_tree(items)
            if cache_valid:
                self.volc_cached_resource_regions = {
                    str(item.get("_region") or "")
                    for item in items
                    if str(item.get("_region") or "")
                }
                self.volc_cached_resource_account_scope = self._account_log_scope()
                self.volc_cached_resource_updated_at = time.monotonic()
            else:
                self.volc_cached_resource_regions.clear()
                self.volc_cached_resource_account_scope = ""
                self.volc_cached_resource_updated_at = 0.0
            self._refresh_volc_outputs()
            if cache_valid:
                self._sync_linked_used_ip_table_from_cache()
            return
        kept_items = [
            dict(item)
            for item in self.volc_cached_resource_items
            if str(item.get("_region") or "") not in region_set
        ]
        refreshed_items = [
            dict(item)
            for item in items
            if str(item.get("_region") or "") in region_set
        ]
        self.populate_volc_manage_tree([*kept_items, *refreshed_items])
        if cache_valid:
            self.volc_cached_resource_regions = (set(self.volc_cached_resource_regions) - region_set) | region_set
            self.volc_cached_resource_account_scope = self._account_log_scope()
            self.volc_cached_resource_updated_at = time.monotonic()
        else:
            self.volc_cached_resource_regions = set(self.volc_cached_resource_regions) - region_set
            if not self.volc_cached_resource_regions:
                self.volc_cached_resource_account_scope = ""
                self.volc_cached_resource_updated_at = 0.0
        self._refresh_volc_outputs()
        if cache_valid:
            self._sync_linked_used_ip_table_from_cache()

    def _record_volc_purchase_resources(
        self,
        plan: RegionPlan,
        ips: Sequence[str],
        flavor_label: str,
        *,
        status: str = "购买完成",
    ) -> None:
        existing = [
            dict(item)
            for item in self.volc_cached_resource_items
            if not (item.get("_region") == plan.region and item.get("_kind") == "ecs" and str(item.get("_name") or "").startswith("wawacloud_ecs"))
        ]
        billing_text = "按需" if self._on_demand_billing_enabled() else "包周期"
        expire_text = display_expire_time_for_billing(billing_text=billing_text)
        for index, ip in enumerate(ips, start=1):
            if not is_public_ip(ip):
                continue
            group_index = 2 if index >= 2 else 1
            existing.append(
                {
                    "_kind": "ecs",
                    "_kind_label": "ECS",
                    "_region": plan.region,
                    "_region_label": self._volc_region_group_label(plan.label, group_index),
                    "_group_index": group_index,
                    "_name": f"wawacloud_ecs{index}",
                    "_status": status,
                    "_billing": billing_text,
                    "_operation_text": "可重装/可删除",
                    "_operation_kind": "ecs",
                    "_public_ip": ip,
                    "_server_id": "",
                    "_resource_id": "",
                    "_expire_time": expire_text,
                    "_flavor": flavor_label,
                    "_raw": {"public_ip": ip},
                }
            )
        self.populate_volc_manage_tree(existing)

    def _selected_volc_manage_items(self, *, kinds: Optional[Set[str]] = None) -> List[Dict[str, Any]]:
        if self.manage_tree is None:
            return []
        selected: List[Dict[str, Any]] = []
        for iid in self._volc_all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            if not values or values[0] != CHECKED:
                continue
            item = dict(self.manage_items.get(iid) or {})
            if not item or item.get("_kind") == "region_group":
                continue
            if kinds is not None and str(item.get("_kind") or "") not in kinds:
                continue
            selected.append(item)
        return selected

    @staticmethod
    def _volc_udp_security_group_ports() -> Tuple[Tuple[int, int], ...]:
        try:
            from Volcengine.api.volcengine import data as volc_data

            configured_ports = getattr(volc_data, "SECURITY_GROUP_UDP_PORTS", None) or [(1, 52), (54, 65535)]
        except Exception:
            configured_ports = [(1, 52), (54, 65535)]
        ports: List[Tuple[int, int]] = []
        for port_value in configured_ports:
            if isinstance(port_value, (list, tuple)) and len(port_value) >= 2:
                start = int(port_value[0])
                end = int(port_value[1])
            else:
                start = int(port_value)
                end = int(port_value)
            ports.append((start, end))
        return tuple(ports)

    def _volc_security_group_targets_for_udp_repair(self) -> List[Dict[str, Any]]:
        selected = self._selected_volc_manage_items(kinds={"security_group"})
        source_items = selected or [
            dict(item)
            for item in self.volc_cached_resource_items
            if item.get("_kind") == "security_group" and item.get("_resource_id")
        ]
        targets: List[Dict[str, Any]] = []
        seen: Set[Tuple[str, str]] = set()
        for item in source_items:
            region = str(item.get("_region") or "").strip()
            security_group_id = str(item.get("_resource_id") or "").strip()
            if not region or not security_group_id:
                continue
            key = (region, security_group_id)
            if key in seen:
                continue
            seen.add(key)
            targets.append(dict(item))
        return targets

    def _repair_volc_udp_security_group_item(self, item: Dict[str, Any], ak: str, sk: str) -> None:
        import volcenginesdkvpc

        region = str(item.get("_region") or "").strip()
        security_group_id = str(item.get("_resource_id") or "").strip()
        label = self._volc_item_label(item)
        if not region:
            raise ValueError("缺少地区")
        if not security_group_id:
            raise ValueError("缺少安全组ID")

        api = volcenginesdkvpc.VPCApi(self._volc_api_client(region, ak, sk))
        ports_text = "、".join(f"{start}-{end}" for start, end in self._volc_udp_security_group_ports())
        self._volc_manage_log(f"[{label}] 开始补齐 UDP 入方向规则：{ports_text}。")
        for port_start, port_end in self._volc_udp_security_group_ports():
            request = self._volc_request(
                volcenginesdkvpc.AuthorizeSecurityGroupIngressRequest,
                security_group_id=security_group_id,
                protocol="udp",
                cidr_ip="0.0.0.0/0",
                port_start=port_start,
                port_end=port_end,
                description=f"auto udp {port_start}-{port_end}",
                policy="accept",
            )
            try:
                self._volc_call_with_retry(lambda: api.authorize_security_group_ingress(request), f"{region} AuthorizeSecurityGroupIngress", attempts=2)
            except Exception as exc:
                if not self._volc_security_rule_exists_error(exc):
                    raise
            self._volc_manage_log(f"[{label}] UDP {port_start}-{port_end} 已补齐或已存在。")

    def repair_volc_udp_security_group_rules(self) -> None:
        items = self._volc_security_group_targets_for_udp_repair()
        if not items:
            self.log("请先读取账号资源，或在资源管理选择需要补齐 UDP 规则的火山安全组。", "manage")
            return
        if not self.ask_confirm_cancel(
            "确认开始补齐火山引擎安全组 UDP 规则？\n\n"
            f"安全组数量：{len(items)}\n"
            "规则：UDP 1-52、54-65535，源地址 0.0.0.0/0。\n\n"
            "该操作只新增缺失的 UDP 入方向规则，不会删除现有规则。\n\n"
            "请确认是否继续？"
        ):
            self.log("已取消补齐火山引擎 UDP 安全组规则。", "manage")
            return
        self._run_volc_resource_action(
            "补齐UDP安全组规则",
            items,
            self._repair_volc_udp_security_group_item,
            max_workers=4,
            refresh_after=True,
            context="manage",
            notify_done=True,
        )

    def _selected_volc_replace_groups(
        self,
        selected_items: Sequence[Dict[str, Any]],
    ) -> Dict[str, Set[int]]:
        groups_by_region: Dict[str, Set[int]] = {}
        for item in selected_items:
            region = str(item.get("_region") or "").strip()
            if not region:
                continue
            group_index = 2 if self._volc_manage_item_group_index(dict(item)) >= 2 else 1
            groups_by_region.setdefault(region, set()).add(group_index)
        if self.manage_tree is None:
            return groups_by_region
        for iid in self.manage_tree.get_children(""):
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            if not values or values[0] != CHECKED or item.get("_kind") != "region_group":
                continue
            region = str(item.get("_region") or "").strip()
            if not region:
                continue
            group_index = 2 if self._volc_manage_item_group_index(dict(item)) >= 2 else 1
            groups_by_region.setdefault(region, set()).add(group_index)
        return groups_by_region

    def _volc_bound_eips_for_replace_groups(
        self,
        items: Sequence[Dict[str, Any]],
        group_indexes: Set[int],
    ) -> List[Dict[str, Any]]:
        selected_groups = {2 if int(group_index or 1) >= 2 else 1 for group_index in group_indexes}
        return [
            item
            for item in items
            if item.get("_kind") == "eip"
            and item.get("_resource_id")
            and self._volc_eip_associated_instance_id(
                item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            )
            and self._volc_manage_item_group_index(dict(item)) in selected_groups
        ]

    def _volc_actionable_clear_items(self) -> List[Dict[str, Any]]:
        return self._volc_actionable_delete_items_from_items(self.volc_cached_resource_items)

    @staticmethod
    def _volc_actionable_delete_items_from_items(items: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
        actionable: List[Dict[str, Any]] = []
        for item in items:
            kind = str(item.get("_kind") or "")
            if kind in {"ecs", "eip"}:
                actionable.append(dict(item))
            elif kind == "network_interface":
                actionable.append(dict(item))
            elif kind in {"security_group", "subnet", "vpc"}:
                actionable.append(dict(item))
        return actionable

    @staticmethod
    def _volc_delete_item_key(item: Dict[str, Any]) -> Tuple[str, str, str]:
        return (
            str(item.get("_region") or ""),
            str(item.get("_kind") or item.get("_operation_kind") or ""),
            str(item.get("_resource_id") or item.get("_server_id") or item.get("_name") or ""),
        )

    @staticmethod
    def _volc_delete_item_name_key(item: Dict[str, Any]) -> Tuple[str, str, str]:
        return (
            str(item.get("_region") or ""),
            str(item.get("_kind") or item.get("_operation_kind") or ""),
            str(item.get("_name") or item.get("_public_ip") or ""),
        )

    @staticmethod
    def _volc_delete_title_clears_all(title: str) -> bool:
        return "清空账号资源" in str(title or "")

    def _dedupe_volc_delete_items(self, items: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
        deduped: List[Dict[str, Any]] = []
        seen_keys: Set[Tuple[str, str, str]] = set()
        for item in items:
            key = self._volc_delete_item_key(item)
            if key in seen_keys:
                continue
            seen_keys.add(key)
            deduped.append(dict(item))
        return deduped

    def _expand_volc_delete_dependencies(
        self,
        items: Sequence[Dict[str, Any]],
        ak: str = "",
        sk: str = "",
        *,
        live_query: bool = False,
    ) -> List[Dict[str, Any]]:
        """补齐 VPC/安全组删除依赖；执行前可实时查询，避免旧表格漏掉网卡或子网导致 InUse。"""
        selected = [dict(item) for item in items]
        selected_keys = {self._volc_delete_item_key(item) for item in selected}
        selected_vpcs_by_region: Dict[str, Set[str]] = {}
        selected_sgs_by_region: Dict[str, Set[str]] = {}
        for item in selected:
            region = str(item.get("_region") or "")
            resource_id = str(item.get("_resource_id") or item.get("_server_id") or "")
            kind = str(item.get("_kind") or "")
            if region and resource_id and kind == "vpc":
                selected_vpcs_by_region.setdefault(region, set()).add(resource_id)
            elif region and resource_id and kind == "security_group":
                selected_sgs_by_region.setdefault(region, set()).add(resource_id)

        if not selected_vpcs_by_region and not selected_sgs_by_region:
            return selected

        def add_dependency(item: Dict[str, Any]) -> None:
            key = self._volc_delete_item_key(item)
            if not key[0] or not key[2] or key in selected_keys:
                return
            item["_auto_added_by_dependency"] = True
            selected.append(item)
            selected_keys.add(key)

        for cached in self.volc_cached_resource_items:
            item = dict(cached)
            region = str(item.get("_region") or "")
            kind = str(item.get("_kind") or "")
            resource_id = str(item.get("_resource_id") or item.get("_server_id") or item.get("_name") or "")
            key = self._volc_delete_item_key(item)
            if not region or not resource_id or key in selected_keys:
                continue
            item_vpc_id = str(item.get("_vpc_id") or "")
            vpc_selected = bool(item_vpc_id and item_vpc_id in selected_vpcs_by_region.get(region, set()))
            sg_ids = {str(value) for value in (item.get("_security_group_ids") or []) if value}
            sg_selected = bool(sg_ids & selected_sgs_by_region.get(region, set()))
            should_add = False
            if kind == "subnet" and vpc_selected:
                should_add = True
            elif kind == "network_interface" and (vpc_selected or sg_selected):
                should_add = True
            elif kind == "security_group" and vpc_selected:
                should_add = str(item.get("_name") or "").strip().lower() != "default"
            if not should_add:
                continue
            add_dependency(item)
        if not live_query or not ak or not sk:
            return selected

        plan_by_region = {plan.region: plan for plan in VOLC_REGION_PLANS}
        for region in sorted(set(selected_vpcs_by_region) | set(selected_sgs_by_region)):
            plan = plan_by_region.get(region)
            if plan is None:
                continue
            vpc_ids = selected_vpcs_by_region.get(region, set())
            selected_sg_ids = selected_sgs_by_region.get(region, set())
            try:
                network_interfaces = self._volc_describe_network_interfaces(region, ak, sk)
            except Exception as exc:
                self._volc_manage_log(f"[{plan.label}] 实时补齐网卡依赖失败，继续按已读取队列删除: {self._volc_exception_text(exc)}")
                network_interfaces = []
            # 删除 VPC 前必须先处理 VPC 内的 ENI；删除安全组前必须先处理引用该安全组的 ENI。
            for nic in network_interfaces:
                item_vpc_id = self._volc_first(nic, "VpcId", "vpc_id")
                sg_ids = self._volc_network_interface_security_group_ids(nic)
                if not ((item_vpc_id and item_vpc_id in vpc_ids) or (sg_ids & selected_sg_ids)):
                    continue
                nic_item = self._volc_network_interface_item(plan, nic)
                add_dependency(nic_item)

            if not vpc_ids:
                continue
            try:
                security_groups = self._volc_describe_security_groups(region, ak, sk)
            except Exception as exc:
                self._volc_manage_log(f"[{plan.label}] 实时补齐安全组依赖失败，继续按已读取队列删除: {self._volc_exception_text(exc)}")
                security_groups = []
            for group in security_groups:
                if self._volc_first(group, "VpcId", "vpc_id") not in vpc_ids:
                    continue
                group_item = self._volc_security_group_item(plan, group, set())
                if group_item:
                    add_dependency(group_item)

            try:
                subnets = self._volc_describe_subnets(region, ak, sk)
            except Exception as exc:
                self._volc_manage_log(f"[{plan.label}] 实时补齐子网依赖失败，继续按已读取队列删除: {self._volc_exception_text(exc)}")
                subnets = []
            for subnet in subnets:
                if self._volc_first(subnet, "VpcId", "vpc_id") in vpc_ids:
                    add_dependency(self._volc_subnet_item(plan, subnet))
        return selected

    @staticmethod
    def _volc_log_resource_name(item: Dict[str, Any]) -> str:
        name = str(item.get("_name") or item.get("_public_ip") or "").strip()
        ids = {
            str(item.get("_resource_id") or "").strip(),
            str(item.get("_server_id") or "").strip(),
        }
        if name and name not in ids:
            return name
        return "未命名资源"

    @staticmethod
    def _volc_item_label(item: Dict[str, Any]) -> str:
        region = str(item.get("_region_label") or item.get("_region") or "")
        kind = str(item.get("_kind_label") or item.get("_kind") or "")
        name = VolcEngineManagerApp._volc_log_resource_name(item)
        return f"{region}/{kind}/{name}".strip("/")

    def _volc_residual_delete_items(
        self,
        title: str,
        target_items: Sequence[Dict[str, Any]],
        affected_plans: Sequence[RegionPlan],
        ak: str,
        sk: str,
        *,
        log_progress: bool = True,
    ) -> List[Dict[str, Any]]:
        if not affected_plans:
            return []
        live_items: List[Dict[str, Any]] = []
        query_errors: List[str] = []
        for plan in affected_plans:
            if self.stop_event.is_set():
                return []
            try:
                live_items.extend(self._query_volc_region_resources(plan, ak, sk, log_progress=log_progress))
            except Exception as exc:
                detail = self._volc_exception_text(exc)
                query_errors.append(f"{plan.label}: {detail}")
                self.after(0, self.log, f"[{plan.label}] 删除后复查资源失败: {detail}", "manage")
        if query_errors:
            raise RuntimeError("；".join(query_errors[:3]))
        candidates = self._volc_actionable_delete_items_from_items(live_items)
        if self._volc_delete_title_clears_all(title):
            return self._dedupe_volc_delete_items(candidates)
        target_keys = {self._volc_delete_item_key(item) for item in target_items}
        target_name_keys = {
            self._volc_delete_item_name_key(item)
            for item in target_items
            if self._volc_delete_item_name_key(item)[2]
        }
        residuals: List[Dict[str, Any]] = []
        for item in candidates:
            if self._volc_delete_item_key(item) in target_keys or self._volc_delete_item_name_key(item) in target_name_keys:
                residuals.append(item)
        return self._dedupe_volc_delete_items(residuals)

    def _volc_manage_confirm_message(self, title: str, items: Sequence[Dict[str, Any]]) -> str:
        regions = []
        for item in items:
            label = str(item.get("_region_label") or item.get("_region") or "")
            if label and label not in regions:
                regions.append(label)
        return (
            f"确认开始{title} {len(items)} 个资源？\n\n"
            f"所选地区：{'、'.join(regions) if regions else '未识别'}\n"
            f"{DESTRUCTIVE_CONFIRM_TEXT}"
        )

    def _run_volc_resource_action(
        self,
        title: str,
        items: Sequence[Dict[str, Any]],
        action: Callable[[Dict[str, Any], str, str], None],
        *,
        max_workers: int = 6,
        refresh_after: bool = True,
        context: str = "manage",
        notify_done: bool = False,
        stop_queue_on_failure: bool = False,
        success_callback: Optional[Callable[[Sequence[Dict[str, Any]]], None]] = None,
    ) -> None:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        if not ak or not sk:
            self.log("请先填写或粘贴火山引擎 AK/SK。", context)
            return
        guard_message = self._customer_guard_block_message(ak, sk)
        if guard_message:
            self._show_customer_guard_block(guard_message, context)
            return
        item_list = [dict(item) for item in items]
        deduped: List[Dict[str, Any]] = []
        seen_keys: Set[Tuple[str, str, str]] = set()
        for item in item_list:
            key = (
                str(item.get("_region") or ""),
                str(item.get("_kind") or item.get("_operation_kind") or ""),
                str(item.get("_resource_id") or item.get("_server_id") or item.get("_name") or ""),
            )
            if key in seen_keys:
                continue
            seen_keys.add(key)
            deduped.append(item)
        item_list = deduped
        if not item_list:
            self.log(f"没有可{title}的火山引擎资源。", context)
            return
        affected_regions = {str(item.get("_region") or "") for item in item_list if item.get("_region")}
        affected_plans = [plan for plan in VOLC_REGION_PLANS if plan.region in affected_regions]
        if not self._begin_volc_task(
            f"火山引擎{title}中",
            context=context,
            regions=[str(item.get("_region") or "") for item in item_list],
        ):
            return

        def worker() -> None:
            success = 0
            successful_items: List[Dict[str, Any]] = []
            failures: List[str] = []
            with ThreadPoolExecutor(max_workers=max(1, min(max_workers, len(item_list)))) as executor:
                future_to_item = {executor.submit(action, item, ak, sk): item for item in item_list}
                for future in as_completed(future_to_item):
                    item = future_to_item[future]
                    label = self._volc_item_label(item)
                    try:
                        future.result()
                        success += 1
                        successful_items.append(dict(item))
                        self.after(0, self.log, f"[{label}] {title}完成", context)
                    except Exception as exc:
                        detail = self._volc_resource_exception_text(
                            exc,
                            item.get("_resource_id"),
                            item.get("_server_id"),
                        )
                        failures.append(f"{label}: {detail}")
                        self.after(0, self.log, f"[{label}] {title}失败: {detail}", context)
            failed = len(failures)
            summary = f"火山引擎{title}结束，成功 {success}/{len(item_list)}，失败 {failed}。"
            self.after(0, self.log, summary, context)
            if failures:
                self.after(0, self.log, "失败项:\n" + "\n".join(failures[:12]), context)
                if stop_queue_on_failure:
                    self.after(
                        0,
                        self._cancel_pending_delayed_actions,
                        f"火山引擎{title}未全部成功，为避免后续任务操作未就绪的云服务器，已停止队列。",
                    )
            if successful_items and success_callback:
                self.after(
                    0,
                    lambda items=list(successful_items): success_callback(items),
                )
            self.after(0, self._finish_volc_task, "就绪", context)
            if refresh_after:
                self.after(0, self.log, "正在自动刷新火山引擎资源状态。", context)
                self.after(
                    300,
                    lambda c=context, p=list(affected_plans): self.scan_resources(
                        context=c,
                        plans_override=p,
                        merge_with_cache=True,
                        final_popup_message=summary if notify_done else None,
                    ),
                )
            elif notify_done:
                self.after(0, self.show_info, summary)

        threading.Thread(target=worker, daemon=True).start()

    def _run_volc_delete_items(self, title: str, items: Sequence[Dict[str, Any]]) -> None:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        if not ak or not sk:
            self.log("请先填写或粘贴火山引擎 AK/SK。", "manage")
            return
        guard_message = self._customer_guard_block_message(ak, sk)
        if guard_message:
            self._show_customer_guard_block(guard_message, "manage")
            return
        item_list = self._dedupe_volc_delete_items(self._expand_volc_delete_dependencies([dict(item) for item in items]))
        if not item_list:
            self.log(f"没有可{title}的火山引擎资源。", "manage")
            return
        affected_regions = {str(item.get("_region") or "") for item in item_list if item.get("_region")}
        affected_plans = [plan for plan in VOLC_REGION_PLANS if plan.region in affected_regions]
        if not self._begin_volc_task(
            f"火山引擎{title}中",
            context="manage",
            regions=[str(item.get("_region") or "") for item in item_list],
        ):
            return

        def worker() -> None:
            failures: List[str] = []
            # 删除前在线程里实时补齐 VPC/安全组依赖，避免资源列表漏掉 ENI/子网时直接删 VPC 触发 InUse。
            worker_items = self._dedupe_volc_delete_items(
                self._expand_volc_delete_dependencies(item_list, ak, sk, live_query=True)
            )
            dependency_added = sum(1 for item in worker_items if item.get("_auto_added_by_dependency"))
            if dependency_added:
                self.after(0, self.log, f"已根据所选 VPC/安全组自动补齐依赖资源 {dependency_added} 个，按依赖顺序统一删除。", "manage")
            total_attempts = len(worker_items)
            pending_by_region: Dict[str, int] = {}
            refreshed_regions: Set[str] = set()
            pending_lock = threading.Lock()
            plan_by_region = {plan.region: plan for plan in VOLC_REGION_PLANS}

            def add_pending_delete_items(pending_items: Sequence[Dict[str, Any]]) -> None:
                with pending_lock:
                    for pending_item in pending_items:
                        region = str(pending_item.get("_region") or "")
                        if not region:
                            continue
                        pending_by_region[region] = pending_by_region.get(region, 0) + 1
                        refreshed_regions.discard(region)

            def mark_volc_item_finished(item: Dict[str, Any], reason: str) -> None:
                region = str(item.get("_region") or "")
                if not region:
                    return
                ready_regions: List[str] = []
                with pending_lock:
                    if region in refreshed_regions:
                        return
                    pending_by_region[region] = max(0, pending_by_region.get(region, 0) - 1)
                    if pending_by_region[region] <= 0:
                        refreshed_regions.add(region)
                        ready_regions.append(region)
                if not ready_regions:
                    return
                plans = [plan_by_region[ready_region] for ready_region in ready_regions if ready_region in plan_by_region]
                if not plans:
                    return
                self.after(
                    0,
                    lambda regions=list(ready_regions): self._clear_volc_outputs_for_regions(
                        regions,
                        context="manage",
                    ),
                )
                try:
                    refreshed: List[Dict[str, Any]] = []
                    for plan in plans:
                        refreshed.extend(self._query_volc_region_resources(plan, ak, sk))
                    refresh_regions = [plan.region for plan in plans]
                    self.after(0, self.apply_volc_resource_refresh, refreshed, refresh_regions, True)
                    labels = "、".join(plan.label for plan in plans)
                    self.after(0, self.log, f"[{labels}] {reason}后已实时刷新购买搭建和资源管理资源库。", "manage")
                except Exception as exc:
                    labels = "、".join(plan.label for plan in plans)
                    self.after(0, self.log, f"[{labels}] 删除后实时刷新资源库失败: {self._volc_exception_text(exc)}", "manage")

            add_pending_delete_items(worker_items)

            def run_delete_phases(delete_items: Sequence[Dict[str, Any]], title_suffix: str = "") -> int:
                phase_success = 0
                display_title = f"{title}{title_suffix}" if title_suffix else title
                phases = [
                    ("云服务器", [item for item in delete_items if item.get("_kind") == "ecs"], 4),
                    ("EIP", [item for item in delete_items if item.get("_kind") == "eip"], 8),
                    ("网卡", [item for item in delete_items if item.get("_kind") == "network_interface"], 4),
                    ("安全组", [item for item in delete_items if item.get("_kind") == "security_group"], 5),
                    ("子网", [item for item in delete_items if item.get("_kind") == "subnet"], 3),
                    ("VPC", [item for item in delete_items if item.get("_kind") == "vpc"], 3),
                ]
                self.after(0, self.log, f"{display_title}队列已建立，资源数={len(delete_items)}，顺序=云服务器 -> EIP -> 网卡 -> 安全组 -> 子网 -> VPC。", "manage")

                def phase_targets_absent(phase_name: str, pending_items: Sequence[Dict[str, Any]]) -> bool:
                    try:
                        residual = self._volc_residual_delete_items(
                            title,
                            pending_items,
                            affected_plans,
                            ak,
                            sk,
                            log_progress=False,
                        )
                    except Exception as exc:
                        self.after(0, self.log, f"{phase_name}阶段云端复查失败，继续等待删除接口返回: {self._volc_exception_text(exc)}", "manage")
                        return False
                    return not residual

                for phase_name, phase_items, workers in phases:
                    if not phase_items:
                        continue
                    if self.stop_event.is_set():
                        self.after(0, self.log, f"已请求停止，跳过火山引擎{phase_name}及后续删除阶段。", "manage")
                        break
                    self.after(0, self.log, f"开始处理火山引擎{phase_name}，数量={len(phase_items)}。", "manage")
                    executor = ThreadPoolExecutor(max_workers=max(1, min(workers, len(phase_items))))
                    future_to_item: Dict[Any, Dict[str, Any]] = {}
                    pending_futures: Set[Any] = set()
                    abandon_pending = False

                    def handle_completed_future(future: Any) -> None:
                        nonlocal phase_success
                        item = future_to_item[future]
                        label = self._volc_item_label(item)
                        try:
                            future.result()
                            phase_success += 1
                            self.after(0, self.log, f"[{label}] {display_title}完成", "manage")
                        except Exception as exc:
                            detail = self._volc_resource_exception_text(
                                exc,
                                item.get("_resource_id"),
                                item.get("_server_id"),
                            )
                            failures.append(f"{label}: {detail}")
                            self.after(0, self.log, f"[{label}] {display_title}失败: {detail}", "manage")
                        finally:
                            mark_volc_item_finished(item, f"{phase_name}处理")

                    try:
                        for item in phase_items:
                            label = self._volc_item_label(item)
                            self.after(0, self.log, f"[{label}] 准备执行{display_title}，操作={item.get('_operation_kind') or item.get('_kind')}", "manage")
                            future_to_item[executor.submit(self._delete_volc_resource_item, item, ak, sk)] = item
                        pending_futures = set(future_to_item)
                        next_verify_at = time.monotonic() + max(3.0, VOLC_RESOURCE_API_TIMEOUT + VOLC_RESOURCE_SLOW_API_GRACE)
                        absent_confirmations = 0
                        while pending_futures:
                            done, _not_done = concurrent.futures.wait(
                                list(pending_futures),
                                timeout=1.0,
                                return_when=concurrent.futures.FIRST_COMPLETED,
                            )
                            for future in done:
                                if future not in pending_futures:
                                    continue
                                pending_futures.discard(future)
                                handle_completed_future(future)
                            if not pending_futures:
                                break
                            if self.stop_event.is_set():
                                self.after(0, self.log, f"已请求停止，跳过等待火山引擎{phase_name}未返回的删除接口。", "manage")
                                abandon_pending = True
                                break
                            now = time.monotonic()
                            if now < next_verify_at:
                                continue
                            next_verify_at = now + VOLC_DELETE_POST_VERIFY_DELAY_SECONDS
                            pending_items = [future_to_item[future] for future in pending_futures if not future.done()]
                            if not pending_items:
                                continue
                            if phase_targets_absent(phase_name, pending_items):
                                absent_confirmations += 1
                                if absent_confirmations < 2:
                                    self.after(0, self.log, f"{phase_name}阶段删除接口仍未返回，但云端首次复查已看不到待删目标，继续确认一次。", "manage")
                                    continue
                                self.after(0, self.log, f"{phase_name}阶段删除接口仍未返回，但云端复查确认待删目标已不存在，按完成处理。", "manage")
                                for future in list(pending_futures):
                                    if future.done():
                                        pending_futures.discard(future)
                                        handle_completed_future(future)
                                        continue
                                    item = future_to_item[future]
                                    label = self._volc_item_label(item)
                                    future.cancel()
                                    pending_futures.discard(future)
                                    phase_success += 1
                                    self.after(0, self.log, f"[{label}] {display_title}完成（云端已不存在）", "manage")
                                    mark_volc_item_finished(item, f"{phase_name}复查")
                                abandon_pending = True
                                break
                            else:
                                absent_confirmations = 0
                    finally:
                        if pending_futures:
                            for future in pending_futures:
                                future.cancel()
                        executor.shutdown(wait=not abandon_pending, cancel_futures=True)
                    if self.stop_event.is_set():
                        break
                    if any(item.get("_kind") == "network_interface" for item in phase_items):
                        # 网卡解绑/删除后安全组引用不会立刻同步释放，稍等再删安全组，避免安全组仍被占用。
                        self.after(0, self.log, "网卡阶段已完成，等待云端释放安全组依赖后再删除安全组。", "manage")
                        time.sleep(3.0)
                    else:
                        time.sleep(1.0)
                return phase_success

            success = run_delete_phases(worker_items)
            retry_items: List[Dict[str, Any]] = []
            residual_items: List[Dict[str, Any]] = []
            verified_no_residual = False
            if not self.stop_event.is_set():
                self.after(0, self.log, "删除指令已完成，等待云端同步后复查是否仍有残留资源。", "manage")
                time.sleep(VOLC_DELETE_POST_VERIFY_DELAY_SECONDS)
                try:
                    residual_items = self._volc_residual_delete_items(title, worker_items, affected_plans, ak, sk)
                    if residual_items:
                        residual_labels = "、".join(self._volc_item_label(item) for item in residual_items[:8])
                        more = " 等" if len(residual_items) > 8 else ""
                        self.after(0, self.log, f"删除后复查仍发现残留资源 {len(residual_items)} 个：{residual_labels}{more}，开始补清理一次。", "manage")
                        retry_items = self._dedupe_volc_delete_items(
                            self._expand_volc_delete_dependencies(residual_items, ak, sk, live_query=True)
                        )
                        total_attempts += len(retry_items)
                        add_pending_delete_items(retry_items)
                        success += run_delete_phases(retry_items, "（残留复查）")
                        if not self.stop_event.is_set():
                            self.after(0, self.log, "残留补清理已执行，等待云端同步后最终复查。", "manage")
                            time.sleep(VOLC_DELETE_POST_VERIFY_DELAY_SECONDS)
                            residual_items = self._volc_residual_delete_items(title, worker_items + retry_items, affected_plans, ak, sk)
                            verified_no_residual = not residual_items
                    else:
                        verified_no_residual = True
                        self.after(0, self.log, "删除后复查未发现残留资源。", "manage")
                except Exception as exc:
                    detail = self._volc_exception_text(exc)
                    failures.append(f"删除后复查: {detail}")
                    self.after(0, self.log, f"删除后复查失败: {detail}", "manage")
            if residual_items and not self.stop_event.is_set():
                for item in residual_items[:12]:
                    failures.append(f"{self._volc_item_label(item)}: 删除后复查仍存在")
            elif verified_no_residual and failures:
                self.after(0, self.log, "复查确认目标资源已不存在，前序云端同步失败提示不再计为失败。", "manage")
                failures = []
                success = total_attempts
            if self._volc_delete_title_clears_all(title) and verified_no_residual:
                self.after(0, self.apply_volc_resource_refresh, [], [plan.region for plan in affected_plans], True)
            failed = len(failures)
            summary = f"火山引擎{title}结束，成功 {success}/{total_attempts}，失败 {failed}。"
            self.after(0, self.log, summary, "manage")
            if failures:
                self.after(0, self.log, "失败项:\n" + "\n".join(failures[:12]), "manage")
            self.after(0, self._finish_volc_task, "就绪", "manage")
            self.after(
                300,
                lambda p=list(affected_plans), message=summary: self.scan_resources(
                    context="manage",
                    plans_override=p,
                    merge_with_cache=True,
                    final_popup_message=message,
                ),
            )

        threading.Thread(target=worker, daemon=True).start()

    def _delete_volc_resource_item(self, item: Dict[str, Any], ak: str, sk: str) -> None:
        kind = str(item.get("_kind") or "")
        region = str(item.get("_region") or "")
        resource_id = str(item.get("_resource_id") or item.get("_server_id") or "").strip()
        operation = str(item.get("_operation_kind") or "")
        label = self._volc_item_label(item)
        if not region:
            raise ValueError(f"{label} 缺少地区，无法删除")
        self._volc_manage_log(f"[{label}] 删除参数 region={region} kind={kind} operation={operation or kind}")
        if kind == "ecs":
            instance_id = str(item.get("_server_id") or item.get("_resource_id") or "").strip()
            self._volc_delete_instance(region, instance_id, ak, sk, log_label=label)
        elif kind == "eip":
            self._volc_release_eip(region, item, ak, sk)
        elif kind == "network_interface":
            if not resource_id:
                raise ValueError("缺少网卡ID")
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            attached_instance_id = str(item.get("_server_id") or self._volc_network_interface_attached_instance_id(raw) or "").strip()
            if item.get("_is_primary_network_interface") and attached_instance_id:
                # 主网卡由 ECS 生命周期管理，火山不允许当作普通辅助网卡解绑；先等待 ECS 删除释放它。
                self._volc_manage_log(
                    f"[{label}] 检测到绑定 ECS 的主网卡，跳过普通网卡解绑，等待绑定云服务器删除后自动释放。"
                )
                if self._volc_wait_instance_absent(
                    region,
                    attached_instance_id,
                    ak,
                    sk,
                    timeout=VOLC_PRIMARY_NIC_RELEASE_TIMEOUT_SECONDS,
                    interval=10.0,
                    log_label=label,
                ):
                    self._volc_manage_log(f"[{label}] 绑定 ECS 已不存在，继续删除/确认残留主网卡。")
                    self._volc_delete_network_interface(region, resource_id, "", ak, sk, log_label=label)
                    self._volc_manage_log(f"[{label}] 绑定 ECS 已不存在，主网卡按随 ECS 释放完成处理。")
                    return
                raise RuntimeError("主网卡仍随绑定 ECS 存在，请稍后重新读取资源后再清理网络资源")
            self._volc_delete_network_interface(region, resource_id, attached_instance_id, ak, sk, log_label=label)
        elif kind == "security_group":
            if not resource_id:
                raise ValueError("缺少安全组ID")
            self._volc_delete_security_group(region, resource_id, ak, sk, log_label=label)
        elif kind == "subnet":
            if not resource_id:
                raise ValueError("缺少子网ID")
            self._volc_delete_subnet(region, resource_id, ak, sk, log_label=label)
        elif kind == "vpc":
            if not resource_id:
                raise ValueError("缺少VPC ID")
            self._volc_delete_vpc(region, resource_id, ak, sk, log_label=label)
        else:
            raise ValueError("该资源当前不可退订/删除")

    def _reinstall_volc_resource_item(self, item: Dict[str, Any], ak: str, sk: str) -> None:
        self._volc_reinstall_instance(str(item.get("_region") or ""), item, ak, sk)

    def _set_region_values(
        self,
        plan: RegionPlan,
        *,
        group_index: int = 1,
        host_status: Optional[str] = None,
        resource_summary: Optional[str] = None,
        server_ips: Optional[List[str]] = None,
        flavor: Optional[str] = None,
        expire_time: Optional[str] = None,
    ) -> None:
        row_id = self._purchase_region_row_id(plan, group_index)
        if row_id not in set(self.region_tree.get_children("")):
            self._ensure_purchase_region_rows({plan.region} if int(group_index or 1) >= 2 else set())
        values = list(self.region_tree.item(row_id, "values"))
        if not values:
            return
        while len(values) < 7:
            values.append("")
        values[1] = self._volc_region_group_label(plan.label, group_index)
        if host_status is not None:
            values[2] = host_status
        if resource_summary is not None:
            values[3] = resource_summary
        if server_ips is not None:
            values[4] = "、".join(server_ips)
        if flavor is not None:
            values[5] = flavor
        if expire_time is not None:
            values[6] = expire_time
        self.region_tree.item(row_id, text=values[0], values=values)
        self.after_idle(
            lambda: autosize_treeview_columns(
                self.region_tree,
                max_widths={"resource_summary": 260, "server_ip": 220, "expire_time": 220, "flavor": 180},
            )
        )

    def _collect_region_ips_from_rows(
        self,
        plans: Sequence[RegionPlan],
        selected_groups_by_region: Optional[Dict[str, Set[int]]] = None,
    ) -> Dict[str, List[str]]:
        result: Dict[str, List[str]] = {}
        row_ids = set(self.region_tree.get_children(""))
        for plan in plans:
            if selected_groups_by_region is None:
                ips = list(self.volc_region_server_ips.get(plan.region) or [])
                group_indexes = (1, 2)
                if not ips:
                    for group_index in group_indexes:
                        row_id = self._purchase_region_row_id(plan, group_index)
                        if row_id not in row_ids:
                            continue
                        values = list(self.region_tree.item(row_id, "values"))
                        if len(values) >= 5:
                            for ip in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(values[4])):
                                if is_public_ip(ip) and ip not in ips:
                                    ips.append(ip)
                if ips:
                    result[plan.region] = ips
                continue

            group_indexes = tuple(sorted(selected_groups_by_region.get(plan.region) or set()))
            known_region_ips = [
                str(ip or "").strip()
                for ip in self.volc_region_server_ips.get(plan.region) or []
                if is_public_ip(str(ip or "").strip())
            ]
            cached_ip_by_group: Dict[int, str] = {}
            for item in self.volc_cached_resource_items:
                if item.get("_kind") != "ecs" or str(item.get("_region") or "") != plan.region:
                    continue
                group_index = self._volc_manage_item_group_index(dict(item))
                public_ip = str(item.get("_public_ip") or "").strip()
                if is_public_ip(public_ip):
                    cached_ip_by_group.setdefault(group_index, public_ip)

            ips: List[str] = []
            for group_index in group_indexes:
                row_ips: List[str] = []
                row_id = self._purchase_region_row_id(plan, group_index)
                if row_id in row_ids:
                    values = list(self.region_tree.item(row_id, "values"))
                    if len(values) >= 5:
                        for ip in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(values[4])):
                            if is_public_ip(ip) and ip not in row_ips:
                                row_ips.append(ip)

                candidates: List[str] = []
                region_position = group_index - 1
                if 0 <= region_position < len(known_region_ips):
                    candidates.append(known_region_ips[region_position])
                cached_ip = cached_ip_by_group.get(group_index, "")
                if cached_ip:
                    candidates.append(cached_ip)
                if len(row_ips) == 1:
                    candidates.extend(row_ips)
                candidates.extend(row_ips)
                candidates.extend(known_region_ips)
                selected_ip = next(
                    (candidate for candidate in candidates if is_public_ip(candidate) and candidate not in ips),
                    "",
                )
                if selected_ip:
                    ips.append(selected_ip)
            if ips:
                result[plan.region] = ips
        return result

    def _volc_output_allowed_regions(self) -> set[str]:
        return {
            str(item.get("_region") or "").strip()
            for item in self.volc_cached_resource_items
            if not bool(item.get("_displayOnly"))
            and str(item.get("_kind") or "").lower() == "ecs"
            and str(item.get("_region") or "").strip()
        }

    def _format_volc_socks_output(self, allowed_regions: Optional[set[str]] = None) -> str:
        allowed = allowed_regions if allowed_regions is not None else self._volc_output_allowed_regions()
        if not allowed:
            return ""
        blocks: List[str] = []
        for plan in VOLC_REGION_PLANS:
            if plan.region not in allowed:
                continue
            lines = [line for line in self.volc_region_socks.get(plan.region, []) if line.strip()]
            if not lines:
                continue
            blocks.append(f"{plan.label}\nIP/端口/用户名/密码\n" + "\n".join(lines))
        if not blocks:
            return ""
        return append_result_footer("\n".join(blocks), "Socks5", self.mark_var.get())

    def _format_volc_l2tp_output(self, allowed_regions: Optional[set[str]] = None) -> str:
        allowed = allowed_regions if allowed_regions is not None else self._volc_output_allowed_regions()
        if not allowed:
            return ""
        blocks: List[str] = []
        for plan in VOLC_REGION_PLANS:
            if plan.region not in allowed:
                continue
            lines = [line for line in self.volc_region_l2tp.get(plan.region, []) if line.strip()]
            if not lines:
                continue
            header = "入口IP/账号/密码/密钥/对应出口IP"
            if all(len(line.split("/")) == 4 for line in lines):
                header = "入口IP/账号/密码/密钥"
            blocks.append(f"{plan.label}\n{header}\n" + "\n".join(lines))
        if not blocks:
            return ""
        return append_result_footer("\n\n".join(blocks), "L2TP", self.mark_var.get())

    def _parse_volc_saved_output_blocks(
        self,
        text: str,
        *,
        kind: str,
        allowed_regions: set[str],
    ) -> Dict[str, List[str]]:
        label_to_region: Dict[str, str] = {}
        for plan in VOLC_REGION_PLANS:
            label_to_region[plan.label] = plan.region
            label_to_region[self._volc_region_group_label(plan.label, 2)] = plan.region
        default_region = next(iter(allowed_regions)) if len(allowed_regions) == 1 else ""
        current_region = ""
        saw_region_header = False
        result: Dict[str, List[str]] = {}
        for raw_line in str(text or "").splitlines():
            value = str(raw_line or "").strip()
            if not value:
                continue
            region = label_to_region.get(value, "")
            if region:
                saw_region_header = True
                current_region = region if region in allowed_regions else ""
                continue
            target_region = current_region
            if not target_region and not saw_region_header:
                target_region = default_region
            if not target_region or target_region not in allowed_regions:
                continue
            if kind == "socks":
                normalized = self._clean_volc_remote_result_line(value) if self._is_volc_socks_result_line(value) else ""
            else:
                normalized = self._normalize_volc_l2tp_result_line(value)
            if normalized and normalized not in result.setdefault(target_region, []):
                result[target_region].append(normalized)
        return result

    def _restore_volc_outputs_from_latest_files(self, allowed_regions: set[str]) -> bool:
        if not allowed_regions:
            return False
        restored = False
        saved_socks = self._parse_volc_saved_output_blocks(
            self._read_latest_result_file("last_socks5.txt"),
            kind="socks",
            allowed_regions=allowed_regions,
        )
        saved_l2tp = self._parse_volc_saved_output_blocks(
            self._read_latest_result_file("last_l2tp.txt"),
            kind="l2tp",
            allowed_regions=allowed_regions,
        )
        for region in allowed_regions:
            if not self.volc_region_socks.get(region) and saved_socks.get(region):
                self.volc_region_socks[region] = list(saved_socks[region])
                restored = True
            if not self.volc_region_l2tp.get(region) and saved_l2tp.get(region):
                self.volc_region_l2tp[region] = list(saved_l2tp[region])
                restored = True
        return restored

    def _refresh_volc_outputs(
        self,
        *,
        restore_saved: bool = True,
        persist_empty: bool = False,
        allowed_regions: Optional[set[str]] = None,
    ) -> None:
        if not self._result_account_scope():
            if self.socks_text is not None:
                self.socks_text.delete("1.0", tk.END)
            if self.l2tp_text is not None:
                self.l2tp_text.delete("1.0", tk.END)
            return

        self._ensure_result_account_cache()
        all_regions = {plan.region for plan in VOLC_REGION_PLANS}
        if restore_saved:
            saved_socks = self._parse_volc_saved_output_blocks(
                self._read_latest_result_file("last_socks5.txt"),
                kind="socks",
                allowed_regions=all_regions,
            )
            saved_l2tp = self._parse_volc_saved_output_blocks(
                self._read_latest_result_file("last_l2tp.txt"),
                kind="l2tp",
                allowed_regions=all_regions,
            )
            complete_current_regions = {
                region
                for region in all_regions
                if self.volc_region_socks.get(region) and self.volc_region_l2tp.get(region)
            }
            for region in complete_current_regions:
                saved_socks[region] = list(self.volc_region_socks[region])
                saved_l2tp[region] = list(self.volc_region_l2tp[region])
            self.volc_region_socks = saved_socks
            self.volc_region_l2tp = saved_l2tp

        stored_regions = {
            region
            for region in all_regions
            if self.volc_region_socks.get(region) or self.volc_region_l2tp.get(region)
        }
        self.volc_region_socks = {
            region: list(lines)
            for region, lines in self.volc_region_socks.items()
            if region in stored_regions
        }
        self.volc_region_l2tp = {
            region: list(lines)
            for region, lines in self.volc_region_l2tp.items()
            if region in stored_regions
        }
        pending_regions = set(getattr(self, "_volc_pending_result_display_regions", set()))
        if pending_regions:
            self._volc_pending_result_display_regions = set()
        display_regions = set(allowed_regions) if allowed_regions is not None else (pending_regions or stored_regions)
        display_regions &= stored_regions

        self._last_socks_lines = []
        self._last_l2tp_lines = []
        for plan in VOLC_REGION_PLANS:
            if plan.region not in display_regions:
                continue
            self._last_socks_lines.extend(self.volc_region_socks.get(plan.region, []))
            self._last_l2tp_lines.extend(self.volc_region_l2tp.get(plan.region, []))
        text = self._format_volc_socks_output(display_regions)
        if self.socks_text is not None:
            self.socks_text.delete("1.0", tk.END)
            if text:
                self.socks_text.insert(tk.END, text + "\n")
        l2tp_text = self._format_volc_l2tp_output(display_regions)
        if self.l2tp_text is not None:
            self.l2tp_text.delete("1.0", tk.END)
            if l2tp_text:
                self.l2tp_text.insert(tk.END, l2tp_text + "\n")
        self._save_latest_result_outputs(
            self._format_volc_l2tp_output(stored_regions),
            self._format_volc_socks_output(stored_regions),
            allow_empty=persist_empty,
        )

    @staticmethod
    def _replace_public_ips_in_text(text: str, replacements: Dict[str, str]) -> str:
        output = str(text or "")
        for old_ip, new_ip in replacements.items():
            old_value = str(old_ip or "").strip()
            new_value = str(new_ip or "").strip()
            if not old_value or not new_value or old_value == new_value:
                continue
            output = re.sub(
                rf"(?<![0-9.]){re.escape(old_value)}(?![0-9.])",
                new_value,
                output,
            )
        return output

    def _recover_volc_result_templates_from_logs(
        self,
        plan: RegionPlan,
        expected_counts: Dict[int, int],
    ) -> Dict[int, Tuple[List[str], List[str]]]:
        account_log_root = Path(APP_LOG_DIR) / self._account_log_scope()
        if not account_log_root.exists():
            return {}
        try:
            log_paths = sorted(
                account_log_root.rglob("*.log"),
                key=lambda path: path.stat().st_mtime,
                reverse=True,
            )
        except OSError:
            return {}

        complete: Dict[int, Tuple[List[str], List[str]]] = {}
        partial: Dict[int, Tuple[List[str], List[str]]] = {}
        partial_scores: Dict[int, int] = {}
        ipv4 = r"(?:\d{1,3}\.){3}\d{1,3}"
        for path in log_paths:
            try:
                lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
            except OSError:
                continue
            server_groups: Dict[str, Tuple[str, int]] = {}
            for line in lines:
                match = re.match(
                    rf"^\[(cn-[a-z]+)-(one|two|1|2)\]\s*主网卡IP:\s*({ipv4})",
                    str(line or "").strip(),
                    flags=re.IGNORECASE,
                )
                if not match:
                    continue
                region, group_token, server_ip = match.groups()
                group_index = 2 if group_token.lower() in {"two", "2"} else 1
                server_groups[server_ip] = (region, group_index)
            if not server_groups:
                continue

            parsed: Dict[int, Tuple[List[str], List[str]]] = {}
            for line in lines:
                match = re.match(rf"^\[({ipv4})\]\s*(.*)$", str(line or "").strip())
                if not match:
                    continue
                server_ip, content = match.groups()
                region_group = server_groups.get(server_ip)
                if not region_group or region_group[0] != plan.region:
                    continue
                socks_lines, l2tp_lines = parsed.setdefault(region_group[1], ([], []))
                self._collect_volc_result_line(content, socks_lines, l2tp_lines)

            for group_index, expected_count in expected_counts.items():
                candidate = parsed.get(group_index)
                if not candidate:
                    continue
                socks_lines, l2tp_lines = candidate
                score = len(socks_lines) + len(l2tp_lines)
                if score > partial_scores.get(group_index, -1):
                    partial[group_index] = (list(socks_lines), list(l2tp_lines))
                    partial_scores[group_index] = score
                if len(socks_lines) >= expected_count and len(l2tp_lines) >= expected_count:
                    complete[group_index] = (list(socks_lines), list(l2tp_lines))
            if all(group_index in complete for group_index in expected_counts):
                break

        for group_index in expected_counts:
            if group_index not in complete and group_index in partial:
                complete[group_index] = partial[group_index]
        return complete

    def _build_volc_region_outputs_from_templates(
        self,
        current_ip_groups: Dict[int, List[str]],
        templates: Dict[int, Tuple[List[str], List[str]]],
    ) -> Tuple[List[str], List[str]]:
        clean_groups = {
            int(group_index): [ip for ip in ips if is_public_ip(ip)]
            for group_index, ips in current_ip_groups.items()
            if ips
        }
        if not clean_groups:
            return [], []
        rebuilt_socks: List[str] = []
        rebuilt_l2tp: List[str] = []
        for group_index in sorted(clean_groups):
            current_ips = clean_groups[group_index]
            template_socks, template_l2tp = templates.get(group_index, ([], []))
            valid_socks = [line.split("/", 3) for line in template_socks if len(line.split("/", 3)) == 4]
            for index, current_ip in enumerate(current_ips):
                if not valid_socks:
                    break
                source = valid_socks[index] if index < len(valid_socks) else valid_socks[0]
                rebuilt_socks.append(f"{current_ip}/{source[1]}/{source[2]}/{source[3]}")

            valid_l2tp = [line.split("/") for line in template_l2tp if len(line.split("/")) >= 4]
            l2tp_by_user = {parts[1]: parts for parts in valid_l2tp if len(parts) >= 4}
            entry_ip = current_ips[0]
            for index, current_ip in enumerate(current_ips, start=1):
                username = f"user{index}"
                source = l2tp_by_user.get(username)
                if source is None and index - 1 < len(valid_l2tp):
                    source = valid_l2tp[index - 1]
                if source is None:
                    continue
                rebuilt_l2tp.append(f"{entry_ip}/{source[1]}/{source[2]}/{source[3]}/{current_ip}")
        return rebuilt_socks, rebuilt_l2tp

    def _rebuild_volc_region_outputs_from_logs(
        self,
        plan: RegionPlan,
        current_ip_groups: Dict[int, List[str]],
    ) -> Tuple[List[str], List[str]]:
        expected_counts = {
            int(group_index): len([ip for ip in ips if is_public_ip(ip)])
            for group_index, ips in current_ip_groups.items()
            if ips
        }
        if not expected_counts:
            return [], []
        templates = self._recover_volc_result_templates_from_logs(plan, expected_counts)
        return self._build_volc_region_outputs_from_templates(current_ip_groups, templates)

    def _saved_volc_result_templates(
        self,
        plan: RegionPlan,
        current_ip_groups: Dict[int, List[str]],
    ) -> Dict[int, Tuple[List[str], List[str]]]:
        self._restore_volc_outputs_from_latest_files({plan.region})
        saved_socks = list(self.volc_region_socks.get(plan.region, []))
        saved_l2tp = list(self.volc_region_l2tp.get(plan.region, []))
        templates: Dict[int, Tuple[List[str], List[str]]] = {}
        socks_offset = 0
        l2tp_offset = 0
        for group_index in sorted(current_ip_groups):
            expected_count = len([ip for ip in current_ip_groups[group_index] if is_public_ip(ip)])
            if expected_count <= 0:
                continue
            group_socks = saved_socks[socks_offset : socks_offset + expected_count]
            group_l2tp = saved_l2tp[l2tp_offset : l2tp_offset + expected_count]
            socks_offset += expected_count
            l2tp_offset += expected_count
            if group_socks or group_l2tp:
                templates[group_index] = (group_socks, group_l2tp)
        return templates

    def _sync_volc_results_after_new_ip_purchase(
        self,
        plan: RegionPlan,
        current_ip_groups: Dict[int, List[str]],
        context: str = "purchase",
    ) -> None:
        display_regions = set(getattr(self, "_volc_new_ip_result_regions", set()))
        display_regions.add(plan.region)
        self._volc_new_ip_result_regions = display_regions
        self._volc_pending_result_display_regions = set(display_regions)

        rebuilt_socks, rebuilt_l2tp = self._rebuild_volc_region_outputs_from_logs(plan, current_ip_groups)
        recovered_from_logs = bool(rebuilt_socks or rebuilt_l2tp)
        if not rebuilt_socks or not rebuilt_l2tp:
            saved_templates = self._saved_volc_result_templates(plan, current_ip_groups)
            saved_socks, saved_l2tp = self._build_volc_region_outputs_from_templates(
                current_ip_groups,
                saved_templates,
            )
            if not rebuilt_socks:
                rebuilt_socks = saved_socks
            if not rebuilt_l2tp:
                rebuilt_l2tp = saved_l2tp

        if rebuilt_socks:
            self.volc_region_socks[plan.region] = rebuilt_socks
        if rebuilt_l2tp:
            self.volc_region_l2tp[plan.region] = rebuilt_l2tp
        if rebuilt_socks or rebuilt_l2tp:
            source_text = "历史搭建日志" if recovered_from_logs else "本地结果记录"
            self.log(f"[{plan.label}] 已从{source_text}恢复新IP的L2TP/Socks5账号配置。", context)
        else:
            warning = "未在本地找到L2TP/Socks5结果记录，请重装云服务器后重新搭建即可显示结果。"
            self._volc_new_ip_result_warning = warning
            self.log(f"[{plan.label}] {warning}", context)
        self._refresh_volc_outputs(allowed_regions=display_regions)

    def _sync_volc_result_ips_after_eip_replace(
        self,
        plan: RegionPlan,
        replacements: Dict[str, str],
        current_ip_groups: Optional[Dict[int, List[str]]] = None,
    ) -> None:
        clean_replacements = {
            old_ip: new_ip
            for old_ip, new_ip in (
                (str(old_ip or "").strip(), str(new_ip or "").strip())
                for old_ip, new_ip in replacements.items()
            )
            if old_ip and new_ip and old_ip != new_ip and is_public_ip(old_ip) and is_public_ip(new_ip)
        }
        if not clean_replacements:
            return

        display_regions = set(getattr(self, "_volc_replace_result_regions", set()))
        display_regions.add(plan.region)
        self._volc_replace_result_regions = display_regions
        self._volc_pending_result_display_regions = set(display_regions)
        self._restore_volc_outputs_from_latest_files({plan.region})
        recovered_from_logs = False
        if not self.volc_region_socks.get(plan.region) and not self.volc_region_l2tp.get(plan.region) and current_ip_groups:
            recovered_socks, recovered_l2tp = self._rebuild_volc_region_outputs_from_logs(plan, current_ip_groups)
            if recovered_socks:
                self.volc_region_socks[plan.region] = recovered_socks
            if recovered_l2tp:
                self.volc_region_l2tp[plan.region] = recovered_l2tp
            recovered_from_logs = bool(recovered_socks or recovered_l2tp)

        region_records_found = bool(
            self.volc_region_socks.get(plan.region) or self.volc_region_l2tp.get(plan.region)
        )
        changed = recovered_from_logs
        old_socks = list(self.volc_region_socks.get(plan.region, []))
        new_socks = [self._replace_public_ips_in_text(line, clean_replacements) for line in old_socks]
        if new_socks != old_socks:
            self.volc_region_socks[plan.region] = new_socks
            changed = True

        old_l2tp = list(self.volc_region_l2tp.get(plan.region, []))
        new_l2tp = [self._replace_public_ips_in_text(line, clean_replacements) for line in old_l2tp]
        if new_l2tp != old_l2tp:
            self.volc_region_l2tp[plan.region] = new_l2tp
            changed = True

        if not changed:
            current_l2tp = self.l2tp_text.get("1.0", tk.END) if self.l2tp_text is not None else ""
            current_socks = self.socks_text.get("1.0", tk.END) if self.socks_text is not None else ""
            widget_l2tp = self._parse_volc_saved_output_blocks(
                current_l2tp,
                kind="l2tp",
                allowed_regions={plan.region},
            )
            widget_socks = self._parse_volc_saved_output_blocks(
                current_socks,
                kind="socks",
                allowed_regions={plan.region},
            )
            region_records_found = region_records_found or bool(
                widget_l2tp.get(plan.region) or widget_socks.get(plan.region)
            )
            replaced_l2tp = self._replace_public_ips_in_text(current_l2tp, clean_replacements)
            replaced_socks = self._replace_public_ips_in_text(current_socks, clean_replacements)
            changed = replaced_l2tp != current_l2tp or replaced_socks != current_socks
            if changed:
                updated_l2tp = self._parse_volc_saved_output_blocks(
                    replaced_l2tp,
                    kind="l2tp",
                    allowed_regions={plan.region},
                )
                updated_socks = self._parse_volc_saved_output_blocks(
                    replaced_socks,
                    kind="socks",
                    allowed_regions={plan.region},
                )
                if updated_l2tp.get(plan.region):
                    self.volc_region_l2tp[plan.region] = list(updated_l2tp[plan.region])
                if updated_socks.get(plan.region):
                    self.volc_region_socks[plan.region] = list(updated_socks[plan.region])

        if not region_records_found:
            warning = "未在本地找到L2TP/Socks5结果记录，请重装云服务器后重新搭建即可显示结果。"
            self._volc_replace_result_warning = warning
            self.log(f"[{plan.label}] {warning}", "manage")

        self._refresh_volc_outputs(allowed_regions=display_regions)

        if changed:
            pairs = ", ".join(f"{old_ip}->{new_ip}" for old_ip, new_ip in clean_replacements.items())
            if recovered_from_logs:
                self.log(f"[{plan.label}] 原结果为空，已从历史搭建日志恢复连接配置。", "manage")
            self.log(f"[{plan.label}] 结果框IP已同步更新: {pairs}", "manage")

    def _volc_region_groups_for_items(self, items: Sequence[Dict[str, Any]]) -> Dict[str, Set[int]]:
        groups_by_region: Dict[str, Set[int]] = {}
        for item in items:
            region = str(item.get("_region") or "").strip()
            if not region:
                continue
            group_index = 2 if self._volc_manage_item_group_index(dict(item)) >= 2 else 1
            groups_by_region.setdefault(region, set()).add(group_index)
        return groups_by_region

    def _clear_volc_outputs_for_region_groups(
        self,
        groups_by_region: Dict[str, Set[int]],
        *,
        context: str = "purchase",
        reason: str = "已清除本次云服务器的旧搭建结果，需重新搭建后生成新结果。",
    ) -> None:
        normalized_groups = {
            str(region or "").strip(): {2 if int(group_index or 1) >= 2 else 1 for group_index in groups}
            for region, groups in groups_by_region.items()
            if str(region or "").strip() and groups
        }
        if not normalized_groups:
            return
        self._restore_volc_outputs_from_latest_files(set(normalized_groups))
        changed = False
        for region, selected_groups in normalized_groups.items():
            region_items = [
                dict(item)
                for item in self.volc_cached_resource_items
                if str(item.get("_region") or "").strip() == region
            ]
            existing_groups = {
                self._volc_manage_item_group_index(item)
                for item in region_items
                if item.get("_kind") == "ecs"
            }
            if existing_groups and existing_groups.issubset(selected_groups):
                changed = self.volc_region_socks.pop(region, None) is not None or changed
                changed = self.volc_region_l2tp.pop(region, None) is not None or changed
                continue

            target_ips: Set[str] = set()
            for item in region_items:
                if (
                    item.get("_kind") not in {"ecs", "eip"}
                    or self._volc_manage_item_group_index(item) not in selected_groups
                ):
                    continue
                raw_values: List[Any] = [item.get("_public_ip")]
                raw = item.get("_raw")
                if isinstance(raw, dict):
                    raw_values.extend(self._volc_public_ips_from_resource(raw))
                for raw_value in raw_values:
                    for candidate in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(raw_value or "")):
                        if is_public_ip(candidate):
                            target_ips.add(candidate)
            if not target_ips:
                continue

            old_socks = list(self.volc_region_socks.get(region, []))
            new_socks = [
                line
                for line in old_socks
                if not target_ips.intersection(re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(line or "")))
            ]
            if new_socks != old_socks:
                self.volc_region_socks[region] = new_socks
                changed = True

            old_l2tp = list(self.volc_region_l2tp.get(region, []))
            new_l2tp = [
                line
                for line in old_l2tp
                if not target_ips.intersection(re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(line or "")))
            ]
            if new_l2tp != old_l2tp:
                self.volc_region_l2tp[region] = new_l2tp
                changed = True
        if changed:
            self._refresh_volc_outputs(restore_saved=False, persist_empty=True)
            self.log(reason, context)

    def _clear_volc_outputs_for_regions(
        self,
        regions: Sequence[str],
        *,
        context: str = "purchase",
        reason: str = "已清除本次地区的旧搭建结果，需重新搭建后生成新结果。",
    ) -> None:
        region_set = {str(region or "").strip() for region in regions if str(region or "").strip()}
        if not region_set:
            return
        changed = False
        matched = False
        for plan in VOLC_REGION_PLANS:
            tokens = {str(plan.region or "").strip(), str(plan.key or "").strip(), str(plan.label or "").strip()}
            if any(token in region_set for token in tokens if token):
                matched = True
                changed = self.volc_region_socks.pop(plan.region, None) is not None or changed
                changed = self.volc_region_l2tp.pop(plan.region, None) is not None or changed
        if matched:
            self._refresh_volc_outputs(restore_saved=False, persist_empty=True)
        if changed:
            self.log(reason, context)

    @staticmethod
    def _append_unique_output_lines(target: List[str], lines: Sequence[str]) -> None:
        seen = {line for line in target if line.strip()}
        for line in lines:
            value = str(line or "").strip()
            if value and value not in seen:
                target.append(value)
                seen.add(value)

    def _merge_volc_region_output_lines(self, region: str, socks_lines: Sequence[str], l2tp_lines: Sequence[str]) -> None:
        if region in self.volc_rebuilding_output_regions:
            current_socks: List[str] = []
            current_l2tp: List[str] = []
            self.volc_rebuilding_output_regions.discard(region)
        else:
            current_socks = list(self.volc_region_socks.get(region, []))
            current_l2tp = list(self.volc_region_l2tp.get(region, []))
        self._append_unique_output_lines(current_socks, socks_lines)
        self._append_unique_output_lines(current_l2tp, l2tp_lines)
        self.volc_region_socks[region] = current_socks
        self.volc_region_l2tp[region] = current_l2tp

    @staticmethod
    def _volc_expected_ecs_names(group_count: int) -> List[str]:
        try:
            from Volcengine.api.volcengine import data as volc_data

            group1 = str(getattr(volc_data, "ECS_NAME_GROUP_1", "") or "wawacloud_ecs1")
            group2 = str(getattr(volc_data, "ECS_NAME_GROUP_2", "") or "wawacloud_ecs2")
        except Exception:
            group1 = "wawacloud_ecs1"
            group2 = "wawacloud_ecs2"
        names = [group1]
        if int(group_count or 1) >= 2:
            names.append(group2)
        return names

    def _volc_cached_items_for_purchase(self, plan: RegionPlan) -> Optional[List[Dict[str, Any]]]:
        """购买前优先复用刚读取的账号资源缓存，避免 AK/SK 后又重复查询云接口。"""
        if self.volc_cached_resource_account_scope != self._account_log_scope():
            return None
        if plan.region not in self.volc_cached_resource_regions:
            return None
        if self.volc_cached_resource_updated_at <= 0:
            return None
        if time.monotonic() - self.volc_cached_resource_updated_at > VOLC_RESOURCE_CACHE_MAX_AGE_SECONDS:
            return None
        return [
            dict(item)
            for item in self.volc_cached_resource_items
            if str(item.get("_region") or "") == plan.region
        ]

    def _precheck_volc_new_ip_purchase_groups(
        self,
        plans: Sequence[RegionPlan],
        selected_groups_by_region: Dict[str, Set[int]],
        context: str,
    ) -> Tuple[List[RegionPlan], Dict[str, Set[int]]]:
        eligible_groups: Dict[str, Set[int]] = {}
        missing_ecs_labels: List[str] = []
        full_eip_labels: List[str] = []
        stale_cache_labels: List[str] = []
        purchase_row_ids = set(self.region_tree.get_children("")) if self.region_tree is not None else set()

        def cancel_group(plan: RegionPlan, group_index: int) -> None:
            row_id = self._purchase_region_row_id(plan, group_index)
            if row_id not in purchase_row_ids:
                return
            values = list(self.region_tree.item(row_id, "values"))
            if values:
                values[0] = UNCHECKED
                self.region_tree.item(row_id, text=values[0], values=values)

        for plan in plans:
            selected_groups = {
                2 if int(group_index or 1) >= 2 else 1
                for group_index in selected_groups_by_region.get(plan.region, set())
            }
            if not selected_groups:
                continue
            cached_items = self._volc_cached_items_for_purchase(plan)
            if cached_items is None:
                for group_index in sorted(selected_groups):
                    stale_cache_labels.append(self._volc_region_group_label(plan.label, group_index))
                    cancel_group(plan, group_index)
                continue
            grouped_items: Dict[int, List[Dict[str, Any]]] = {1: [], 2: []}
            for item in cached_items:
                group_index = self._volc_manage_item_group_index(dict(item))
                grouped_items[2 if group_index >= 2 else 1].append(dict(item))
            for group_index in sorted(selected_groups):
                group_items = grouped_items.get(group_index, [])
                reusable_ecs = False
                for item in group_items:
                    if item.get("_kind") != "ecs":
                        continue
                    status_text = str(item.get("_status") or "").strip()
                    status_upper = status_text.upper()
                    if any(token in status_text for token in ("停止", "删除")):
                        continue
                    if status_upper in {"STOPPED", "STOPPING", "DELETED", "DELETING", "TERMINATED"}:
                        continue
                    reusable_ecs = True
                    break
                group_label = self._volc_region_group_label(plan.label, group_index)
                if not reusable_ecs:
                    missing_ecs_labels.append(group_label)
                    cancel_group(plan, group_index)
                    continue
                eip_count = sum(1 for item in group_items if item.get("_kind") == "eip")
                if eip_count >= VOLC_IP_QUOTA_PER_REGION:
                    full_eip_labels.append(group_label)
                    cancel_group(plan, group_index)
                    continue
                eligible_groups.setdefault(plan.region, set()).add(group_index)

        eligible_plans = [plan for plan in plans if eligible_groups.get(plan.region)]
        notices: List[str] = []
        if missing_ecs_labels:
            notices.append(
                "以下选择未发现可复用的云服务器，已取消："
                f"{'、'.join(missing_ecs_labels)}。此功能只为已有云服务器购买并绑定新公网IP，不会创建云服务器。"
            )
        if full_eip_labels:
            notices.append(
                f"以下选择已达到每组{VOLC_IP_QUOTA_PER_REGION}条公网IP上限，已取消："
                f"{'、'.join(full_eip_labels)}。请先释放对应组的旧公网IP，等待超过2小时后再重新购买，"
                "可降低再次获得旧IP的概率。"
            )
        if stale_cache_labels:
            notices.append(
                "以下选择没有最新资源信息，已取消："
                f"{'、'.join(stale_cache_labels)}。请先点击“读取账号资源”刷新，再重新选择；"
                "已读取的资源缓存超过10分钟也需要重新刷新。"
            )
        if notices:
            notices.append("其他符合条件的已选资源将继续执行。" if eligible_plans else "本次没有符合条件的资源，任务已取消。")
            message = "\n\n".join(notices)
            self.log(message, context)
            self.show_info(message)
        return eligible_plans, eligible_groups

    def _volc_existing_purchase_ips_from_items(
        self,
        plan: RegionPlan,
        group_count: int,
        items: Sequence[Dict[str, Any]],
        *,
        requested_groups: Optional[Set[int]] = None,
        reason: str,
    ) -> List[str]:
        requested_group_count = 2 if int(group_count or 1) >= 2 else 1
        all_group_names = self._volc_expected_ecs_names(2)
        while len(all_group_names) < 2:
            all_group_names.append("wawacloud_ecs2")
        group_name_by_index = {1: all_group_names[0], 2: all_group_names[1]}
        ecs_by_group: Dict[int, Dict[str, Any]] = {}
        for item in items:
            if item.get("_kind") != "ecs" or not item.get("_public_ip"):
                continue
            status_text = str(item.get("_status") or "").strip()
            status_upper = status_text.upper()
            if status_text in {"已停止", "删除中", "DELETED"} or status_upper in {"STOPPED", "DELETED", "DELETING"}:
                continue
            name = str(item.get("_name") or "").strip()
            for group_index, expected_name in group_name_by_index.items():
                if name == expected_name and group_index not in ecs_by_group:
                    ecs_by_group[group_index] = dict(item)
        eip_count_by_group = {1: 0, 2: 0}
        for item in items:
            if item.get("_kind") != "eip":
                continue
            group_index = self._volc_manage_item_group_index(dict(item))
            eip_count_by_group[2 if int(group_index or 1) >= 2 else 1] += 1

        def public_ip_for_group(group_index: int) -> str:
            ecs_item = ecs_by_group.get(group_index) or {}
            return str(ecs_item.get("_public_ip") or "").strip()

        def complete_group(group_index: int) -> bool:
            return bool(public_ip_for_group(group_index)) and eip_count_by_group.get(group_index, 0) >= VOLC_IP_QUOTA_PER_REGION

        def ips_for_groups(groups: Sequence[int]) -> List[str]:
            ips = [public_ip_for_group(group_index) for group_index in groups]
            return [ip for ip in ips if ip]

        explicit_groups = sorted(
            {
                2 if int(group_index or 1) >= 2 else 1
                for group_index in (requested_groups or set())
            }
        )
        if explicit_groups:
            incomplete_groups = [group_index for group_index in explicit_groups if not complete_group(group_index)]
            if not incomplete_groups:
                ips = ips_for_groups(explicit_groups)
                selected_labels = "、".join(
                    self._volc_region_group_label(plan.label, group_index)
                    for group_index in explicit_groups
                )
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] {reason}: 所选资源 {selected_labels} 已完整，跳过重复购买。",
                )
                return ips
            group_details = "、".join(
                f"{self._volc_region_group_label(plan.label, group_index)} "
                f"ECS={'1/1' if group_index in ecs_by_group else '0/1'} "
                f"EIP={eip_count_by_group.get(group_index, 0)}/{VOLC_IP_QUOTA_PER_REGION}"
                for group_index in explicit_groups
            )
            self.after(
                0,
                self.log,
                f"[{plan.label}] {reason}: 所选资源尚未完整（{group_details}），继续购买/补齐所选分组。",
            )
            return []

        if requested_group_count == 1:
            group1_complete = complete_group(1)
            group2_complete = complete_group(2)
            if group1_complete and group2_complete:
                ips = ips_for_groups([1, 2])
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] {reason}: 已存在两套 ECS/EIP，跳过重复购买，直接进入下一步。",
                )
                return ips
            if group1_complete and not group2_complete:
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] {reason}: 已有第一套 ECS/EIP 正在运行，继续购买第二套资源。",
                )
                return []
            if group2_complete and not group1_complete:
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] {reason}: 已有第二套 ECS/EIP 正在运行，继续购买第一套资源。",
                )
                return []
            if ecs_by_group:
                found_groups = "、".join(f"第{group_index}套ECS入口IP" for group_index in sorted(ecs_by_group))
                eip_details = "、".join(
                    f"第{group_index}套EIP={eip_count_by_group.get(group_index, 0)}/{VOLC_IP_QUOTA_PER_REGION}"
                    for group_index in sorted(ecs_by_group)
                )
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] {reason}: 已发现 {found_groups}，但 {eip_details}，继续购买/补齐资源。",
                )
            return []

        # 48 条必须按第一组/第二组 ECS 名称各取一台，避免只按地区排序后重复使用第一组入口 IP。
        expected_groups = [1, 2]
        expected_ecs = len(expected_groups)
        expected_eips = expected_ecs * VOLC_IP_QUOTA_PER_REGION
        if not ecs_by_group:
            return []
        missing_groups = [group_index for group_index in expected_groups if group_index not in ecs_by_group]
        if missing_groups:
            found_names = "、".join(
                group_name_by_index[group_index] for group_index in expected_groups if group_index in ecs_by_group
            ) or "无"
            missing_names = "、".join(group_name_by_index[group_index] for group_index in missing_groups)
            self.after(
                0,
                self.log,
                f"[{plan.label}] {reason}: 已发现 {found_names}，缺少 {missing_names}，继续购买/补齐第二组，避免把第一组误当 48 条完成。",
            )
            return []
        ips = ips_for_groups(expected_groups)
        if not ips:
            return []
        eip_count = sum(eip_count_by_group.get(group_index, 0) for group_index in expected_groups)
        missing_eip_groups = [
            f"{self._volc_region_group_label(plan.label, group_index)} EIP={eip_count_by_group.get(group_index, 0)}/{VOLC_IP_QUOTA_PER_REGION}"
            for group_index in expected_groups
            if eip_count_by_group.get(group_index, 0) < VOLC_IP_QUOTA_PER_REGION
        ]
        if missing_eip_groups:
            self.after(
                0,
                self.log,
                f"[{plan.label}] {reason}: 已有ECS入口IP {len(ips)}/{expected_ecs}，但 {'、'.join(missing_eip_groups)}，继续购买/补齐公网IP。",
            )
            return []
        self.after(
            0,
            self.log,
            f"[{plan.label}] {reason}: 已有ECS入口IP {len(ips)}/{expected_ecs}，EIP={eip_count}/{expected_eips}，跳过重复购买，直接进入下一步。",
        )
        return ips

    def _volc_existing_purchase_ips(
        self,
        plan: RegionPlan,
        group_count: int,
        ak: str,
        sk: str,
        *,
        requested_groups: Optional[Set[int]] = None,
        reason: str,
        use_cache: bool = True,
    ) -> List[str]:
        if not ak or not sk:
            return []
        cached_items = self._volc_cached_items_for_purchase(plan) if use_cache else None
        if cached_items is not None:
            ips = self._volc_existing_purchase_ips_from_items(
                plan,
                group_count,
                cached_items,
                requested_groups=requested_groups,
                reason=f"{reason}/使用刚读取缓存",
            )
            if ips:
                return ips
            self.after(0, self.log, f"[{plan.label}] {reason}: 已使用刚读取的资源缓存判断，无需重复查询云接口。")
            return []
        try:
            items = self._query_volc_region_resources(plan, ak, sk)
        except Exception as exc:
            self.after(0, self.log, f"[{plan.label}] 已有资源反查失败，继续尝试购买: {self._volc_exception_text(exc)}")
            return []
        self.after(0, self.apply_volc_resource_refresh, items, [plan.region], True)
        return self._volc_existing_purchase_ips_from_items(
            plan,
            group_count,
            items,
            requested_groups=requested_groups,
            reason=f"{reason}/实时反查",
        )

    def _volc_capture_purchase_failure_hint(
        self,
        line: str,
        current_region: str,
        ecs_name_by_region: Dict[str, str],
        instance_id_by_region: Dict[str, str],
        failure_reasons: Dict[str, str],
    ) -> str:
        task_match = re.search(r"开始执行地域\s+(cn-[a-z]+)\s+第\s*\d+\s*组", line or "", flags=re.IGNORECASE)
        if task_match:
            current_region = task_match.group(1)
        region_match = re.match(r"^\[(cn-[a-z]+)\]\s*(.*)$", line or "")
        if region_match:
            current_region = region_match.group(1)
            detail = region_match.group(2).strip()
            lower_detail = detail.lower()
            if "ecs" in lower_detail and "已存在" in detail:
                if "wawacloud_ecs2" in lower_detail or lower_detail.startswith("two ecs"):
                    ecs_name_by_region[current_region] = "wawacloud_ecs2"
                elif "wawacloud_ecs1" in lower_detail or lower_detail.startswith("one ecs"):
                    ecs_name_by_region[current_region] = "wawacloud_ecs1"
                instance_match = re.search(r"\b(i-[A-Za-z0-9-]+)\b", detail)
                if instance_match:
                    instance_id_by_region[current_region] = instance_match.group(1)
        if current_region and "实例未就绪" in (line or "") and ("STOPPED" in line.upper() or "已停止" in line):
            reason = self._volc_stopped_old_ecs_reason(
                ecs_name_by_region.get(current_region, ""),
                instance_id_by_region.get(current_region, ""),
            )
            if current_region not in failure_reasons:
                failure_reasons[current_region] = reason
                label = next((plan.label for plan in VOLC_REGION_PLANS if plan.region == current_region), current_region)
                self.after(0, self.log, f"[{label}] 失败原因识别：{reason}")
        if current_region and "账户可支付余额不足" in (line or ""):
            failure_reasons.setdefault(current_region, "火山引擎返回账户可支付余额不足，请检查余额及包年包月订单金额后重试")
        daily_limit_match = re.search(r"当前账号在\s+(cn-[a-z]+)\s+今日公网IP已购买", line or "")
        if daily_limit_match:
            limit_region = daily_limit_match.group(1)
            failure_reasons.setdefault(limit_region, str(line or "").strip())
        return current_region

    def _run_volc_purchase_for_plan(self, plan: RegionPlan, group_count: int) -> Tuple[RegionPlan, int, List[str]]:
        env = os.environ.copy()
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        spec_key = self.volc_spec_var.get().strip()
        spec = VOLC_SPECS.get(spec_key, VOLC_SPECS[VOLC_SPEC_2C2G])
        self.volc_purchase_failure_reasons = {}
        existing_ips = self._volc_existing_purchase_ips(plan, group_count, ak, sk, reason="购买前检查")
        if existing_ips:
            return plan, 0, existing_ips
        if ak and sk:
            env["VOLC_AK"] = ak
            env["VOLC_SK"] = sk
        env["VOLC_REGION_COUNTS"] = f"{plan.region}={group_count}"
        env["VOLC_INSTANCE_TYPE"] = spec["instance_type"]
        env["PYTHONUTF8"] = "1"
        env["PYTHONIOENCODING"] = "utf-8"
        env["PYTHONUNBUFFERED"] = "1"
        cmd = self._volc_worker_command()
        self.after(0, self.log, f"[{plan.label}] 火山引擎购买启动，规格={spec['label']}({spec['instance_type']})，组数={group_count}")
        proc = subprocess.Popen(
            cmd,
            cwd=os.path.dirname(os.path.abspath(__file__)),
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
        self.child_processes.add(proc)
        ips: List[str] = []
        current_region = ""
        ecs_name_by_region: Dict[str, str] = {}
        instance_id_by_region: Dict[str, str] = {}
        failure_reasons: Dict[str, str] = {}
        if proc.stdout is not None:
            for raw in proc.stdout:
                if self.stop_event.is_set():
                    proc.terminate()
                    break
                line = decode_process_output_line(raw)
                if line:
                    self.after(
                        0,
                        self.log,
                        self._volc_purchase_chinese_region_line(line, {plan.region: plan.label}, {}),
                    )
                    current_region = self._volc_capture_purchase_failure_hint(
                        line,
                        current_region,
                        ecs_name_by_region,
                        instance_id_by_region,
                        failure_reasons,
                    )
                match = re.search(r"\[(cn-[a-z]+)-(?:one|two)\]\s*主网卡IP:\s*((?:\d{1,3}\.){3}\d{1,3})", line)
                if match and match.group(1) == plan.region:
                    ip = match.group(2)
                    if ip not in ips:
                        ips.append(ip)
        return_code = proc.wait()
        try:
            if proc.stdout is not None:
                proc.stdout.close()
        except Exception:
            pass
        self.child_processes.discard(proc)
        self.volc_purchase_failure_reasons.update(failure_reasons)
        if return_code != 0:
            existing_ips = self._volc_existing_purchase_ips(
                plan,
                group_count,
                ak,
                sk,
                reason=f"购买进程退出码={return_code} 后反查",
                use_cache=False,
            )
            if existing_ips:
                return plan, 0, existing_ips
        return plan, return_code, ips

    @staticmethod
    def _volc_purchase_chinese_region_line(
        line: str,
        label_by_region: Dict[str, str],
        step_context: Dict[str, Any],
    ) -> str:
        text = str(line or "").strip()
        match = re.match(r"^\[(cn-[a-z]+)(?:-(one|two|1|2))?\]\s*(.*)$", text, flags=re.IGNORECASE)
        if not match:
            return text
        region, group_token, detail = match.groups()
        group_index = 2 if str(group_token or "").lower() in {"two", "2"} else 1
        if not group_token and step_context.get("region") == region:
            group_index = int(step_context.get("group_index") or 1)
        label = label_by_region.get(region, region)
        if group_index >= 2:
            label = f"{label}{group_index}"
        return f"[{label}] {detail}".strip()

    @staticmethod
    def _volc_purchase_display_step(
        line: str,
        label_by_region: Dict[str, str],
        seen_steps: Set[Tuple[str, int, str]],
        step_context: Dict[str, Any],
    ) -> str:
        text = str(line or "").strip()
        if not text:
            return ""
        if "步骤:" in text:
            return ""
        region_match = re.search(r"\[(cn-[a-z]+)(?:-(one|two|1|2))?\]", text, flags=re.IGNORECASE)
        region = region_match.group(1) if region_match else ""
        group_token = (region_match.group(2) if region_match else "") or ""
        group_index = 2 if group_token.lower() in {"two", "2"} else 1
        task_match = re.search(r"开始执行地域\s+(cn-[a-z]+)\s+第\s*(\d+)\s*组", text, flags=re.IGNORECASE)
        if task_match:
            region = task_match.group(1)
            group_index = max(1, int(task_match.group(2)))
            step_context["region"] = region
            step_context["group_index"] = group_index
        elif region:
            if group_token:
                step_context["region"] = region
                step_context["group_index"] = group_index
            elif step_context.get("region") == region:
                group_index = int(step_context.get("group_index") or 1)
        elif step_context.get("region"):
            region = str(step_context.get("region") or "")
            group_index = int(step_context.get("group_index") or 1)
        label = label_by_region.get(region, region or "火山引擎")
        if group_index >= 2 and label != "火山引擎":
            label = f"{label}{group_index}"
        step_patterns = (
            ("已有公网 IP 数量", "check-eip", "检查已有公网IP。"),
            ("余额查询成功", "balance", "余额检查通过。"),
            ("开始执行地域", "start", "开始创建资源。"),
            ("VPC 创建成功", "vpc", "VPC创建完成。"),
            ("子网创建成功", "subnet", "子网创建完成。"),
            ("安全组创建成功", "security-group", "安全组创建完成。"),
            ("实例创建成功", "ecs", "云服务器创建完成。"),
            ("私网 IP 绑定完成", "private-ip", "私网IP绑定完成。"),
            ("公网 IP 绑定完成", "public-ip-binding", "公网IP绑定中。"),
            ("公网 IP 申请与绑定完成", "public-ip-done", "公网IP绑定完成。"),
            ("主网卡IP:", "primary-ip", "云服务器公网IP已返回。"),
        )
        for marker, step_key, message in step_patterns:
            if marker not in text:
                continue
            dedupe_key = (region or label, group_index, step_key)
            if dedupe_key in seen_steps:
                return ""
            seen_steps.add(dedupe_key)
            return f"[{label}] 步骤: {message}"
        lower_text = text.lower()
        if any(word in lower_text for word in ("error", "failed", "exception", "traceback")) or any(
            word in text for word in ("失败", "错误", "异常", "超时", "余额不足", "配额")
        ):
            if re.match(r"^\[[^\]]+\]", text):
                return text
            return f"[{label}] {text}" if region else text
        return ""

    def _run_volc_purchase_for_plans(
        self,
        plans: List[RegionPlan],
        group_count: int,
        *,
        new_ip_only: bool = False,
        used_ip_filter: Sequence[str] = (),
        selected_groups_by_region: Optional[Dict[str, Set[int]]] = None,
    ) -> Dict[str, Tuple[int, List[str]]]:
        env = os.environ.copy()
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        spec_key = self.volc_spec_var.get().strip()
        spec = VOLC_SPECS.get(spec_key, VOLC_SPECS[VOLC_SPEC_2C2G])
        results: Dict[str, Tuple[int, List[str]]] = {}
        pending_plans: List[RegionPlan] = []
        self.volc_purchase_failure_reasons = {}
        explicit_groups_by_region: Dict[str, Set[int]] = {}
        for plan in plans:
            normalized_groups = {
                2 if int(group_index or 1) >= 2 else 1
                for group_index in set((selected_groups_by_region or {}).get(plan.region) or ())
            }
            if normalized_groups:
                explicit_groups_by_region[plan.region] = normalized_groups
        plan_labels = "、".join(plan.label for plan in plans)
        self.after(
            0,
            self.log,
            f"购买前复用检查，地区={plan_labels}，优先使用刚读取的账号资源缓存；缓存缺失或超过10分钟才实时反查云接口。",
        )
        for plan in plans:
            existing_ips = self._volc_existing_purchase_ips(
                plan,
                group_count,
                ak,
                sk,
                requested_groups=explicit_groups_by_region.get(plan.region),
                reason="购买前检查",
            )
            if existing_ips:
                results[plan.region] = (0, existing_ips)
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] 已有资源满足本次需求，复用 {len(existing_ips)} 个云服务器公网IP，跳过新购。",
                )
            else:
                pending_plans.append(plan)
                pending_text = "需要过滤旧IP库并补齐公网IP" if new_ip_only else "需要补齐公网IP/云服务器"
                self.after(0, self.log, f"[{plan.label}] {pending_text}，加入批量购买队列。")
        if not pending_plans:
            self.after(0, self.log, "所选地区均可复用已有资源，无需启动购买子进程。")
            return results
        if ak and sk:
            env["VOLC_AK"] = ak
            env["VOLC_SK"] = sk
        env.pop("VOLC_NEW_IP_ONLY", None)
        env.pop("VOLC_USED_IP_FILTER", None)
        env.pop("VOLC_REGION_GROUPS", None)
        pending_groups_by_region: Dict[str, Set[int]] = {}
        for plan in pending_plans:
            normalized_groups = set(explicit_groups_by_region.get(plan.region) or ())
            if not normalized_groups:
                normalized_groups = set(range(1, (2 if int(group_count or 1) >= 2 else 1) + 1))
            pending_groups_by_region[plan.region] = normalized_groups
        env["VOLC_REGION_COUNTS"] = ",".join(
            f"{plan.region}={max(pending_groups_by_region[plan.region])}"
            for plan in pending_plans
        )
        env["VOLC_INSTANCE_TYPE"] = spec["instance_type"]
        if new_ip_only:
            env["VOLC_NEW_IP_ONLY"] = "1"
            env["VOLC_USED_IP_FILTER"] = ",".join(sorted({str(ip or "").strip() for ip in used_ip_filter if str(ip or "").strip()}))
        if new_ip_only or explicit_groups_by_region:
            env["VOLC_REGION_GROUPS"] = ",".join(
                f"{plan.region}={'|'.join(str(group_index) for group_index in sorted(pending_groups_by_region[plan.region]))}"
                for plan in pending_plans
            )
        env["PYTHONUTF8"] = "1"
        env["PYTHONIOENCODING"] = "utf-8"
        env["PYTHONUNBUFFERED"] = "1"
        cmd = self._volc_worker_command()
        region_labels = "、".join(plan.label for plan in pending_plans)
        task_label = "购买选择地区新IP" if new_ip_only else "购买"
        self.after(0, self.log, f"火山引擎{task_label}子进程已启动，地区={region_labels}，规格={spec['label']}。")
        for plan in pending_plans:
            self.after(0, self.log, f"[{plan.label}] 步骤: 已进入购买队列。")
        proc = subprocess.Popen(
            cmd,
            cwd=os.path.dirname(os.path.abspath(__file__)),
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
        self.child_processes.add(proc)
        ips_by_region: Dict[str, List[str]] = {plan.region: [] for plan in pending_plans}
        label_by_region = {plan.region: plan.label for plan in pending_plans}
        seen_purchase_steps: Set[Tuple[str, int, str]] = set()
        purchase_step_context: Dict[str, Any] = {}
        current_region = ""
        ecs_name_by_region: Dict[str, str] = {}
        instance_id_by_region: Dict[str, str] = {}
        failure_reasons: Dict[str, str] = {}
        try:
            if proc.stdout is not None:
                for raw in proc.stdout:
                    if self.stop_event.is_set():
                        proc.terminate()
                        break
                    line = decode_process_output_line(raw)
                    if line:
                        display_line = self._volc_purchase_display_step(
                            line,
                            label_by_region,
                            seen_purchase_steps,
                            purchase_step_context,
                        )
                        if display_line and "步骤:" not in display_line:
                            self.after(0, self.log, display_line)
                        else:
                            self.after(
                                0,
                                self.log,
                                self._volc_purchase_chinese_region_line(line, label_by_region, purchase_step_context),
                            )
                            if display_line:
                                self.after(0, self.log, display_line)
                        current_region = self._volc_capture_purchase_failure_hint(
                            line,
                            current_region,
                            ecs_name_by_region,
                            instance_id_by_region,
                            failure_reasons,
                        )
                    match = re.search(r"\[(cn-[a-z]+)-(?:one|two)\]\s*主网卡IP:\s*((?:\d{1,3}\.){3}\d{1,3})", line)
                    if match:
                        region = match.group(1)
                        ip = match.group(2)
                        region_ips = ips_by_region.setdefault(region, [])
                        if ip not in region_ips:
                            region_ips.append(ip)
            return_code = proc.wait()
        finally:
            try:
                if proc.stdout is not None:
                    proc.stdout.close()
            except Exception:
                pass
            self.child_processes.discard(proc)
        self.volc_purchase_failure_reasons = dict(failure_reasons)
        if return_code == 2:
            balance_reason = "官方规定购买公网IP资源时账户可用余额需≥100元；余额不足，任务已停止，请先充值至100元以上后重试。"
            for plan in pending_plans:
                results[plan.region] = (2, [])
                self.volc_purchase_failure_reasons[plan.region] = balance_reason
                self.after(0, self.log, f"[{plan.label}] {balance_reason}")
            return results
        shared_failure_reason = next(iter(failure_reasons.values()), "")
        for plan in pending_plans:
            ips = ips_by_region.get(plan.region, [])
            if return_code == 0 and ips:
                results[plan.region] = (0, ips)
                self.after(0, self.log, f"[{plan.label}] 购买返回成功，已识别云服务器公网IP {len(ips)} 个。")
                continue
            existing_ips = self._volc_existing_purchase_ips(
                plan,
                group_count,
                ak,
                sk,
                requested_groups=explicit_groups_by_region.get(plan.region),
                reason=f"购买进程退出码={return_code} 后反查",
                use_cache=False,
            )
            if existing_ips:
                results[plan.region] = (0, existing_ips)
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] 购买进程退出码={return_code}，但反查确认资源已创建，继续复用 {len(existing_ips)} 个公网IP。",
                )
            else:
                if shared_failure_reason and plan.region not in self.volc_purchase_failure_reasons:
                    self.volc_purchase_failure_reasons[plan.region] = shared_failure_reason
                results[plan.region] = (return_code or 1, ips)
                self.after(0, self.log, f"[{plan.label}] 购买后反查仍未确认资源完成，退出码={return_code or 1}。")
        return results

    def _start_volc_purchase_task(
        self,
        *,
        build_after: bool,
        new_ip_only: bool = False,
        selected_regions: Optional[Sequence[str]] = None,
        task_context: str = "purchase",
    ) -> None:
        task_context = "manage" if task_context == "manage" else "purchase"
        config_key = self._selected_volc_config_or_warn()
        if not config_key:
            return
        selected_groups_by_region: Dict[str, Set[int]] = {}
        # 便捷数量（满配/大满配）本身就是固定的三地区购买方案；
        # 只有未选择便捷数量时，才要求按用户勾选地区执行，避免 24/48 条被地区选择拦住。
        if new_ip_only:
            if selected_regions is None:
                selected_groups_by_region = self._selected_purchase_region_groups()
                plans = [plan for plan in VOLC_REGION_PLANS if plan.region in selected_groups_by_region]
            else:
                selected_region_values = {str(region or "").strip() for region in selected_regions}
                plans = [plan for plan in VOLC_REGION_PLANS if plan.region in selected_region_values]
                selected_groups_by_region = {plan.region: {1, 2} for plan in plans}
            if not plans:
                self.log("请先选择要购买新IP的火山引擎地区。", task_context)
                return
        elif config_key in VOLC_CONFIGS:
            plans = list(VOLC_REGION_PLANS)
        else:
            selected_groups_by_region = self._selected_purchase_region_groups()
            plans = [plan for plan in VOLC_REGION_PLANS if plan.region in selected_groups_by_region]
            if not plans:
                self.log("请先选择要处理的火山引擎地区。", task_context)
                return
        group_count = 2 if new_ip_only else self._volc_group_count()
        config = VOLC_CONFIGS.get(config_key, VOLC_CONFIGS[VOLC_CONFIG_FULL])
        if config_key not in VOLC_CONFIGS:
            config = dict(config)
            config["label"] = self._volc_config_choice_label(config_key)
        spec_key = self.volc_spec_var.get().strip()
        spec = VOLC_SPECS.get(spec_key, VOLC_SPECS[VOLC_SPEC_2C2G])
        action_name = "购买选择地区新IP" if new_ip_only else ("购买并搭建" if build_after else "购买")
        guard_message = self._customer_guard_block_message()
        if guard_message:
            self._show_customer_guard_block(guard_message, task_context)
            return
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        used_ip_filter: Set[str] = set()
        if new_ip_only:
            plans, selected_groups_by_region = self._precheck_volc_new_ip_purchase_groups(
                plans,
                selected_groups_by_region,
                task_context,
            )
            if not plans:
                return
            used_ip_path = self._used_ip_table_path()
            try:
                with self._used_ip_table_file_guard(used_ip_path):
                    used_ip_rows = self._read_used_ip_csv_rows(used_ip_path)
                used_ip_filter = {
                    self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]))
                    for row in used_ip_rows
                    if self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]))
                }
            except Exception as exc:
                message = f"读取旧IP库失败，已停止购买选择地区新IP：{exc}"
                self.log(message, task_context)
                self.show_info(message)
                return
            if not used_ip_filter:
                message = "旧IP库为空，无法过滤已使用IP；请先在更换新IP右键配置中保存，或链接旧IP库并合并。"
                self.log(message, task_context)
                self.show_info(message)
                return
        try:
            daily_status_by_region = {
                plan.region: get_eip_daily_purchase_status(ak, sk, plan.region)
                for plan in plans
            }
        except Exception as exc:
            message = f"读取公网IP每日购买记录失败，为避免超过限额已停止购买：{self._volc_exception_text(exc)}"
            self.log(message, task_context)
            self.show_info(message)
            return
        daily_status_text = "；".join(
            f"{plan.label}{daily_status_by_region[plan.region]['count']}/{VOLC_EIP_DAILY_PURCHASE_LIMIT}"
            f"（剩余{daily_status_by_region[plan.region]['remaining']}）"
            for plan in plans
        )
        if new_ip_only:
            details = [
                "购买信息：",
                "购买选项：按所选地区已有云服务器补齐公网IP",
                "仅补齐所选地区已有云服务器缺少的公网IP，不创建新云服务器。",
                f"新购公网IP将过滤当前旧IP库 {len(used_ip_filter)} 条记录，合格后绑定到缺少公网IP的私网IP。",
                "随机新IP连续3次命中旧IP库时停止该地区，避免浪费次数和费用。",
                "余额要求：官方规定购买公网IP资源时，账户可用余额需≥100元，请确认余额及本次费用充足。",
                f"每日公网IP购买上限：每账号每地区 {VOLC_EIP_DAILY_PURCHASE_LIMIT} 次",
                f"今日购买记录：{daily_status_text}",
            ]
        else:
            details = [
                "购买信息：",
                f"购买选项：{self._volc_config_choice_label(config_key)}",
                f"购买规格：{spec.get('label') or spec_key}",
                f"计费方式：{self._volc_purchase_billing_label()}",
                "余额要求：官方规定购买公网IP资源时，账户可用余额需≥100元，请确认余额及本次费用充足。",
                "任务开始后会在实际新购公网IP前校验余额；不足100元将停止任务并弹窗提醒充值。",
                f"每日公网IP购买上限：每账号每地区 {VOLC_EIP_DAILY_PURCHASE_LIMIT} 次",
                f"今日购买记录：{daily_status_text}",
                *self._volc_build_confirm_details(build_enabled=build_after),
                self._volc_task_popup_details(
                    plans,
                    purchase_spec=str(spec.get("label") or spec_key),
                    build_enabled=build_after,
                ),
            ]
        if not self.ask_confirm_cancel(
            self._purchase_confirm_message(
                action_name,
                plans,
                details=details,
            )
        ):
            self.log(f"已取消火山引擎{action_name}。", task_context)
            return
        if not self._begin_volc_task(
            "火山引擎购买搭建中" if build_after else ("火山引擎购买选择地区新IP中" if new_ip_only else "火山引擎购买中"),
            context=task_context,
            regions=[plan.region for plan in plans],
        ):
            return
        if new_ip_only:
            self._volc_new_ip_result_warning = ""
            self._volc_new_ip_result_regions = set()
            self._volc_pending_result_display_regions = set()
            self.log(f"购买选择地区新IP：旧IP库过滤 {len(used_ip_filter)} 条，仅复用已有云服务器。", task_context)
        else:
            self.log(f"自动购买映射: {VOLC_AUTOBUY_SOURCE_MODES.get(config_key, config['label'])}", task_context)
        existing_build_ips = self._collect_region_ips_from_rows(plans) if build_after else {}
        for plan in plans:
            flavor_label = spec["label"]
            task_status = "购买新IP中" if new_ip_only else "购买中"
            self.after(0, lambda p=plan, f=flavor_label, s=task_status: self._set_region_values(p, host_status=s, server_ips=[], flavor=f))

        def worker() -> None:
            success = 0
            failed = 0
            build_success = 0
            build_failed = 0
            balance_blocked = False
            purchase_only_success_regions: List[str] = []
            try:
                purchase_results = self._run_volc_purchase_for_plans(
                    plans,
                    group_count,
                    new_ip_only=new_ip_only,
                    used_ip_filter=sorted(used_ip_filter),
                    selected_groups_by_region=selected_groups_by_region,
                )
            except Exception as exc:
                failed = len(plans)
                for plan in plans:
                    self.after(0, self.log, f"[{plan.label}] 购买失败: {exc}")
                    self.after(0, lambda p=plan: self._set_region_values(p, host_status="购买失败"))
            else:
                balance_blocked = any(code == 2 for code, _ips in purchase_results.values())
                ready_for_auto_build = True
                if build_after and any(code == 0 and ips for code, ips in purchase_results.values()):
                    ready_for_auto_build = self._wait_for_volc_purchase_build_ssh_ready()
                for plan in plans:
                    code, ips = purchase_results.get(plan.region, (1, []))
                    if code != 0:
                        failed += 1
                        reason = self._volc_purchase_exit_message(code)
                        detail = self.volc_purchase_failure_reasons.get(plan.region) or next(iter(self.volc_purchase_failure_reasons.values()), "")
                        full_reason = f"{reason}；{detail}" if detail else reason
                        status_text = "旧ECS需清理" if "旧 ECS" in detail else (f"新IP购买失败({code})" if new_ip_only else f"购买失败({code})")
                        failure_action = "购买新IP失败" if new_ip_only else "购买失败"
                        self.after(0, self.log, f"[{plan.label}] {failure_action}：{full_reason}（退出码={code}）。")
                        self.after(0, lambda p=plan, s=status_text, ip_list=list(ips): self._set_region_values(p, host_status=s, server_ips=ip_list))
                        continue
                    success += 1
                    if not build_after and not new_ip_only:
                        purchase_only_success_regions.append(plan.region)
                    if new_ip_only:
                        try:
                            refreshed_items = self._query_volc_region_resources(plan, ak, sk)
                            bound_ips: List[str] = []
                            current_ip_groups: Dict[int, List[str]] = {}
                            for item in refreshed_items:
                                raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
                                public_ip = self._normalize_used_ip_value(item.get("_public_ip"))
                                if (
                                    item.get("_kind") != "eip"
                                    or not self._volc_eip_associated_instance_id(raw)
                                    or not public_ip
                                ):
                                    continue
                                if public_ip not in bound_ips:
                                    bound_ips.append(public_ip)
                                group_index = self._volc_manage_item_group_index(dict(item))
                                group_ips = current_ip_groups.setdefault(group_index, [])
                                if public_ip not in group_ips:
                                    group_ips.append(public_ip)
                            _path, added_count, _all_used_ips = self._append_used_ip_table(bound_ips, region_label=plan.label)
                            self.after(0, self.log, f"[{plan.label}] 新IP购买绑定完成，旧IP库新增 {added_count} 条已绑定IP。")
                            if current_ip_groups:
                                self.after(
                                    0,
                                    self._sync_volc_results_after_new_ip_purchase,
                                    plan,
                                    {group_index: list(group_ips) for group_index, group_ips in current_ip_groups.items()},
                                    task_context,
                                )
                        except Exception as exc:
                            self.after(0, self.log, f"[{plan.label}] 新IP已购买绑定，但同步旧IP库失败: {exc}")
                    self.volc_region_server_ips[plan.region] = ips
                    success_status = "新IP购买完成" if new_ip_only else "购买完成"
                    self.after(0, lambda p=plan, ip_list=list(ips), s=success_status: self._set_region_values(p, host_status=s, server_ips=ip_list))
                    if build_after and ips:
                        if not ready_for_auto_build:
                            build_failed += 1
                            self.after(0, lambda p=plan: self._set_region_values(p, host_status="搭建已停止"))
                            continue
                        previous_ips = existing_build_ips.get(plan.region, [])
                        is_additional_server = bool(previous_ips) and any(ip not in previous_ips for ip in ips)
                        self.after(0, self.log, f"[{plan.label}] 购买确认完成，开始 SSH 搭建并收集 L2TP/Socks5 输出。")
                        socks_lines, l2tp_lines, built_ecs_count = self._build_ips_for_plan(plan, ips)
                        if socks_lines and l2tp_lines and built_ecs_count >= len(ips):
                            if is_additional_server:
                                current_socks = list(self.volc_region_socks.get(plan.region, []))
                                current_l2tp = list(self.volc_region_l2tp.get(plan.region, []))
                                self._append_unique_output_lines(current_socks, socks_lines)
                                self._append_unique_output_lines(current_l2tp, l2tp_lines)
                                self.volc_region_socks[plan.region] = current_socks
                                self.volc_region_l2tp[plan.region] = current_l2tp
                            else:
                                self.volc_region_socks[plan.region] = list(dict.fromkeys(socks_lines))
                                self.volc_region_l2tp[plan.region] = list(dict.fromkeys(l2tp_lines))
                            build_success += 1
                            self.after(
                                0,
                                lambda p=plan, ip_list=list(ips), label=str(spec.get("label") or ""): self._record_volc_purchase_resources(
                                    p,
                                    ip_list,
                                    label,
                                    status="购买并搭建完成",
                                ),
                            )
                            self.after(0, lambda p=plan: self._set_region_values(p, host_status="购买并搭建完成"))
                            self.after(0, self._refresh_volc_outputs)
                            self.after(0, self.log, f"[{plan.label}] 购买并搭建完成，已刷新结果框。")
                        else:
                            build_failed += 1
                            self.after(
                                0,
                                lambda p=plan, ip_list=list(ips), label=str(spec.get("label") or ""): self._record_volc_purchase_resources(
                                    p,
                                    ip_list,
                                    label,
                                    status="搭建失败",
                                ),
                            )
                            self.after(0, lambda p=plan: self._set_region_values(p, host_status="搭建失败"))
                            self.after(0, self.log, f"[{plan.label}] 购买已完成，但未收集到搭建输出，已标记为搭建失败。")
                    elif not new_ip_only:
                        self.after(
                            0,
                            lambda p=plan, ip_list=list(ips), label=str(spec.get("label") or ""): self._record_volc_purchase_resources(p, ip_list, label),
                        )
            if purchase_only_success_regions:
                regions_to_clear = list(purchase_only_success_regions)
                self.after(
                    0,
                    lambda regions=regions_to_clear: self._clear_volc_outputs_for_regions(
                        regions,
                        context=task_context,
                        reason="购买任务仅更新云资源，已清除本次成功地区的旧搭建结果。",
                    ),
                )
            self.after(0, self._finish_volc_task, "就绪", task_context)
            if build_after:
                self.volc_rebuilding_output_regions.difference_update(plan.region for plan in plans)
                self.after(0, self._reset_one_time_build_config_after_task)
            purchase_spec = (
                "旧IP库过滤，按已有云服务器补齐公网IP"
                if new_ip_only
                else f"{spec.get('label') or spec_key}，{config.get('label') or self._volc_config_choice_label(config_key)}"
            )
            if build_after:
                total_failed = failed + build_failed
                message = f"火山引擎购买并搭建任务结束，购买成功 {success}/{len(plans)} 个地区，搭建完成 {build_success}/{len(plans)} 个地区，失败 {total_failed}。"
            elif new_ip_only:
                message = f"火山引擎购买选择地区新IP任务结束，成功 {success}/{len(plans)} 个地区，失败 {failed}。"
            else:
                message = f"火山引擎购买任务结束，购买成功 {success}/{len(plans)} 个地区，失败 {failed}。"
            message += self._volc_task_popup_details(plans, purchase_spec=purchase_spec, build_enabled=build_after)
            failure_details = sorted({detail for detail in self.volc_purchase_failure_reasons.values() if detail})
            if failure_details and (failed or build_failed):
                message = f"{message}\n失败原因：{'；'.join(failure_details)}"
            self.after(0, self.log, message, task_context)
            if balance_blocked:
                self.after(
                    100,
                    lambda base_message=message, successful_count=success: self.show_info(
                        self._volc_new_ip_result_popup_message(
                            base_message,
                            success=successful_count,
                            warning=getattr(self, "_volc_new_ip_result_warning", ""),
                        )
                        if new_ip_only
                        else base_message
                    ),
                )
            else:
                self.after(
                    300,
                    lambda p=list(plans), final_message=message, successful_count=success: self.scan_resources(
                        context=task_context,
                        plans_override=p,
                        merge_with_cache=True,
                        final_popup_message=(
                            self._volc_new_ip_result_popup_message(
                                final_message,
                                success=successful_count,
                                warning=getattr(self, "_volc_new_ip_result_warning", ""),
                            )
                            if new_ip_only
                            else final_message
                        ),
                    ),
                )

        threading.Thread(target=worker, daemon=True).start()

    def scan_resources(
        self,
        context: str = "purchase",
        plans_override: Optional[Sequence[RegionPlan]] = None,
        merge_with_cache: bool = False,
        final_popup_message: Optional[str] = None,
    ) -> None:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        if not ak or not sk:
            self.log("请先填写或粘贴火山引擎 AK/SK。", context)
            return
        guard_message = self._customer_guard_block_message(ak, sk)
        if guard_message:
            if guard_message == CUSTOMER_OWNERSHIP_CHECKING_MESSAGE:
                self.customer_guard_pending_resource_query = (
                    (),
                    {
                        "context": context,
                        "plans_override": plans_override,
                        "merge_with_cache": merge_with_cache,
                        "final_popup_message": final_popup_message,
                    },
                )
            self._show_customer_guard_block(guard_message, context)
            return
        plans = list(VOLC_REGION_PLANS if plans_override is None else plans_override)
        if not plans:
            self.log("未识别到需要刷新的火山引擎地区，已跳过自动刷新资源。", context)
            if final_popup_message:
                self.show_info(final_popup_message)
            self._mark_customer_resource_query_ready()
            return
        if not self._begin_volc_task(
            "火山引擎读取账号资源中",
            context=context,
            affects_global_busy=False,
            regions=[plan.region for plan in plans],
            risk_group=READ_RESOURCE_RISK_GROUP,
        ):
            return
        self.log(f"开始读取火山引擎资源，地区={ '、'.join(plan.label for plan in plans) }。", context)
        if not merge_with_cache:
            self.populate_volc_manage_tree([])
        for plan in plans:
            self._set_region_values(plan, host_status="读取中", server_ips=[], flavor="")

        def worker() -> None:
            all_items: List[Dict[str, Any]] = []
            try:
                identity_executor = ThreadPoolExecutor(max_workers=1)
                identity_future = identity_executor.submit(self._volc_check_identity, ak, sk)
                try:
                    identity_future.result(timeout=VOLC_IDENTITY_TIMEOUT)
                except concurrent.futures.TimeoutError:
                    identity_future.cancel()
                    self.after(0, self.log, f"火山引擎身份校验超过{VOLC_IDENTITY_TIMEOUT:.0f}秒，已停止本次读取，请检查网络或 AK/SK 权限。")
                    for plan in plans:
                        self.after(0, lambda p=plan: self._set_region_values(p, host_status="身份校验超时"))
                    self.after(0, self.apply_volc_resource_refresh, [], [plan.region for plan in plans], False)
                    self.after(0, self._finish_volc_task, "火山引擎资源读取失败", context, False)
                    if final_popup_message:
                        self.after(0, self.show_info, final_popup_message)
                    return
                except Exception as exc:
                    detail = self._volc_exception_text(exc)
                    self.after(0, self.log, f"火山引擎身份校验失败: {detail}")
                    if self._volc_auth_error(exc):
                        for plan in plans:
                            self.after(0, lambda p=plan: self._set_region_values(p, host_status="AK/SK鉴权失败"))
                        self.after(0, self.apply_volc_resource_refresh, [], [plan.region for plan in plans], False)
                        self.after(0, self._finish_volc_task, "火山引擎资源读取失败", context, False)
                        if final_popup_message:
                            self.after(0, self.show_info, final_popup_message)
                        return
                    self.after(0, self.log, "身份校验未通过但不阻断，继续尝试资源接口。")
                finally:
                    identity_executor.shutdown(wait=False, cancel_futures=True)
                executor = ThreadPoolExecutor(max_workers=max(1, len(plans)))
                futures = {executor.submit(self._query_volc_region_resources, plan, ak, sk): plan for plan in plans}
                try:
                    pending = set(futures.keys())
                    try:
                        completed_iter = as_completed(list(pending), timeout=VOLC_RESOURCE_SCAN_TIMEOUT)
                        for future in completed_iter:
                            pending.discard(future)
                            plan = futures[future]
                            try:
                                region_items = future.result()
                            except Exception as exc:
                                self.after(0, self.log, f"[{plan.label}] 资源读取失败: {self._volc_exception_text(exc)}")
                                self.after(0, lambda p=plan: self._set_region_values(p, host_status="读取失败"))
                                continue
                            all_items.extend(region_items)
                            self.after(0, self.apply_volc_resource_refresh, region_items, [plan.region], True)
                    except concurrent.futures.TimeoutError:
                        pass
                    for future in pending:
                        plan = futures[future]
                        future.cancel()
                        self.after(0, self.log, f"[{plan.label}] 资源读取超过{VOLC_RESOURCE_SCAN_TIMEOUT:.0f}秒，已停止等待。")
                        self.after(0, lambda p=plan: self._set_region_values(p, host_status="读取超时"))
                finally:
                    executor.shutdown(wait=False, cancel_futures=True)
            except Exception as exc:
                self.after(0, self.log, f"火山引擎资源读取异常: {self._volc_exception_text(exc)}")
                self.after(0, self.apply_volc_resource_refresh, all_items, [plan.region for plan in plans], False)
                self.after(0, self._finish_volc_task, "火山引擎资源读取失败", context, False)
                if final_popup_message:
                    self.after(0, self.show_info, final_popup_message)
                return
            self.after(0, self.apply_volc_resource_refresh, all_items, [plan.region for plan in plans], True)
            self.after(0, self.log, f"火山引擎资源读取完成，共 {len(all_items)} 条。", context)
            self.after(0, self._finish_volc_task, "火山引擎资源读取完成", context, False)
            if final_popup_message:
                self.after(0, self.show_info, final_popup_message)
            self.after(50, self._mark_customer_resource_query_ready)

        threading.Thread(target=worker, daemon=True).start()

    def purchase_selected_regions(self) -> None:
        self._start_volc_purchase_task(build_after=False)

    def purchase_selected_region_new_ips(
        self,
        selected_regions: Optional[Sequence[str]] = None,
        task_context: str = "purchase",
    ) -> None:
        self._start_volc_purchase_task(
            build_after=False,
            new_ip_only=True,
            selected_regions=selected_regions,
            task_context=task_context,
        )

    def purchase_and_build_selected_regions(self) -> None:
        self._start_volc_purchase_task(build_after=True)

    def reinstall_selected_manage_servers(self) -> None:
        if self._standalone_limited_features():
            return
        items = self._selected_volc_manage_items(kinds={"ecs"})
        if not items:
            self.log("请先在资源管理选择要重装的火山引擎云服务器。", "manage")
            return
        if not self.ask_confirm_cancel(self._volc_manage_confirm_message("重装云服务器", items)):
            self.log("已取消火山引擎重装云服务器。", "manage")
            return
        self._run_volc_resource_action(
            "重装云服务器",
            items,
            self._reinstall_volc_resource_item,
            max_workers=3,
            context="manage",
            notify_done=True,
            stop_queue_on_failure=True,
            success_callback=lambda successful_items: self._clear_volc_outputs_for_region_groups(
                self._volc_region_groups_for_items(successful_items),
                context="manage",
            ),
        )

    def reinstall_selected_servers(self) -> None:
        selected_groups_by_region = self._selected_purchase_region_groups()
        items = [
            dict(item)
            for item in self.volc_cached_resource_items
            if item.get("_kind") == "ecs"
            and self._volc_manage_item_group_index(dict(item))
            in selected_groups_by_region.get(str(item.get("_region") or ""), set())
        ]
        if not items:
            self.log("请先在购买搭建选择要重装的火山引擎云服务器。", "purchase")
            return
        if not self.ask_confirm_cancel(self._volc_manage_confirm_message("重装云服务器", items)):
            self.log("已取消火山引擎重装云服务器。", "purchase")
            return
        self._run_volc_resource_action(
            "重装云服务器",
            items,
            self._reinstall_volc_resource_item,
            max_workers=3,
            context="purchase",
            notify_done=True,
            stop_queue_on_failure=True,
            success_callback=lambda successful_items: self._clear_volc_outputs_for_region_groups(
                self._volc_region_groups_for_items(successful_items),
                context="purchase",
            ),
        )

    def unsubscribe_selected_resources(self) -> None:
        selected_items = self._selected_volc_manage_items()
        items: List[Dict[str, Any]] = []
        for item in selected_items:
            kind = str(item.get("_kind") or "")
            operation = str(item.get("_operation_kind") or "")
            if kind in {"ecs", "eip"}:
                items.append(item)
            elif kind == "network_interface":
                items.append(item)
            elif kind == "security_group":
                # 用户同时勾选网卡和安全组时，即使安全组当前显示“有绑定”，也必须进入统一删除队列；
                # 队列会先删除/解绑网卡，再删除安全组，避免安全组被提前过滤导致遗留。
                items.append(item)
            elif kind in {"subnet", "vpc"}:
                # 用户选中 VPC/子网时先进入统一队列；队列会补齐同 VPC 内 ENI、安全组、子网后再删 VPC。
                items.append(item)
        if not items:
            self.log("请先选择要退订/删除/释放的火山引擎资源。", "manage")
            return
        release_ip_groups = self._volc_release_ip_groups(items)
        release_action = self._ask_cleanup_release_action(
            self._volc_manage_confirm_message("退订/删除/释放资源", items),
            allow_used_ip_import=bool(release_ip_groups),
        )
        if release_action == "cancel":
            self.log("已取消火山引擎退订/删除资源。", "manage")
            return
        if release_action == "confirm_and_import":
            try:
                self._import_volc_release_ip_groups(release_ip_groups)
            except Exception as exc:
                message = f"导入本次待释放IP失败，已停止释放资源：{exc}"
                self.log(message, "manage")
                self.show_info(message)
                return
        self._run_volc_delete_items("退订/删除/释放资源", items)

    def _used_ip_table_account_label(self) -> str:
        username = self._volc_clean_account_mark(self.mark_var.get())
        account_scope = self._account_log_scope()
        if username and account_scope:
            return f"{username}-{account_scope}"
        return username or account_scope or "账号-未识别"

    def _used_ip_table_path(self) -> Path:
        self._used_ip_table_migration_error = ""
        linked_path = self._linked_used_ip_table_path()
        if linked_path is not None:
            if linked_path.suffix.lower() == ".csv":
                try:
                    return self._migrate_used_ip_csv_to_xlsx(linked_path, update_link=True)
                except Exception as exc:
                    self._used_ip_table_migration_error = str(exc)
            return linked_path
        default_path = self._default_used_ip_table_path()
        if default_path.exists():
            return default_path
        legacy_csv_path = default_path.with_suffix(".csv")
        if legacy_csv_path.exists():
            try:
                return self._migrate_used_ip_csv_to_xlsx(legacy_csv_path)
            except Exception as exc:
                self._used_ip_table_migration_error = str(exc)
                return legacy_csv_path
        return default_path

    def _default_used_ip_table_path(self) -> Path:
        filename = safe_log_name(self._used_ip_table_account_label()) + ".xlsx"
        return cloud_data_dir("Volcengine") / "used_ip_tables" / filename

    def _migrate_used_ip_csv_to_xlsx(self, csv_path: Path, *, update_link: bool = False) -> Path:
        source_path = Path(csv_path).expanduser()
        if source_path.suffix.lower() != ".csv":
            return source_path
        target_path = source_path.with_suffix(".xlsx")
        with self._used_ip_table_file_guard(target_path):
            source_rows = self._read_used_ip_csv_rows(source_path)
            existing_rows = self._read_used_ip_csv_rows(target_path)
            merged_rows = self._dedupe_used_ip_csv_rows([*existing_rows, *source_rows])
            failed_rows = self._read_failed_ip_rows(target_path)
            self._write_used_ip_csv_rows(target_path, merged_rows, failed_rows=failed_rows)
        if update_link:
            self._set_linked_used_ip_table_path(target_path)
        self._used_ip_table_migration_error = ""
        return target_path

    def _used_ip_table_link_config_path(self) -> Path:
        filename = safe_log_name(self._used_ip_table_account_label()) + ".link"
        return cloud_data_dir("Volcengine") / "used_ip_tables" / filename

    def _linked_used_ip_table_path(self) -> Optional[Path]:
        config_path = self._used_ip_table_link_config_path()
        if not config_path.exists():
            return None
        try:
            text = config_path.read_text(encoding="utf-8").strip().strip('"')
        except Exception:
            return None
        if not text:
            return None
        path = Path(text).expanduser()
        if path.suffix.lower() not in {".csv", ".xlsx"}:
            return None
        return path

    def _set_linked_used_ip_table_path(self, path: Path) -> None:
        target = Path(path).expanduser()
        if target.suffix.lower() not in {".csv", ".xlsx"}:
            raise ValueError("旧IP库必须为 .csv 或 .xlsx 文件。")
        config_path = self._used_ip_table_link_config_path()
        config_path.parent.mkdir(parents=True, exist_ok=True)
        config_path.write_text(str(target), encoding="utf-8")

    def _ask_used_ip_csv_path(self, parent: tk.Misc, *, mode: str, title: str, initialfile: str = "") -> str:
        command = "tk_getSaveFile" if mode == "save" else "tk_getOpenFile"
        filetypes = (
            (("Excel文件", "*.xlsx"), ("所有文件", "*.*"))
            if mode == "save"
            else (("Excel文件", "*.xlsx"), ("CSV文件（将自动升级）", "*.csv"), ("所有文件", "*.*"))
        )
        options: List[Any] = [
            command,
            "-parent",
            str(parent),
            "-title",
            title,
            "-filetypes",
            filetypes,
        ]
        if mode == "save":
            options.extend(["-defaultextension", ".xlsx"])
            if initialfile:
                options.extend(["-initialfile", initialfile])
        try:
            return str(self.tk.call(*options) or "").strip()
        except Exception as exc:
            self.show_info(f"打开文件选择窗口失败：{exc}")
            return ""

    def _show_used_ip_table_config_dialog(self, _event: Optional[tk.Event] = None) -> str:
        if not self._allow_sensitive_config_dialog():
            return "break"
        win = tk.Toplevel(self)
        win.title(cloud_dialog_title("更换新IP配置"))
        win.resizable(False, False)
        apply_dialog_icon(win)
        win.transient(self.winfo_toplevel())
        win.grab_set()
        win.columnconfigure(0, weight=1)
        win.rowconfigure(0, weight=1)
        body = ttk.Frame(win, padding=12)
        body.grid(row=0, column=0, sticky="nsew")
        body.columnconfigure(1, weight=1)

        path_var = tk.StringVar(value=str(self._used_ip_table_path()))
        status_var = tk.StringVar(value="")

        def close_dialog() -> None:
            try:
                win.grab_release()
            except tk.TclError:
                pass
            try:
                win.destroy()
            except tk.TclError:
                pass

        def ask_csv_path(*, mode: str, title: str, initialfile: str = "") -> str:
            try:
                win.grab_release()
            except tk.TclError:
                pass
            try:
                return self._ask_used_ip_csv_path(win, mode=mode, title=title, initialfile=initialfile)
            finally:
                try:
                    if win.winfo_exists():
                        win.deiconify()
                        win.lift(self.winfo_toplevel())
                        win.grab_set()
                except tk.TclError:
                    pass

        def set_status(message: str) -> None:
            status_var.set(message)
            if message:
                status_label.grid()
            else:
                status_label.grid_remove()

            def resize_dialog() -> None:
                try:
                    if win.winfo_exists():
                        self._center_dialog(win, min_width=0, min_height=0)
                except tk.TclError:
                    pass

            win.after_idle(resize_dialog)

        ttk.Label(body, text="当前旧IP库").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=(0, 8))
        ttk.Label(body, textvariable=path_var, wraplength=520, justify=tk.LEFT).grid(
            row=0,
            column=1,
            sticky="ew",
            pady=(0, 8),
        )
        ttk.Label(
            body,
            text="旧IP库用于避开已使用IP来获取新IP，IP库务必准确填写格式，否则出错影响账号！",
            wraplength=620,
            justify=tk.LEFT,
        ).grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 10))

        file_actions = ttk.Frame(body)
        file_actions.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(2, 0))
        file_actions.columnconfigure(0, weight=1, uniform="used_ip_actions")
        file_actions.columnconfigure(1, weight=1, uniform="used_ip_actions")

        def save_current_template() -> None:
            default_name = safe_log_name(self._used_ip_table_account_label()) + ".xlsx"
            target = ask_csv_path(mode="save", title="保存当前旧IP库(模板)", initialfile=default_name)
            if not target:
                return
            path = Path(target).expanduser()
            if path.suffix.lower() != ".xlsx":
                set_status("保存失败：旧IP库请使用 .xlsx 格式，CSV无法保存更换失败IP工作表。")
                return
            try:
                source_path = self._used_ip_table_path()
                with self._used_ip_table_file_guard(path):
                    source_rows = self._read_used_ip_csv_rows(source_path)
                    source_failed_rows = self._read_failed_ip_rows(source_path)
                    target_rows = self._dedupe_used_ip_csv_rows(self._read_used_ip_csv_rows(path))
                    target_failed_rows = self._read_failed_ip_rows(path)
                    original_count = len(target_rows)
                    rows = self._dedupe_used_ip_csv_rows([*target_rows, *source_rows])
                    failed_rows = self._dedupe_used_ip_csv_rows([*target_failed_rows, *source_failed_rows])
                    rows, added_count, updated_count = self._merge_used_ip_rows(
                        rows,
                        self._current_account_public_ip_groups_for_used_table(),
                    )
                    merged_count = max(0, len(rows) - original_count)
                    self._write_used_ip_csv_rows(path, rows, failed_rows=failed_rows)
                self._set_linked_used_ip_table_path(path)
            except Exception as exc:
                set_status(f"保存失败：{exc}")
                return
            path_var.set(str(path))
            set_status(
                f"已合并保存并链接：{path}，原有 {original_count} 条，新增 {merged_count} 条，"
                f"补齐已有记录 {updated_count} 条，共 {len(rows)} 条。"
            )

        def link_existing_table() -> None:
            target = ask_csv_path(mode="open", title="链接旧IP库并合并")
            if not target:
                return
            path = Path(target).expanduser()
            if path.suffix.lower() not in {".csv", ".xlsx"}:
                set_status("链接失败：旧IP库必须为 .csv 或 .xlsx 文件。")
                return
            try:
                if path.suffix.lower() == ".csv":
                    active_path = self._migrate_used_ip_csv_to_xlsx(path)
                else:
                    active_path = path
                with self._used_ip_table_file_guard(active_path):
                    rows = self._read_used_ip_csv_rows(active_path)
                    failed_rows = self._read_failed_ip_rows(active_path)
                    original_count = len(rows)
                    rows, added_count, updated_count = self._merge_used_ip_rows(
                        rows,
                        self._current_account_public_ip_groups_for_used_table(),
                    )
                    self._write_used_ip_csv_rows(active_path, rows, failed_rows=failed_rows)
                self._set_linked_used_ip_table_path(active_path)
            except Exception as exc:
                set_status(f"链接失败：{exc}")
                return
            path_var.set(str(active_path))
            migrated_text = "，CSV已自动升级为Excel并保留原文件" if path.suffix.lower() == ".csv" else ""
            set_status(
                f"已链接并合并旧IP库：原有 {original_count} 条，新增当前账号已绑定IP {added_count} 条，"
                f"补齐记录 {updated_count} 条，共 {len(rows)} 条{migrated_text}。"
            )

        ttk.Button(file_actions, text="保存当前旧IP库(模板)", command=save_current_template).grid(
            row=0, column=0, sticky="ew", padx=(0, 6)
        )
        ttk.Button(file_actions, text="链接旧IP库并合并", command=link_existing_table).grid(
            row=0, column=1, sticky="ew", padx=(6, 0)
        )
        status_label = ttk.Label(body, textvariable=status_var, wraplength=620, justify=tk.LEFT)
        status_label.grid(
            row=3,
            column=0,
            columnspan=2,
            sticky="w",
            pady=(10, 0),
        )
        status_label.grid_remove()

        win.protocol("WM_DELETE_WINDOW", close_dialog)
        win.bind("<Escape>", lambda _e: close_dialog())
        self._center_dialog(win, min_width=0, min_height=0)
        return "break"

    def _used_ip_table_account_value(self) -> str:
        username = self._volc_clean_account_mark(self.mark_var.get())
        account_scope = re.sub(r"^账号-", "", self._account_log_scope())
        if username and account_scope:
            return f"{username}{account_scope}"
        return username or account_scope or self._used_ip_table_account_label()

    @staticmethod
    def _normalize_used_ip_value(value: Any) -> str:
        text = str(value or "").strip()
        try:
            parsed = str(ipaddress.IPv4Address(text))
        except Exception:
            return ""
        return parsed if is_public_ip(parsed) else ""

    @staticmethod
    def _used_ip_csv_row(timestamp: Any, account_value: Any, region_label: Any, ip: Any) -> Dict[str, str]:
        return {
            USED_IP_CSV_HEADERS[0]: str(timestamp or "").strip(),
            USED_IP_CSV_HEADERS[1]: str(account_value or "").strip(),
            USED_IP_CSV_HEADERS[2]: str(region_label or "").strip(),
            USED_IP_CSV_HEADERS[3]: str(ip or "").strip(),
        }

    def _used_ip_csv_account_from_row(self, row: Dict[str, Any]) -> str:
        account_value = str(row.get(USED_IP_CSV_HEADERS[1]) or "").strip()
        if account_value:
            return account_value
        username = str(row.get("username") or "").strip()
        account_scope = re.sub(r"^账号-", "", str(row.get("account_scope") or "").strip())
        if username and account_scope:
            return f"{username}{account_scope}"
        return username or account_scope

    def _read_used_ip_csv_rows(self, path: Path) -> List[Dict[str, str]]:
        rows: List[Dict[str, str]] = []
        if not path.exists():
            return rows
        if path.suffix.lower() == ".xlsx":
            for row in read_excel_table_rows(path, USED_IP_CSV_HEADERS):
                value = self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]) or row.get("IP") or row.get("ip"))
                if not value:
                    continue
                rows.append(
                    self._used_ip_csv_row(
                        row.get(USED_IP_CSV_HEADERS[0]) or row.get("time"),
                        self._used_ip_csv_account_from_row(row),
                        row.get(USED_IP_CSV_HEADERS[2]) or row.get("region"),
                        value,
                    )
                )
            return rows
        try:
            with path.open("r", encoding="utf-8-sig", newline="") as handle:
                reader = csv.DictReader(handle)
                fieldnames = [str(name or "").strip() for name in (reader.fieldnames or [])]
                if fieldnames and any(name.lower() == "ip" for name in fieldnames):
                    for row in reader:
                        value = self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]) or row.get("IP") or row.get("ip"))
                        if not value:
                            continue
                        rows.append(
                            self._used_ip_csv_row(
                                row.get(USED_IP_CSV_HEADERS[0]) or row.get("time"),
                                self._used_ip_csv_account_from_row(row),
                                row.get(USED_IP_CSV_HEADERS[2]) or row.get("region"),
                                value,
                            )
                        )
                    return rows
                handle.seek(0)
                for raw_row in csv.reader(handle):
                    for cell in raw_row:
                        value = self._normalize_used_ip_value(cell)
                        if value:
                            rows.append(self._used_ip_csv_row("", "", "", value))
        except Exception:
            return rows
        return rows

    def _read_failed_ip_rows(self, path: Path) -> List[Dict[str, str]]:
        if not path.exists() or path.suffix.lower() != ".xlsx":
            return []
        rows: List[Dict[str, str]] = []
        for row in read_excel_table_rows(path, USED_IP_CSV_HEADERS, worksheet_name=FAILED_IP_WORKSHEET_NAME):
            value = self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]) or row.get("IP") or row.get("ip"))
            if not value:
                continue
            rows.append(
                self._used_ip_csv_row(
                    row.get(USED_IP_CSV_HEADERS[0]) or row.get("time"),
                    self._used_ip_csv_account_from_row(row),
                    row.get(USED_IP_CSV_HEADERS[2]) or row.get("region"),
                    value,
                )
            )
        return self._dedupe_used_ip_csv_rows(rows)

    def _used_ip_table_guard(self) -> threading.RLock:
        lock = getattr(self, "used_ip_table_lock", None)
        if lock is None:
            lock = threading.RLock()
            self.used_ip_table_lock = lock
        return lock

    @contextmanager
    def _used_ip_table_file_guard(self, path: Path) -> Iterator[None]:
        target = Path(path).expanduser()
        target.parent.mkdir(parents=True, exist_ok=True)
        lock_path = target.with_suffix(target.suffix + ".lock")
        deadline = time.monotonic() + 8.0
        lock_fd: Optional[int] = None
        with self._used_ip_table_guard():
            while lock_fd is None:
                try:
                    lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
                except FileExistsError:
                    try:
                        if time.time() - lock_path.stat().st_mtime > 30.0:
                            lock_path.unlink()
                            continue
                    except FileNotFoundError:
                        continue
                    if time.monotonic() >= deadline:
                        raise RuntimeError("旧IP库正在被其他账号窗口写入，请稍后重试。")
                    time.sleep(0.05)
            try:
                os.write(lock_fd, str(os.getpid()).encode("ascii", errors="ignore"))
                yield
            finally:
                try:
                    os.close(lock_fd)
                finally:
                    try:
                        lock_path.unlink()
                    except FileNotFoundError:
                        pass

    def _dedupe_used_ip_csv_rows(self, rows: Sequence[Dict[str, str]]) -> List[Dict[str, str]]:
        deduped: List[Dict[str, str]] = []
        rows_by_ip: Dict[str, Dict[str, str]] = {}
        for row in rows:
            ip = self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]) or row.get("IP") or row.get("ip"))
            if not ip:
                continue
            normalized_row = self._used_ip_csv_row(
                row.get(USED_IP_CSV_HEADERS[0]),
                row.get(USED_IP_CSV_HEADERS[1]),
                row.get(USED_IP_CSV_HEADERS[2]),
                ip,
            )
            existing_row = rows_by_ip.get(ip)
            if existing_row is None:
                rows_by_ip[ip] = normalized_row
                deduped.append(normalized_row)
                continue
            for header in USED_IP_CSV_HEADERS[:3]:
                if not existing_row.get(header) and normalized_row.get(header):
                    existing_row[header] = normalized_row[header]
        return deduped

    def _merge_used_ip_rows(
        self,
        rows: Sequence[Dict[str, str]],
        region_ip_groups: Dict[str, Sequence[str]],
    ) -> Tuple[List[Dict[str, str]], int, int]:
        merged = self._dedupe_used_ip_csv_rows(rows)
        rows_by_ip = {row[USED_IP_CSV_HEADERS[3]]: row for row in merged}
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        account_value = self._used_ip_table_account_value()
        added_count = 0
        updated_count = 0
        for region_label, ips in region_ip_groups.items():
            for raw_ip in ips:
                ip = self._normalize_used_ip_value(raw_ip)
                if not ip:
                    continue
                existing_row = rows_by_ip.get(ip)
                if existing_row is None:
                    new_row = self._used_ip_csv_row(timestamp, account_value, region_label, ip)
                    merged.append(new_row)
                    rows_by_ip[ip] = new_row
                    added_count += 1
                    continue
                changed = False
                for header, value in zip(
                    USED_IP_CSV_HEADERS[:3],
                    (timestamp, account_value, str(region_label or "").strip()),
                ):
                    if not existing_row.get(header) and value:
                        existing_row[header] = value
                        changed = True
                if changed:
                    updated_count += 1
        return merged, added_count, updated_count

    def _write_used_ip_csv_rows(
        self,
        path: Path,
        rows: Sequence[Dict[str, str]],
        *,
        failed_rows: Optional[Sequence[Dict[str, str]]] = None,
    ) -> None:
        with self._used_ip_table_guard():
            path.parent.mkdir(parents=True, exist_ok=True)
            normalized = self._dedupe_used_ip_csv_rows(rows)
            if path.suffix.lower() == ".xlsx":
                normalized_failed = self._dedupe_used_ip_csv_rows(
                    self._read_failed_ip_rows(path) if failed_rows is None else failed_rows
                )
                try:
                    written = write_excel_view_file(
                        path,
                        USED_IP_CSV_HEADERS,
                        normalized,
                        column_widths=USED_IP_TABLE_COLUMN_WIDTHS,
                        target_path=path,
                        side_note=USED_IP_TABLE_NOTE,
                        extra_sheets=[
                            {
                                "name": FAILED_IP_WORKSHEET_NAME,
                                "headers": USED_IP_CSV_HEADERS,
                                "rows": normalized_failed,
                                "column_widths": USED_IP_TABLE_COLUMN_WIDTHS,
                                "side_note": FAILED_IP_TABLE_NOTE,
                            }
                        ],
                        raise_errors=True,
                    )
                except PermissionError as exc:
                    raise PermissionError(
                        "旧IP库文件正被 WPS/Excel 占用，或当前路径没有写入权限；"
                        f"请关闭该文件后重试。系统原因：{exc}"
                    ) from exc
                except OSError as exc:
                    raise OSError(f"旧IP库 Excel 文件无法写入。系统原因：{exc}") from exc
                except Exception as exc:
                    raise OSError(f"旧IP库 Excel 文件格式异常或已损坏。系统原因：{exc}") from exc
                if written is None:
                    raise OSError("旧IP库 Excel 文件写入失败。")
                return
            with path.open("w", encoding="utf-8-sig", newline="") as handle:
                writer = csv.DictWriter(handle, fieldnames=USED_IP_CSV_HEADERS)
                writer.writeheader()
                for row in normalized:
                    writer.writerow(row)

    def _create_used_ip_csv_table(self, path: Path) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        self._write_used_ip_csv_rows(path, [])

    def _used_ip_seed_values_for_region(self, rows: Sequence[Dict[str, str]], region_label: str) -> List[str]:
        exact: List[str] = []
        blank_region: List[str] = []
        seen: Set[str] = set()
        target_region = str(region_label or "").strip()
        for row in rows:
            ip = self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]))
            if not ip or ip in seen:
                continue
            row_region = str(row.get(USED_IP_CSV_HEADERS[2]) or "").strip()
            if row_region == target_region:
                seen.add(ip)
                exact.append(ip)
            elif not row_region:
                seen.add(ip)
                blank_region.append(ip)
        return exact or blank_region

    def _append_used_ip_table(self, ips: Sequence[str], *, region_label: str) -> Tuple[Path, int, Set[str]]:
        path = self._used_ip_table_path()
        cleaned: List[str] = []
        for ip in ips:
            value = self._normalize_used_ip_value(ip)
            if value and value not in cleaned:
                cleaned.append(value)
        with self._used_ip_table_file_guard(path):
            existing_rows = self._read_used_ip_csv_rows(path)
            rows, added_count, updated_count = self._merge_used_ip_rows(existing_rows, {region_label: cleaned})
            if rows or added_count or updated_count:
                self._write_used_ip_csv_rows(path, rows)
            elif not path.exists():
                self._create_used_ip_csv_table(path)
            existing = {row[USED_IP_CSV_HEADERS[3]] for row in rows}
        return path, added_count, set(existing) | set(cleaned)

    def _update_failed_ip_table(
        self,
        *,
        add_ips: Sequence[str] = (),
        remove_ips: Sequence[str] = (),
        region_label: str = "",
    ) -> Tuple[int, int, bool]:
        path = self._used_ip_table_path()
        if path.suffix.lower() != ".xlsx":
            return 0, 0, False
        cleaned_add = [
            value
            for value in dict.fromkeys(self._normalize_used_ip_value(ip) for ip in add_ips)
            if value
        ]
        cleaned_remove = {
            value
            for value in (self._normalize_used_ip_value(ip) for ip in remove_ips)
            if value
        }
        with self._used_ip_table_file_guard(path):
            failed_rows = self._read_failed_ip_rows(path)
            before_ips = {row[USED_IP_CSV_HEADERS[3]] for row in failed_rows}
            failed_rows, added_count, _updated_count = self._merge_used_ip_rows(
                failed_rows,
                {region_label: cleaned_add},
            )
            if cleaned_remove:
                failed_rows = [
                    row
                    for row in failed_rows
                    if row.get(USED_IP_CSV_HEADERS[3]) not in cleaned_remove
                ]
            removed_count = len(before_ips & cleaned_remove)
            main_rows = self._read_used_ip_csv_rows(path)
            self._write_used_ip_csv_rows(path, main_rows, failed_rows=failed_rows)
        return added_count, removed_count, True

    def _current_account_public_ip_groups_for_used_table(self) -> Dict[str, List[str]]:
        groups: Dict[str, List[str]] = {}
        for item in self.volc_cached_resource_items:
            if bool(item.get("_displayOnly")):
                continue
            kind = str(item.get("_kind") or "").lower()
            if kind not in {"ecs", "eip"}:
                continue
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            if kind == "eip" and not (
                str(item.get("_associated_id") or "").strip()
                or self._volc_eip_associated_instance_id(raw)
            ):
                continue
            region = str(item.get("_region") or "").strip()
            region_label = next(
                (plan.label for plan in VOLC_REGION_PLANS if plan.region == region),
                str(item.get("_region_label") or region or "当前账号").strip() or "当前账号",
            )
            raw_values = [
                item.get("_public_ip"),
                item.get("_name"),
            ]
            if raw:
                raw_values.extend(self._volc_public_ips_from_resource(raw))
            for raw_value in raw_values:
                for candidate in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(raw_value or "")):
                    ip = self._normalize_used_ip_value(candidate)
                    if not ip:
                        continue
                    bucket = groups.setdefault(region_label, [])
                    if ip not in bucket:
                        bucket.append(ip)
        return groups

    def _volc_release_ip_groups(self, items: Sequence[Dict[str, Any]]) -> Dict[str, List[str]]:
        groups: Dict[str, List[str]] = {}
        for item in items:
            if bool(item.get("_displayOnly")) or str(item.get("_kind") or "").lower() != "eip":
                continue
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            region = str(item.get("_region") or "").strip()
            region_label = next(
                (plan.label for plan in VOLC_REGION_PLANS if plan.region == region),
                str(item.get("_region_label") or region or "未识别地区").strip() or "未识别地区",
            )
            raw_values = [item.get("_public_ip"), item.get("_name")]
            if raw:
                raw_values.extend(self._volc_public_ips_from_resource(raw))
            for raw_value in raw_values:
                for candidate in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", str(raw_value or "")):
                    public_ip = self._normalize_used_ip_value(candidate)
                    if not public_ip:
                        continue
                    bucket = groups.setdefault(region_label, [])
                    if public_ip not in bucket:
                        bucket.append(public_ip)
        return groups

    def _import_volc_release_ip_groups(self, groups: Dict[str, List[str]]) -> int:
        imported_count = 0
        for region_label, ips in groups.items():
            path, added_count, _used_ip_table = self._append_used_ip_table(ips, region_label=region_label)
            imported_count += added_count
            self.log(
                f"[{region_label}] 释放IP已合并导入旧IP库 {len(ips)} 条，本次新增 {added_count} 条：{path}",
                "manage",
            )
        self.log(f"确认并导入旧IP库完成：本次新增 {imported_count} 条，随后执行资源释放。", "manage")
        return imported_count

    def _sync_current_account_ips_to_used_ip_table(self) -> int:
        region_ip_groups = self._current_account_public_ip_groups_for_used_table()
        if not region_ip_groups:
            return 0
        path = self._used_ip_table_path()
        with self._used_ip_table_file_guard(path):
            rows = self._read_used_ip_csv_rows(path)
            failed_rows = self._read_failed_ip_rows(path)
            rows, added_count, updated_count = self._merge_used_ip_rows(rows, region_ip_groups)
            if added_count or updated_count or not path.exists():
                self._write_used_ip_csv_rows(path, rows, failed_rows=failed_rows)
        return added_count

    def _sync_linked_used_ip_table_from_cache(self) -> None:
        if self._linked_used_ip_table_path() is None:
            return
        try:
            added_count = self._sync_current_account_ips_to_used_ip_table()
        except Exception as exc:
            self.log(f"已链接旧IP库自动合并失败：{exc}", "manage")
            return
        if added_count:
            self.log(f"已将当前账号新增的已绑定IP {added_count} 条合并到旧IP库。", "manage")

    def replace_selected_region_ips(self) -> None:
        if self.manage_tree is None:
            return
        selected_items = self._selected_volc_manage_items()
        selected_groups_by_region = self._selected_volc_replace_groups(selected_items)
        selected_eip_ids_by_region: Dict[str, Set[str]] = {}
        selected_full_groups_by_region: Dict[str, Set[int]] = {}
        for item in selected_items:
            region = str(item.get("_region") or "").strip()
            if not region:
                continue
            group_index = 2 if self._volc_manage_item_group_index(dict(item)) >= 2 else 1
            if item.get("_kind") == "eip":
                resource_id = str(item.get("_resource_id") or "").strip()
                if resource_id:
                    selected_eip_ids_by_region.setdefault(region, set()).add(resource_id)
                continue
            selected_full_groups_by_region.setdefault(region, set()).add(group_index)
        for iid in self.manage_tree.get_children(""):
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            if not values or values[0] != CHECKED or item.get("_kind") != "region_group":
                continue
            region = str(item.get("_region") or "").strip()
            if not region:
                continue
            group_index = 2 if self._volc_manage_item_group_index(dict(item)) >= 2 else 1
            selected_full_groups_by_region.setdefault(region, set()).add(group_index)
        selected_regions = set(selected_groups_by_region)
        plans = [plan for plan in VOLC_REGION_PLANS if plan.region in selected_regions]
        if not plans:
            self.log("请先在资源管理里选择要更换IP的火山引擎地区。", "manage")
            return
        selected_group_labels = [
            self._volc_region_group_label(plan.label, group_index)
            for plan in plans
            for group_index in sorted(selected_groups_by_region.get(plan.region) or {1})
        ]
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        if not ak or not sk:
            self.log("请先填写或粘贴火山引擎 AK/SK。", "manage")
            return
        guard_message = self._customer_guard_block_message(ak, sk)
        if guard_message:
            self._show_customer_guard_block(guard_message, "manage")
            return
        required_balance = self._volc_balance_threshold_for_ip_count(1)
        try:
            daily_status_by_region = {
                plan.region: get_eip_daily_purchase_status(ak, sk, plan.region)
                for plan in plans
            }
        except Exception as exc:
            message = f"读取公网IP每日购买记录失败，为避免超过限额已停止更换：{self._volc_exception_text(exc)}"
            self.log(message, "manage")
            self.show_info(message)
            return
        daily_status_text = "；".join(
            f"{plan.label}{daily_status_by_region[plan.region]['count']}/{VOLC_EIP_DAILY_PURCHASE_LIMIT}最多"
            f"（剩余{daily_status_by_region[plan.region]['remaining']}）"
            for plan in plans
        )
        if not self.ask_confirm_cancel(
            action_confirm_message(
                "确认更换新IP",
                regions=selected_group_labels,
                details=[
                    f"按火山每地区 {VOLC_EIP_REGION_QUOTA_LIMIT} 条EIP配额滚动更换。",
                    f"今日记录：{daily_status_text}。",
                    f"余额要求：购买公网IP资源时账户可用余额需≥{required_balance}元，确认后将自动校验。",
                    "请确认账户余额及本次公网IP费用充足。",
                    "剩余购买次数不足本次需要时，会在解绑旧IP前停止该地区。",
                    "随机申请新IP，并使用旧IP库避开已使用IP。",
                    "更换新IP后将更新L2TP/Socks5结果框。",
                    "该操作会造成已使用IP的设备断网，请确认执行任务?",
                ],
                cost=True,
            )
        ):
            self.log("已取消火山引擎更换新IP。", "manage")
            return
        if not self._begin_volc_task("火山引擎更换新IP中", context="manage", regions=[plan.region for plan in plans]):
            return
        self._volc_replace_result_warning = ""
        self._volc_replace_result_regions = set()
        self._volc_pending_result_display_regions = set()

        def allocation_id(eip: Dict[str, Any]) -> str:
            return self._volc_first(eip, "AllocationId", "allocation_id", "EipId", "eip_id", "id")

        def eip_address(eip: Dict[str, Any]) -> str:
            addr = self._volc_first(eip, "EipAddress", "eip_address", "IpAddress", "ip_address")
            if addr:
                return addr
            ips = self._volc_public_ips_from_resource(eip)
            return ips[0] if ips else ""

        def process_plan(plan: RegionPlan) -> Tuple[int, int]:
            selected_group_indexes = set(selected_groups_by_region.get(plan.region) or {1})
            replace_label = "、".join(
                self._volc_region_group_label(plan.label, group_index)
                for group_index in sorted(selected_group_indexes)
            )
            self.after(0, self.log, f"[{replace_label}] 开始更换公网IP。", "manage")
            items = self._query_volc_region_resources(plan, ak, sk)
            target_ids_by_group: Dict[int, Set[str]] = {1: set(), 2: set()}
            for resource_item in items:
                if resource_item.get("_kind") not in {"ecs", "network_interface"}:
                    continue
                resource_id = str(resource_item.get("_resource_id") or resource_item.get("_server_id") or "").strip()
                if not resource_id:
                    continue
                resource_group = 2 if self._volc_manage_item_group_index(dict(resource_item)) >= 2 else 1
                target_ids_by_group[resource_group].add(resource_id)
            selected_eip_ids = selected_eip_ids_by_region.get(plan.region, set())
            selected_full_groups = selected_full_groups_by_region.get(plan.region, set())
            old_eips = [
                item
                for item in items
                if item.get("_kind") == "eip"
                and item.get("_resource_id")
                and self._volc_eip_associated_instance_id(
                    item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
                )
                and (
                    self._volc_manage_item_group_index(dict(item)) in selected_full_groups
                    or str(item.get("_resource_id") or "").strip() in selected_eip_ids
                )
            ]
            if not old_eips:
                self.after(0, self.log, f"[{replace_label}] 未找到已绑定的旧公网IP，无法更换。", "manage")
                return 0, 1
            mismatch_count = sum(1 for item in old_eips if item.get("_group_binding_mismatch"))
            if mismatch_count:
                self.after(
                    0,
                    self.log,
                    f"[{replace_label}] 检测到 {mismatch_count} 条公网IP名称与实际网卡归属不一致，本次按实际ECS/网卡归属修复。",
                    "manage",
                )

            private_order = {ip: index for index, ip in enumerate(self._volc_entry_private_ip_order())}
            old_eips.sort(
                key=lambda item: (
                    private_order.get(
                        str(item.get("_private_ip") or self._volc_eip_private_ip(item.get("_raw") if isinstance(item.get("_raw"), dict) else {})),
                        len(private_order),
                    ),
                    str(item.get("_name") or item.get("_public_ip") or ""),
                )
            )
            old_ip_values = [
                self._normalize_used_ip_value(item.get("_public_ip"))
                for item in old_eips
                if self._normalize_used_ip_value(item.get("_public_ip"))
            ]
            failed_old_ip_values: Set[str] = set()
            succeeded_old_ip_values: Set[str] = set()

            def persist_failed_ip_status() -> None:
                try:
                    added_count, removed_count, supported = self._update_failed_ip_table(
                        add_ips=sorted(failed_old_ip_values),
                        remove_ips=sorted(succeeded_old_ip_values),
                        region_label=plan.label,
                    )
                    if not supported and failed_old_ip_values:
                        migration_error = str(getattr(self, "_used_ip_table_migration_error", "") or "").strip()
                        detail = f"，CSV自动升级失败：{migration_error}" if migration_error else ""
                        self.after(
                            0,
                            self.log,
                            f"[{replace_label}] 未保存更换失败IP：当前旧IP库不是 .xlsx 文件{detail}；"
                            "请在右键配置中保存或链接 Excel 文件。",
                            "manage",
                        )
                    elif added_count or removed_count:
                        self.after(
                            0,
                            self.log,
                            f"[{replace_label}] 更换失败IP库已更新：新增 {added_count} 条，移除 {removed_count} 条。",
                            "manage",
                        )
                except Exception as exc:
                    self.after(0, self.log, f"[{replace_label}] 更换失败IP库更新失败: {exc}", "manage")

            required_new_count = len(old_eips)
            daily_status = get_eip_daily_purchase_status(ak, sk, plan.region)
            daily_count = int(daily_status.get("count") or 0)
            daily_remaining = int(daily_status.get("remaining") or 0)
            if daily_remaining < required_new_count:
                message = (
                    f"[{replace_label}] 今日公网IP已购买 {daily_count}/{VOLC_EIP_DAILY_PURCHASE_LIMIT} 次，"
                    f"仅剩 {daily_remaining} 次，本次至少需要 {required_new_count} 次；"
                    "为避免只更换部分线路，已在解绑旧IP前停止该地区，请明日再试。"
                )
                self.after(0, self.log, message, "manage")
                self.after(0, self.show_info, message)
                failed_old_ip_values.update(old_ip_values)
                persist_failed_ip_status()
                return 0, required_new_count
            all_region_eips = [item for item in items if item.get("_kind") == "eip" and item.get("_resource_id")]
            current_eip_count = len(all_region_eips)
            available_slots = max(0, VOLC_EIP_REGION_QUOTA_LIMIT - current_eip_count)
            if available_slots <= 0:
                message = f"[{replace_label}] 当前地区IP数量为{current_eip_count}条，正确应为16/8条，请检查删除后再继续。"
                self.after(
                    0,
                    self.log,
                    message,
                    "manage",
                )
                self.after(0, self.show_info, message)
                failed_old_ip_values.update(old_ip_values)
                persist_failed_ip_status()
                return 0, required_new_count
            self.after(
                0,
                self.log,
                f"[{replace_label}] 识别到已绑定旧公网IP {required_new_count} 条，当前EIP {current_eip_count}/{VOLC_EIP_REGION_QUOTA_LIMIT}，每批最多预申请 {available_slots} 条新IP。",
                "manage",
            )

            ok = 0
            failed = 0
            used_ip_rows: List[Dict[str, str]] = []
            region_seed_ip_values: List[str] = []
            try:
                used_ip_path, added_count, used_ip_table = self._append_used_ip_table(old_ip_values, region_label=plan.label)
                with self._used_ip_table_file_guard(used_ip_path):
                    used_ip_rows = self._read_used_ip_csv_rows(used_ip_path)
                region_seed_ip_values = self._used_ip_seed_values_for_region(used_ip_rows, plan.label)
                self.after(
                    0,
                    self.log,
                    f"[{replace_label}] 旧IP库已加载 {len(used_ip_table)} 条，本次新增 {added_count} 条: {used_ip_path}",
                    "manage",
                )
            except Exception as exc:
                used_ip_path = self._used_ip_table_path()
                try:
                    with self._used_ip_table_file_guard(used_ip_path):
                        used_ip_rows = self._read_used_ip_csv_rows(used_ip_path)
                    used_ip_table = {
                        row[USED_IP_CSV_HEADERS[3]]
                        for row in used_ip_rows
                        if self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]))
                    } | set(old_ip_values)
                    region_seed_ip_values = self._used_ip_seed_values_for_region(used_ip_rows, plan.label)
                    self.after(
                        0,
                        self.log,
                        f"[{replace_label}] 旧IP库写入失败，但已读取 {len(used_ip_rows)} 条用于避开；本次旧IP仅使用内存记录: {exc}",
                        "manage",
                    )
                except Exception as read_exc:
                    used_ip_table = set(old_ip_values)
                    self.after(0, self.log, f"[{replace_label}] 旧IP库读取/写入失败，本次仅使用内存记录: {exc}; 读取提示: {read_exc}", "manage")
            if region_seed_ip_values:
                preview = ", ".join(region_seed_ip_values[:12])
                more = f"，另有 {len(region_seed_ip_values) - 12} 条" if len(region_seed_ip_values) > 12 else ""
                self.after(0, self.log, f"[{replace_label}] 同地区旧IP段学习样本: {preview}{more}", "manage")
            self.after(
                0,
                self.log,
                f"[{replace_label}] 将随机申请新公网IP，并使用旧IP库避开已使用IP。",
                "manage",
            )

            def release_new_eip_after_failure(new_eip: Dict[str, Any]) -> None:
                new_id = allocation_id(new_eip)
                if not new_id:
                    return
                try:
                    self._volc_release_eip(
                        plan.region,
                        {
                            "_kind": "eip",
                            "_kind_label": "EIP",
                            "_region": plan.region,
                            "_region_label": replace_label,
                            "_name": eip_address(new_eip) or new_id,
                            "_public_ip": eip_address(new_eip),
                            "_resource_id": new_id,
                            "_raw": new_eip,
                        },
                        ak,
                        sk,
                    )
                except Exception as exc:
                    self.after(0, self.log, f"[{replace_label}] 新公网IP绑定失败后释放提示: {self._volc_exception_text(exc)}", "manage")

            duplicate_hit_streak = 0

            def create_replacement_eip(index: int, group_index: int) -> Dict[str, Any]:
                nonlocal duplicate_hit_streak
                attempt = 0
                group_prefix = "wawacloud_two" if int(group_index or 1) >= 2 else "wawacloud_one"
                while True:
                    attempt += 1
                    new_eip = self._volc_allocate_eip(
                        plan.region,
                        ak,
                        sk,
                        f"{group_prefix}-replace-{index}-try{attempt}",
                    )
                    new_ip = eip_address(new_eip)
                    if new_ip and new_ip in used_ip_table:
                        duplicate_hit_streak += 1
                        release_new_eip_after_failure(new_eip)
                        self.after(
                            0,
                            self.log,
                            f"[{replace_label}] 随机新公网IP命中旧IP库 {new_ip}，已释放后重试 {duplicate_hit_streak}/{VOLC_REPLACE_RANDOM_OLD_IP_HIT_LIMIT}。",
                            "manage",
                        )
                        if duplicate_hit_streak >= VOLC_REPLACE_RANDOM_OLD_IP_HIT_LIMIT:
                            message = (
                                f"[{replace_label}] 随机新公网IP连续 {duplicate_hit_streak} 次命中旧IP库，已停止本地区换IP，"
                                "避免浪费次数和费用；请超过2小时再试，可大大降低买到旧IP概率。"
                            )
                            self.after(0, self.show_info, message)
                            raise RuntimeError(message)
                        continue
                    duplicate_hit_streak = 0
                    return new_eip

            def release_preallocated_eips(new_eips: Sequence[Dict[str, Any]]) -> None:
                for new_eip in new_eips:
                    release_new_eip_after_failure(new_eip)

            def eip_target_still_bound(exc: Exception) -> bool:
                text = self._volc_exception_text(exc).lower()
                return any(
                    token in text
                    for token in (
                        "invalidinstance.inuse",
                        "invalideni.inuse",
                        "already been associated",
                        "has already been associated",
                        "inuse",
                        "in use",
                    )
                )

            def associate_new_eip_with_sync_wait(new_id: str, target_id: str, private_ip: str, target_type: str, new_ip: str) -> None:
                last_exc: Optional[Exception] = None
                for attempt in range(1, 7):
                    try:
                        self._volc_associate_eip(plan.region, new_id, target_id, private_ip, target_type, ak, sk)
                        return
                    except Exception as exc:
                        last_exc = exc
                        if not eip_target_still_bound(exc) or attempt >= 6:
                            break
                        wait_seconds = 2.0 if attempt < 3 else 4.0
                        self.after(
                            0,
                            self.log,
                            f"[{replace_label}] 新公网IP {new_ip} 绑定目标仍在同步释放，等待 {wait_seconds:g} 秒后重试 {attempt}/6: {self._volc_exception_text(exc)}",
                            "manage",
                        )
                        time.sleep(wait_seconds)
                if last_exc:
                    raise last_exc

            def wait_eip_binding_verified(
                allocation_value: str,
                target_id: str,
                private_ip: str,
                target_type: str,
                public_ip: str,
                group_index: int,
                *,
                verify_name_group: bool = True,
            ) -> None:
                expected_group = 2 if int(group_index or 1) >= 2 else 1
                for attempt in range(1, 7):
                    current_eip: Dict[str, Any] = {}
                    for candidate in self._volc_describe_eips(plan.region, ak, sk):
                        if allocation_id(candidate) == allocation_value:
                            current_eip = candidate
                            break
                    current_target_id = self._volc_eip_associated_instance_id(current_eip) if current_eip else ""
                    current_private_ip = self._volc_eip_private_ip(current_eip) if current_eip else ""
                    current_name = self._volc_first(current_eip, "Name", "name") if current_eip else ""
                    current_name_group = self._volc_known_resource_group_index_from_name(current_name)
                    if private_ip and not current_private_ip and public_ip:
                        try:
                            for nic in self._volc_describe_network_interfaces(plan.region, ak, sk):
                                mapped_private_ip = self._volc_public_to_private_ip_map_from_nic(nic).get(public_ip, "")
                                if mapped_private_ip:
                                    current_private_ip = mapped_private_ip
                                    break
                        except Exception:
                            current_private_ip = ""
                    target_ok = bool(current_target_id and current_target_id == target_id)
                    private_ok = (
                        not private_ip
                        or current_private_ip == private_ip
                        or (str(target_type or "").lower() == "ecsinstance" and not current_private_ip)
                    )
                    name_ok = not verify_name_group or current_name_group == expected_group
                    if target_ok and private_ok and name_ok:
                        return
                    self.after(
                        0,
                        self.log,
                        f"[{replace_label}] 查询公网IP绑定状态：等待云端确认（{attempt}/6）。",
                        "manage",
                    )
                    if attempt < 6:
                        time.sleep(2.0)
                raise RuntimeError("新公网IP绑定后未确认到原ECS/网卡和私网IP，已停止后续释放")

            def replace_one(old_item: Dict[str, Any], new_eip: Dict[str, Any]) -> Tuple[bool, str, str]:
                raw = old_item.get("_raw") if isinstance(old_item.get("_raw"), dict) else {}
                old_id = str(old_item.get("_resource_id") or "").strip()
                target_id = str(old_item.get("_associated_id") or self._volc_eip_associated_instance_id(raw)).strip()
                target_type = str(old_item.get("_associated_type") or self._volc_eip_associated_type(raw)).strip()
                private_ip = str(old_item.get("_private_ip") or self._volc_eip_private_ip(raw)).strip()
                old_ip = str(old_item.get("_public_ip") or eip_address(raw) or old_id)
                old_group_index = 2 if self._volc_manage_item_group_index(dict(old_item)) >= 2 else 1
                if not old_id or not target_id:
                    release_new_eip_after_failure(new_eip)
                    self.after(0, self.log, f"[{replace_label}] 换IP参数缺失，已跳过旧IP {old_ip}。", "manage")
                    return False, old_ip, ""
                new_id = allocation_id(new_eip)
                new_ip = eip_address(new_eip) or new_id
                if not new_id:
                    release_new_eip_after_failure(new_eip)
                    self.after(0, self.log, f"[{replace_label}] 新公网IP参数缺失，旧IP {old_ip} 未解绑。", "manage")
                    return False, old_ip, new_ip
                if target_id not in target_ids_by_group.get(old_group_index, set()):
                    release_new_eip_after_failure(new_eip)
                    self.after(
                        0,
                        self.log,
                        f"[{replace_label}] 旧公网IP {old_ip} 的实际绑定目标不属于当前第{old_group_index}套资源，已停止以避免跨套绑定。",
                        "manage",
                    )
                    return False, old_ip, new_ip
                if not private_ip and required_new_count > 1:
                    release_new_eip_after_failure(new_eip)
                    self.after(0, self.log, f"[{replace_label}] 旧公网IP {old_ip} 未识别到对应私网IP，已跳过以避免解绑后无法回绑。", "manage")
                    return False, old_ip, new_ip
                try:
                    self._volc_disassociate_eip(plan.region, old_id, ak, sk)
                    time.sleep(2.0)
                except Exception as exc:
                    text = self._volc_exception_text(exc).lower()
                    if not any(token in text for token in ("not associated", "notassociate", "invalidassociation", "not found")):
                        release_new_eip_after_failure(new_eip)
                        self.after(0, self.log, f"[{replace_label}] 旧公网IP解绑失败 {old_ip}: {self._volc_exception_text(exc)}", "manage")
                        return False, old_ip, new_ip
                try:
                    associate_new_eip_with_sync_wait(new_id, target_id, private_ip, target_type, new_ip)
                    wait_eip_binding_verified(
                        new_id,
                        target_id,
                        private_ip,
                        target_type,
                        new_ip,
                        old_group_index,
                    )
                    try:
                        self._volc_release_eip(plan.region, old_item, ak, sk)
                    except Exception as exc:
                        self.after(0, self.log, f"[{replace_label}] 旧公网IP {old_ip} 释放提示: {self._volc_exception_text(exc)}", "manage")
                    if new_ip:
                        used_ip_table.add(new_ip)
                    try:
                        _, added_count, _ = self._append_used_ip_table([new_ip], region_label=plan.label)
                        if added_count:
                            self.after(0, self.log, f"[{replace_label}] 新公网IP绑定成功，已写入旧IP库: {new_ip}", "manage")
                    except Exception as exc:
                        self.after(0, self.log, f"[{replace_label}] 新公网IP已绑定，但写入旧IP库失败: {exc}", "manage")
                    self.after(0, self.log, f"[{replace_label}] 公网IP已更换 {old_ip} -> {new_ip}", "manage")
                    return True, old_ip, new_ip
                except Exception as exc:
                    rollback_error = ""
                    try:
                        self._volc_disassociate_eip(plan.region, new_id, ak, sk)
                    except Exception:
                        pass
                    time.sleep(2.0)
                    try:
                        self._volc_associate_eip(plan.region, old_id, target_id, private_ip, target_type, ak, sk)
                        wait_eip_binding_verified(
                            old_id,
                            target_id,
                            private_ip,
                            target_type,
                            old_ip,
                            old_group_index,
                            verify_name_group=False,
                        )
                    except Exception as rollback_exc:
                        rollback_error = self._volc_exception_text(rollback_exc)
                    release_new_eip_after_failure(new_eip)
                    rollback_text = f"；旧IP回滚未确认: {rollback_error}" if rollback_error else "；旧IP已回滚原目标"
                    self.after(
                        0,
                        self.log,
                        f"[{replace_label}] 公网IP更换失败 {old_ip}: {self._volc_exception_text(exc)}{rollback_text}",
                        "manage",
                    )
                    return False, old_ip, new_ip

            result_ip_replacements: Dict[str, str] = {}
            pending_old_eips = list(old_eips)
            next_index = 1
            while pending_old_eips:
                batch_size = min(available_slots, len(pending_old_eips))
                self.after(0, self.log, f"[{replace_label}] 开始预申请本批新公网IP {batch_size} 条。", "manage")
                batch_old_eips = pending_old_eips[:batch_size]
                batch_new_eips: List[Dict[str, Any]] = []
                try:
                    for old_item in batch_old_eips:
                        old_group_index = 2 if self._volc_manage_item_group_index(dict(old_item)) >= 2 else 1
                        batch_new_eips.append(create_replacement_eip(next_index, old_group_index))
                        next_index += 1
                except Exception as exc:
                    release_preallocated_eips(batch_new_eips)
                    failed += len(pending_old_eips)
                    failed_old_ip_values.update(
                        self._normalize_used_ip_value(item.get("_public_ip"))
                        for item in pending_old_eips
                        if self._normalize_used_ip_value(item.get("_public_ip"))
                    )
                    self.after(0, self.log, f"[{replace_label}] 新公网IP预申请停止: {self._volc_exception_text(exc)}", "manage")
                    if isinstance(exc, EipDailyPurchaseLimitError):
                        message = (
                            f"[{replace_label}] 今日公网IP购买次数已达到 {VOLC_EIP_DAILY_PURCHASE_LIMIT} 次，"
                            "已在解绑旧IP前停止该地区，请明日再试。"
                        )
                        self.after(0, self.show_info, message)
                    break

                pending_old_eips = pending_old_eips[batch_size:]
                for old_item, new_eip in zip(batch_old_eips, batch_new_eips):
                    success, old_ip, new_ip = replace_one(old_item, new_eip)
                    if success:
                        ok += 1
                        if old_ip and new_ip:
                            result_ip_replacements[old_ip] = new_ip
                        normalized_old_ip = self._normalize_used_ip_value(old_ip)
                        if normalized_old_ip:
                            succeeded_old_ip_values.add(normalized_old_ip)
                    else:
                        failed += 1
                        normalized_old_ip = self._normalize_used_ip_value(old_ip)
                        if normalized_old_ip:
                            failed_old_ip_values.add(normalized_old_ip)
            persist_failed_ip_status()
            if result_ip_replacements:
                current_ip_groups: Dict[int, List[str]] = {}
                for old_item in old_eips:
                    raw = old_item.get("_raw") if isinstance(old_item.get("_raw"), dict) else {}
                    old_ip = str(old_item.get("_public_ip") or eip_address(raw) or "").strip()
                    current_ip = str(result_ip_replacements.get(old_ip) or old_ip).strip()
                    if not is_public_ip(current_ip):
                        continue
                    group_index = self._volc_manage_item_group_index(dict(old_item))
                    group_ips = current_ip_groups.setdefault(group_index, [])
                    if current_ip not in group_ips:
                        group_ips.append(current_ip)
                self.after(
                    0,
                    self._sync_volc_result_ips_after_eip_replace,
                    plan,
                    dict(result_ip_replacements),
                    current_ip_groups,
                )
            return ok, failed

        def worker() -> None:
            self.after(0, self.log, "正在校验火山引擎账户余额。", "manage")
            try:
                balance_ok, balance_message, balance = get_available_balance(ak, sk, plans[0].region)
            except Exception as exc:
                balance_ok, balance_message, balance = False, self._volc_exception_text(exc), None
            if not balance_ok or balance is None:
                message = (
                    f"火山引擎可用余额校验失败，已停止更换新IP：{self._volc_exception_text(balance_message)}。\n"
                    f"官方规定购买公网IP资源时账户可用余额需≥{required_balance}元，请确认余额后重试。"
                )
                self.after(0, self.log, message, "manage")
                self.after(0, self._finish_volc_task, "就绪", "manage")
                self.after(100, self.show_info, message)
                return
            balance_text = format(balance, "f")
            if "." in balance_text:
                balance_text = balance_text.rstrip("0").rstrip(".")
            if balance < required_balance:
                message = (
                    f"官方规定：购买公网IP资源时，账户可用余额需≥{required_balance}元。\n"
                    f"当前可用余额: {balance_text}元。\n"
                    f"余额不足，任务已停止，请先充值至{required_balance}元以上后重试。"
                )
                self.after(0, self.log, message, "manage")
                self.after(0, self._finish_volc_task, "就绪", "manage")
                self.after(100, self.show_info, message)
                return
            self.after(
                0,
                self.log,
                f"更换新IP余额校验通过：当前可用余额 {balance_text} 元，官方门槛≥{required_balance}元。",
                "manage",
            )
            success = 0
            failed = 0
            with ThreadPoolExecutor(max_workers=max(1, len(plans))) as executor:
                futures = {executor.submit(process_plan, plan): plan for plan in plans}
                for future in as_completed(futures):
                    plan = futures[future]
                    try:
                        ok, fail = future.result()
                    except Exception as exc:
                        ok, fail = 0, 1
                        self.after(0, self.log, f"[{plan.label}] 更换公网IP失败: {self._volc_exception_text(exc)}", "manage")
                    success += ok
                    failed += fail
            self.after(0, self.log, f"火山引擎更换新IP完成，成功 {success} 条，失败 {failed} 条。", "manage")
            self.after(0, self._finish_volc_task, "就绪", "manage")
            final_popup = f"火山引擎更换新IP完成，成功 {success} 条，失败 {failed} 条。"
            self.after(
                300,
                lambda p=list(plans), message=final_popup, successful_count=success: self.scan_resources(
                    context="manage",
                    plans_override=p,
                    merge_with_cache=True,
                    final_popup_message=self._volc_new_ip_result_popup_message(
                        message,
                        success=successful_count,
                        warning=getattr(self, "_volc_replace_result_warning", ""),
                    ),
                ),
            )

        threading.Thread(target=worker, daemon=True).start()

    def start_build_selected_regions(self) -> None:
        selected_groups_by_region = self._selected_purchase_region_groups()
        plans = [plan for plan in VOLC_REGION_PLANS if plan.region in selected_groups_by_region]
        if not plans:
            self.log("请先选择要搭建的火山引擎地区。")
            return
        region_ips = self._collect_region_ips_from_rows(plans, selected_groups_by_region)
        selected_ecs_count = sum(len(groups) for groups in selected_groups_by_region.values())
        resolved_ecs_count = sum(len(ips) for ips in region_ips.values())
        if resolved_ecs_count != selected_ecs_count:
            message = (
                f"已勾选 {selected_ecs_count} 台ECS，但当前仅识别到 {resolved_ecs_count} 台服务器公网IP，"
                "为避免漏搭建，本次任务已停止。请先读取账号资源后重新执行。"
            )
            self.log(message)
            self.show_info(message)
            return
        guard_message = self._customer_guard_block_message()
        if guard_message:
            self._show_customer_guard_block(guard_message, "purchase")
            return
        details = [
            *self._volc_build_confirm_details(build_enabled=True),
            self._volc_task_popup_details(
                plans,
                purchase_spec="未购买，复用已有资源",
                build_enabled=True,
            ),
            *[
                f"{self._volc_region_group_label(plan.label, group_index)}: 1 个ECS"
                for plan in plans
                for group_index in sorted(selected_groups_by_region.get(plan.region) or set())
            ],
        ]
        selected_group_labels = [
            self._volc_region_group_label(plan.label, group_index)
            for plan in plans
            for group_index in sorted(selected_groups_by_region.get(plan.region) or set())
        ]
        if not self.ask_confirm_cancel(
            action_confirm_message(
                f"确认开始火山引擎搭建 {len(plans)} 个地区",
                regions=selected_group_labels,
                details=details,
                warning="将通过 SSH 在目标云服务器执行搭建脚本。",
                cost=False,
            )
        ):
            self.log("已取消火山引擎搭建。")
            return
        if not self._begin_volc_task("火山引擎搭建中", context="purchase", regions=[plan.region for plan in plans]):
            return

        self.volc_rebuilding_output_regions.difference_update(plan.region for plan in plans)

        def worker() -> None:
            success = 0
            successful_ecs = 0
            failed = 0
            for plan in plans:
                ips = region_ips.get(plan.region, [])
                if not ips:
                    continue
                group_indexes = sorted(selected_groups_by_region.get(plan.region) or set())
                selected_label = "、".join(
                    self._volc_region_group_label(plan.label, group_index)
                    for group_index in group_indexes
                ) or plan.label
                self.after(0, self.log, f"[{selected_label}] 开始 SSH 搭建，云服务器 {len(ips)} 个。")
                socks_lines, l2tp_lines, built_ecs_count = self._build_ips_for_plan(plan, ips)
                if socks_lines and l2tp_lines and built_ecs_count >= len(ips):
                    success += 1
                    successful_ecs += built_ecs_count
                    selected_groups = {
                        plan.region: set(selected_groups_by_region.get(plan.region) or set())
                    }

                    def apply_complete_result(
                        groups: Dict[str, Set[int]] = selected_groups,
                        region: str = plan.region,
                        new_socks: List[str] = list(socks_lines),
                        new_l2tp: List[str] = list(l2tp_lines),
                    ) -> None:
                        self._clear_volc_outputs_for_region_groups(
                            groups,
                            context="purchase",
                            reason="",
                        )
                        current_socks = list(self.volc_region_socks.get(region, []))
                        current_l2tp = list(self.volc_region_l2tp.get(region, []))
                        self._append_unique_output_lines(current_socks, new_socks)
                        self._append_unique_output_lines(current_l2tp, new_l2tp)
                        self.volc_region_socks[region] = current_socks
                        self.volc_region_l2tp[region] = current_l2tp
                        self._refresh_volc_outputs(restore_saved=False)

                    self.after(0, apply_complete_result)
                    status_text = "搭建完成"
                    for group_index in group_indexes:
                        self.after(
                            0,
                            lambda p=plan, g=group_index, s=status_text: self._set_region_values(
                                p,
                                group_index=g,
                                host_status=s,
                            ),
                        )
                else:
                    failed += 1
                    for group_index in group_indexes:
                        self.after(
                            0,
                            lambda p=plan, g=group_index: self._set_region_values(
                                p,
                                group_index=g,
                                host_status="搭建失败",
                            ),
                        )
            self.after(0, self._refresh_volc_outputs)
            self.after(0, self._finish_volc_task, "就绪", "purchase")
            self.after(0, self._reset_one_time_build_config_after_task)
            message = f"火山引擎搭建任务结束，成功 {success} 个地区的 {successful_ecs} 个ECS，失败 {failed}。"
            message += self._volc_task_popup_details(
                plans,
                purchase_spec="未购买，复用已有资源",
                build_enabled=True,
            )
            self.after(0, self.log, message)
            self.after(
                300,
                lambda p=list(plans), final_message=message: self.scan_resources(
                    context="purchase",
                    plans_override=p,
                    merge_with_cache=True,
                    final_popup_message=final_message,
                ),
            )

        threading.Thread(target=worker, daemon=True).start()

    def _wait_for_volc_purchase_build_ssh_ready(self) -> bool:
        delay = int(VOLC_PURCHASE_BUILD_SSH_READY_DELAY_SECONDS)
        if delay <= 0:
            return not self.stop_event.is_set()
        self.after(0, self.log, f"购买完成后等待 {delay} 秒，等待云服务器 SSH 端口就绪。")
        for _ in range(delay):
            if self.stop_event.is_set():
                self.after(0, self.log, "已停止任务，取消等待 SSH 就绪。")
                return False
            time.sleep(1)
        return not self.stop_event.is_set()

    def _build_ips_for_plan(self, plan: RegionPlan, ips: Sequence[str]) -> Tuple[List[str], List[str], int]:
        socks_lines: List[str] = []
        l2tp_lines: List[str] = []
        built_ecs_count = 0
        with ThreadPoolExecutor(max_workers=min(4, max(1, len(ips)))) as executor:
            futures = {executor.submit(self._build_one_ip, ip): ip for ip in ips}
            for future in as_completed(futures):
                ip = futures[future]
                try:
                    build_socks, build_l2tp = future.result()
                    socks_lines.extend(build_socks)
                    l2tp_lines.extend(build_l2tp)
                    if build_socks and build_l2tp:
                        built_ecs_count += 1
                except Exception as exc:
                    self.after(0, self.log, f"[{plan.label}/{ip}] 搭建失败: {exc}")
        return socks_lines, l2tp_lines, built_ecs_count

    @staticmethod
    def _clean_volc_remote_result_line(text: str) -> str:
        value = (text or "").strip()
        value = re.sub(r"^\[(?:\d{1,3}\.){3}\d{1,3}\]\s*", "", value).strip()
        return value

    @classmethod
    def _is_volc_socks_result_line(cls, text: str) -> bool:
        value = cls._clean_volc_remote_result_line(text)
        return bool(re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}/\d{2,5}/[^/\s]+/[^/\s]+", value))

    @classmethod
    def _is_volc_l2tp_result_line(cls, text: str) -> bool:
        return bool(cls._normalize_volc_l2tp_result_line(text))

    @classmethod
    def _normalize_volc_l2tp_result_line(cls, text: str) -> str:
        value = cls._clean_volc_remote_result_line(text)
        ipv4 = r"(?:\d{1,3}\.){3}\d{1,3}"
        slash_match = re.fullmatch(
            rf"({ipv4})/(user\d+)/([^/\s]+)/([^/\s]+)(?:/(?:(?:出口IP|对应出口IP)[:：]\s*)?({ipv4})?)?",
            value,
        )
        if slash_match:
            main_ip, username, password, key, outbound_ip = slash_match.groups()
            parts = [main_ip, username, password, key]
            if outbound_ip:
                parts.append(outbound_ip)
            return "/".join(parts)
        match = re.fullmatch(
            rf"服务器地址[:：]\s*({ipv4})\s+用户名[:：]\s*(user\d+)\s+密码[:：]\s*([^/\s]+)\s+密钥[:：]\s*([^/\s]+)(?:\s+(?:出口IP|对应出口IP)[:：]\s*({ipv4}))?",
            value,
        )
        if not match:
            return ""
        main_ip, username, password, key, outbound_ip = match.groups()
        parts = [main_ip, username, password, key]
        if outbound_ip:
            parts.append(outbound_ip)
        return "/".join(parts)

    @classmethod
    def _volc_l2tp_outbound_ip_line(cls, text: str) -> str:
        value = cls._clean_volc_remote_result_line(text)
        ipv4 = r"(?:\d{1,3}\.){3}\d{1,3}"
        match = re.fullmatch(rf"(?:出口IP|对应出口IP)[:：]\s*({ipv4})", value)
        return match.group(1) if match else ""

    @staticmethod
    def _attach_volc_l2tp_outbound_ip(l2tp_lines: List[str], outbound_ip: str) -> bool:
        if not outbound_ip:
            return False
        for index in range(len(l2tp_lines) - 1, -1, -1):
            parts = l2tp_lines[index].split("/")
            if len(parts) != 4:
                continue
            updated = f"{l2tp_lines[index]}/{outbound_ip}"
            if updated in l2tp_lines:
                del l2tp_lines[index]
            else:
                l2tp_lines[index] = updated
            return True
        return False

    @staticmethod
    def _has_incomplete_volc_l2tp_lines(l2tp_lines: Sequence[str]) -> bool:
        return any(len(str(line or "").strip().split("/")) == 4 for line in l2tp_lines if str(line or "").strip())

    def _collect_volc_result_line(self, text: str, socks_lines: List[str], l2tp_lines: List[str]) -> None:
        value = self._clean_volc_remote_result_line(text)
        outbound_ip = self._volc_l2tp_outbound_ip_line(value)
        if outbound_ip:
            self._attach_volc_l2tp_outbound_ip(l2tp_lines, outbound_ip)
            return
        if self._is_volc_socks_result_line(value):
            if value in socks_lines:
                return
            socks_lines.append(value)
        else:
            l2tp_value = self._normalize_volc_l2tp_result_line(value)
            if not l2tp_value or l2tp_value in l2tp_lines:
                return
            l2tp_lines.append(l2tp_value)

    def _build_one_ip(self, ip: str) -> Tuple[List[str], List[str]]:
        if self.stop_event.is_set():
            return [], []
        ssh = None
        channel = None
        try:
            last_connect_exc: Optional[Exception] = None
            for attempt in range(1, 4):
                if self.stop_event.is_set():
                    return [], []
                self.after(0, self.log, f"[{ip}] SSH连接 {DEFAULT_SSH_USER}@{ip}:22，尝试 {attempt}/3")
                try:
                    ssh = self._open_volc_ssh_client(ip)
                    last_connect_exc = None
                    break
                except Exception as exc:
                    last_connect_exc = exc
                    if attempt >= 3:
                        break
                    self.after(
                        0,
                        self.log,
                        f"[{ip}] SSH暂未就绪，5秒后重试: {self._volc_exception_text(exc)}",
                    )
                    if self.stop_event.wait(5.0):
                        return [], []
            if ssh is None:
                if last_connect_exc is not None:
                    raise last_connect_exc
                raise RuntimeError("SSH连接未就绪")
            ensure_remote_xray(
                ssh,
                lambda message: self.after(0, self.log, f"[{ip}] {message}"),
            )
            command = self._volc_remote_command()
            self.after(0, self.log, f"[{ip}] 开始执行搭建脚本。")
            _stdin, stdout, stderr = ssh.exec_command(command, get_pty=True, timeout=900)
            channel = stdout.channel
            with self.ssh_channel_lock:
                self.ssh_channel = channel
            socks_lines: List[str] = []
            l2tp_lines: List[str] = []
            for raw in iter(stdout.readline, ""):
                if not raw:
                    break
                text = raw.rstrip()
                self.after(0, self.log, f"[{ip}] {text}")
                self._collect_volc_result_line(text, socks_lines, l2tp_lines)
            err = stderr.read().decode("utf-8", errors="replace").strip()
            if err:
                self.after(0, self.log, f"[{ip}] STDERR: {err}")
            code = stdout.channel.recv_exit_status()
            self.after(0, self.log, f"[{ip}] 远程脚本退出码: {code}")
            if code != 0:
                raise RuntimeError(f"远程脚本退出码 {code}")
            if not l2tp_lines or self._has_incomplete_volc_l2tp_lines(l2tp_lines):
                # 远程脚本会把完整结果写入 /root/socks5_L2TP.txt；部分运行只实时刷出 SOCKS5，结束后再兜底读取一次。
                try:
                    _stdin2, stdout2, stderr2 = ssh.exec_command("test -f /root/socks5_L2TP.txt && cat /root/socks5_L2TP.txt", timeout=30)
                    for file_line in stdout2.read().decode("utf-8", errors="replace").splitlines():
                        self._collect_volc_result_line(file_line, socks_lines, l2tp_lines)
                    err2 = stderr2.read().decode("utf-8", errors="replace").strip()
                    if err2:
                        self.after(0, self.log, f"[{ip}] 读取L2TP结果提示: {err2}")
                except Exception as exc:
                    self.after(0, self.log, f"[{ip}] 读取L2TP结果文件失败: {exc}")
            return socks_lines, l2tp_lines
        finally:
            with self.ssh_channel_lock:
                if channel is not None and self.ssh_channel is channel:
                    self.ssh_channel = None
            if ssh is not None:
                ssh.close()


