from common import shared as _shared
from contextlib import contextmanager
import csv
import ftplib
import importlib
import ipaddress
import json
import msvcrt
import os
from pathlib import Path
import re
import secrets
import shutil
import socket
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
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.realtime_log_filter import (
    FAILURE_EXPORT_NOTICE,
    RealtimeLogDisplayFilter,
    clean_realtime_display_line,
    filter_realtime_log_text as _base_filter_realtime_log_text,
    format_realtime_log_text_without_filter,
    remove_failure_export_notice,
)
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.connection_command import configured_default_script_command, shell_env_assignments
from common.credential_parser import append_mark_to_output, extract_mark
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.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 is_on_demand_debug_enabled, limited_vendor_features_enabled, set_on_demand_debug_enabled
from common.user_messages import friendly_popup_text
from common.update_guard import read_installed_update_metadata
from dataclasses import replace
from datetime import date, datetime, timedelta
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple
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,
    ask_app_confirm_dialog,
    build_cloud_page_layout,
    create_fixed_tab_pages,
    cloud_dialog_title,
    apply_dialog_icon,
    autosize_treeview_columns,
    build_realtime_log_panel,
    build_result_output_panel,
    play_task_completion_sound,
    show_app_message_dialog,
    treeview_display_column_name,
)
from HUAWEI import cloud_client as _cloud_client
from HUAWEI.ros_builder import (
    ROS_ADMIN_PASSWORD,
    ROS_ADMIN_USER,
    ROS_CHR_FILENAME,
    ROS_DEFAULT_L2TP_USERNAME,
    ROS_DEFAULT_PPP_PASSWORD,
    ROS_L2TP_PSK,
    ROS_READY_INTERVAL_SECONDS,
    ROS_READY_TIMEOUT_SECONDS,
    ROS_SSH_PORT,
    ROS_WINBOX_PORT,
    RosBuildConfig,
    chr_image_path,
    fetch_server_ros_license,
    load_ros_config,
    render_ros_rsc,
    ros_l2tp_psk,
    ros_l2tp_username,
)
from HUAWEI.autobuy_helpers import huawei_autobuy_ready_summaries_by_plan

import_legacy_names(globals(), _shared, _cloud_client)


class DelayedActionPreconfirmAccepted(Exception):
    pass


class DelayedActionPreconfirmCancelled(Exception):
    pass


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

# 华为云日志单独存放，避免读取排错时混入天翼云或火山云地区。
APP_TITLE = "华为云管理"
APP_LOG_DIR = str(cloud_log_dir("HUAWEI"))
APP_RESULT_DIR = cloud_data_dir("HUAWEI") / "latest_results"
HUAWEI_STANDALONE_LIMITED_ENV = "WAWA_HUAWEI_STANDALONE_LIMITED"
MANAGER_AUTH_CODE_ENV = "WAWA_MANAGER_AUTH_CODE"
CUSTOMER_MANAGER_AUTH_PREFIX = "KHJL"
MANAGER_AUTH_API_URL = "https://www.wawaapi.com/api/software/manager-auth/huawei-permissions"
WT_INTERNAL_AUTH_CODE = "WT"
MANAGER_AUTH_PERMISSION_TIMEOUT_SECONDS = 5
CHECKED = "☑"
UNCHECKED = "☐"
PARTIAL_CHECKED = "◩"
LEGACY_CHECKED_VALUES = {"✓", "✔", CHECKED}
LEGACY_UNCHECKED_VALUES = {"□", "☐", UNCHECKED, ""}
TREE_CONTROL_SIZE = 15
TREE_CONTROL_GAP = 5
TREE_CONTROL_ROW_WIDTH = TREE_CONTROL_SIZE * 2 + TREE_CONTROL_GAP
HUAWEI_TREEVIEW_STYLE = "Huawei.Grid.Treeview"
HUAWEI_SSH_PORT_WAIT_TIMEOUT = 35
BUILD_MODE_DEFAULT = "default"
BUILD_MODE_RANDOM = "random"
BUILD_MODE_ROS = "ros"
HUAWEI_DEFAULT_SCRIPT_URL = "https://www.wawaapi.com/static/scripts/Huawei/fifteenhuawei-default.sh"
HUAWEI_DEFAULT_REMOTE_COMMAND = f"bash <(curl -fsSL {HUAWEI_DEFAULT_SCRIPT_URL})"
HUAWEI_RANDOM_SCRIPT_URL = "https://www.wawaapi.com/static/scripts/Huawei/fifteenhuawei.sh"
HUAWEI_RANDOM_REMOTE_COMMAND = f"bash <(curl -fsSL {HUAWEI_RANDOM_SCRIPT_URL})"
HUAWEI_20_DEFAULT_SCRIPT_URL = "https://www.wawaapi.com/static/scripts/Huawei/fifteenhuawei20-default.sh"
HUAWEI_20_DEFAULT_REMOTE_COMMAND = f"bash <(curl -fsSL {HUAWEI_20_DEFAULT_SCRIPT_URL})"
HUAWEI_20_RANDOM_SCRIPT_URL = "https://www.wawaapi.com/static/scripts/Huawei/fifteenhuawei20.sh"
HUAWEI_20_RANDOM_REMOTE_COMMAND = f"bash <(curl -fsSL {HUAWEI_20_RANDOM_SCRIPT_URL})"
HUAWEI_X1_1C1G_FLAVOR_ID = "x1.1u.1g"
HUAWEI_CUSTOM_CONNECTION_DEFAULT_USER = "user"
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；后续更换成功后会自动移除。"
USED_IP_CSV_TEMPLATE_NAME = "used_ip_table_template.csv"
HUAWEI_IP_RANGE_MANIFEST_URL = "https://www.wawaapi.com/YunTools/ip_ranges/huawei/manifest.json"
HUAWEI_IP_RANGE_CACHE_SECONDS = 6 * 60 * 60
HUAWEI_IP_SEGMENT_REPORT_URL = "https://www.wawaapi.com/api/software/huawei/ip-segments/report"
HUAWEI_IP_SEGMENT_REPORT_DELAY_SECONDS = 5.0
HUAWEI_IP_SEGMENT_REPORT_RETRY_SECONDS = 5 * 60.0
HUAWEI_IP_SEGMENT_REPORT_TIMEOUT_SECONDS = 20
HUAWEI_REGION_PROJECT_WARMUP_WAIT_SECONDS = 45
HUAWEI_EIP_REPLACE_ACTIVE_LIMIT = 4
HUAWEI_EIP_REPLACE_MAX_ACTIVE_LIMIT = 10
HUAWEI_EIP_REPLACE_RATE_LIMIT_ACTIVE_LIMIT = 2
HUAWEI_EIP_REPLACE_CONCURRENCY_RAISE_SUCCESSES = 8
HUAWEI_EIP_REPLACE_RATE_LIMIT_RECOVERY_SUCCESSES = 4
HUAWEI_EIP_REPLACE_REQUEST_INTERVAL_SECONDS = 0.25
HUAWEI_EIP_REPLACE_RATE_LIMIT_COOLDOWN_SECONDS = 65.0
HUAWEI_EIP_REPLACE_CANDIDATES_PER_SEGMENT = 2
HUAWEI_EIP_RESTORE_ACTIVE_LIMIT = 5
HUAWEI_EIP_RESTORE_MIN_ACTIVE_LIMIT = 1
HUAWEI_EIP_RESTORE_MAX_ATTEMPTS = 5
USED_IP_TABLE_LOCK_TIMEOUT_SECONDS = 15.0
USED_IP_TABLE_LOCK_POLL_SECONDS = 0.05
USED_IP_TABLE_PENDING_RETRY_SECONDS = 5.0
USED_IP_TABLE_PENDING_RETRY_ATTEMPTS = 120
USED_IP_RETENTION_DAYS = 2
REGION_ACCESS_TUTORIAL_GUIDANCE = (
    "请点击软件右上角“查看华为云获取Ak/Sk教程”在页面内“访问7地区”，获取该地区访问权限。"
)
ROS_RANDOM_PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
ROS_RANDOM_PASSWORD_LENGTH = 10
L2TP_RESULT_HEADER = "入口IP/账号/密码/密钥/对应出口IP"
SOCKS5_RESULT_HEADER = "IP/端口/用户名/密码"
SHANGHAI_NEW_PURCHASE_CONFIRM_ATTEMPTS = 8
SHANGHAI_NEW_PURCHASE_CONFIRM_DELAY_SECONDS = 8
SHANGHAI_NEW_PURCHASE_BUILD_RETRY_ATTEMPTS = 3
SHANGHAI_NEW_PURCHASE_BUILD_RETRY_DELAY_SECONDS = 20
_USED_IP_TABLE_PROCESS_LOCK = threading.RLock()
_USED_IP_TABLE_PENDING_WORKERS_LOCK = threading.Lock()
_USED_IP_TABLE_PENDING_WORKERS: Set[str] = set()
_HUAWEI_EIP_REGION_LOCKS_LOCK = threading.Lock()
_HUAWEI_EIP_REGION_LOCKS: Dict[str, threading.Lock] = {}


def _huawei_eip_region_lock(region: str) -> threading.Lock:
    key = str(region or "")
    with _HUAWEI_EIP_REGION_LOCKS_LOCK:
        lock = _HUAWEI_EIP_REGION_LOCKS.get(key)
        if lock is None:
            lock = threading.Lock()
            _HUAWEI_EIP_REGION_LOCKS[key] = lock
        return lock


@contextmanager
def _used_ip_table_file_lock(path: Path):
    target = Path(path)
    target.parent.mkdir(parents=True, exist_ok=True)
    lock_path = target.with_name(f"{target.name}.lock")
    with _USED_IP_TABLE_PROCESS_LOCK:
        with lock_path.open("a+b") as handle:
            handle.seek(0, os.SEEK_END)
            if handle.tell() == 0:
                handle.write(b"\0")
                handle.flush()
            deadline = time.monotonic() + USED_IP_TABLE_LOCK_TIMEOUT_SECONDS
            while True:
                handle.seek(0)
                try:
                    msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
                    break
                except OSError as exc:
                    if time.monotonic() >= deadline:
                        raise TimeoutError(f"旧IP库正在被其他软件进程写入，请稍后重试: {target}") from exc
                    time.sleep(USED_IP_TABLE_LOCK_POLL_SECONDS)
            try:
                yield
            finally:
                handle.seek(0)
                msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
        try:
            lock_path.unlink(missing_ok=True)
        except OSError:
            pass
HUAWEI_SCRIPT_VISIBLE_KEYWORDS = (
    ">> [0/9]",
    ">> [1/9]",
    ">> [2/9]",
    ">> [3/9]",
    ">> [4/9]",
    ">> [5/9]",
    ">> [6/9]",
    ">> [7/9]",
    ">> [8/9]",
    ">> [9/9]",
    "检测到物理网卡",
    "正在探测所有出口",
    "下载 xray",
    "Socks5格式",
    "L2格式",
    "IP/端口/用户名/密码",
    "IP/账号/密码/密钥/对应出口IP",
    "后台修改命令",
    "远程脚本退出码",
    "远程脚本已执行完成",
)


def huawei_standalone_limited_features() -> bool:
    if os.environ.get(HUAWEI_STANDALONE_LIMITED_ENV, "").strip() == "1":
        return True
    if limited_vendor_features_enabled("huawei"):
        return True
    legacy_vendor = os.environ.get("WAWA_SINGLE_VENDOR", "").strip().lower()
    return legacy_vendor in {"huawei", "huaweicloud", "hw"}
HUAWEI_REALTIME_ALWAYS_HIDDEN_PATTERNS = (
    r"^华为云地区项目纯API补全中(?:[，,].*)?$",
    r"^华为云地区项目纯API补全完成(?:[，,].*)?$",
    r"服务器IP段候选已处理.+改用随机申请。最后提示:\s*HTTP\s*409\b.*(?:EIP\.7910|Allocate\s+ip\s+from\s+\[5_bgp\]\s+failed)",
)
HUAWEI_REGION_PROJECT_WARNING_PREFIXES = (
    "华为云地区项目纯API补全后仍未识别",
    "华为云地区项目纯API补全失败",
)
HUAWEI_REALTIME_NOISE_PATTERNS = (
    r"^华为云AK/SK合作伙伴校验第\s*\d+/\d+\s*次[:：].+",
    r"^华为云AK/SK合作伙伴第\s*\d+\s*次返回非合作伙伴，正在进行第\s*\d+\s*次复核。",
    r"^\[[^\]]+\]\s*第\s*\d+/\d+\s*次失败[:：]",
    r"^\[[^\]]+\]\s*搭建第\s*\d+/\d+\s*次失败[，,]",
    r"自动购买后反查第\s*\d+/\d+\s*次失败[:：]",
    r"自动购买后反查第\s*\d+/\d+\s*次未就绪",
    r"复用资源检查失败，转入自动购买补齐流程",
    r"自动购买流程返回异常，将继续反查已创建资源",
    r"自动购买异常尾部日志",
    r"Traceback \(most recent call last\)",
    r"^\s*File \".*\", line \d+",
    r"huaweicloudsdk",
    r"^(?:自动购买:\s*)?\[[^\]]+\]\s*服务器密码[:：]",
    r"^(?:自动购买:\s*)?\[[^\]]+\]\s*cat\s+>\s+ip\.txt",
    r"^(?:自动购买:\s*)?\[[^\]]+\]\s*EOF$",
    r"^(?:自动购买:\s*)?\[[^\]]+\]\s*\d+\s+[0-9a-f]{2}(?::[0-9a-f]{2}){5}\s+\d{1,3}(?:\.\d{1,3}){3}",
)


def _is_huawei_realtime_always_hidden_line(line: str) -> bool:
    text = str(line or "").strip()
    return bool(text) and any(
        re.search(pattern, text, re.IGNORECASE)
        for pattern in HUAWEI_REALTIME_ALWAYS_HIDDEN_PATTERNS
    )


def _filter_huawei_realtime_always_hidden_lines(text: str) -> str:
    visible = [
        line
        for line in str(text or "").splitlines()
        if not _is_huawei_realtime_always_hidden_line(line)
    ]
    return "\n".join(visible) + ("\n" if visible else "")


def _is_huawei_realtime_noise_line(line: str) -> bool:
    """过滤华为云实时框里的中途重试和 SDK 原文，完整内容仍写入运行日志文件。"""
    text = str(line or "").strip()
    return _is_huawei_realtime_always_hidden_line(text) or (
        bool(text)
        and any(re.search(pattern, text, re.IGNORECASE) for pattern in HUAWEI_REALTIME_NOISE_PATTERNS)
    )


def filter_realtime_log_text(text: str) -> str:
    """华为实时日志额外展示搭建脚本关键输出，避免用户误判脚本停在探测公网IP。"""
    base = _base_filter_realtime_log_text(text)
    visible = [line for line in base.splitlines() if not _is_huawei_realtime_noise_line(line)]
    seen = {line.strip() for line in visible if line.strip()}
    for raw_line in str(text or "").splitlines():
        line = raw_line.rstrip()
        stripped = line.strip()
        if not stripped or stripped in seen:
            continue
        if any(stripped.startswith(prefix) for prefix in HUAWEI_REGION_PROJECT_WARNING_PREFIXES):
            display_line = clean_realtime_display_line(line)
            if display_line:
                visible.append(display_line)
                seen.add(display_line.strip())
            continue
        if any(keyword in stripped for keyword in HUAWEI_SCRIPT_VISIBLE_KEYWORDS):
            display_line = clean_realtime_display_line(line)
            if display_line:
                visible.append(display_line)
                seen.add(display_line.strip())
            continue
        if "/23456/root/" in stripped or re.search(r"/user\d+/wawacloud/wawacloud/", stripped):
            display_line = clean_realtime_display_line(line)
            if display_line:
                visible.append(display_line)
                seen.add(display_line.strip())
    return "\n".join(visible) + ("\n" if visible else "")


class RouterOsAccessError(RuntimeError):
    """RouterOS 管理账号或终端权限异常，需要重装系统恢复干净状态。"""


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中，正在校验合作伙伴归属，请稍等。"
CUSTOMER_CREDENTIAL_CHECKING_MESSAGE = "正在校验AK/SK，请稍等。"
CUSTOMER_CREDENTIAL_CHECKING_LOG_MESSAGE = "华为云查询AK/SK中，正在校验账号凭证，请稍等。"
CUSTOMER_CREDENTIAL_READY_MESSAGE = "AK/SK校验通过，账号资源读取完毕。"


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


class HuaweiAutoBuyApp(ttk.Frame):
    def __init__(self, master: Optional[tk.Misc] = None, *, embedded: bool = False) -> None:
        super().__init__(master)
        self.embedded = embedded
        self.standalone_limited_features = huawei_standalone_limited_features()
        if not embedded:
            top = self.winfo_toplevel()
            top.title(APP_TITLE)
            top.geometry("1240x840")
            top.minsize(1080, 720)
            apply_window_icon(top)

        self.busy = False
        self.stop_event = threading.Event()
        self.region_iids: Dict[str, str] = {}
        self.region_projects: Dict[str, ProjectPair] = {}
        self.region_row_meta: Dict[str, Dict[str, str]] = {}
        self.region_ecs_items: Dict[str, Dict[str, Any]] = {}
        self.region_stage: Dict[str, str] = {}
        self.tree_sort_reverse: Dict[str, bool] = {}
        self.last_results: List[RegionBuildResult] = []
        self.results_credential_fingerprint = ""
        self.manage_items: Dict[str, Dict[str, Any]] = {}
        self.manage_resource_items: List[Dict[str, Any]] = []
        self.confirmed_no_ecs_regions: Set[str] = set()

        self.ak_var = tk.StringVar()
        self.sk_var = tk.StringVar()
        self.mark_var = tk.StringVar()
        self.domain_var = tk.StringVar()
        self.ssh_port_var = tk.StringVar(value=DEFAULT_SSH_PORT)
        self.ssh_user_var = tk.StringVar(value=DEFAULT_SSH_USER)
        self.password_var = tk.StringVar(value=DEFAULT_PASSWORD)
        self.resource_prefix_var = tk.StringVar(value="")
        self.flavor_mode_var = tk.StringVar(value=FLAVOR_MODE_WAWACLOUD_LOW)
        self.purchase_eip_quota_var = tk.StringVar(value="")
        self.purchase_eip_quota_applied = False
        self.purchase_eip_quota_once_var = tk.BooleanVar(value=False)
        self.x1_flavor_var = tk.BooleanVar(value=False)
        self.x1_purchase_ip_count_var = tk.StringVar(value="")
        self.x1_purchase_ip_count_applied = False
        self.x1_purchase_ip_count_once_var = tk.BooleanVar(value=False)
        self.static_bgp_var = tk.BooleanVar(value=False)
        set_on_demand_debug_enabled("huawei", False)
        self.on_demand_billing_var = tk.BooleanVar(value=False)
        self.build_mode_var = tk.StringVar(value=BUILD_MODE_RANDOM)
        self.ros_build_var = tk.BooleanVar(value=False)
        self.default_conn_user_var = tk.StringVar(value=HUAWEI_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)
        ros_config = load_ros_config()
        self.ros_l2tp_username_var = tk.StringVar(value=ros_l2tp_username(ros_config))
        self.ros_ppp_password_var = tk.StringVar(value=ros_config.ppp_password or ROS_DEFAULT_PPP_PASSWORD)
        self.ros_config_custom_saved = False
        self.ros_config_once_var = tk.BooleanVar(value=False)
        self._one_time_default_conn_reset_pending = False
        self._one_time_ros_config_reset_pending = False
        self._one_time_x1_ip_count_reset_pending = False
        self._one_time_purchase_eip_quota_reset_pending = False
        self.manage_confirm_var = tk.BooleanVar(value=False)
        self.manage_only_main_var = tk.BooleanVar(value=True)
        self.manage_unsubscribe_type_var = tk.IntVar(value=1)
        self.manage_reason_var = tk.StringVar(value="不再使用")
        self.manage_last_clicked_iid = ""
        self.status_var = tk.StringVar(value="就绪")
        self.purchase_log_text: Optional[tk.Text] = None
        self.manage_log_text: Optional[tk.Text] = None
        self.log_text: Optional[tk.Text] = None
        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_after_id: Optional[str] = None
        self.customer_guard_token = 0
        self.manager_auth_partner_bypass_checked = False
        self.manager_auth_partner_bypass = False
        self.ip_segment_report_auth_code = self._current_manager_auth_code()
        self.ip_segment_report_timer: Optional[threading.Timer] = None
        self.ip_segment_report_timer_lock = threading.Lock()
        self.ip_segment_report_running = False
        self.region_project_warmup_fingerprint = ""
        self.region_project_warmup_running = False
        self.suspend_customer_guard_trace = False
        self.customer_guard_dialog = None
        self.ssh_channel = None
        self.ssh_channel_lock = threading.Lock()
        self.child_processes: Set[subprocess.Popen] = set()
        self.current_log_context = "purchase"
        self.active_task_contexts: Dict[str, int] = {}
        self.log_file_paths: Dict[str, str] = {}
        self.log_action_names: Dict[str, str] = {}
        self.huawei_api_network_diagnostics: List[str] = []
        self.huawei_api_network_diagnostic_seen: Set[str] = set()
        self.huawei_api_network_diagnostic_lock = threading.Lock()
        self.realtime_log_filter = RealtimeLogDisplayFilter()
        self.active_task_count = 0
        self.blocking_task_count = 0
        self.task_lock = threading.Lock()
        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_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.delayed_preconfirm_methods = {
            "reinstall_selected_region_servers",
            "start_purchase_selected_regions",
            "purchase_selected_region_new_ips",
            "start_build_selected_regions",
            "start_purchase_and_build",
            "clear_actionable_manage_resources",
            "reinstall_selected_manage_servers",
            "unsubscribe_selected_manage_resources",
            "unsubscribe_selected_region_resources",
            "replace_selected_region_ips",
        }
        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)

    def _manager_auth_skips_partner_check(self) -> bool:
        if self._current_manager_auth_code() != WT_INTERNAL_AUTH_CODE:
            return False
        if self.manager_auth_partner_bypass_checked:
            return self.manager_auth_partner_bypass

        self.manager_auth_partner_bypass_checked = True
        self.manager_auth_partner_bypass = False
        body = json.dumps(
            {
                "code": WT_INTERNAL_AUTH_CODE,
                "package": "huawei",
            },
            ensure_ascii=False,
        ).encode("utf-8")
        request = urllib.request.Request(
            MANAGER_AUTH_API_URL,
            data=body,
            method="POST",
            headers={
                "Content-Type": "application/json; charset=utf-8",
                "Accept": "application/json",
                "User-Agent": "WawaCloudHuaweiPartnerGuard/1.0",
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=MANAGER_AUTH_PERMISSION_TIMEOUT_SECONDS) as response:
                payload = json.loads(response.read().decode("utf-8-sig"))
        except (OSError, ValueError, urllib.error.URLError, urllib.error.HTTPError):
            return False
        self.manager_auth_partner_bypass = bool(
            isinstance(payload, dict)
            and payload.get("ok")
            and payload.get("valid")
            and payload.get("skip_huawei_partner_check") is True
        )
        return self.manager_auth_partner_bypass

    def _customer_guard_checking_message(self) -> str:
        if self._current_manager_auth_code() == WT_INTERNAL_AUTH_CODE:
            return CUSTOMER_CREDENTIAL_CHECKING_MESSAGE
        return CUSTOMER_OWNERSHIP_CHECKING_MESSAGE

    @staticmethod
    def _business_source_dirs() -> List[Path]:
        huawei_dir = Path(__file__).resolve().parent
        return [huawei_dir, huawei_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:
                    resolved = str(path.resolve())
                    tracked[resolved] = path.stat().st_mtime
                except OSError:
                    continue
        return tracked

    @staticmethod
    def _module_source_mtime(module: Any) -> Optional[float]:
        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 in {"HUAWEI.cloud_client", "HUAWEI.ros_builder"}:
            return -50
        if name == "HUAWEI.app":
            return 1000
        return 0

    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", "HUAWEI")
        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("HUAWEI.app")
        refreshed_class = getattr(refreshed_module, "HuaweiAutoBuyApp", 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:
            self.show_error(f"业务代码自动加载失败：{exc}")
            return False
        if not reloaded:
            return False
        method = getattr(self, method_name, None)
        if not callable(method):
            self.show_error(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) -> Callable[[], 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 in {"query_manage_resources", "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 = {
            "query_manage_resources": "读取账号资源",
            "reinstall_selected_region_servers": "重装选定云服务器",
            "start_purchase_selected_regions": "购买选择地区",
            "purchase_selected_region_new_ips": "购买选择地区新IP",
            "start_build_selected_regions": "搭建选择地区",
            "start_purchase_and_build": "一键购买并搭建选择地区",
            "clear_actionable_manage_resources": "一键清空账号资源",
            "reinstall_selected_manage_servers": "重装选定云服务器",
            "unsubscribe_selected_manage_resources": "退订/删除选定资源",
            "unsubscribe_selected_region_resources": "退订/删除选定资源",
            "replace_selected_region_ips": "更换新IP",
        }
        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
        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(str(message or "")).strip()
        if not text:
            return True
        self.delayed_queue_result_entries.append(
            {
                "token": token,
                "label": label,
                "message": text,
                "is_error": bool(is_error),
            }
        )
        return True

    def _queue_result_message_or_popup(self, message: str, *, is_error: bool = False) -> Optional[str]:
        text = str(message or "").strip()
        if not text:
            return None
        if threading.current_thread() is threading.main_thread():
            captured = self._record_delayed_queue_result_popup(text, is_error=is_error)
        else:
            captured = self.call_ui_sync(
                lambda: self._record_delayed_queue_result_popup(text, is_error=is_error)
            )
        return None if captured else text

    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,
                    "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 entries and not action_order:
            return
        entries_by_token: Dict[str, List[dict[str, Any]]] = {}
        for entry in entries:
            entries_by_token.setdefault(str(entry.get("token") or ""), []).append(entry)
        lines = ["华为云-队列任务执行完毕，任务结果："]
        used_tokens: set[str] = set()
        index = 1
        for token, label in action_order:
            token_entries = entries_by_token.get(token) or []
            if not token_entries:
                token_entries = [{"message": "已执行完成。", "is_error": False}]
            used_tokens.add(token)
            lines.append("")
            lines.append(f"{index}. {label}")
            for entry in token_entries:
                prefix = "错误：" if entry.get("is_error") else ""
                message = str(entry.get("message") or "").strip()
                if message:
                    lines.append(prefix + message)
            index += 1
        for entry in entries:
            token = str(entry.get("token") or "")
            if token in used_tokens:
                continue
            lines.append("")
            lines.append(f"{index}. {entry.get('label') or '当前任务'}")
            prefix = "错误：" if entry.get("is_error") else ""
            lines.append(prefix + str(entry.get("message") or "").strip())
            index += 1
        self.delayed_queue_result_suppress_dialog = True
        try:
            play_task_completion_sound()
            self._message_dialog("\n".join(lines))
        finally:
            self.delayed_queue_result_suppress_dialog = False

    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.delayed_queue_result_suppress_dialog = True
                try:
                    self.show_info(f"当前云延时执行队列已满，最多保留 6 个按序任务。\n\n{summary}\n\n请等待前面的任务完成后再添加。")
                finally:
                    self.delayed_queue_result_suppress_dialog = False
                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._choice_dialog(confirm_message, "确认并加入队列任务", "取消"):
                return None
            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队列会按顺序执行，前一个任务完成后自动开始下一个。")
            self._begin_delayed_queue_result_batch(self._current_delayed_action_label())
            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._start_delayed_queue_result_action(label)
        self._refresh_delayed_queue_status()
        try:
            action()
        finally:
            if not self._is_delayed_action_busy():
                finished_label = self.delayed_action_running_label or self.delayed_current_action_label
                self._finish_delayed_queue_result_action(finished_label)
                self.delayed_action_running_label = ""
                self.delayed_current_action_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)

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

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        body = ttk.Frame(self)
        body.grid(row=0, column=0, sticky="nsew")
        body.columnconfigure(0, weight=1)
        body.rowconfigure(0, weight=1)

        content = ttk.Frame(body)
        content.grid(row=0, column=0, sticky="nsew")
        content.columnconfigure(0, weight=1)
        content.rowconfigure(0, weight=1)

        pages = create_fixed_tab_pages(content, (("purchase", "购买搭建"), ("manage", "资源管理")))
        purchase_page = pages["purchase"]
        manage_page = pages["manage"]

        purchase_content, purchase_log_area = build_cloud_page_layout(purchase_page)
        purchase_content.rowconfigure(0, weight=0)
        purchase_content.rowconfigure(1, weight=0)
        purchase_content.rowconfigure(2, weight=1)
        self._build_top_panel(purchase_content)
        self._build_region_panel(purchase_content)
        self._build_output_panel(purchase_content)
        self._build_log_only_panel(purchase_log_area, "purchase_log_text")

        manage_content, manage_log_area = build_cloud_page_layout(manage_page)
        manage_content.rowconfigure(1, weight=1)
        self._build_manage_panel(manage_content)
        self._build_log_only_panel(manage_log_area, "manage_log_text")
        self._set_log_context("purchase")

    def _build_top_panel(self, parent: ttk.Frame) -> None:
        top = ttk.Frame(parent, padding=(0, 0, 0, SECTION_GAP))
        top.grid(row=0, column=0, sticky="ew")
        top.columnconfigure(0, weight=1)

        params = ttk.LabelFrame(top, text="任务选项", padding=PANEL_PADDING)
        params.grid(row=0, column=0, sticky="ew")
        params.columnconfigure(3, weight=1)
        ttk.Label(params, text="购买", width=4, anchor="center").grid(row=0, column=0, sticky="ew", padx=(2, 4), pady=(3, 1))
        ttk.Label(params, text="选项", width=4, anchor="center").grid(row=1, column=0, sticky="ew", padx=(2, 4), pady=(1, 3))
        tk.Frame(params, width=1, bg=BORDER_COLOR).grid(row=0, column=1, rowspan=2, sticky="ns", padx=(4, 8), pady=3)
        purchase_row1 = ttk.Frame(params)
        purchase_row1.grid(row=0, column=2, sticky="w", pady=(3, 1))
        low_flavor_radio = ttk.Radiobutton(
            purchase_row1,
            text="1vCPUs|1GiB",
            variable=self.flavor_mode_var,
            value=FLAVOR_MODE_WAWACLOUD_LOW,
        )
        low_flavor_radio.pack(side=tk.LEFT, padx=(0, 18))
        low_flavor_radio.bind("<Button-3>", self._show_purchase_eip_quota_dialog)
        if not self._standalone_limited_features():
            ttk.Radiobutton(purchase_row1, text="20(1vCPUs|1GiB)", variable=self.flavor_mode_var, value=FLAVOR_MODE_WAWACLOUD_20).pack(side=tk.LEFT)
        purchase_row2 = ttk.Frame(params)
        purchase_row2.grid(row=1, column=2, sticky="w", pady=(1, 3))
        ttk.Radiobutton(purchase_row2, text="2vCPUs|2GiB", variable=self.flavor_mode_var, value=FLAVOR_MODE_FIXED).pack(side=tk.LEFT, padx=(0, 18))
        if not self._standalone_limited_features():
            ttk.Checkbutton(purchase_row2, text="静态BGP(北上广贵)", variable=self.static_bgp_var).pack(side=tk.LEFT)
        purchase_flags = ttk.Frame(params)
        purchase_flags.grid(row=0, column=3, rowspan=2, sticky="e", padx=(18, 0))
        x1_check = ttk.Checkbutton(purchase_flags, text="X1", variable=self.x1_flavor_var)
        x1_check.grid(row=0, column=0, sticky="w", pady=(3, 1))
        x1_check.bind("<Button-3>", self._show_x1_purchase_ip_count_dialog)
        if not self._standalone_limited_features():
            ttk.Checkbutton(
                purchase_flags,
                text="按需",
                variable=self.on_demand_billing_var,
                command=self._sync_on_demand_billing,
            ).grid(row=1, column=0, sticky="w", pady=(1, 3))

        ttk.Label(params, text="搭建", width=4, anchor="center").grid(row=0, column=4, sticky="ew", padx=(28, 4), pady=(3, 1))
        ttk.Label(params, text="选项", width=4, anchor="center").grid(row=1, column=4, sticky="ew", padx=(28, 4), pady=(1, 3))
        tk.Frame(params, width=1, bg=BORDER_COLOR).grid(row=0, column=5, rowspan=2, sticky="ns", padx=(4, 8), pady=3)
        build_group = ttk.Frame(params)
        build_group.grid(row=0, column=6, rowspan=2, sticky="e")
        build_row1 = ttk.Frame(build_group)
        build_row1.grid(row=0, column=0, sticky="w", pady=(3, 1))
        ttk.Radiobutton(build_row1, text="随机连接账密", variable=self.build_mode_var, value=BUILD_MODE_RANDOM).pack(side=tk.LEFT)
        build_row2 = ttk.Frame(build_group)
        build_row2.grid(row=1, column=0, sticky="w", pady=(1, 3))
        default_radio = ttk.Radiobutton(
            build_row2,
            text="默认连接账密(右键自定义)",
            variable=self.build_mode_var,
            value=BUILD_MODE_DEFAULT,
        )
        default_radio.pack(side=tk.LEFT, padx=(0, 18))
        default_radio.bind("<Button-3>", self._show_default_connection_dialog)
        if not self._standalone_limited_features():
            ros_check = ttk.Checkbutton(build_row2, text="Ros镜像(右键自定义|L2TP)", variable=self.ros_build_var)
            ros_check.pack(side=tk.LEFT)
            ros_check.bind("<Button-3>", self._show_ros_options_dialog)

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

    def _static_bgp_purchase_enabled(self) -> bool:
        if self._standalone_limited_features():
            self.static_bgp_var.set(False)
            return False
        return bool(self.static_bgp_var.get())

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

    def _show_x1_purchase_ip_count_dialog(self, _event: Optional[tk.Event] = None) -> str:
        win = tk.Toplevel(self)
        win.title(f"{APP_TITLE}-X1配置")
        win.resizable(False, False)
        win.transient(self.winfo_toplevel())
        win.grab_set()
        apply_dialog_icon(win)
        win.columnconfigure(0, weight=1)

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

        value_var = tk.StringVar(value=self.x1_purchase_ip_count_var.get())
        once_var = tk.BooleanVar(value=bool(self.x1_purchase_ip_count_once_var.get()))
        entry = ttk.Entry(body, textvariable=value_var, width=28)
        entry.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 10))

        def apply() -> None:
            if value_var.get().strip() != "10":
                win.destroy()
                return
            self.x1_purchase_ip_count_var.set("10")
            self.x1_purchase_ip_count_applied = True
            self.x1_purchase_ip_count_once_var.set(bool(once_var.get()))
            win.destroy()

        ttk.Checkbutton(body, text="生效1次", variable=once_var).grid(row=1, column=0, sticky="w")
        button_bar = ttk.Frame(body)
        button_bar.grid(row=1, column=1, sticky="e")
        ttk.Button(button_bar, text="应用", width=10, command=apply).pack(side=tk.LEFT, padx=(0, 8))
        ttk.Button(button_bar, text="取消", width=10, command=win.destroy).pack(side=tk.LEFT)
        win.bind("<Return>", lambda _e: apply())
        win.bind("<Escape>", lambda _e: win.destroy())
        self._center_dialog(win, min_width=320, min_height=0)
        entry.focus_set()
        return "break"

    def _show_purchase_eip_quota_dialog(self, _event: Optional[tk.Event] = None) -> str:
        win = tk.Toplevel(self)
        win.title(f"{APP_TITLE}-EIP配额配置")
        win.resizable(False, False)
        win.transient(self.winfo_toplevel())
        win.grab_set()
        apply_dialog_icon(win)
        win.columnconfigure(0, weight=1)

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

        value_var = tk.StringVar(value=self.purchase_eip_quota_var.get())
        once_var = tk.BooleanVar(value=bool(self.purchase_eip_quota_once_var.get()))
        ttk.Label(body, text="输入EIP配额数量").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=(0, 10))
        entry = ttk.Entry(body, textvariable=value_var, width=20)
        entry.grid(row=0, column=1, sticky="ew", pady=(0, 10))

        def apply() -> None:
            try:
                quota = int(value_var.get().strip())
            except ValueError:
                win.destroy()
                return
            if quota <= 0 or quota % 5:
                win.destroy()
                return
            self.purchase_eip_quota_var.set(str(quota))
            self.purchase_eip_quota_applied = True
            self.purchase_eip_quota_once_var.set(bool(once_var.get()))
            win.destroy()

        ttk.Checkbutton(body, text="生效1次", variable=once_var).grid(row=1, column=0, sticky="w")
        button_bar = ttk.Frame(body)
        button_bar.grid(row=1, column=1, sticky="e")
        ttk.Button(button_bar, text="应用", width=10, command=apply).pack(side=tk.LEFT, padx=(0, 8))
        ttk.Button(button_bar, text="取消", width=10, command=win.destroy).pack(side=tk.LEFT)
        win.bind("<Return>", lambda _e: apply())
        win.bind("<Escape>", lambda _e: win.destroy())
        self._center_dialog(win, min_width=360, min_height=0)
        entry.focus_set()
        return "break"

    def _allow_sensitive_config_dialog(self) -> bool:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        fingerprint = self._credential_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 = "请先填写并通过K/SK合作伙伴校验后再打开配置。"
        self.show_error(message)
        return False

    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)
        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)

        username_var = tk.StringVar(value=self.default_conn_user_var.get().strip() or HUAWEI_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 HUAWEI_CUSTOM_CONNECTION_DEFAULT_USER
            password = password_var.get().strip() or SOCKS_PASSWORD
            port_text = port_var.get().strip() or str(SOCKS_PORT)
            if not re.fullmatch(r"[A-Za-z0-9_.-]{1,24}", username):
                self.show_info("用户名仅支持字母、数字、下划线、点和横线，长度1-24位。")
                return
            if not port_text.isdigit() or not (1 <= int(port_text) <= 65535):
                self.show_info("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"

    def _show_ros_options_dialog(self, _event: Optional[tk.Event] = None) -> str:
        if self._standalone_limited_features():
            return "break"
        if not self._allow_sensitive_config_dialog():
            return "break"
        win = tk.Toplevel(self)
        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)

        username_var = tk.StringVar(value=self.ros_l2tp_username_var.get().strip() or ROS_DEFAULT_L2TP_USERNAME)
        password_var = tk.StringVar(value=self.ros_ppp_password_var.get().strip() or ROS_DEFAULT_PPP_PASSWORD)
        once_var = tk.BooleanVar(value=bool(self.ros_config_once_var.get()))

        rows = [
            ("L2TP连接用户名", username_var, False, 0),
            ("L2TP连接密码", password_var, False, 1),
        ]
        for label, variable, show_secret, row in rows:
            ttk.Label(body, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
            entry = ttk.Entry(body, textvariable=variable, width=28, show="*" if show_secret else "")
            entry.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 ROS_DEFAULT_L2TP_USERNAME
            ppp_password = password_var.get().strip() or ROS_DEFAULT_PPP_PASSWORD
            if not re.fullmatch(r"[A-Za-z0-9_.-]{1,24}", username):
                self.show_info("L2TP连接用户名仅支持字母、数字、下划线、点和横线，长度1-24位。")
                return
            if any(ch.isspace() for ch in ppp_password) or "/" in ppp_password or '"' in ppp_password:
                self.show_info("L2TP连接密码不能包含空格、斜杠或双引号。")
                return
            self.ros_l2tp_username_var.set(username)
            self.ros_ppp_password_var.set(ppp_password)
            self.ros_config_custom_saved = True
            self.ros_config_once_var.set(bool(once_var.get()))
            win.destroy()

        ttk.Checkbutton(actions, text="生效1次", variable=once_var).grid(row=0, column=0, sticky="w")
        action_buttons = ttk.Frame(actions)
        action_buttons.grid(row=0, column=1, sticky="e")
        ttk.Button(action_buttons, text="保存", width=10, command=save).pack(side=tk.LEFT, padx=(0, 8))
        ttk.Button(action_buttons, text="取消", width=10, command=win.destroy).pack(side=tk.LEFT)
        win.bind("<Return>", lambda _e: save())
        win.bind("<Escape>", lambda _e: win.destroy())
        self._center_dialog(win, min_width=360, min_height=135)
        return "break"

    def _ros_connection_config_source(self) -> str:
        if self.ros_config_custom_saved:
            return "ros_custom"
        if self.default_conn_custom_saved:
            return "default_custom"
        if self.build_mode_var.get().strip() == BUILD_MODE_RANDOM:
            return "random"
        return "default"

    def _current_ros_config(self) -> RosBuildConfig:
        ros_enabled = self._ros_build_enabled()
        config_source = self._ros_connection_config_source() if ros_enabled else "default"
        if config_source == "default_custom":
            l2tp_username = self.default_conn_user_var.get().strip() or ROS_DEFAULT_L2TP_USERNAME
            ppp_password = self.default_conn_password_var.get().strip() or ROS_DEFAULT_PPP_PASSWORD
        else:
            l2tp_username = self.ros_l2tp_username_var.get().strip() or ROS_DEFAULT_L2TP_USERNAME
            ppp_password = self.ros_ppp_password_var.get().strip() or ROS_DEFAULT_PPP_PASSWORD
        if not re.fullmatch(r"[A-Za-z0-9_.-]{1,24}", l2tp_username):
            raise RuntimeError("L2TP连接用户名仅支持字母、数字、下划线、点和横线，长度1-24位。")
        if any(ch.isspace() for ch in ppp_password) or "/" in ppp_password or '"' in ppp_password:
            raise RuntimeError("L2TP连接密码不能包含空格、斜杠或双引号。")
        if ros_enabled and config_source == "random":
            ppp_password = "".join(
                secrets.choice(ROS_RANDOM_PASSWORD_ALPHABET)
                for _ in range(ROS_RANDOM_PASSWORD_LENGTH)
            )
        license_account = ""
        license_password = ""
        if ros_enabled:
            try:
                license_account, license_password = fetch_server_ros_license()
            except Exception as exc:
                raise RuntimeError(f"获取服务器ROS许可证失败: {exc}") from exc
        return RosBuildConfig(
            ppp_password=ppp_password,
            license_account=license_account,
            license_password=license_password,
            l2tp_username=l2tp_username,
            l2tp_psk=ppp_password,
        )

    def _start_ros_chr_prefetch(self) -> None:
        ros_builder = importlib.import_module("HUAWEI.ros_builder")
        try:
            if ros_builder.chr_image_available():
                self.after(0, self.log, "步骤: ROS CHR 镜像本地缓存已就绪。")
                return
        except Exception:
            pass

        self.after(
            0,
            self.log,
            "步骤: 本地未检测到完整 ROS CHR 镜像，已启动后台下载，当前任务继续执行。",
        )

        def emit(message: str) -> None:
            self.after(0, self.log, localize_region_tokens(message))

        def worker() -> None:
            try:
                ros_builder.ensure_chr_image(emit)
            except Exception as exc:
                self.after(
                    0,
                    self.log,
                    f"步骤: ROS CHR 镜像后台准备失败，搭建阶段将自动重试: {exc}",
                )

        threading.Thread(target=worker, daemon=True, name="huawei-ros-chr-prefetch").start()

    def _build_region_panel(self, parent: ttk.Frame) -> None:
        panel = ttk.Frame(parent, padding=(0, 0, 0, SECTION_GAP))
        panel.grid(row=1, column=0, sticky="ew")
        panel.columnconfigure(0, weight=1)
        panel.rowconfigure(0, weight=0)
        panel.rowconfigure(1, weight=0)
        panel.rowconfigure(2, weight=0)

        toolbar = ttk.Frame(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("query_manage_resources", silent=True, context="purchase")).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_region_servers")).pack(side=tk.LEFT)

        self.region_tree = self._make_tree(
            panel,
            columns=("checked", "label", "host_status", "resource_summary", "server_ip", "flavor", "expire_time"),
            headings=("选择", "地区", "状态", "资源", "服务器IP", "规格", "到期时间"),
            widths=(44, 65, 110, 190, 130, 125, 180),
            anchors=("center", "center", "center", "center", "center", "center", "center"),
            height=7,
            show_tree=True,
            tree_heading="选择",
            tree_width=60,
        )
        self.region_tree.configure(displaycolumns=("label", "host_status", "resource_summary", "server_ip", "flavor", "expire_time"))
        self.region_tree.bind("<ButtonRelease-1>", self._on_region_click)
        self.region_tree.bind("<<TreeviewOpen>>", self._on_region_tree_open_close)
        self.region_tree.bind("<<TreeviewClose>>", self._on_region_tree_open_close)
        for plan in REGION_PLANS:
            iid = plan.key
            self.region_iids[plan.key] = iid
            self.region_tree.insert(
                "",
                tk.END,
                iid=iid,
                text="",
                image=self._tree_control_image(CHECKED),
                values=(CHECKED, plan.label, "未读取", "", "", "", ""),
                tags=("evenrow" if len(self.region_iids) % 2 == 0 else "oddrow",),
            )
        selection_bar = ttk.Frame(panel)
        selection_bar.grid(row=2, column=0, sticky="ew", pady=(SECTION_GAP, 0))
        selection_bar.columnconfigure(5, weight=1)
        ttk.Button(selection_bar, text="全选地区", width=ACTION_BUTTON_WIDTH, command=lambda: self.set_all_regions(True)).grid(row=0, column=0, sticky="w", padx=(0, 6))
        ttk.Button(selection_bar, text="取消选择", width=ACTION_BUTTON_WIDTH, command=lambda: self.set_all_regions(False)).grid(row=0, column=1, sticky="w", padx=6)
        ttk.Button(selection_bar, text="购买选择地区", width=ACTION_BUTTON_WIDTH, command=self._hot_command("start_purchase_selected_regions")).grid(row=0, column=2, sticky="w", padx=6)
        ttk.Button(selection_bar, 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(selection_bar, text="搭建选择地区", width=ACTION_BUTTON_WIDTH, command=self._hot_command("start_build_selected_regions")).grid(row=0, column=4, sticky="w", padx=6)
        ttk.Button(selection_bar, text="一键购买并搭建选择地区", width=PURCHASE_BUILD_BUTTON_WIDTH, command=self._hot_command("start_purchase_and_build")).grid(row=0, column=6, sticky="e", padx=(6, 0))

    def _build_output_panel(self, parent: ttk.Frame) -> None:
        output_frame = ttk.Frame(parent)
        output_frame.grid(row=2, column=0, sticky="nsew")
        self.l2tp_text, self.socks_text = build_result_output_panel(
            output_frame,
            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),
        )

    def _build_log_only_panel(self, parent: ttk.Frame, attr_name: str) -> None:
        log_context = "manage" if attr_name == "manage_log_text" else "purchase"
        log_text, _export_button, _stop_button = build_realtime_log_panel(
            parent,
            export_command=lambda widget=None, ctx=log_context: self.export_log_text(getattr(self, attr_name), ctx),
            stop_command=self.request_stop,
        )
        setattr(self, attr_name, log_text)

    def _build_log_panel(self, parent: ttk.Frame) -> None:
        panel = ttk.PanedWindow(parent, orient=tk.HORIZONTAL)
        panel.grid(row=1, column=0, sticky="nsew")

        log_holder = ttk.Frame(panel, width=LOG_PANEL_WIDTH)
        log_holder.grid_propagate(False)
        self.log_text, _export_button, _stop_button = build_realtime_log_panel(
            log_holder,
            export_command=self.export_log_text,
            stop_command=self.request_stop,
        )
        output_frame = ttk.Frame(panel)
        output_frame.columnconfigure(0, weight=1)
        output_frame.rowconfigure(0, weight=1)
        self.l2tp_text, self.socks_text = build_result_output_panel(
            output_frame,
            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),
        )

        panel.add(output_frame, weight=4)
        panel.add(log_holder, weight=1)

    def _build_manage_panel(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(0, weight=1)
        parent.rowconfigure(1, weight=1)

        top_actions = ttk.Frame(parent)
        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("query_manage_resources")).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() and not self._customer_manage_eip_only():
            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)

        self.manage_tree = self._make_tree(
            parent,
            columns=("checked", "region_label", "kind", "resource_type", "name", "status", "billing", "operation", "server_id", "resource_id"),
            headings=("选择", "地区", "资源", "类型", "名称/IP", "状态", "计费", "可执行操作", "到期时间", "规格"),
            widths=(44, 75, 80, 95, 230, 210, 80, 170, 180, 135),
            anchors=("center", "center", "center", "center", "center", "center", "center", "center", "center", "center"),
            height=18,
            show_tree=True,
            tree_heading="选择",
            tree_width=60,
        )
        self.manage_tree.configure(displaycolumns=("region_label", "kind", "resource_type", "name", "status", "billing", "operation", "server_id", "resource_id"))
        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)

        bottom_actions = ttk.Frame(parent)
        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() and not self._customer_manage_eip_only():
            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
        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_manage_resources")).grid(row=0, column=next_col, sticky="e", padx=(6, 0), pady=4)

    def _build_status_bar(self) -> None:
        bar = ttk.Frame(self, padding=(10, 0, 10, 8))
        bar.grid(row=1, column=0, sticky="ew")
        bar.columnconfigure(0, weight=1)
        ttk.Label(bar, textvariable=self.status_var).grid(row=0, column=0, sticky="w")

    def _selected_flavor_mode_or_warn(self) -> str:
        flavor_mode = self.flavor_mode_var.get().strip()
        if self._standalone_limited_features() and flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            flavor_mode = FLAVOR_MODE_WAWACLOUD_LOW
            self.flavor_mode_var.set(flavor_mode)
        if flavor_mode not in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED, FLAVOR_MODE_WAWACLOUD_20}:
            self.show_info("请先选择规格。")
            return ""
        return flavor_mode

    def _current_flavor_mode_or_default(self) -> str:
        flavor_mode = self.flavor_mode_var.get() or FLAVOR_MODE_WAWACLOUD_LOW
        if self._standalone_limited_features() and flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            return FLAVOR_MODE_WAWACLOUD_LOW
        return flavor_mode

    @staticmethod
    def _effective_plan_for_flavor_mode(
        plan: RegionPlan,
        flavor_mode: str,
        *,
        x1_ten_ip_enabled: bool = False,
    ) -> RegionPlan:
        if flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            # 20 模式购买和搭建共用这套拓扑：2 个弹性网卡、2 个虚拟 IP、16 个辅助弹性网卡，搭建只上传16条辅助网卡 ip.txt。
            return replace(
                plan,
                aux_count=16,
                aux_start_octet=14,
                standard_nic_count=2,
                virtual_ip_count=2,
                virtual_ip_start_octet=12,
                aux_parent_primary=True,
            )
        if x1_ten_ip_enabled and plan.key not in {"east2", "qingdao"}:
            return replace(
                plan,
                aux_count=8,
                aux_start_octet=12,
                standard_nic_count=2,
                virtual_ip_count=0,
                virtual_ip_start_octet=0,
                aux_parent_primary=True,
            )
        return plan

    def _x1_ten_ip_purchase_enabled(self, x1_flavor_enabled: Optional[bool] = None) -> bool:
        x1_enabled = bool(self.x1_flavor_var.get()) if x1_flavor_enabled is None else bool(x1_flavor_enabled)
        return bool(
            x1_enabled
            and self.x1_purchase_ip_count_applied
            and self.x1_purchase_ip_count_var.get().strip() == "10"
        )

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

    @staticmethod
    def _connection_env(username: str, password: str, socks_port: str) -> Dict[str, str]:
        return {
            "VPN_USER_PREFIX": username,
            "VPN_PASSWORD": password,
            "VPN_IPSEC_PSK": password,
            "SOCKS_USER": username,
            "SOCKS_PASSWORD": password,
            "SOCKS_PASS": password,
            "SOCKS_PORT": socks_port,
            "XRAY_USER": username,
            "XRAY_PASS": password,
            "XRAY_PORT": socks_port,
            "PSK_KEY": password,
            "VPN_PASS": password,
            "L2TP_PSK": password,
            "L2TP_PASS": password,
        }

    def _default_connection_env(self) -> Dict[str, str]:
        username = self.default_conn_user_var.get().strip() or HUAWEI_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)
        env = self._connection_env(username, password, socks_port)
        env["VPN_USER_PREFIX"] = "user"
        return env

    @staticmethod
    def _random_connection_env() -> Dict[str, str]:
        env = HuaweiAutoBuyApp._connection_env(secrets.token_hex(2), secrets.token_hex(3), str(SOCKS_PORT))
        env["VPN_USER_PREFIX"] = "user"
        return env

    def _task_connection_env(self) -> Dict[str, str]:
        mode = self.build_mode_var.get().strip() or BUILD_MODE_RANDOM
        if mode == BUILD_MODE_DEFAULT:
            if not self.default_conn_custom_saved:
                return {}
            return self._default_connection_env()
        return self._random_connection_env()

    @staticmethod
    def _apply_connection_env_to_result(result: RegionBuildResult, env: Dict[str, str]) -> None:
        if not env:
            return
        l2tp_username = str(env.get("VPN_USER_PREFIX") or "").strip()
        result.socks_user = str(env.get("SOCKS_USER") or env.get("XRAY_USER") or "").strip()
        result.socks_password = str(env.get("SOCKS_PASSWORD") or env.get("SOCKS_PASS") or "").strip()
        result.socks_port = str(env.get("SOCKS_PORT") or env.get("XRAY_PORT") or "").strip()
        result.l2tp_password = str(env.get("VPN_PASSWORD") or env.get("L2TP_PASS") or "").strip()
        result.l2tp_psk = str(env.get("VPN_IPSEC_PSK") or env.get("L2TP_PSK") or "").strip()
        if l2tp_username:
            setattr(result, "_huawei_l2tp_username", l2tp_username)
        if result.l2tp_password:
            setattr(result, "_huawei_l2tp_password", result.l2tp_password)
        if result.l2tp_psk:
            setattr(result, "_huawei_l2tp_psk", result.l2tp_psk)

    def _default_connection_confirm_details(self) -> List[str]:
        if self._ros_build_enabled() or not self.default_conn_custom_saved or self.build_mode_var.get().strip() != 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"L2TP账号：{env['VPN_USER_PREFIX']}1、{env['VPN_USER_PREFIX']}2...",
            f"密码：{env['SOCKS_PASSWORD']}（L2TP PSK密钥相同）",
            f"Socks5端口：{env['SOCKS_PORT']}",
        ]

    def _reset_default_connection_config(self) -> None:
        self.default_conn_user_var.set(HUAWEI_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_ros_connection_config(self) -> None:
        self.ros_l2tp_username_var.set(ROS_DEFAULT_L2TP_USERNAME)
        self.ros_ppp_password_var.set(ROS_DEFAULT_PPP_PASSWORD)
        self.ros_config_custom_saved = False
        self.ros_config_once_var.set(False)

    def _reset_x1_purchase_ip_count_config(self) -> None:
        self.x1_purchase_ip_count_var.set("")
        self.x1_purchase_ip_count_applied = False
        self.x1_purchase_ip_count_once_var.set(False)

    def _reset_purchase_eip_quota_config(self) -> None:
        self.purchase_eip_quota_var.set("")
        self.purchase_eip_quota_applied = False
        self.purchase_eip_quota_once_var.set(False)

    def _mark_one_time_x1_ip_count_reset(self, *, x1_ten_ip_enabled: bool) -> None:
        self._one_time_x1_ip_count_reset_pending = bool(
            x1_ten_ip_enabled
            and self.x1_purchase_ip_count_once_var.get()
        )

    def _reset_one_time_x1_ip_count_after_task(self) -> None:
        try:
            if (
                self._one_time_x1_ip_count_reset_pending
                and self.x1_purchase_ip_count_applied
                and self.x1_purchase_ip_count_once_var.get()
            ):
                self._reset_x1_purchase_ip_count_config()
        finally:
            self._one_time_x1_ip_count_reset_pending = False

    def _purchase_eip_quota_for_task(self, flavor_mode: str) -> int:
        if flavor_mode != FLAVOR_MODE_WAWACLOUD_LOW or not self.purchase_eip_quota_applied:
            return 0
        try:
            quota = int(self.purchase_eip_quota_var.get().strip())
        except ValueError:
            return 0
        return quota if quota > 0 and quota % 5 == 0 else 0

    @staticmethod
    def _resource_set_count_for_eip_quota(plan: RegionPlan, eip_quota: int) -> int:
        if not eip_quota:
            return 1
        return max(1, eip_quota // 10)

    def _mark_one_time_purchase_eip_quota_reset(self, *, eip_quota: int) -> None:
        self._one_time_purchase_eip_quota_reset_pending = bool(
            eip_quota
            and self.purchase_eip_quota_once_var.get()
        )

    def _reset_one_time_purchase_eip_quota_after_task(self) -> None:
        try:
            if (
                self._one_time_purchase_eip_quota_reset_pending
                and self.purchase_eip_quota_applied
                and self.purchase_eip_quota_once_var.get()
            ):
                self._reset_purchase_eip_quota_config()
        finally:
            self._one_time_purchase_eip_quota_reset_pending = False

    def _mark_one_time_build_config_reset(self, *, build_used: bool, ros_build_enabled: bool) -> None:
        mode = self.build_mode_var.get().strip() or BUILD_MODE_RANDOM
        ros_config_source = self._ros_connection_config_source() if ros_build_enabled else ""
        self._one_time_default_conn_reset_pending = bool(
            build_used
            and self.default_conn_custom_saved
            and self.default_conn_once_var.get()
            and (
                (not ros_build_enabled and mode == BUILD_MODE_DEFAULT)
                or (ros_build_enabled and ros_config_source == "default_custom")
            )
        )
        self._one_time_ros_config_reset_pending = bool(
            build_used
            and ros_build_enabled
            and ros_config_source == "ros_custom"
            and self.ros_config_custom_saved
            and self.ros_config_once_var.get()
        )

    def _reset_one_time_build_config_after_task(self) -> None:
        try:
            if self._one_time_default_conn_reset_pending and self.default_conn_custom_saved and self.default_conn_once_var.get():
                self._reset_default_connection_config()
            if self._one_time_ros_config_reset_pending and self.ros_config_custom_saved and self.ros_config_once_var.get():
                self._reset_ros_connection_config()
        finally:
            self._one_time_default_conn_reset_pending = False
            self._one_time_ros_config_reset_pending = False

    def _default_connection_command(self, command: str, env: Optional[Dict[str, str]] = None) -> str:
        env_values = env or self._default_connection_env()
        if command == HUAWEI_DEFAULT_REMOTE_COMMAND:
            return configured_default_script_command(HUAWEI_DEFAULT_SCRIPT_URL, env_values)
        if command == HUAWEI_20_DEFAULT_REMOTE_COMMAND:
            return configured_default_script_command(HUAWEI_20_DEFAULT_SCRIPT_URL, env_values)
        return f"{self._shell_env_assignments(env_values)} {command}"

    def _random_connection_command(self, command: str, flavor_mode: str, env: Dict[str, str]) -> str:
        settings_file = (
            "/etc/wawacloud/fifteenhuawei20.conf"
            if flavor_mode == FLAVOR_MODE_WAWACLOUD_20
            else "/etc/wawacloud/settings.conf"
        )
        assignments = self._shell_env_assignments(env)
        settings_rows = " ".join(
            (
                '"VPN_IPSEC_PSK=\\"$VPN_IPSEC_PSK\\""',
                '"VPN_USER_PREFIX=\\"$VPN_USER_PREFIX\\""',
                '"VPN_PASSWORD=\\"$VPN_PASSWORD\\""',
                '"SOCKS_USER=\\"$SOCKS_USER\\""',
                '"SOCKS_PASSWORD=\\"$SOCKS_PASSWORD\\""',
                '"SOCKS_PORT=\\"$SOCKS_PORT\\""',
            )
        )
        return (
            f"{assignments}; umask 077; install -d -m 700 /etc/wawacloud; "
            f"printf '%s\\n' {settings_rows} > {settings_file}; {command}"
        )

    def _ros_build_enabled(self) -> bool:
        if self._standalone_limited_features():
            return False
        return bool(self.ros_build_var.get())

    def _remote_command_for_flavor_mode(
        self,
        flavor_mode: str,
        task_connection_env: Optional[Dict[str, str]] = None,
    ) -> Tuple[Optional[str], str, Dict[str, str]]:
        mode = self.build_mode_var.get().strip() or BUILD_MODE_DEFAULT
        flavor_label = flavor_mode_label(flavor_mode)
        default_command = HUAWEI_20_DEFAULT_REMOTE_COMMAND if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 else HUAWEI_DEFAULT_REMOTE_COMMAND
        random_command = HUAWEI_20_RANDOM_REMOTE_COMMAND if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 else HUAWEI_RANDOM_REMOTE_COMMAND
        if mode == BUILD_MODE_DEFAULT:
            if not self.default_conn_custom_saved:
                return default_command, f"{flavor_label} 默认连接配置", {}
            env = dict(task_connection_env or self._default_connection_env())
            return self._default_connection_command(default_command, env), f"{flavor_label} 默认连接配置", env
        if mode == BUILD_MODE_RANDOM:
            env = dict(task_connection_env or self._random_connection_env())
            return self._random_connection_command(random_command, flavor_mode, env), f"{flavor_label} 随机连接账密", env
        env = dict(task_connection_env or self._random_connection_env())
        return self._random_connection_command(random_command, flavor_mode, env), f"{flavor_label} 随机连接账密", env

    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 = 160,
    ) -> ttk.Treeview:
        frame = tk.Frame(parent, bg=TABLE_BORDER_COLOR, highlightthickness=1, highlightbackground=TABLE_BORDER_COLOR, highlightcolor=TABLE_BORDER_COLOR)
        frame.grid(row=1, column=0, sticky="nsew")
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=1)
        tree_style = self._ensure_tree_control_style() if show_tree else "Grid.Treeview"
        tree = ttk.Treeview(frame, columns=columns, show=("tree headings" if show_tree else "headings"), height=height, style=tree_style)
        tree.grid(row=0, column=0, sticky="nsew", padx=1, pady=1)
        tree.tag_configure("oddrow", background=TABLE_ALT_BG)
        tree.tag_configure("evenrow", background=TEXT_BG)
        tree.tag_configure("regiongroup", background=TABLE_GROUP_BG)
        if show_tree:
            tree.heading("#0", text=tree_heading, anchor="center")
            tree.column("#0", width=tree_width, minwidth=min(tree_width, 120), stretch=False, anchor="center")
        anchor_map = {column: anchors[index] for index, column in enumerate(columns)} if anchors else {}
        for column, heading, width in zip(columns, headings, widths):
            anchor = anchor_map.get(column, "w")
            tree.heading(column, text=heading, anchor=anchor, command=lambda col=column, tv=tree: self._sort_tree_column(tv, col))
            minwidth = 25 if column == "checked" else 50
            tree.column(
                column,
                width=width,
                minwidth=minwidth,
                stretch=column not in {"checked", "label", "host_status", "server_ip", "expire_time", "flavor"},
                anchor=anchor,
            )
        y_scroll = ttk.Scrollbar(frame, command=tree.yview, style="App.Vertical.TScrollbar")
        y_scroll.grid(row=0, column=1, sticky="ns")
        x_scroll = ttk.Scrollbar(frame, orient=tk.HORIZONTAL, command=tree.xview, style="App.Horizontal.TScrollbar")
        x_scroll.grid(row=1, column=0, sticky="ew")
        tree.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
        return tree

    @staticmethod
    def _paint_tree_control_box(
        image: tk.PhotoImage,
        *,
        x_offset: int = 0,
        symbol: str = "",
    ) -> None:
        size = TREE_CONTROL_SIZE
        border = "#555555"
        foreground = "#202020"
        image.put("#ffffff", to=(x_offset, 0, x_offset + size, size))
        image.put(border, to=(x_offset, 0, x_offset + size, 1))
        image.put(border, to=(x_offset, size - 1, x_offset + size, size))
        image.put(border, to=(x_offset, 0, x_offset + 1, size))
        image.put(border, to=(x_offset + size - 1, 0, x_offset + size, size))
        if symbol in {"+", "-", "partial"}:
            image.put(foreground, to=(x_offset + 3, 7, x_offset + 12, 8))
        if symbol == "+":
            image.put(foreground, to=(x_offset + 7, 3, x_offset + 8, 12))
        elif symbol == "check":
            for x, y in ((3, 7), (4, 8), (5, 9), (6, 8), (7, 7), (8, 6), (9, 5), (10, 4), (11, 4)):
                px = x_offset + x
                image.put(foreground, to=(px, y, px + 1, y + 1))
            for x, y in ((4, 7), (5, 8), (6, 7), (7, 6), (8, 5), (9, 4), (10, 3)):
                px = x_offset + x
                image.put(foreground, to=(px, y, px + 1, y + 1))

    def _ensure_tree_control_style(self) -> str:
        owner = self.winfo_toplevel()
        assets = getattr(owner, "_huawei_tree_control_assets", None)
        if not isinstance(assets, dict):
            assets = {}
            check_symbols = {
                "unchecked": "",
                "checked": "check",
                "partial": "partial",
            }
            for toggle_state, toggle_symbol in (("none", ""), ("closed", "+"), ("open", "-")):
                for check_state, check_symbol in check_symbols.items():
                    image = tk.PhotoImage(
                        master=owner,
                        width=TREE_CONTROL_ROW_WIDTH,
                        height=TREE_CONTROL_SIZE,
                    )
                    if toggle_symbol:
                        self._paint_tree_control_box(image, symbol=toggle_symbol)
                    self._paint_tree_control_box(
                        image,
                        x_offset=TREE_CONTROL_SIZE + TREE_CONTROL_GAP,
                        symbol=check_symbol,
                    )
                    assets[f"{toggle_state}_{check_state}"] = image
            setattr(owner, "_huawei_tree_control_assets", assets)

        style = ttk.Style(self)
        base_config = style.configure("Grid.Treeview")
        if base_config:
            style.configure(HUAWEI_TREEVIEW_STYLE, **base_config)
        style.configure(HUAWEI_TREEVIEW_STYLE, foreground="#202020")
        base_map = style.map("Grid.Treeview")
        if base_map:
            style.map(HUAWEI_TREEVIEW_STYLE, **base_map)
        heading_config = style.configure("Grid.Treeview.Heading")
        if heading_config:
            style.configure(f"{HUAWEI_TREEVIEW_STYLE}.Heading", **heading_config)
        style.layout(
            f"{HUAWEI_TREEVIEW_STYLE}.Item",
            [
                (
                    "Treeitem.padding",
                    {
                        "sticky": "nswe",
                        "children": [
                            ("Treeitem.image", {"side": "left", "sticky": ""}),
                            ("Treeitem.text", {"sticky": "nswe"}),
                        ],
                    },
                )
            ],
        )
        return HUAWEI_TREEVIEW_STYLE

    def _tree_control_image(
        self,
        value: Any,
        *,
        expandable: bool = False,
        opened: bool = False,
    ) -> tk.PhotoImage:
        owner = self.winfo_toplevel()
        assets = getattr(owner, "_huawei_tree_control_assets", None)
        if not isinstance(assets, dict):
            self._ensure_tree_control_style()
            assets = getattr(owner, "_huawei_tree_control_assets")
        marker = str(value or "")
        if marker == PARTIAL_CHECKED:
            check_state = "partial"
        elif self._is_checked_marker(marker):
            check_state = "checked"
        else:
            check_state = "unchecked"
        toggle_state = "open" if expandable and opened else "closed" if expandable else "none"
        return assets[f"{toggle_state}_{check_state}"]

    def _refresh_tree_row_control(
        self,
        tree: ttk.Treeview,
        iid: str,
        value: Optional[str] = None,
    ) -> None:
        if not tree.exists(iid):
            return
        values = list(tree.item(iid, "values"))
        marker = str(value if value is not None else (values[0] if values else UNCHECKED))
        expandable = bool(tree.get_children(iid))
        tree.item(
            iid,
            text="",
            image=self._tree_control_image(
                marker,
                expandable=expandable,
                opened=expandable and bool(tree.item(iid, "open")),
            ),
        )

    @staticmethod
    def _tree_control_hit(tree: ttk.Treeview, row_id: str, event: tk.Event) -> str:
        bbox = tree.bbox(row_id, "#0")
        if not bbox:
            return ""
        cell_x, cell_y, cell_width, cell_height = bbox
        scan_y = cell_y + max(0, cell_height // 2)
        image_xs = [
            x
            for x in range(cell_x, cell_x + cell_width)
            if "image" in str(tree.identify("element", x, scan_y) or "").lower()
        ]
        if not image_xs:
            return ""
        offset = int(getattr(event, "x", 0)) - min(image_xs)
        if 0 <= offset < TREE_CONTROL_SIZE and tree.get_children(row_id):
            return "toggle"
        checkbox_x = TREE_CONTROL_SIZE + TREE_CONTROL_GAP
        if checkbox_x <= offset < checkbox_x + TREE_CONTROL_SIZE:
            return "check"
        return ""

    def _sort_tree_column(self, tree: ttk.Treeview, column: str) -> None:
        reverse = self.tree_sort_reverse.get(column, False)
        self.tree_sort_reverse[column] = not reverse
        rows = [(str(tree.set(iid, column)), iid) for iid in tree.get_children("")]
        rows.sort(key=lambda item: item[0], reverse=reverse)
        for index, (_value, iid) in enumerate(rows):
            tree.move(iid, "", index)

    @staticmethod
    def _is_checked_marker(value: Any) -> bool:
        return str(value or "") in LEGACY_CHECKED_VALUES

    def _toggle_checked_marker(self, value: Any) -> str:
        return UNCHECKED if self._is_checked_marker(value) else CHECKED

    def _on_region_click(self, event: tk.Event) -> None:
        region = self.region_tree.identify("region", event.x, event.y)
        if region not in {"cell", "tree"}:
            return
        row_id = self.region_tree.identify_row(event.y)
        if not row_id:
            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 self._tree_control_hit(self.region_tree, row_id, event) == "toggle":
                self._set_region_group_open(
                    row_id,
                    not bool(self.region_tree.item(row_id, "open")),
                )
                return
            column_name = "checked"
        values = list(self.region_tree.item(row_id, "values"))
        if column_name == "checked":
            target = self._toggle_checked_marker(values[0] if values else "")
            if row_id in REGION_BY_KEY:
                self._set_region_group_checked(row_id, target)
            else:
                self._set_region_row_checked(row_id, target)
                parent_iid = self.region_tree.parent(row_id)
                if parent_iid:
                    self._refresh_region_group_check(parent_iid)
            return
        if column_name == "server_ip":
            server_ip = str(self.region_tree.set(row_id, "server_ip") or "").strip()
            if server_ip:
                self.clipboard_clear()
                self.clipboard_append(server_ip)
                self.status_var.set(f"已复制服务器IP：{server_ip}")

    def paste_and_extract(self) -> None:
        try:
            text = self.clipboard_get()
        except tk.TclError:
            text = ""
        if text:
            self.raw_text.delete("1.0", tk.END)
            self.raw_text.insert("1.0", text)
        self.extract_from_raw_box()

    def extract_from_raw_box(self) -> None:
        raw = self.raw_text.get("1.0", tk.END)
        ak, sk = extract_ak_sk(raw)
        mark = extract_mark(raw, "huawei")
        if not ak or not sk:
            self.show_info("未识别到完整 AK/SK。请粘贴访问密钥ID和Secret Key。")
            return
        new_fingerprint = self._credential_fingerprint(ak, sk)
        if new_fingerprint != self.results_credential_fingerprint:
            self.prepare_for_new_credentials()
        else:
            self.log("AK/SK未变化，保留L2TP/SK5结果框内容。")
        self.ak_var.set(ak)
        self.sk_var.set(sk)
        if mark:
            self.mark_var.set(mark)
        self.results_credential_fingerprint = new_fingerprint
        self.after(300, lambda: self.query_manage_resources(silent=True, context="purchase"))

    @staticmethod
    def _clean_account_mark(value: Any) -> str:
        # 中文注释：User(可编辑) 要给用户看，保留中文、字母、数字和常见账号符号，去掉空白和外层标点。
        text = "".join(str(value or "").split()).strip(" ,;:/\\|，；：、\"'<>[](){}")
        return text[:80]

    @classmethod
    def _first_account_mark_from_payload(cls, data: Any) -> str:
        preferred_keys = {
            "account",
            "account_name",
            "domainname",
            "domain_name",
            "username",
            "user_name",
            "name",
            "displayname",
            "display_name",
        }
        if isinstance(data, dict):
            domains = data.get("domains")
            if isinstance(domains, list):
                for domain in domains:
                    if isinstance(domain, dict):
                        value = cls._clean_account_mark(domain.get("name") or domain.get("domain_name"))
                        if value:
                            return value
            for key, value in data.items():
                if str(key).replace("-", "_").replace(" ", "_").lower() in preferred_keys and not isinstance(value, (dict, list)):
                    mark = cls._clean_account_mark(value)
                    if mark:
                        return mark
            for value in data.values():
                mark = cls._first_account_mark_from_payload(value)
                if mark:
                    return mark
        elif isinstance(data, list):
            for value in data:
                mark = cls._first_account_mark_from_payload(value)
                if mark:
                    return mark
        return ""

    def resolve_mark_from_credentials(self, ak: str, sk: str) -> str:
        """根据华为云 AK/SK 自动读取账号名，用于填充 User(可编辑)。"""
        ak_value = (ak or "").strip()
        sk_value = (sk or "").strip()
        if not ak_value or not sk_value:
            return ""
        client = HuaweiCloudClient(
            ak_value,
            sk_value,
            self.domain_var.get().strip(),
            network_diagnostic=self._record_huawei_api_network_diagnostic,
        )
        _status, data, _raw = client.request(
            "GET",
            f"{IAM_ENDPOINT}/v3/auth/domains",
            content_type="application/json;charset=utf8",
            timeout=20,
            retry_attempts=2,
        )
        return self._first_account_mark_from_payload(data)

    def clear_credentials(self) -> None:
        self.raw_text.delete("1.0", tk.END)
        self.ak_var.set("")
        self.sk_var.set("")
        self.mark_var.set("")
        self.domain_var.set("")

    def prepare_for_new_credentials(self) -> None:
        self._reset_customer_guard_state()
        self.flavor_mode_var.set(FLAVOR_MODE_WAWACLOUD_LOW)
        self.last_results = []
        self.results_credential_fingerprint = ""
        self.results_by_region = {}
        self.region_projects = {}
        self.region_row_meta = {}
        self.region_ecs_items = {}
        self.region_stage = {}
        self.manage_resource_items = []
        self.confirmed_no_ecs_regions = set()
        self.clear_results()
        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(""):
            for child_iid in self.region_tree.get_children(iid):
                self.region_tree.delete(child_iid)
            values = list(self.region_tree.item(iid, "values"))
            while len(values) < 7:
                values.append("")
            values[1] = str(self.region_tree.set(iid, "label") or (REGION_BY_KEY.get(iid).label if iid in REGION_BY_KEY else ""))
            values[2] = ""
            values[3] = ""
            values[4] = ""
            values[5] = ""
            values[6] = ""
            self.region_tree.item(iid, open=False, values=values)
            self._refresh_tree_row_control(self.region_tree, iid)
        self.populate_manage_tree([])
        self.status_var.set("已切换账号，正在读取资源")

    def set_credentials_silently(self, ak: str, sk: str, mark: str = "") -> None:
        old_fingerprint = self._credential_fingerprint()
        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
        new_fingerprint = self._credential_fingerprint()
        if new_fingerprint != old_fingerprint:
            self._reset_customer_guard_state()
            if self._credentials_ready_for_customer_guard(self.ak_var.get(), self.sk_var.get()):
                self._schedule_customer_guard_check(delay_ms=100, force=True)

    def set_all_regions(self, checked: bool) -> None:
        value = CHECKED if checked else UNCHECKED
        for iid in self.region_tree.get_children(""):
            self._set_region_group_checked(iid, value)

    def _set_region_row_checked(self, iid: str, value: str) -> None:
        values = list(self.region_tree.item(iid, "values"))
        if not values:
            return
        values[0] = value
        self.region_tree.item(iid, values=values)
        self._refresh_tree_row_control(self.region_tree, iid, value)

    def _set_region_group_checked(self, group_iid: str, value: str) -> None:
        self._set_region_row_checked(group_iid, value)
        for child_iid in self.region_tree.get_children(group_iid):
            self._set_region_row_checked(child_iid, value)

    def _refresh_region_group_check(self, group_iid: str) -> None:
        child_iids = list(self.region_tree.get_children(group_iid))
        if not child_iids:
            return
        checked_count = sum(
            1
            for child_iid in child_iids
            if self._is_checked_marker(self.region_tree.set(child_iid, "checked"))
        )
        if checked_count == len(child_iids):
            marker = CHECKED
        elif checked_count:
            marker = PARTIAL_CHECKED
        else:
            marker = UNCHECKED
        self._set_region_row_checked(group_iid, marker)

    def _set_region_group_open(self, group_iid: str, opened: bool) -> None:
        self.region_tree.item(group_iid, open=opened)
        self._sync_region_group_toggle_text(group_iid)

    def _sync_region_group_toggle_text(self, group_iid: str) -> None:
        self._refresh_tree_row_control(self.region_tree, group_iid)

    def _on_region_tree_open_close(self, _event: tk.Event) -> None:
        selected = self.region_tree.focus()
        if selected in REGION_BY_KEY:
            self._sync_region_group_toggle_text(selected)

    def selected_plans(self) -> List[RegionPlan]:
        plans: List[RegionPlan] = []
        for iid in self.region_tree.get_children(""):
            values = self.region_tree.item(iid, "values")
            if values and self._is_checked_marker(values[0]) and iid in REGION_BY_KEY:
                plans.append(REGION_BY_KEY[iid])
        return plans

    def selected_region_server_items(self) -> List[Dict[str, str]]:
        items: List[Dict[str, str]] = []
        for iid in self.region_tree.get_children(""):
            if iid not in REGION_BY_KEY:
                continue
            plan = REGION_BY_KEY[iid]
            child_iids = list(self.region_tree.get_children(iid))
            if child_iids:
                for child_iid in child_iids:
                    values = self.region_tree.item(child_iid, "values")
                    if not values or not self._is_checked_marker(values[0]):
                        continue
                    ecs_item = self.region_ecs_items.get(child_iid) or {}
                    server_id = str(ecs_item.get("_server_id") or ecs_item.get("_resource_id") or "")
                    project_id = str(ecs_item.get("_project_id") or "")
                    if not server_id or not project_id:
                        continue
                    server_ip = str(ecs_item.get("_public_ip") or "")
                    server_name = str(ecs_item.get("_name") or server_id)
                    items.append(
                        {
                            "region": plan.region,
                            "project_id": project_id,
                            "server_id": server_id,
                            "server_ip": server_ip,
                            "server_name": server_name,
                            "label": f"{plan.label} / {server_name}",
                            "region_label": plan.label,
                            "row_iid": child_iid,
                            "exact_server": "1",
                        }
                    )
                continue
            values = self.region_tree.item(iid, "values")
            if not values or not self._is_checked_marker(values[0]):
                continue
            meta = self.region_row_meta.get(iid) or {}
            server_id = str(meta.get("server_id") or "")
            project_id = str(meta.get("project_id") or "")
            server_ip = str(meta.get("server_ip") or "")
            if not server_id or not project_id:
                continue
            plan = REGION_BY_KEY[iid]
            items.append(
                {
                    "region": plan.region,
                    "project_id": project_id,
                    "server_id": server_id,
                    "server_ip": server_ip,
                    "server_name": str(meta.get("server_name") or ""),
                    "label": plan.label,
                    "region_label": plan.label,
                    "row_iid": "",
                    "exact_server": "",
                }
            )
        return items

    def update_region_row(
        self,
        plan: RegionPlan,
        *,
        project_id: Optional[str] = None,
        status: Optional[str] = None,
        host_status: Optional[str] = None,
        server_ip: Optional[str] = None,
        server_id: Optional[str] = None,
        server_name: Optional[str] = None,
        flavor: Optional[str] = None,
        expire_time: Optional[str] = None,
        resource_summary: Optional[str] = None,
    ) -> None:
        iid = plan.key
        if iid not in self.region_iids:
            return
        meta = self.region_row_meta.setdefault(iid, {})
        if project_id is not None:
            meta["project_id"] = project_id
        if server_id is not None:
            meta["server_id"] = server_id
        if server_name is not None:
            meta["server_name"] = server_name
        if server_ip is not None:
            meta["server_ip"] = server_ip
        values = list(self.region_tree.item(iid, "values"))
        if len(values) < 7:
            values.extend([""] * (7 - len(values)))
        values[1] = plan.label
        if host_status is not None:
            values[2] = host_status
        elif status is not None:
            values[2] = status
        if resource_summary is not None:
            values[3] = resource_summary
        if server_ip is not None:
            values[4] = server_ip
        if flavor is not None:
            values[5] = flavor
        if expire_time is not None:
            values[6] = expire_time
        self.region_tree.item(iid, values=values)
        self._refresh_tree_row_control(self.region_tree, iid)
        self.after_idle(
            lambda: autosize_treeview_columns(
                self.region_tree,
                max_widths={"resource_summary": 260, "server_ip": 220, "expire_time": 220, "flavor": 180},
            )
        )

    def _rebuild_purchase_ecs_children(
        self,
        plan: RegionPlan,
        ecs_items: Sequence[Dict[str, Any]],
    ) -> None:
        parent_iid = plan.key
        old_children = list(self.region_tree.get_children(parent_iid))
        old_open = bool(self.region_tree.item(parent_iid, "open"))
        checked_by_server_id: Dict[str, str] = {}
        for child_iid in old_children:
            old_item = self.region_ecs_items.get(child_iid) or {}
            old_server_id = str(old_item.get("_server_id") or old_item.get("_resource_id") or "")
            old_values = self.region_tree.item(child_iid, "values")
            if old_server_id and old_values:
                checked_by_server_id[old_server_id] = str(old_values[0])
            self.region_ecs_items.pop(child_iid, None)
            self.region_tree.delete(child_iid)

        if len(ecs_items) <= 1:
            self._set_region_group_open(parent_iid, False)
            return

        parent_values = self.region_tree.item(parent_iid, "values")
        parent_checked = bool(parent_values and self._is_checked_marker(parent_values[0]))
        parent_project_id = str((self.region_row_meta.get(parent_iid) or {}).get("project_id") or "")
        ordered_ecs = sorted(
            (dict(item) for item in ecs_items),
            key=lambda item: (
                str(item.get("_name") or "").casefold(),
                str(item.get("_server_id") or item.get("_resource_id") or ""),
            ),
        )
        for index, item in enumerate(ordered_ecs, start=1):
            server_id = str(item.get("_server_id") or item.get("_resource_id") or "")
            if not item.get("_project_id") and parent_project_id:
                item["_project_id"] = parent_project_id
            identity = server_id or str(item.get("_name") or index)
            safe_identity = re.sub(r"[^0-9A-Za-z_.-]+", "-", identity).strip("-") or str(index)
            child_iid = f"purchase-ecs-{plan.key}-{safe_identity}"
            if self.region_tree.exists(child_iid):
                child_iid = f"{child_iid}-{index}"
            checked = checked_by_server_id.get(
                server_id,
                CHECKED if parent_checked else UNCHECKED,
            )
            self.region_tree.insert(
                parent_iid,
                tk.END,
                iid=child_iid,
                text="",
                image=self._tree_control_image(checked),
                values=(
                    checked,
                    f"ECS {index}",
                    str(item.get("_status") or "未知"),
                    str(item.get("_name") or server_id or f"ECS {index}"),
                    str(item.get("_public_ip") or ""),
                    str(item.get("_flavor") or ""),
                    str(item.get("_expire_time") or ""),
                ),
                tags=("evenrow" if index % 2 == 0 else "oddrow",),
            )
            self.region_ecs_items[child_iid] = item

        self._set_region_group_open(parent_iid, old_open)
        self._refresh_region_group_check(parent_iid)

    def update_region_ecs_row(
        self,
        row_iid: str,
        *,
        status: Optional[str] = None,
        server_ip: Optional[str] = None,
        server_id: Optional[str] = None,
    ) -> None:
        if not row_iid or not self.region_tree.exists(row_iid):
            return
        values = list(self.region_tree.item(row_iid, "values"))
        while len(values) < 7:
            values.append("")
        if status is not None:
            values[2] = status
        if server_ip is not None:
            values[4] = server_ip
        item = self.region_ecs_items.get(row_iid)
        if item is not None:
            if server_ip is not None:
                item["_public_ip"] = server_ip
            if server_id is not None:
                item["_server_id"] = server_id
        self.region_tree.item(row_iid, values=values)
        self._refresh_tree_row_control(self.region_tree, row_iid)

    def _on_manage_click(self, event: tk.Event) -> None:
        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 self._tree_control_hit(self.manage_tree, row_id, event) == "toggle":
                self._toggle_manage_group_open(row_id)
                return
            column_name = "checked"
        if item.get("_kind") == "region_group" and column_name == "operation":
            self._toggle_manage_group_open(row_id)
            return
        if column_name != "checked":
            return
        values = list(self.manage_tree.item(row_id, "values"))
        target = self._toggle_checked_marker(values[0] if values else "")
        shift_pressed = bool(int(getattr(event, "state", 0)) & 0x0001)
        if shift_pressed and self.manage_last_clicked_iid:
            ordered = self._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_manage_row_checked(iid, target)
                self._refresh_manage_group_checks()
                return
        if item.get("_kind") == "region_group":
            self._set_manage_group_checked(row_id, target)
        else:
            self._set_manage_row_checked(row_id, target)
            self.manage_last_clicked_iid = row_id
        self._refresh_manage_group_checks()

    def _on_manage_double_click(self, event: tk.Event) -> str:
        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_manage_group_open(row_id)
        return "break"

    def _on_manage_tree_open_close(self, _event: tk.Event) -> None:
        selected = self.manage_tree.focus()
        item = self.manage_items.get(selected) or {}
        if item.get("_kind") == "region_group":
            self._sync_manage_group_toggle_text(selected)

    def _manage_visible_iids(self, *, include_groups: bool = True) -> List[str]:
        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 _all_manage_iids(self, parent: str = "") -> List[str]:
        ordered: List[str] = []
        for iid in self.manage_tree.get_children(parent):
            ordered.append(iid)
            ordered.extend(self._all_manage_iids(iid))
        return ordered

    def _set_manage_row_checked(self, iid: str, value: str) -> None:
        values = list(self.manage_tree.item(iid, "values"))
        if values:
            values[0] = value
            self.manage_tree.item(iid, values=values)
            self._refresh_tree_row_control(self.manage_tree, iid, value)

    def _set_manage_group_checked(self, group_iid: str, value: str) -> None:
        self._set_manage_row_checked(group_iid, value)
        for child in self._all_manage_iids(group_iid):
            self._set_manage_row_checked(child, value)

    def _toggle_manage_group_open(self, group_iid: str) -> None:
        self._set_manage_group_open(group_iid, not bool(self.manage_tree.item(group_iid, "open")))

    def _set_manage_group_open(self, group_iid: str, opened: bool) -> None:
        self.manage_tree.item(group_iid, open=opened)
        self._sync_manage_group_toggle_text(group_iid)

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

    def _refresh_manage_group_checks(self) -> None:
        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._all_manage_iids(group_iid) if (self.manage_items.get(iid) or {}).get("_kind") != "region_group"]
            if not children:
                self._set_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 self._is_checked_marker(values[0]))
            self._set_manage_row_checked(group_iid, CHECKED if checked_count == len(children) else UNCHECKED)

    def set_manage_checked(self, checked: bool) -> None:
        value = CHECKED if checked else UNCHECKED
        for iid in self._all_manage_iids():
            self._set_manage_row_checked(iid, value)

    def select_manage_actionable(self) -> None:
        for iid in self._all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            actionable = item.get("_operation_kind") not in {"", "none", None}
            values[0] = CHECKED if actionable else UNCHECKED
            self._set_manage_row_checked(iid, values[0])
        self._refresh_manage_group_checks()

    def select_manage_vpc_security_groups(self) -> None:
        for iid in self._all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            actionable = item.get("_operation_kind") not in {"", "none", None}
            is_network_resource = item.get("_kind") in {"vpc", "security_group"}
            values[0] = CHECKED if actionable and is_network_resource else UNCHECKED
            self._set_manage_row_checked(iid, values[0])
        self._refresh_manage_group_checks()

    def select_manage_servers(self) -> None:
        if self._standalone_limited_features() or self._customer_manage_eip_only():
            return
        for iid in self._all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            values[0] = CHECKED if item.get("_kind") == "ecs" else UNCHECKED
            self._set_manage_row_checked(iid, values[0])
        self._refresh_manage_group_checks()

    def select_manage_ips(self) -> None:
        for iid in self._all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            values[0] = CHECKED if item.get("_kind") in {"eip", "virtual_ip"} else UNCHECKED
            self._set_manage_row_checked(iid, values[0])
        self._refresh_manage_group_checks()

    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 _used_ip_table_file_lock(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._all_manage_iids():
            values = list(self.manage_tree.item(iid, "values"))
            item = self.manage_items.get(iid) or {}
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            public_ip = self._normalize_used_ip_value(
                item.get("_public_ip") or item.get("_name") or public_ip_address(raw)
            )
            port_id = str(item.get("_port_id") or raw.get("port_id") or raw.get("portId") or "").strip()
            is_target = item.get("_kind") == "eip" and bool(port_id) and public_ip in failed_ips
            if values:
                values[0] = CHECKED if is_target else UNCHECKED
                self._set_manage_row_checked(iid, values[0])
            if is_target:
                matched_ips.add(public_ip)
        self._refresh_manage_group_checks()
        missing_count = len(failed_ips - matched_ips)
        if matched_ips:
            message = f"已选中更换失败IP {len(matched_ips)} 条"
            if missing_count:
                message += f"，另有 {missing_count} 条已不在当前资源中"
            self.log(message + "。", "manage")
        elif failed_ips:
            self.log("更换失败IP库中的IP均不在当前已读取资源中。", "manage")
        else:
            self.log("更换失败IP库为空。", "manage")
        return len(matched_ips), missing_count, len(failed_ips)

    def selected_manage_items(self) -> List[Dict[str, Any]]:
        items: List[Dict[str, Any]] = []
        for iid in self._all_manage_iids():
            values = self.manage_tree.item(iid, "values")
            item = self.manage_items.get(iid)
            if values and self._is_checked_marker(values[0]) and item and item.get("_kind") != "region_group":
                items.append(item)
        return items

    def update_domain_if_discovered(self, client: HuaweiCloudClient) -> None:
        if client.domain_id and not self.domain_var.get().strip():
            self.domain_var.set(client.domain_id)
            self._ensure_result_account_cache()
            self.render_results()

    def populate_manage_tree(
        self,
        items: Sequence[Dict[str, Any]],
        purchase_regions: Optional[Sequence[str]] = None,
    ) -> None:
        for iid in self.manage_tree.get_children():
            self.manage_tree.delete(iid)
        self.manage_items = {}
        self.manage_resource_items = [dict(item) for item in items]
        self.manage_last_clicked_iid = ""
        display_items = self._manage_display_items(self.manage_resource_items)
        source_grouped: Dict[str, List[Dict[str, Any]]] = {}
        for item in self.manage_resource_items:
            source_grouped.setdefault(str(item.get("_region") or "unknown"), []).append(dict(item))
        grouped: Dict[str, List[Dict[str, Any]]] = {}
        for item in display_items:
            grouped.setdefault(str(item.get("_region") or "unknown"), []).append(dict(item))
        ordered_regions = [plan.region for plan in REGION_PLANS] + [
            region for region in source_grouped.keys() if region not in {plan.region for plan in REGION_PLANS}
        ]
        eip_only = self._customer_manage_eip_only()
        index = 0
        for region in ordered_regions:
            source_region_items = source_grouped.get(region) or []
            if not source_region_items:
                continue
            region_items = grouped.get(region) or []
            if not region_items and not eip_only:
                continue
            kind_order = {"ecs": 0, "eip": 1, "vpc": 2, "security_group": 4, "virtual_ip": 5}
            region_items = sorted(
                region_items,
                key=lambda item: (
                    kind_order.get(str(item.get("_kind") or ""), 99),
                    self._ipv4_sort_tuple(str(item.get("_eip_bound_private_ip") or ""))
                    if str(item.get("_kind") or "") == "eip"
                    else (999, 999, 999, 999),
                    str(item.get("_name") or ""),
                ),
            )
            plan = REGION_BY_CODE.get(region)
            label_item = region_items[0] if region_items else source_region_items[0]
            region_label = plan.label if plan else str(label_item.get("_region_label") or region or "未知地区")
            counts = {"ECS": 0, "EIP": 0, "VPC": 0, "子网": 0, "安全组": 0}
            for item in region_items:
                kind = str(item.get("_kind") or "")
                label = str(item.get("_kind_label") or "")
                if kind == "ecs" or "ECS" in label:
                    counts["ECS"] += 1
                elif kind == "eip" or "EIP" in label:
                    counts["EIP"] += 1
                elif kind == "vpc":
                    counts["VPC"] += 1
                    counts["子网"] += len([subnet_id for subnet_id in (item.get("_subnet_ids") or []) if subnet_id])
                elif kind == "security_group":
                    counts["安全组"] += 1
            if eip_only:
                summary = f"EIP:{counts['EIP']}"
            else:
                summary = (
                    f"ECS:{counts['ECS']}/EIP:{counts['EIP']}/VPC:{counts['VPC']}/"
                    f"子网:{counts['子网']}/安全组:{counts['安全组']}"
                )
            group_iid = f"manage-group-{region}"
            group_values = (
                UNCHECKED,
                region_label,
                "地区汇总",
                "",
                "",
                summary,
                "",
                "＋",
                "",
                "",
            )
            self.manage_tree.insert(
                "",
                tk.END,
                iid=group_iid,
                text="",
                image=self._tree_control_image(UNCHECKED),
                values=group_values,
                open=False,
                tags=("regiongroup",),
            )
            self.manage_items[group_iid] = {
                "_kind": "region_group",
                "_region": region,
                "_region_label": region_label,
                "_operation_kind": "none",
            }
            for item in region_items:
                index += 1
                iid = f"manage-{index}"
                checked = UNCHECKED
                values = (
                    checked,
                    "",
                    item.get("_kind_label", ""),
                    item.get("_resource_type", ""),
                    item.get("_name", ""),
                    item.get("_status", ""),
                    item.get("_billing", ""),
                    item.get("_operation_text", ""),
                    item.get("_expire_time", ""),
                    item.get("_flavor", ""),
                )
                self.manage_items[iid] = dict(item)
                self.manage_tree.insert(
                    group_iid,
                    tk.END,
                    iid=iid,
                    text="",
                    image=self._tree_control_image(checked),
                    values=values,
                    tags=("evenrow" if index % 2 == 0 else "oddrow",),
                )
        self._refresh_manage_group_checks()
        self.after_idle(
            lambda: autosize_treeview_columns(
                self.manage_tree,
                max_widths={
                    "#0": 60,
                    "region_label": 110,
                    "kind": 90,
                    "resource_type": 120,
                    "name": 280,
                    "status": 260,
                    "operation": 280,
                    "server_id": 220,
                    "resource_id": 180,
                },
            )
        )
        # 中文注释：资源管理树和购买搭建页使用同一份资源查询结果，避免两边显示不同步。
        self.sync_purchase_rows_from_manage_items(self.manage_resource_items, purchase_regions)

    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"]
        return [dict(item) for item in items]

    def apply_manage_resource_refresh(
        self,
        items: Sequence[Dict[str, Any]],
        refreshed_regions: Optional[Sequence[str]] = None,
        confirmed_regions: Optional[Sequence[str]] = None,
    ) -> None:
        """把指定地区的新资源结果合并进缓存；未刷新地区保持原显示，避免重复查询。"""
        region_set = {str(region or "").strip() for region in (refreshed_regions or []) if str(region or "").strip()}
        if not region_set:
            self.populate_manage_tree(items)
        else:
            kept_items = [
                dict(item)
                for item in self.manage_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_manage_tree(
                self._dedupe_manage_items([*kept_items, *refreshed_items]),
                region_set,
            )

        confirmed_set = {
            str(region or "").strip()
            for region in (confirmed_regions or [])
            if str(region or "").strip()
        }
        if confirmed_set:
            ecs_regions = {
                str(item.get("_region") or "").strip()
                for item in self.manage_resource_items
                if str(item.get("_kind") or "").lower() == "ecs"
            }
            no_ecs_regions = confirmed_set - ecs_regions
            self.confirmed_no_ecs_regions.difference_update(confirmed_set)
            self.confirmed_no_ecs_regions.update(no_ecs_regions)
            if no_ecs_regions:
                self.last_results = [
                    result
                    for result in self.last_results
                    if result.plan.region not in no_ecs_regions
                ]
        self.render_results()

    def _subscription_manage_item(self, item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        region = str(item.get("region_code") or item.get("region") or "").strip()
        if not is_target_region(region):
            return None
        if not (is_ecs_subscription_resource(item) or is_eip_subscription_resource(item)):
            return None
        resource_id = next(iter(resource_id_candidates(item)), "")
        operation_kind = subscription_operation_kind(item)
        is_ecs = is_ecs_subscription_resource(item)
        if is_ecs and subscription_status_text(item) in {"已关闭", "已关机"}:
            return None
        return {
            "_kind": "ecs" if is_ecs else "eip",
            "_kind_label": "ECS" if is_ecs else "EIP",
            "_resource_type": "" if is_ecs else self._eip_actual_type_label(item),
            "_region": region,
            "_region_label": display_region(region),
            "_project_id": str(item.get("project_id") or item.get("projectId") or ""),
            "_name": str(item.get("resource_name") or item.get("resource_id") or resource_id or ""),
            "_status": subscription_status_text(item),
            "_billing": "包周期",
            "_operation_text": "包周期可退订" if operation_kind != "none" else "不可退订",
            "_operation_kind": operation_kind,
            "_public_ip": str(item.get("resource_name") or "") if not is_ecs else "",
            "_server_id": resource_id if is_ecs else "",
            "_resource_id": resource_id,
            "_expire_time": display_expire_time_for_billing(item, billing_text="包周期"),
            "_flavor": str(item.get("resource_spec") or item.get("resourceSpec") or item.get("spec") or ""),
            "_raw": item,
        }

    def _query_one_manage_region(
        self,
        client: HuaweiCloudClient,
        plan: RegionPlan,
        pair: ProjectPair,
        subscription_ids: set,
        *,
        full: bool = False,
    ) -> List[Dict[str, Any]]:
        items: List[Dict[str, Any]] = []
        fetched: Dict[str, Any] = {}
        request_timeout = MANAGE_FULL_REQUEST_TIMEOUT if full else SCAN_REQUEST_TIMEOUT
        retry_attempts = MANAGE_FULL_REQUEST_RETRY_ATTEMPTS if full else SCAN_REQUEST_RETRY_ATTEMPTS
        complete_timeout = MANAGE_FULL_REGION_TIMEOUT if full else SCAN_REGION_COMPLETE_TIMEOUT
        retry_timeout = MANAGE_FULL_REQUEST_TIMEOUT if full else 10
        retry_count = MANAGE_FULL_REQUEST_RETRY_ATTEMPTS if full else 2
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=7)
        try:
            def retry_manage_api(name: str) -> Optional[List[Dict[str, Any]]]:
                retry_map = {
                    "servers": lambda: client.list_cloud_servers(pair, timeout=retry_timeout, retry_attempts=retry_count),
                    "eips": lambda: client.list_public_ips(pair, timeout=retry_timeout, retry_attempts=retry_count),
                    "vpcs": lambda: client.list_vpcs(pair, timeout=retry_timeout, retry_attempts=retry_count),
                    "subnets": lambda: client.list_subnets(pair, timeout=retry_timeout, retry_attempts=retry_count),
                    "ports": lambda: client.list_ports(pair, timeout=retry_timeout, retry_attempts=retry_count),
                    "sub_network_interfaces": lambda: client.list_sub_network_interfaces(pair, timeout=retry_timeout, retry_attempts=retry_count),
                    "security_groups": lambda: client.list_security_groups(pair, timeout=retry_timeout, retry_attempts=retry_count),
                }
                retry = retry_map.get(name)
                if not retry:
                    return None
                self.after(0, self.log, f"[{plan.label}] {name} 慢接口补齐中，避免资源库漏显示。")
                return retry()

            futures = {
                "servers": executor.submit(client.list_cloud_servers, pair, timeout=request_timeout, retry_attempts=retry_attempts),
                "eips": executor.submit(client.list_public_ips, pair, timeout=request_timeout, retry_attempts=retry_attempts),
                "vpcs": executor.submit(client.list_vpcs, pair, timeout=request_timeout, retry_attempts=retry_attempts),
                "subnets": executor.submit(client.list_subnets, pair, timeout=request_timeout, retry_attempts=retry_attempts),
                "security_groups": executor.submit(client.list_security_groups, pair, timeout=request_timeout, retry_attempts=retry_attempts),
                "ports": executor.submit(client.list_ports, pair, timeout=request_timeout, retry_attempts=retry_attempts),
                "sub_network_interfaces": executor.submit(
                    client.list_sub_network_interfaces,
                    pair,
                    timeout=request_timeout,
                    retry_attempts=retry_attempts,
                ),
            }
            future_to_name = {future: name for name, future in futures.items()}
            done, not_done = concurrent.futures.wait(list(future_to_name.keys()), timeout=complete_timeout)
            for future in done:
                name = future_to_name[future]
                try:
                    fetched[name] = future.result()
                except Exception as exc:
                    if not full:
                        fetched[name] = []
                        self.after(0, self.log, f"[{plan.label}] {name} 快读失败: {cloud_error_message_cn(exc)}")
                        continue
                    try:
                        retry_result = retry_manage_api(name)
                    except Exception as retry_exc:
                        fetched[name] = []
                        self.after(0, self.log, f"[{plan.label}] {name} 查询失败: {exc}; 补齐仍失败: {retry_exc}")
                    else:
                        fetched[name] = retry_result if retry_result is not None else []
                        if retry_result is None:
                            self.after(0, self.log, f"[{plan.label}] {name} 查询失败: {exc}")
            for future in not_done:
                name = future_to_name[future]
                future.cancel()
                if not full:
                    fetched[name] = []
                    self.after(0, self.log, f"[{plan.label}] {name} 快读跳过: 超过{complete_timeout:.1f}秒未返回。")
                    continue
                try:
                    retry_result = retry_manage_api(name)
                except Exception as exc:
                    fetched[name] = []
                    self.after(0, self.log, f"[{plan.label}] {name} 超过{complete_timeout:.1f}秒未返回，补齐失败: {exc}")
                else:
                    fetched[name] = retry_result if retry_result is not None else []
                    if retry_result is None:
                        self.after(0, self.log, f"[{plan.label}] {name} 超过{complete_timeout:.1f}秒未返回，已快速跳过。")
        finally:
            executor.shutdown(wait=False, cancel_futures=True)

        servers = list(fetched.get("servers") or [])
        eips = list(fetched.get("eips") or [])
        self._queue_huawei_ip_segments(plan, eips)
        vpcs = list(fetched.get("vpcs") or [])
        subnets = list(fetched.get("subnets") or [])
        security_groups = list(fetched.get("security_groups") or [])
        ports = list(fetched.get("ports") or [])
        sub_network_interfaces = list(fetched.get("sub_network_interfaces") or [])
        eip_port_labels, eip_port_ips = self._eip_port_display_maps(ports, sub_network_interfaces)

        bound_vpc_ids = {str(port.get("vpc_id") or "") for port in ports if port.get("vpc_id")}
        bound_vpc_ids.update(str(server.get("vpc_id") or server.get("vpcId") or "") for server in servers if server.get("vpc_id") or server.get("vpcId"))
        subnets_by_vpc: Dict[str, List[str]] = {}
        for subnet in subnets:
            vpc_id = str(subnet.get("vpc_id") or "")
            subnet_id = str(subnet.get("id") or "")
            if vpc_id and subnet_id:
                subnets_by_vpc.setdefault(vpc_id, []).append(subnet_id)
        referenced_sg_ids = set()
        for port in ports:
            groups = port.get("security_groups") or []
            if isinstance(groups, list):
                for group in groups:
                    if isinstance(group, dict) and group.get("id"):
                        referenced_sg_ids.add(str(group.get("id")))
                    elif group:
                        referenced_sg_ids.add(str(group))

        for server in servers:
            server_id = str(server.get("id") or "")
            server_billing = server_charging_text(server)
            operation_kind = "ecs_delete" if server_id.casefold() not in subscription_ids and is_on_demand_server(server) else "none"
            operation_text = "按需ECS可删除" if operation_kind == "ecs_delete" else "可重装"
            items.append(
                {
                    "_kind": "ecs",
                    "_kind_label": "ECS",
                    "_resource_type": "",
                    "_region": plan.region,
                    "_region_label": plan.label,
                    "_project_id": pair.project_id,
                    "_name": str(server.get("name") or server_id),
                    "_status": server_status_text(server),
                    "_billing": server_billing,
                    "_operation_text": operation_text,
                    "_operation_kind": operation_kind,
                    "_public_ip": HuaweiCloudClient.public_ip_from_server(server),
                    "_server_id": server_id,
                    "_resource_id": server_id,
                    "_expire_time": display_expire_time_for_billing(
                        server,
                        billing_text=server_billing,
                        explicit_expire_time=server_expire_time(server),
                    ),
                    "_flavor": server_flavor_label(server),
                    "_raw": server,
                }
            )

        for eip in eips:
            eip_id = str(eip.get("id") or eip.get("publicip_id") or "")
            port_id = str(eip.get("port_id") or "")
            bound_label = eip_port_labels.get(port_id, "")
            bound_private_ip = eip_port_ips.get(port_id, "")
            if port_id:
                eip_status_text = bound_label if bound_label else "端口"
            else:
                eip_status_text = "未绑定"
            subscribed = eip_id.casefold() in subscription_ids
            operation_kind = "eip_delete" if eip_id and not subscribed else "none"
            if operation_kind == "eip_delete":
                operation_text = "EIP可直接释放" if port_id else "未绑定EIP可释放"
            elif subscribed:
                operation_text = "包周期EIP请退订"
            elif port_id:
                operation_text = "EIP缺少ID不可释放"
            else:
                operation_text = "不可释放"
            eip_billing = str(eip.get("billing_info") or eip.get("charge_mode") or "按需")
            items.append(
                {
                    "_kind": "eip",
                    "_kind_label": "EIP",
                    "_resource_type": self._eip_actual_type_label(eip),
                    "_region": plan.region,
                    "_region_label": plan.label,
                    "_project_id": pair.project_id,
                    "_name": public_ip_address(eip),
                    "_status": eip_status_text,
                    "_billing": eip_billing,
                    "_operation_text": operation_text,
                    "_operation_kind": operation_kind,
                    "_public_ip": public_ip_address(eip),
                    "_port_id": port_id,
                    "_eip_bound_label": bound_label,
                    "_eip_bound_private_ip": bound_private_ip,
                    "_resource_id": eip_id,
                    "_expire_time": display_expire_time_for_billing(eip, billing_text=eip_billing),
                    "_flavor": "",
                    "_raw": eip,
                }
            )

        for port in ports:
            port_id = str(port.get("id") or "")
            owner = str(port.get("device_owner") or "").lower()
            if not port_id or "vip" not in owner:
                continue
            fixed_ips = self._port_fixed_ip_addresses(port)
            ip_addr = fixed_ips[0] if fixed_ips else ""
            bound_eips = [
                public_ip_address(eip)
                for eip in eips
                if str(eip.get("port_id") or "") == port_id and public_ip_address(eip)
            ]
            operation_text = "虚拟IP可删除"
            if bound_eips:
                operation_text = f"虚拟IP已绑定EIP，删除前会先解绑 {len(bound_eips)} 条"
            items.append(
                {
                    "_kind": "virtual_ip",
                    "_kind_label": "虚拟IP",
                    "_resource_type": "",
                    "_region": plan.region,
                    "_region_label": plan.label,
                    "_project_id": pair.project_id,
                    "_name": ip_addr or str(port.get("name") or port_id),
                    "_status": str(port.get("status") or ""),
                    "_billing": "-",
                    "_operation_text": operation_text,
                    "_operation_kind": "virtual_ip_delete",
                    "_public_ip": ",".join(bound_eips),
                    "_port_id": port_id,
                    "_resource_id": port_id,
                    "_expire_time": "",
                    "_flavor": "",
                    "_raw": port,
                }
            )

        for vpc in vpcs:
            vpc_id = str(vpc.get("id") or "")
            operation_kind = "vpc_delete" if vpc_id else "none"
            subnet_ids = subnets_by_vpc.get(vpc_id, [])
            if operation_kind != "none":
                operation_text = "VPC将清理子网/端口后删除" if subnet_ids or vpc_id in bound_vpc_ids else "空VPC可删除"
            else:
                operation_text = "VPC有绑定"
            items.append(
                {
                    "_kind": "vpc",
                    "_kind_label": "VPC",
                    "_resource_type": "",
                    "_region": plan.region,
                    "_region_label": plan.label,
                    "_project_id": pair.project_id,
                    "_name": str(vpc.get("name") or vpc_id),
                    "_status": str(vpc.get("status") or ""),
                    "_billing": "-",
                    "_operation_text": operation_text,
                    "_operation_kind": operation_kind,
                    "_resource_id": vpc_id,
                    "_subnet_ids": subnet_ids,
                    "_expire_time": "",
                    "_flavor": "",
                    "_raw": vpc,
                }
            )

        for group in security_groups:
            group_id = str(group.get("id") or "")
            group_name = str(group.get("name") or group_id)
            is_default_group = bool(group.get("default")) or group_name.strip().lower() in {"default", "sys-default", "default-sg"}
            if is_default_group:
                continue
            operation_kind = "security_group_delete" if group_id and not is_default_group else "none"
            if operation_kind != "none":
                operation_text = "安全组将清理规则后删除" if group_id in referenced_sg_ids else "安全组可删除"
            else:
                operation_text = "安全组有绑定"
            items.append(
                {
                    "_kind": "security_group",
                    "_kind_label": "安全组",
                    "_resource_type": "",
                    "_region": plan.region,
                    "_region_label": plan.label,
                    "_project_id": pair.project_id,
                    "_name": group_name,
                    "_status": "-",
                    "_billing": "-",
                    "_operation_text": operation_text,
                    "_operation_kind": operation_kind,
                    "_resource_id": group_id,
                    "_expire_time": "",
                    "_flavor": "",
                    "_raw": group,
                }
            )

        self.after(
            0,
            self.log,
            f"[{plan.label}] 一键查询: ECS={len(servers)} EIP={len(eips)} "
            f"虚拟IP={sum(1 for port in ports if 'vip' in str(port.get('device_owner') or '').lower())} "
            f"VPC={len(vpcs)} 安全组={len(security_groups)}",
        )
        return items

    @staticmethod
    def _merge_manage_ecs_item(primary: Dict[str, Any], secondary: Dict[str, Any]) -> Dict[str, Any]:
        merged = dict(primary)
        for key in ("_name", "_status", "_billing", "_public_ip", "_server_id", "_resource_id", "_expire_time", "_flavor"):
            if not str(merged.get(key) or "").strip() and str(secondary.get(key) or "").strip():
                merged[key] = secondary.get(key)
        if str(merged.get("_status") or "") in {"", "未知", "未发现"} and str(secondary.get("_status") or "").strip():
            merged["_status"] = secondary.get("_status")
        if str(merged.get("_operation_kind") or "none") == "none" and str(secondary.get("_operation_kind") or "none") != "none":
            merged["_operation_kind"] = secondary.get("_operation_kind")
            merged["_operation_text"] = secondary.get("_operation_text")
        return merged

    @staticmethod
    def _manage_ecs_score(item: Dict[str, Any]) -> Tuple[int, int, int, int]:
        operation = str(item.get("_operation_kind") or "none")
        return (
            3 if operation == "subscription_unsubscribe" else 2 if operation == "ecs_delete" else 1,
            1 if str(item.get("_public_ip") or "").strip() else 0,
            1 if str(item.get("_flavor") or "").strip() else 0,
            1 if str(item.get("_status") or "").strip() else 0,
        )

    def _dedupe_manage_items(self, items: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
        deduped: List[Dict[str, Any]] = []
        ecs_by_id: Dict[Tuple[str, str], Dict[str, Any]] = {}
        for item in items:
            if str(item.get("_kind") or "") != "ecs":
                deduped.append(dict(item))
                continue
            region = str(item.get("_region") or "")
            if not region:
                deduped.append(dict(item))
                continue
            server_id = str(item.get("_server_id") or item.get("_resource_id") or "").strip()
            if not server_id:
                deduped.append(dict(item))
                continue
            key = (region, server_id.casefold())
            current = ecs_by_id.get(key)
            if current is None:
                ecs_by_id[key] = dict(item)
                continue
            if self._manage_ecs_score(item) > self._manage_ecs_score(current):
                ecs_by_id[key] = self._merge_manage_ecs_item(dict(item), current)
            else:
                ecs_by_id[key] = self._merge_manage_ecs_item(current, item)
        deduped.extend(ecs_by_id.values())
        return deduped

    def _collect_manage_resources(
        self,
        client: HuaweiCloudClient,
        plans: Sequence[RegionPlan],
        only_main: bool,
        *,
        full: bool = False,
        on_partial: Optional[Callable[[Sequence[Dict[str, Any]], Optional[Sequence[str]]], None]] = None,
        query_status: Optional[Dict[str, Any]] = None,
    ) -> List[Dict[str, Any]]:
        mode_text = "完整" if full else "快速"
        self.after(0, self.log, f"资源管理一键查询启动({mode_text})，地区={ [plan.label for plan in plans] }")
        self._wait_region_project_warmup()
        pairs = client.selected_project_pairs(plans)
        self.after(0, self.update_domain_if_discovered, client)
        requested_regions = {plan.region for plan in plans}
        if query_status is not None:
            query_status.clear()
            query_status.update(
                {
                    "requested_regions": set(requested_regions),
                    "successful_regions": set(),
                    "failed_regions": set(),
                    "subscription_query_failed": False,
                }
            )

        subscription_items: List[Dict[str, Any]] = []
        subscription_by_region: Dict[str, List[Dict[str, Any]]] = {}
        subscription_ids = set()
        try:
            raw_resources = client.list_subscription_resources(only_main, log=lambda m: self.after(0, self.log, m))
            for raw in raw_resources:
                item = self._subscription_manage_item(raw)
                if not item:
                    continue
                subscription_items.append(item)
                subscription_by_region.setdefault(str(item.get("_region") or ""), []).append(item)
                for resource_id in resource_id_candidates(raw):
                    subscription_ids.add(resource_id.casefold())
            self.after(0, self.log, f"包年包月资源查询完成，可显示 {len(subscription_items)} 条。")
            if on_partial and subscription_items:
                partial_subscription_items = [
                    item
                    for item in subscription_items
                    if str(item.get("_region") or "") in requested_regions
                ]
                partial_subscription_regions = [
                    region
                    for region in subscription_by_region
                    if region in requested_regions
                ]
                if partial_subscription_items:
                    self.after(
                        0,
                        on_partial,
                        partial_subscription_items,
                        partial_subscription_regions or None,
                    )
        except Exception as exc:
            if query_status is not None:
                query_status["subscription_query_failed"] = True
            self.after(0, self.log, f"包年包月资源查询失败，继续读取按需资源: {exc}")

        all_items: List[Dict[str, Any]] = [
            item
            for item in subscription_items
            if str(item.get("_region") or "") in requested_regions
        ]
        with concurrent.futures.ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(plans) or 1)) as executor:
            futures = {}
            for plan in plans:
                pair = pairs.get(plan.region)
                if not pair:
                    if query_status is not None:
                        query_status["failed_regions"].add(plan.region)
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] 未打通该地区，无法查询该地区资源，{REGION_ACCESS_TUTORIAL_GUIDANCE}",
                    )
                    continue
                futures[executor.submit(self._query_one_manage_region, client, plan, pair, subscription_ids, full=full)] = plan
            for future in concurrent.futures.as_completed(futures):
                plan = futures[future]
                try:
                    region_items = future.result()
                except Exception as exc:
                    if query_status is not None:
                        query_status["failed_regions"].add(plan.region)
                    self.after(0, self.log, f"[{plan.label}] 地区资源查询失败: {exc}")
                    continue
                if query_status is not None:
                    query_status["successful_regions"].add(plan.region)
                all_items.extend(region_items)
                if on_partial:
                    partial_items = [*subscription_by_region.get(plan.region, []), *region_items]
                    self.after(0, on_partial, self._dedupe_manage_items(partial_items), [plan.region])

        all_items = self._dedupe_manage_items(all_items)
        region_order = {plan.region: index for index, plan in enumerate(REGION_PLANS)}
        kind_order = {"ecs": 0, "eip": 1, "vpc": 2, "security_group": 4, "virtual_ip": 5}
        all_items.sort(
            key=lambda item: (
                region_order.get(str(item.get("_region") or ""), 99),
                kind_order.get(str(item.get("_kind") or ""), 99),
                self._ipv4_sort_tuple(str(item.get("_eip_bound_private_ip") or "")) if str(item.get("_kind") or "") == "eip" else (999, 999, 999, 999),
                str(item.get("_name") or ""),
            )
        )
        return all_items

    def sync_purchase_rows_from_manage_items(
        self,
        items: Sequence[Dict[str, Any]],
        refreshed_regions: Optional[Sequence[str]] = None,
    ) -> None:
        grouped: Dict[str, List[Dict[str, Any]]] = {}
        for item in items:
            region = str(item.get("_region") or "")
            if region:
                grouped.setdefault(region, []).append(dict(item))

        region_set = {
            str(region or "").strip()
            for region in (refreshed_regions or [])
            if str(region or "").strip()
        }
        plans = [plan for plan in REGION_PLANS if not region_set or plan.region in region_set]
        for plan in plans:
            region_items = grouped.get(plan.region) or []
            counts = {"ecs": 0, "eip": 0, "virtual_ip": 0, "vpc": 0, "security_group": 0}
            for item in region_items:
                kind = str(item.get("_kind") or "")
                if kind in counts:
                    counts[kind] += 1
            resource_summary = f"ECS:{counts['ecs']} EIP:{counts['eip']}"

            ecs_items = [item for item in region_items if str(item.get("_kind") or "") == "ecs"]

            def ecs_score(item: Dict[str, Any]) -> Tuple[int, int, int]:
                status = str(item.get("_status") or "")
                return (
                    1 if status in {"运行中", "ACTIVE", "RUNNING"} else 0,
                    1 if str(item.get("_public_ip") or "").strip() else 0,
                    1 if str(item.get("_flavor") or "").strip() else 0,
                )

            ecs_item = sorted(ecs_items, key=ecs_score, reverse=True)[0] if ecs_items else None
            if ecs_item:
                self.update_region_row(
                    plan,
                    project_id=str(ecs_item.get("_project_id") or ""),
                    host_status=str(ecs_item.get("_status") or "无"),
                    server_ip=str(ecs_item.get("_public_ip") or ""),
                    server_id=str(ecs_item.get("_server_id") or ecs_item.get("_resource_id") or ""),
                    server_name=str(ecs_item.get("_name") or ""),
                    flavor=str(ecs_item.get("_flavor") or ""),
                    expire_time=str(ecs_item.get("_expire_time") or ""),
                    resource_summary=resource_summary,
                )
            else:
                project_id = next((str(item.get("_project_id") or "") for item in region_items if item.get("_project_id")), "")
                self.update_region_row(
                    plan,
                    project_id=project_id,
                    host_status="无",
                    server_ip="",
                    server_id="",
                    server_name="",
                    flavor="",
                    expire_time="",
                    resource_summary=resource_summary,
                )
            self._rebuild_purchase_ecs_children(plan, ecs_items)

    def get_client(self) -> HuaweiCloudClient:
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        if not ak or not sk:
            raise ValueError("请先填写或粘贴识别 AK/SK。")
        guard_message = self._customer_guard_block_message(ak, sk)
        if guard_message:
            raise CustomerOwnershipGuardError(guard_message)
        return HuaweiCloudClient(
            ak,
            sk,
            self.domain_var.get().strip(),
            network_diagnostic=self._record_huawei_api_network_diagnostic,
        )

    def _record_huawei_api_network_diagnostic(self, message: str) -> None:
        text = sanitize_log_text(message).strip()
        if not text:
            return
        with self.huawei_api_network_diagnostic_lock:
            if text not in self.huawei_api_network_diagnostic_seen:
                self.huawei_api_network_diagnostic_seen.add(text)
                self.huawei_api_network_diagnostics.append(text)
                del self.huawei_api_network_diagnostics[:-40]
        contexts = list(self.active_task_contexts)
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        for context in contexts:
            self._append_log_file(f"[{timestamp}] 华为云API网络: {text}", context)

    def _installed_update_log_lines(self) -> List[str]:
        package_key = self._huawei_ip_segment_report_package()
        try:
            metadata = read_installed_update_metadata(package_key)
        except Exception:
            metadata = {}
        return [
            f"软件版本号: {_shared.APP_VERSION}",
            f"补丁更新号: {str(metadata.get('patch_version') or '未记录')}",
            f"更新通道: {str(metadata.get('package_key') or package_key or 'desktop')}",
        ]

    def _export_diagnostic_text(self) -> str:
        lines = self._installed_update_log_lines()
        with self.huawei_api_network_diagnostic_lock:
            diagnostics = list(self.huawei_api_network_diagnostics)
        lines.append("华为云API代理/直连状态:")
        if diagnostics:
            lines.extend(f"- {item}" for item in diagnostics)
        else:
            lines.append("- 尚无请求记录")
        return "\n".join(lines)

    def _schedule_region_project_warmup(self, fingerprint: str) -> None:
        fingerprint = str(fingerprint or "").strip()
        if not fingerprint or self.region_project_warmup_running:
            return
        if self.region_project_warmup_fingerprint == fingerprint:
            return
        ak = self.ak_var.get().strip()
        sk = self.sk_var.get().strip()
        domain_id = self.domain_var.get().strip()
        if not self._credentials_ready_for_customer_guard(ak, sk):
            return
        self.region_project_warmup_fingerprint = fingerprint
        self.region_project_warmup_running = True
        self.log("华为云地区项目纯API补全中，正在准备七地区访问权限...")

        def worker() -> None:
            client = HuaweiCloudClient(
                ak,
                sk,
                domain_id,
                network_diagnostic=self._record_huawei_api_network_diagnostic,
            )
            plans = list(REGION_PLANS)
            try:
                pairs = client.selected_project_pairs(plans)
                missing_labels = [plan.label for plan in plans if plan.region not in pairs]

                def apply_result() -> None:
                    self.region_project_warmup_running = False
                    self.update_domain_if_discovered(client)
                    self.region_projects.update(pairs)
                    if missing_labels:
                        self.log(
                            "华为云地区项目纯API补全后仍未识别："
                            f"{compact_name_list(missing_labels)}。{REGION_ACCESS_TUTORIAL_GUIDANCE}",
                        )
                    else:
                        self.log("华为云地区项目纯API补全完成，7个地区均已识别。")

                self.after(0, apply_result)
            except Exception:
                def apply_failure() -> None:
                    self.region_project_warmup_running = False
                    self.region_project_warmup_fingerprint = ""
                    self.log(f"华为云地区项目纯API补全失败。{REGION_ACCESS_TUTORIAL_GUIDANCE}")

                self.after(0, apply_failure)

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

    def _wait_region_project_warmup(self, timeout: float = HUAWEI_REGION_PROJECT_WARMUP_WAIT_SECONDS) -> None:
        if not self.region_project_warmup_running:
            return
        deadline = time.time() + max(1.0, float(timeout))
        while self.region_project_warmup_running and time.time() < deadline:
            if self.stop_event.is_set():
                return
            time.sleep(0.2)

    def parse_ssh_port(self) -> int:
        raw = self.ssh_port_var.get().strip() or DEFAULT_SSH_PORT
        try:
            port = int(raw)
        except ValueError as exc:
            raise ValueError("SSH端口必须是数字。") from exc
        if port < 1 or port > 65535:
            raise ValueError("SSH端口范围必须是 1-65535。")
        return port

    def _task_log_path(self, context: str, name: str) -> str:
        section = self._log_section(context)
        action = self._log_action_name(context, name)
        prefix = "huawei-manage" if context == "manage" else "huawei-purchase"
        return str(create_task_log_path("HUAWEI", section, action, prefix=prefix, account_key=self._account_log_scope()))

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

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

    def _log_action_name(self, context: str, name: 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 _activate_task_context(self, context: str, name: str) -> None:
        self.active_task_contexts[context] = self.active_task_contexts.get(context, 0) + 1
        self.current_log_context = context
        self._set_log_context(context)
        self.realtime_log_filter.reset(context)
        self.log_action_names[context] = self._log_action_name(context, name)
        self.log_file_paths[context] = self._task_log_path(context, name)
        self._append_log_file(f"===== {name} 开始 {time.strftime('%Y-%m-%d %H:%M:%S')} =====\n", context)
        self._append_log_file(f"日志文件: {self.log_file_paths[context]}\n", context)
        self._append_log_file("\n".join(self._installed_update_log_lines()), context)

    def _deactivate_task_context(self, context: str) -> None:
        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._append_log_file(f"===== 任务结束 {time.strftime('%Y-%m-%d %H:%M:%S')} =====\n", context)

    def _append_log_file(self, text: str, context: str) -> None:
        path = self.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) as handle:
                handle.write(text if text.endswith("\n") else text + "\n")
        except Exception:
            pass

    def run_task(
        self,
        name: str,
        worker: Callable[[], None],
        *,
        exclusive: bool = True,
        context: Optional[str] = None,
        affects_global_busy: bool = True,
        regions: Optional[Sequence[str]] = None,
        risk_group: str = WRITE_RESOURCE_RISK_GROUP,
    ) -> None:
        task_context = context or self.current_log_context or "purchase"
        try:
            task_guard = TaskGuard(
                cloud="HUAWEI",
                ak=self.ak_var.get(),
                sk=self.sk_var.get(),
                action=name,
                regions=list(regions or ["global"]),
                risk_group=risk_group,
            ).acquire()
        except TaskGuardError as exc:
            self.status_var.set("任务被并发保护拦截")
            self.show_info(str(exc))
            return
        blocked_by_active_task = False
        with self.task_lock:
            if exclusive and self.blocking_task_count > 0:
                blocked_by_active_task = True
            else:
                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
        if blocked_by_active_task:
            task_guard.release()
            self.status_var.set("已有任务运行中")
            self.show_info("已有购买、搭建或资源管理任务正在运行，请等待完成后再开始新任务，避免华为云接口并发超时。")
            return
        if was_idle:
            self.stop_event.clear()
        self._activate_task_context(task_context, name)
        self.status_var.set(self._task_running_status(name))
        self.log(f"{name}已异步启动", task_context)

        def wrapped() -> None:
            try:
                worker()
            except Exception as exc:
                trace = traceback.format_exc()
                self._append_log_file(f"{name}失败: {exc}\n{trace}", task_context)
                friendly = remove_failure_export_notice(friendly_popup_text(f"{name}失败：{exc}") or str(exc))
                self.after(0, self.log, f"{name}失败: {friendly}", task_context)
                self.after(0, self.show_error, f"{friendly}\n\n{FAILURE_EXPORT_NOTICE}")
            finally:
                task_guard.release()
                if name in {"购买", "购买并搭建"}:
                    self.after(0, self._reset_one_time_x1_ip_count_after_task)
                    self.after(0, self._reset_one_time_purchase_eip_quota_after_task)
                if name in {"搭建", "购买并搭建"}:
                    self.after(0, self._reset_one_time_build_config_after_task)
                self.after(0, self._finish_task, name, exclusive, task_context, affects_global_busy)

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

    def call_ui(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
        self.after(0, lambda: func(*args, **kwargs))

    def call_ui_sync(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
        if threading.current_thread() is threading.main_thread():
            return func(*args, **kwargs)
        done = threading.Event()
        result: Dict[str, Any] = {"value": None, "error": None}

        def run() -> None:
            try:
                result["value"] = func(*args, **kwargs)
            except Exception as exc:
                result["error"] = exc
            finally:
                done.set()

        self.after(0, run)
        done.wait()
        if result["error"] is not None:
            raise result["error"]
        return result["value"]

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

    def _has_blocking_tasks(self) -> bool:
        with self.task_lock:
            return self.blocking_task_count > 0

    def _credential_fingerprint(self, ak: Optional[str] = None, sk: Optional[str] = None) -> str:
        ak_value = (ak if ak is not None else self.ak_var.get()).strip()
        sk_value = (sk if sk is not None else self.sk_var.get()).strip()
        if not ak_value or not sk_value:
            return ""
        return f"{ak_value}\n{sk_value}"

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

    @staticmethod
    def _mask_customer_guard_value(value: Any) -> str:
        text = str(value or "").strip()
        if not text:
            return ""
        if len(text) <= 8:
            return "*" * len(text)
        return f"{text[:4]}***{text[-4:]}"

    def _customer_guard_detail_summary(self, detail: Any) -> str:
        if not isinstance(detail, dict):
            return ""
        identity = detail.get("identity")
        if not isinstance(identity, dict):
            return ""
        parts: list[str] = []
        account_id = self._mask_customer_guard_value(identity.get("account_id"))
        account = self._mask_customer_guard_value(identity.get("account"))
        if account_id:
            parts.append(f"账号ID={account_id}")
        if account:
            parts.append(f"账号名={account}")
        if identity.get("enabled") is not None:
            parts.append(f"启用={identity.get('enabled')}")
        return "，" + "，".join(parts) if parts else ""

    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)

    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._credential_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
        wt_internal_auth = self._current_manager_auth_code() == WT_INTERNAL_AUTH_CODE
        checking_message = CUSTOMER_CREDENTIAL_CHECKING_MESSAGE if wt_internal_auth else CUSTOMER_OWNERSHIP_CHECKING_MESSAGE
        checking_log_message = CUSTOMER_CREDENTIAL_CHECKING_LOG_MESSAGE if wt_internal_auth else CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE
        self.customer_guard_status = "checking"
        self.customer_guard_fingerprint = fingerprint
        self.customer_guard_message = checking_message
        if not self._has_active_tasks():
            self.status_var.set(checking_log_message)
        if self.customer_guard_log_fingerprint != fingerprint:
            self.customer_guard_log_fingerprint = fingerprint
            self._log_customer_guard_message(checking_log_message)

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

            def worker() -> None:
                status = "blocked"
                message = CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
                try:
                    from HUAWEI.api import huawei_customer_guard

                    ok = False
                    guard_message = CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
                    skip_partner_check = self._manager_auth_skips_partner_check()
                    total_attempts = 1 if skip_partner_check else 2

                    def emit_guard_log(text: str) -> None:
                        self.after(0, lambda msg=text: self._log_customer_guard_message(msg))

                    for attempt in range(total_attempts):
                        attempt_no = attempt + 1
                        if not skip_partner_check:
                            emit_guard_log(f"华为云AK/SK合作伙伴校验第{attempt_no}/{total_attempts}次：正在请求服务端。")
                        ok, guard_message, detail = huawei_customer_guard.validate_customer_ownership(
                            ak,
                            sk,
                            skip_partner_check=skip_partner_check,
                        )
                        summary = self._customer_guard_detail_summary(detail)
                        if skip_partner_check and ok:
                            emit_guard_log("华为云AK/SK校验：账号凭证有效。")
                        elif ok:
                            emit_guard_log(f"华为云AK/SK合作伙伴校验第{attempt_no}/{total_attempts}次：合作伙伴{summary}。")
                        elif guard_message == NON_PARTNER_MESSAGE:
                            emit_guard_log(f"华为云AK/SK合作伙伴校验第{attempt_no}/{total_attempts}次：非合作伙伴{summary}。")
                        else:
                            emit_guard_log(f"华为云AK/SK合作伙伴校验第{attempt_no}/{total_attempts}次：{guard_message}{summary}")
                        if ok or guard_message != NON_PARTNER_MESSAGE or attempt >= total_attempts - 1:
                            break
                        emit_guard_log("华为云AK/SK合作伙伴第1次返回非合作伙伴，正在进行第2次复核。")
                        time.sleep(1.2 + attempt)
                    if ok:
                        status = "ok"
                    message = guard_message or CUSTOMER_OWNERSHIP_REQUIRED_MESSAGE
                except Exception as exc:
                    status = "blocked"
                    message = classify_auth_exception(exc)
                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._credential_fingerprint() != 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(message or 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()
            self._schedule_region_project_warmup(fingerprint)
            pending = self.customer_guard_pending_resource_query
            self.customer_guard_pending_resource_query = None
            if pending:
                self.after(100, lambda args=pending: self.query_manage_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 self._credentials_ready_for_customer_guard(ak_value, sk_value):
            return ""
        fingerprint = self._credential_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 self._customer_guard_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, force=True)
        return self._customer_guard_checking_message()

    def _append_customer_guard_log_widget(self, widget: Optional[tk.Text], text: str) -> None:
        if widget is None:
            return
        if _is_huawei_realtime_noise_line(text):
            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_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._credential_fingerprint():
            return
        ready_message = CUSTOMER_CREDENTIAL_READY_MESSAGE if self.manager_auth_partner_bypass else CUSTOMER_OWNERSHIP_READY_MESSAGE
        self.status_var.set(ready_message)
        self._log_customer_guard_message(ready_message)

    def _ensure_result_scope_for_current_credentials(self) -> None:
        fingerprint = self._credential_fingerprint()
        if not fingerprint:
            return
        # 只有账号凭据变化时才清空结果框；同一账号重复购买/搭建应保留旧结果并增量更新。
        if self.results_credential_fingerprint and fingerprint != self.results_credential_fingerprint:
            self.last_results = []
            self.clear_results()
        self.results_credential_fingerprint = fingerprint

    @staticmethod
    def _static_bgp_enabled_for_plan(plan: RegionPlan, enabled: bool) -> bool:
        return bool(enabled and plan.key in STATIC_BGP_REGION_KEYS)

    @staticmethod
    def _eip_type_for_plan(plan: RegionPlan, static_bgp_enabled: bool) -> str:
        return STATIC_BGP_EIP_TYPE if HuaweiAutoBuyApp._static_bgp_enabled_for_plan(plan, static_bgp_enabled) else EIP_TYPE

    @staticmethod
    def _eip_actual_type_label(eip: Dict[str, Any]) -> str:
        values = []
        for key in (
            "type",
            "publicip_type",
            "public_ip_type",
            "publicipType",
            "publicIpType",
            "ip_type",
            "ipType",
            "network_type",
            "networkType",
            "line",
            "line_id",
            "lineId",
        ):
            value = eip.get(key)
            if value not in (None, ""):
                values.append(str(value))
        bandwidth = eip.get("bandwidth")
        if isinstance(bandwidth, dict):
            for key in ("type", "line", "line_id", "lineId", "publicip_type", "public_ip_type"):
                value = bandwidth.get(key)
                if value not in (None, ""):
                    values.append(str(value))
        text = " ".join(values)
        compact = text.replace(" ", "").replace("_", "").replace("-", "").lower()
        raw_lower = text.lower()
        if "5sbgp" in compact or "sbgp" in compact or "static" in raw_lower or "静态" in text:
            return "静态BGP"
        if "5bgp" in compact or "bgp" in compact or "dynamic" in raw_lower or "动态" in text:
            return "全动态BGP"
        return ""

    @staticmethod
    def _is_confirmed_static_bgp_sold_out(exc: BaseException) -> bool:
        text = str(exc)
        compact = text.replace(" ", "").replace("\u3000", "")
        lower = text.lower()
        compact_lower = compact.lower()
        static_markers = ("静态bgp", "静态BGP", "5_sbgp", "sbgp", STATIC_BGP_EIP_TYPE)
        sold_out_markers = ("已售罄", "售罄", "sold out", "sell out", "sellout")
        # 只有错误内容同时指向“静态BGP线路”和“明确售罄”才弹窗；普通 capacity/网络抖动继续走重试。
        return any(marker.lower() in compact_lower for marker in static_markers) and any(
            marker.lower() in lower or marker.lower() in compact_lower for marker in sold_out_markers
        )

    @staticmethod
    def _is_confirmed_eip_sold_out_text(text: str) -> bool:
        lower = str(text or "").lower()
        compact = lower.replace(" ", "").replace("\u3000", "")
        return any(
            marker in lower or marker in compact
            for marker in (
                "vpc.0532",
                "eip pool has been sold out",
                "no more ip addresses can be allocated",
                "public ip sold out",
                "公网ip售罄",
                "弹性公网ip售罄",
                "eip售罄",
            )
        )

    @classmethod
    def _is_confirmed_eip_sold_out(cls, exc: BaseException) -> bool:
        return cls._is_confirmed_eip_sold_out_text(f"{exc} {getattr(exc, 'body', '')}")

    @staticmethod
    def _is_eip_quota_insufficient_text(text: str) -> bool:
        lower = str(text or "").lower()
        compact = lower.replace(" ", "").replace("\u3000", "")
        quota_markers = (
            "quota exceeded",
            "quotaexceeded",
            "insufficient quota",
            "quota insufficient",
            "not enough quota",
            "exceed quota",
            "exceeded quota",
            "request exceeds the quota",
            "配额不足",
            "配额已满",
            "超过配额",
            "达到配额",
            "已达配额",
            "配额限制",
        )
        eip_markers = (
            "eip",
            "publicip",
            "public ip",
            "elastic ip",
            "弹性公网",
            "公网ip",
            "公网 ip",
            "带宽",
            "bandwidth",
        )
        return any(marker in lower or marker in compact for marker in quota_markers) and any(
            marker in lower or marker in compact for marker in eip_markers
        )

    @classmethod
    def _is_eip_quota_insufficient(cls, exc: BaseException) -> bool:
        return cls._is_eip_quota_insufficient_text(f"{exc} {getattr(exc, 'body', '')}")

    @staticmethod
    def _is_subeni_quota_text(text: str) -> bool:
        lower = str(text or "").lower()
        return (
            "flavor_contain_subeni" in lower
            or "vpc.9905" in lower
            or ("quota exceeded" in lower and ("subeni" in lower or "网卡" in lower or "弹性网卡" in lower))
        )

    @classmethod
    def _is_subeni_quota_error(cls, exc: BaseException) -> bool:
        return cls._is_subeni_quota_text(f"{exc} {getattr(exc, 'body', '')}")

    @staticmethod
    def _eip_quota_message(plan: RegionPlan, expected_count: int, missing_count: int, original_error: str) -> str:
        missing_text = f"，当前还缺 {missing_count} 条" if missing_count > 0 else ""
        return (
            f"{plan.label} 弹性公网IP配额不足：20模式需要购买/绑定 {expected_count} 条EIP{missing_text}。\n\n"
            "已保留已创建的ECS、网卡和已绑定EIP；请在华为云提升该地区弹性公网IP/带宽配额，"
            "或减少地区后重新执行，程序会优先复用已有资源继续补齐。\n\n"
            f"原始错误：{original_error}"
        )

    def _finish_task(
        self,
        name: str,
        exclusive: bool = True,
        context: Optional[str] = None,
        affects_global_busy: bool = True,
    ) -> 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)
            self.busy = self.blocking_task_count > 0
        if context:
            self._deactivate_task_context(context)
        self.status_var.set("任务运行中" if self.active_task_count else self._task_finished_status(name))
        if not self.active_task_count:
            self.after(800, self._complete_delayed_action_if_idle)

    @staticmethod
    def _task_running_status(name: str) -> str:
        if name in {"读取账号资源", "资源一键查询"}:
            return "正在读取账号资源"
        if name.endswith("中"):
            return name
        return f"{name}运行中"

    @staticmethod
    def _task_finished_status(name: str) -> str:
        if name in {"读取账号资源", "资源一键查询"}:
            return "账号资源读取完成"
        return f"{name}结束"

    def request_stop(self) -> None:
        self.stop_event.set()
        self.log("已请求停止：正在执行中的云接口/SSH步骤不会强制中断，后续步骤会尽量跳过。")

    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 _subscription_expiry_maps(
        self,
        client: HuaweiCloudClient,
    ) -> Tuple[Dict[str, str], Dict[str, str]]:
        expire_by_id: Dict[str, str] = {}
        expire_by_name: Dict[str, str] = {}
        try:
            resources = client.list_subscription_resources(False, log=lambda m: self.after(0, self.log, m))
        except Exception as exc:
            self.after(0, self.log, f"包年包月到期时间补充查询失败，继续按ECS接口显示: {exc}")
            return expire_by_id, expire_by_name
        for item in resources:
            if not isinstance(item, dict):
                continue
            expire_time = resource_expire_time(item)
            if not expire_time:
                continue
            for resource_id in resource_id_candidates(item):
                expire_by_id[str(resource_id).casefold()] = expire_time
            for name_key in ("resource_name", "resourceName", "name", "resource_id", "resourceId"):
                name = str(item.get(name_key) or "").strip()
                if name:
                    expire_by_name[name.casefold()] = expire_time
        self.after(0, self.log, f"到期时间补充查询完成，可匹配ID {len(expire_by_id)} 个、名称 {len(expire_by_name)} 个。")
        return expire_by_id, expire_by_name

    def scan_resources(self, context: str = "purchase", auto: bool = False) -> None:
        if auto and self._has_blocking_tasks():
            self.log("已有任务运行中，跳过本次自动读取账号资源。", context)
            return
        try:
            client = self.get_client()
            plans = list(REGION_PLANS)
        except Exception as exc:
            self.show_error(str(exc))
            return
        self._set_log_context("purchase")

        def worker() -> None:
            self.after(0, self.log, "开始读取账号资源...")
            self._wait_region_project_warmup()
            pairs = client.selected_project_pairs(plans)
            self.region_projects = pairs
            expire_by_id, expire_by_name = self._subscription_expiry_maps(client)
            region_workers = min(SCAN_REGION_WORKERS, len(plans))
            self.after(
                0,
                self.log,
                f"资源读取并发: 地区={region_workers}，单地区接口={SCAN_API_WORKERS}，先等{SCAN_REGION_SOFT_TIMEOUT:.1f}秒；慢接口继续等到{SCAN_REGION_COMPLETE_TIMEOUT:.1f}秒，避免资源误显示为0。",
            )

            def scan_one_region(plan: RegionPlan) -> None:
                if self.stop_event.is_set():
                    return
                pair = pairs.get(plan.region)
                if not pair:
                    self.call_ui(self.update_region_row, plan, project_id="", host_status="无")
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] 未打通该地区，无法查询该地区资源，{REGION_ACCESS_TUTORIAL_GUIDANCE}",
                    )
                    return
                self.call_ui(self.update_region_row, plan, project_id=pair.project_id, status="读取中")
                start_time = time.monotonic()
                try:
                    executor = concurrent.futures.ThreadPoolExecutor(max_workers=SCAN_API_WORKERS)
                    try:
                        def retry_scan_api(label: str) -> List[Dict[str, Any]]:
                            retry_timeout = max(10, int(SCAN_REQUEST_TIMEOUT))
                            retry_attempts = max(2, int(SCAN_REQUEST_RETRY_ATTEMPTS))
                            retry_map = {
                                "VPC": lambda: client.list_vpcs(pair, timeout=retry_timeout, retry_attempts=retry_attempts),
                                "子网": lambda: client.list_subnets(pair, timeout=retry_timeout, retry_attempts=retry_attempts),
                                "安全组": lambda: client.list_security_groups(pair, timeout=retry_timeout, retry_attempts=retry_attempts),
                                "EIP": lambda: client.list_public_ips(pair, timeout=retry_timeout, retry_attempts=retry_attempts),
                                "辅助网卡": lambda: client.list_sub_network_interfaces(pair, timeout=retry_timeout, retry_attempts=retry_attempts),
                                "ECS": lambda: client.list_cloud_servers(pair, timeout=retry_timeout, retry_attempts=retry_attempts),
                            }
                            return retry_map[label]()

                        futures = {
                            "VPC": executor.submit(
                                client.list_vpcs,
                                pair,
                                timeout=SCAN_REQUEST_TIMEOUT,
                                retry_attempts=SCAN_REQUEST_RETRY_ATTEMPTS,
                            ),
                            "子网": executor.submit(
                                client.list_subnets,
                                pair,
                                timeout=SCAN_REQUEST_TIMEOUT,
                                retry_attempts=SCAN_REQUEST_RETRY_ATTEMPTS,
                            ),
                            "安全组": executor.submit(
                                client.list_security_groups,
                                pair,
                                timeout=SCAN_REQUEST_TIMEOUT,
                                retry_attempts=SCAN_REQUEST_RETRY_ATTEMPTS,
                            ),
                            "EIP": executor.submit(
                                client.list_public_ips,
                                pair,
                                timeout=SCAN_REQUEST_TIMEOUT,
                                retry_attempts=SCAN_REQUEST_RETRY_ATTEMPTS,
                            ),
                            "辅助网卡": executor.submit(
                                client.list_sub_network_interfaces,
                                pair,
                                timeout=SCAN_REQUEST_TIMEOUT,
                                retry_attempts=SCAN_REQUEST_RETRY_ATTEMPTS,
                            ),
                            "ECS": executor.submit(
                                client.list_cloud_servers,
                                pair,
                                timeout=SCAN_REQUEST_TIMEOUT,
                                retry_attempts=SCAN_REQUEST_RETRY_ATTEMPTS,
                            ),
                        }
                        future_to_label = {future: label for label, future in futures.items()}
                        counts = {}
                        fetched = {}
                        errors = {}
                        done, not_done = concurrent.futures.wait(
                            list(future_to_label.keys()),
                            timeout=SCAN_REGION_SOFT_TIMEOUT,
                        )
                        if not_done:
                            slow_labels = "、".join(future_to_label[future] for future in not_done)
                            self.after(0, self.log, f"[{plan.label}] 慢接口继续等待: {slow_labels} 超过{SCAN_REGION_SOFT_TIMEOUT:.1f}秒未返回。")
                            remaining_timeout = max(0.5, SCAN_REGION_COMPLETE_TIMEOUT - SCAN_REGION_SOFT_TIMEOUT)
                            more_done, not_done = concurrent.futures.wait(list(not_done), timeout=remaining_timeout)
                            done = set(done) | set(more_done)
                        for future in done:
                            label = future_to_label[future]
                            try:
                                result_items = future.result()
                                fetched[label] = result_items
                                counts[label] = len(result_items)
                            except Exception as exc:
                                errors[label] = cloud_error_message_cn(exc)
                                try:
                                    self.after(0, self.log, f"[{plan.label}] {label} 快读失败，立即补齐读取。")
                                    result_items = retry_scan_api(label)
                                except Exception as retry_exc:
                                    errors[label] = f"{errors[label]}；补齐失败: {cloud_error_message_cn(retry_exc)}"
                                else:
                                    fetched[label] = result_items
                                    counts[label] = len(result_items)
                                    errors.pop(label, None)
                        for future in not_done:
                            label = future_to_label[future]
                            future.cancel()
                            try:
                                self.after(0, self.log, f"[{plan.label}] {label} 慢接口未返回，立即补齐读取，避免误显示为0。")
                                result_items = retry_scan_api(label)
                            except Exception as retry_exc:
                                errors[label] = f"超过{SCAN_REGION_COMPLETE_TIMEOUT:.1f}秒未返回；补齐失败: {cloud_error_message_cn(retry_exc)}"
                            else:
                                fetched[label] = result_items
                                counts[label] = len(result_items)
                    finally:
                        executor.shutdown(wait=False, cancel_futures=True)
                    if errors:
                        details = "；".join(f"{label}: {error}" for label, error in errors.items())
                        self.after(0, self.log, f"[{plan.label}] 快读未完整返回: {details}")
                    elapsed = time.monotonic() - start_time
                    def count_text(label: str) -> str:
                        return str(counts[label]) if label in counts else "?"
                    resource_summary = f"ECS:{count_text('ECS')} EIP:{count_text('EIP')}"
                    status = " | ".join(f"{label}={counts.get(label, 0)}" for label in ("VPC", "子网", "安全组", "EIP", "辅助网卡", "ECS"))
                    status = f"{status} | 用时={elapsed:.1f}s"
                    servers = list(fetched.get("ECS") or [])
                    self._queue_huawei_ip_segments(plan, list(fetched.get("EIP") or []))
                    display_server = next((server for server in servers if str(server.get("status") or "").upper() in {"ACTIVE", "RUNNING"}), None)
                    if display_server is None and servers:
                        display_server = servers[0]
                    if display_server:
                        self.after(0, self.log, f"[{plan.label}] ECS属性: {self._server_attribute_display_summary(display_server)}")
                    server_ip = HuaweiCloudClient.public_ip_from_server(display_server) if display_server else ""
                    server_id = str(display_server.get("id") or "") if display_server else ""
                    flavor = server_flavor_label(display_server) if display_server else ""
                    server_name = str(display_server.get("name") or "") if display_server else ""
                    host_status = server_status_text(display_server) if display_server else "无"
                    expire_time = ""
                    if display_server:
                        server_billing = server_charging_text(display_server)
                        expire_time = (
                            server_expire_time(display_server)
                            or expire_by_id.get(server_id.casefold(), "")
                            or expire_by_name.get(server_name.casefold(), "")
                        )
                        expire_time = display_expire_time_for_billing(
                            display_server,
                            billing_text=server_billing,
                            explicit_expire_time=expire_time,
                        )
                    self.call_ui(
                        self.update_region_row,
                        plan,
                        project_id=pair.project_id,
                        host_status=host_status,
                        server_ip=server_ip,
                        server_id=server_id,
                        flavor=flavor,
                        expire_time=expire_time,
                        resource_summary=resource_summary,
                    )
                    self.after(0, self.log, f"[{plan.label}] {status}")
                except Exception as exc:
                    self.call_ui(self.update_region_row, plan, project_id=pair.project_id, host_status="无")
                    self.after(0, self.log, f"[{plan.label}] 读取失败: {cloud_error_message_cn(exc)}")

            with concurrent.futures.ThreadPoolExecutor(max_workers=region_workers) as executor:
                futures = [executor.submit(scan_one_region, plan) for plan in plans]
                for future in concurrent.futures.as_completed(futures):
                    future.result()
            self.after(0, self.log, "账号资源读取完成。")

        self.run_task(
            "读取账号资源",
            worker,
            exclusive=False,
            context=context,
            affects_global_busy=False,
            regions=[plan.region for plan in plans],
            risk_group=READ_RESOURCE_RISK_GROUP,
        )

    def query_manage_resources(
        self,
        silent: bool = False,
        context: str = "manage",
        plans_override: Optional[Sequence[RegionPlan]] = None,
        merge_with_cache: bool = False,
        final_popup_message: Optional[str] = None,
    ) -> None:
        try:
            client = self.get_client()
        except Exception as exc:
            if str(exc).strip() == CUSTOMER_OWNERSHIP_CHECKING_MESSAGE:
                self.customer_guard_pending_resource_query = (
                    (),
                    {
                        "silent": silent,
                        "context": context,
                        "plans_override": plans_override,
                        "merge_with_cache": merge_with_cache,
                        "final_popup_message": final_popup_message,
                    },
                )
            self.show_error(str(exc))
            return
        self._set_log_context(context)
        plans = list(REGION_PLANS if plans_override is None else plans_override)
        if not plans:
            self.log("未识别到需要刷新的华为云地区，已跳过自动刷新资源。", context)
            self._mark_customer_resource_query_ready()
            return
        only_main = self.manage_only_main_var.get()
        refreshed_regions = [plan.region for plan in plans] if merge_with_cache else []
        if not merge_with_cache:
            self.populate_manage_tree([])

        def apply_partial_resource_items(
            partial_items: Sequence[Dict[str, Any]],
            partial_regions: Optional[Sequence[str]] = None,
        ) -> None:
            self.apply_manage_resource_refresh(partial_items, partial_regions or [])

        def worker() -> None:
            query_status: Dict[str, Any] = {}
            try:
                all_items = self._collect_manage_resources(
                    client,
                    plans,
                    only_main,
                    full=True,
                    on_partial=apply_partial_resource_items,
                    query_status=query_status,
                )
            except Exception:
                if final_popup_message:
                    self.after(0, self.show_info, final_popup_message)
                raise
            confirmed_regions = sorted(query_status.get("successful_regions") or ())
            self.after(
                0,
                self.apply_manage_resource_refresh,
                all_items,
                refreshed_regions,
                confirmed_regions,
            )
            display_count = len(self._manage_display_items(all_items))
            self.after(0, self.log, f"资源管理一键查询完成，共 {display_count} 条。")
            if not silent:
                self.after(0, self.show_info, f"一键查询完成，共 {display_count} 条资源。")
            if final_popup_message:
                self.after(0, self.show_info, final_popup_message)
            self.after(50, self._mark_customer_resource_query_ready)

        self.run_task(
            "资源一键查询",
            worker,
            exclusive=False,
            context=context,
            affects_global_busy=False,
            regions=[plan.region for plan in plans],
            risk_group=READ_RESOURCE_RISK_GROUP,
        )

    def refresh_all_resource_views(self) -> None:
        try:
            self.query_manage_resources(silent=True)
        except Exception as exc:
            self.after(0, self.log, f"资源同步刷新失败: {exc}")

    def refresh_resource_views_for_plans(
        self,
        plans: Sequence[RegionPlan],
        context: str = "manage",
        *,
        final_popup_message: Optional[str] = None,
    ) -> None:
        if final_popup_message:
            final_popup_message = self._queue_result_message_or_popup(final_popup_message)
        refresh_plans = [plan for plan in plans if plan is not None]
        if not refresh_plans:
            if final_popup_message:
                self.after(0, self.show_info, final_popup_message)
            return
        try:
            self.query_manage_resources(
                silent=True,
                context=context,
                plans_override=refresh_plans,
                merge_with_cache=True,
                final_popup_message=final_popup_message,
            )
        except Exception as exc:
            labels = "、".join(getattr(plan, "label", str(plan)) for plan in refresh_plans)
            self.after(0, self.log, f"{labels} 资源同步刷新失败: {exc}")
            if final_popup_message:
                self.after(0, self.show_info, final_popup_message)

    def _resolve_current_reinstall_server_id(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        server_id: str,
        label: str,
        emit: Callable[[str], None],
        *,
        preferred_name: str = "",
        preferred_public_ip: str = "",
        flavor_mode: str = FLAVOR_MODE_FIXED,
        strict_server_id: bool = False,
    ) -> str:
        original_id = str(server_id or "").strip()
        if original_id:
            try:
                detail = client.show_server(pair, original_id)
                status = str(detail.get("status") or "").upper()
                if status not in {"DELETED", "SOFT_DELETED"}:
                    return original_id
                if strict_server_id:
                    raise RuntimeError(f"所选云服务器已删除或不可用: {original_id}")
            except Exception as exc:
                if not is_instance_not_found_error(exc):
                    raise
                if strict_server_id:
                    raise RuntimeError(f"所选云服务器已不存在，已停止重装: {original_id}") from exc
                emit(f"[{label}] 原ECS ID已失效，正在按地区刷新当前云服务器: {original_id}")
        if strict_server_id:
            raise RuntimeError("所选云服务器缺少有效ECS ID，已停止重装。")

        servers = client.list_cloud_servers(pair, timeout=20, retry_attempts=1)
        candidates = []
        for server in servers:
            current_id = str(server.get("id") or "").strip()
            status = str(server.get("status") or "").upper()
            if not current_id or status in {"DELETED", "SOFT_DELETED"}:
                continue
            if status not in {"ACTIVE", "RUNNING", "SHUTOFF"}:
                continue
            candidates.append(server)
        if not candidates:
            raise RuntimeError("当前地区未读取到可重装的云服务器，已停止重装。")

        def candidate_score(server: Dict[str, Any]) -> Tuple[int, int, int, int, str]:
            name = str(server.get("name") or "")
            public_ip = client.public_ip_from_server(server)
            status = str(server.get("status") or "").upper()
            config_match = 0
            try:
                if client.server_matches_purchase_config(server, flavor_mode, pair.region):
                    config_match = 1
            except Exception:
                config_match = 0
            created = str(server.get("created") or server.get("updated") or "")
            return (
                1 if preferred_name and name == preferred_name else 0,
                1 if preferred_public_ip and public_ip == preferred_public_ip else 0,
                config_match,
                1 if status in {"ACTIVE", "RUNNING"} else 0,
                created,
            )

        selected = sorted(candidates, key=candidate_score, reverse=True)[0]
        resolved_id = str(selected.get("id") or "")
        name = str(selected.get("name") or "未命名")
        status_text = server_status_text(selected)
        public_ip = client.public_ip_from_server(selected)
        emit(f"[{label}] 已刷新为当前云服务器: {resolved_id} ({name}, {status_text}, {public_ip or '无公网IP'})")
        if not resolved_id:
            raise RuntimeError("已找到云服务器但缺少服务器ID。")
        return resolved_id

    def reinstall_selected_region_servers(self) -> None:
        items = self.selected_region_server_items()
        if not items:
            self.show_info("请先读取账号资源，并选择已识别到 ECS 的地区。")
            return
        password = self.password_var.get().strip() or DEFAULT_PASSWORD
        flavor_mode = self._current_flavor_mode_or_default()
        if not self.ask_confirm_cancel(
            action_confirm_message(
                "确认重装选定云服务器",
                regions=[str(item.get("label") or "") for item in items],
                warning=DESTRUCTIVE_CONFIRM_TEXT,
            )
        ):
            return
        try:
            client = self.get_client()
        except Exception as exc:
            self.show_error(str(exc))
            return
        self._set_log_context("purchase")

        def reinstall_one(item: Dict[str, Any]) -> Tuple[str, str, str]:
            if self.stop_event.is_set():
                return str(item["label"]), "failed", "用户请求停止"
            label = str(item["label"])
            row_iid = str(item.get("row_iid") or "")
            strict_server_id = str(item.get("exact_server") or "") == "1"
            pair = ProjectPair(item["region"], item["project_id"])
            routeros_host = self._routeros_reinstall_host([str(item.get("server_ip") or "")])
            if routeros_host:
                self.after(0, self.log, f"[{label}] 已识别当前系统为RouterOS，重装后将确认Debian/OpenSSH恢复。")
            if row_iid:
                self.call_ui(self.update_region_ecs_row, row_iid, status="准备重装")
            resolved_id = self._resolve_current_reinstall_server_id(
                client,
                pair,
                item["server_id"],
                label,
                lambda message: self.after(0, self.log, message),
                preferred_name=item.get("server_name", ""),
                preferred_public_ip=item.get("server_ip", ""),
                flavor_mode=flavor_mode,
                strict_server_id=strict_server_id,
            )
            if resolved_id != item["server_id"]:
                plan = REGION_BY_CODE.get(item["region"])
                if row_iid:
                    self.call_ui(self.update_region_ecs_row, row_iid, server_id=resolved_id)
                elif plan:
                    self.call_ui(self.update_region_row, plan, server_id=resolved_id)
            try:
                result = client.reinstall_server_with_cloud_init(pair, resolved_id, password)
            except Exception as exc:
                if not is_instance_not_found_error(exc) or strict_server_id:
                    raise
                self.after(0, self.log, f"[{label}] 重装提交时ECS ID再次失效，重新刷新后再提交。")
                resolved_id = self._resolve_current_reinstall_server_id(
                    client,
                    pair,
                    "",
                    label,
                    lambda message: self.after(0, self.log, message),
                    preferred_public_ip=item.get("server_ip", ""),
                    flavor_mode=flavor_mode,
                )
                result = client.reinstall_server_with_cloud_init(pair, resolved_id, password)
            job_id = HuaweiCloudClient.job_id_from_create_response(result)
            if not job_id:
                if row_iid:
                    self.call_ui(self.update_region_ecs_row, row_iid, status="重装已提交，待确认")
                return label, "submitted", "重装任务已提交但未返回job_id，未确认成功"
            self.after(0, self.log, f"[{label}] 重装任务已提交，重装中。")
            if row_iid:
                self.call_ui(self.update_region_ecs_row, row_iid, status="重装中")
            try:
                client.wait_job(pair, job_id, lambda m: self.after(0, self.log, f"[{label}] {self._reinstall_job_log_text(m)}"), timeout=1200, interval=8)
            except Exception as wait_exc:
                if row_iid:
                    self.call_ui(self.update_region_ecs_row, row_iid, status="重装已提交，待确认")
                return label, "submitted", f"重装已提交，但Job状态未确认成功: {wait_exc}"
            if routeros_host:
                if row_iid:
                    self.call_ui(self.update_region_ecs_row, row_iid, status="确认Debian")
                try:
                    self._wait_for_debian_after_routeros_reinstall(
                        [routeros_host],
                        lambda message: self.after(0, self.log, f"[{label}] {message}"),
                    )
                except Exception as ready_exc:
                    if row_iid:
                        self.call_ui(self.update_region_ecs_row, row_iid, status="未恢复Debian")
                    return label, "failed", str(ready_exc)
            if row_iid:
                self.call_ui(self.update_region_ecs_row, row_iid, status="重装完成")
            return label, "success", "重装完成"

        def worker() -> None:
            success_count = 0
            submitted_count = 0
            failed_count = 0
            successful_regions: Set[str] = set()
            max_workers = min(MAX_WORKERS, len(items))
            self.after(0, self.log, f"步骤: 开始并发重装 并发数={max_workers} 地区={[item['label'] for item in items]}")
            with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
                futures = {executor.submit(reinstall_one, item): item for item in items}
                for future in concurrent.futures.as_completed(futures):
                    try:
                        label, status, message = future.result()
                    except Exception as exc:
                        item = futures[future]
                        label, status, message = str(item.get("label") or "重装系统"), "failed", cloud_error_message_cn(exc)
                        row_iid = str(item.get("row_iid") or "")
                        if row_iid:
                            self.call_ui(self.update_region_ecs_row, row_iid, status=f"重装失败: {message}")
                    # 中文注释：重装任务只有Job最终SUCCESS才计入成功；已提交但未确认单独提示，避免误报全成功。
                    if status == "success":
                        success_count += 1
                        region = str(futures[future].get("region") or "").strip()
                        if region:
                            successful_regions.add(region)
                    elif status == "submitted":
                        submitted_count += 1
                    else:
                        failed_count += 1
                    self.after(0, self.log, f"[{label}] 重装结果: {message}")
            if successful_regions:
                self.after(
                    0,
                    lambda regions=sorted(successful_regions): self._clear_results_for_regions(
                        regions,
                        context="purchase",
                    ),
                )
            summary = f"重装任务处理完成，成功 {success_count}/{len(items)}，提交未确认 {submitted_count}，失败 {failed_count}。"
            self.after(0, self.show_error if failed_count else self.show_info, summary)

        self.run_task("重装系统", worker, context="purchase", regions=[item["region"] for item in items])

    def reinstall_selected_manage_servers(self) -> None:
        items = [item for item in self.selected_manage_items() if item.get("_kind") == "ecs" and item.get("_server_id")]
        if not items:
            self.show_info("请先选择需要重装的 ECS 行。")
            return
        password = self.password_var.get().strip() or DEFAULT_PASSWORD
        flavor_mode = self._current_flavor_mode_or_default()
        if not self.ask_confirm_cancel(
            action_confirm_message(
                f"确认重装 {len(items)} 台云服务器",
                regions=[str(item.get("_region_label") or "") for item in items],
                resources=[str(item.get("_name") or item.get("_server_id") or "") for item in items],
                warning=DESTRUCTIVE_CONFIRM_TEXT,
            )
        ):
            return
        try:
            client = self.get_client()
        except Exception as exc:
            self.show_error(str(exc))
            return
        self._set_log_context("manage")

        def reinstall_one(item: Dict[str, Any]) -> Tuple[str, str, str]:
            pair = ProjectPair(str(item["_region"]), str(item["_project_id"]))
            label = f"{item.get('_region_label')} / {item.get('_name')}"
            routeros_host = self._routeros_reinstall_host([str(item.get("_public_ip") or "")])
            if routeros_host:
                self.after(0, self.log, f"[{label}] 已识别当前系统为RouterOS，重装后将确认Debian/OpenSSH恢复。")
            resolved_id = self._resolve_current_reinstall_server_id(
                client,
                pair,
                str(item["_server_id"]),
                label,
                lambda message: self.after(0, self.log, message),
                preferred_name=str(item.get("_name") or ""),
                preferred_public_ip=str(item.get("_public_ip") or ""),
                flavor_mode=flavor_mode,
            )
            try:
                result = client.reinstall_server_with_cloud_init(pair, resolved_id, password)
            except Exception as exc:
                if not is_instance_not_found_error(exc):
                    raise
                self.after(0, self.log, f"[{label}] 重装提交时ECS ID再次失效，重新刷新后再提交。")
                resolved_id = self._resolve_current_reinstall_server_id(
                    client,
                    pair,
                    "",
                    label,
                    lambda message: self.after(0, self.log, message),
                    preferred_name=str(item.get("_name") or ""),
                    preferred_public_ip=str(item.get("_public_ip") or ""),
                    flavor_mode=flavor_mode,
                )
                result = client.reinstall_server_with_cloud_init(pair, resolved_id, password)
            job_id = HuaweiCloudClient.job_id_from_create_response(result)
            if not job_id:
                return label, "submitted", f"已提交但未返回job_id，未确认成功: {json.dumps(result, ensure_ascii=False)}"
            self.after(0, self.log, f"[{label}] 重装任务已提交，重装中。")
            try:
                client.wait_job(pair, job_id, lambda m: self.after(0, self.log, f"[{label}] {self._reinstall_job_log_text(m)}"), timeout=1200, interval=8)
            except Exception as exc:
                return label, "submitted", f"已提交，但Job状态未确认成功: {cloud_error_message_cn(exc)}"
            if routeros_host:
                try:
                    self._wait_for_debian_after_routeros_reinstall(
                        [routeros_host],
                        lambda message: self.after(0, self.log, f"[{label}] {message}"),
                    )
                except Exception as ready_exc:
                    return label, "failed", str(ready_exc)
            return label, "success", "成功"

        def worker() -> None:
            success_count = 0
            submitted_count = 0
            failed_count = 0
            successful_regions: Set[str] = set()
            with concurrent.futures.ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(items))) as executor:
                futures = {executor.submit(reinstall_one, item): item for item in items}
                for future in concurrent.futures.as_completed(futures):
                    try:
                        label, status, message = future.result()
                    except Exception as exc:
                        item = futures[future]
                        label = f"{item.get('_region_label')} / {item.get('_name')}"
                        status, message = "failed", f"失败: {cloud_error_message_cn(exc)}"
                    # 中文注释：资源管理重装同样只把Job SUCCESS计入成功，避免网络异常或查询超时误报。
                    if status == "success":
                        success_count += 1
                        region = str(futures[future].get("_region") or "").strip()
                        if region:
                            successful_regions.add(region)
                    elif status == "submitted":
                        submitted_count += 1
                    else:
                        failed_count += 1
                    self.after(0, self.log, f"[{label}] 重装结果: {message}")
            if successful_regions:
                self.after(
                    0,
                    lambda regions=sorted(successful_regions): self._clear_results_for_regions(
                        regions,
                        context="manage",
                    ),
                )
            summary = f"重装任务处理完成，成功 {success_count}/{len(items)}，提交未确认 {submitted_count}，失败 {failed_count}。"
            self.after(0, self.show_error if failed_count else self.show_info, summary)

        self.run_task("重装系统", worker, context="manage", regions=[str(item.get("_region") or "") for item in items])

    def _process_manage_items(self, client: HuaweiCloudClient, items: Sequence[Dict[str, Any]]) -> Tuple[int, int]:
        success = 0
        failed = 0
        pending_by_region: Dict[str, int] = {}
        refreshed_regions: Set[str] = set()
        pending_lock = threading.Lock()
        plan_by_region = {plan.region: plan for plan in REGION_PLANS}
        for pending_item in items:
            region = str(pending_item.get("_region") or "")
            if region:
                pending_by_region[region] = pending_by_region.get(region, 0) + 1

        def mark_items_finished(done_items: Sequence[Dict[str, Any]], reason: str) -> None:
            ready_regions: List[str] = []
            with pending_lock:
                for done_item in done_items:
                    region = str(done_item.get("_region") or "")
                    if not region or region in refreshed_regions:
                        continue
                    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[region] for region in ready_regions if region in plan_by_region]
            if not plans:
                return
            try:
                refreshed = self._collect_manage_resources(client, plans, self.manage_only_main_var.get(), full=True)
                self.after(0, self.apply_manage_resource_refresh, refreshed, ready_regions)
                labels = "、".join(display_region(region) for region in ready_regions)
                self.after(0, self.log, f"[{labels}] {reason}后已实时刷新购买搭建和资源管理资源库。")
            except Exception as exc:
                labels = "、".join(display_region(region) for region in ready_regions)
                self.after(0, self.log, f"[{labels}] 删除后实时刷新资源库失败: {exc}")

        subscription_items = [
            item
            for item in items
            if item.get("_operation_kind") == "subscription_unsubscribe" and item.get("_resource_id")
        ]
        subscription_ids = [
            str(item.get("_resource_id") or "")
            for item in subscription_items
        ]
        subscription_ecs_by_pair: Dict[Tuple[str, str], List[str]] = {}
        for item in items:
            if item.get("_operation_kind") != "subscription_unsubscribe" or item.get("_kind") != "ecs":
                continue
            key = (str(item.get("_region") or ""), str(item.get("_project_id") or ""))
            subscription_ecs_by_pair.setdefault(key, []).append(str(item.get("_server_id") or item.get("_resource_id") or ""))

        def wait_subscription_ecs(pair_key: Tuple[str, str], server_ids: Sequence[str]) -> bool:
            region, project_id = pair_key
            if not region or not project_id:
                return True
            return client.wait_cloud_servers_deleted(
                ProjectPair(region, project_id),
                server_ids,
                lambda message, label=display_region(region): self.after(0, self.log, f"[{label}] {message}"),
                timeout=600,
            )

        if subscription_ids:
            try:
                results = client.unsubscribe_subscription_resources(
                    subscription_ids,
                    self.manage_unsubscribe_type_var.get(),
                    self.manage_reason_var.get().strip() or "不再使用",
                )
                for result in results:
                    self.after(0, self.log, f"包周期退订返回: {json.dumps(result, ensure_ascii=False)}")
                wait_ok = True
                if subscription_ecs_by_pair:
                    workers = min(MAX_WORKERS, len(subscription_ecs_by_pair))
                    self.after(0, self.log, f"包周期ECS退订等待并发启动：{len(subscription_ecs_by_pair)} 个地区/项目，并发 {workers}。")
                    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
                        futures = [
                            executor.submit(wait_subscription_ecs, pair_key, server_ids)
                            for pair_key, server_ids in subscription_ecs_by_pair.items()
                        ]
                        for future in concurrent.futures.as_completed(futures):
                            wait_ok = future.result() and wait_ok
                if wait_ok:
                    success += len(subscription_ids)
                else:
                    failed += len(subscription_ids)
                    self.after(0, self.log, "包周期退订已提交，但部分ECS未在等待时间内消失。")
            except Exception as exc:
                failed += len(subscription_ids)
                self.after(0, self.log, f"包周期退订失败: {exc}")
            finally:
                mark_items_finished(subscription_items, "包周期退订")

        ecs_by_pair: Dict[Tuple[str, str], List[str]] = {}
        ecs_items_by_pair: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}
        for item in items:
            if item.get("_operation_kind") != "ecs_delete":
                continue
            key = (str(item.get("_region") or ""), str(item.get("_project_id") or ""))
            ecs_by_pair.setdefault(key, []).append(str(item.get("_server_id") or item.get("_resource_id") or ""))
            ecs_items_by_pair.setdefault(key, []).append(item)

        def cleanup_ecs_group(pair_key: Tuple[str, str], server_ids: Sequence[str]) -> bool:
            region, project_id = pair_key
            try:
                pair = ProjectPair(region, project_id)
                result = client.delete_cloud_servers(pair, server_ids, delete_publicip=False, delete_volume=True)
                self.after(0, self.log, f"[{display_region(region)}] 按需ECS删除返回: {json.dumps(result, ensure_ascii=False)}")
                if client.wait_cloud_servers_deleted(
                    pair,
                    server_ids,
                    lambda message, label=display_region(region): self.after(0, self.log, f"[{label}] {message}"),
                    timeout=600,
                ):
                    return True
                else:
                    self.after(0, self.log, f"[{display_region(region)}] 按需ECS删除已提交，但未在等待时间内确认消失。")
                    return False
            except Exception as exc:
                self.after(0, self.log, f"[{display_region(region)}] 按需ECS删除失败: {exc}")
                return False

        if ecs_by_pair:
            workers = min(MAX_WORKERS, len(ecs_by_pair))
            self.after(0, self.log, f"按需ECS按地区/项目并发删除启动：{len(ecs_by_pair)} 组，并发 {workers}。")
            with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
                future_map = {
                    executor.submit(cleanup_ecs_group, pair_key, server_ids): pair_key
                    for pair_key, server_ids in ecs_by_pair.items()
                }
                for future in concurrent.futures.as_completed(future_map):
                    pair_key = future_map[future]
                    group_items = ecs_items_by_pair.get(pair_key, [])
                    server_ids = ecs_by_pair.get(pair_key, [])
                    try:
                        if future.result():
                            success += len(server_ids)
                        else:
                            failed += len(server_ids)
                    except Exception as exc:
                        failed += len(server_ids)
                        region = pair_key[0]
                        self.after(0, self.log, f"[{display_region(region)}] 按需ECS删除线程异常: {exc}")
                    finally:
                        mark_items_finished(group_items, "按需ECS删除")

        if subscription_ids or ecs_by_pair:
            self.after(0, self.log, "云服务器退订/删除已提交，等待云端释放网卡和安全组绑定关系 8 秒。")
            time.sleep(8)

        eip_items = [item for item in items if item.get("_operation_kind") == "eip_delete"]

        def cleanup_eip_item(item: Dict[str, Any]) -> bool:
            pair = ProjectPair(str(item.get("_region") or ""), str(item.get("_project_id") or ""))
            resource_id = str(item.get("_resource_id") or "")
            port_id = str(item.get("_port_id") or "")
            label = f"[{item.get('_region_label')}] EIP {item.get('_name') or resource_id}"
            try:
                client.delete_public_ip(pair, resource_id)
                if client.wait_public_ip_deleted(pair, resource_id, lambda message: self.after(0, self.log, f"{label} {message}")):
                    self.after(0, self.log, f"{label} 释放成功。")
                    return True
                else:
                    self.after(0, self.log, f"{label} 释放已提交，但未在等待时间内确认消失。")
                    return False
            except Exception as exc:
                if not port_id:
                    self.after(0, self.log, f"{label} 释放失败: {exc}")
                    return False
                try:
                    self.after(0, self.log, f"{label} 直接释放未完成，尝试解绑后释放: {exc}")
                    client.unbind_public_ip(pair, resource_id)
                    client.wait_public_ip_unbound(pair, resource_id)
                    client.delete_public_ip(pair, resource_id)
                    if client.wait_public_ip_deleted(pair, resource_id, lambda message: self.after(0, self.log, f"{label} {message}")):
                        self.after(0, self.log, f"{label} 解绑后释放成功。")
                        return True
                    else:
                        self.after(0, self.log, f"{label} 解绑后释放已提交，但未在等待时间内确认消失。")
                        return False
                except Exception as retry_exc:
                    self.after(0, self.log, f"{label} 释放失败: {retry_exc}")
                    return False

        if eip_items:
            workers = min(10, len(eip_items))
            self.after(0, self.log, f"EIP并发释放启动：{len(eip_items)} 条，并发 {workers}。")
            with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
                future_map = {executor.submit(cleanup_eip_item, item): item for item in eip_items}
                for future in concurrent.futures.as_completed(future_map):
                    item = future_map[future]
                    try:
                        if future.result():
                            success += 1
                        else:
                            failed += 1
                    except Exception as exc:
                        failed += 1
                        self.after(0, self.log, f"EIP释放线程异常: {exc}")
                    finally:
                        mark_items_finished([item], "EIP释放")

        def cleanup_virtual_ip_item(item: Dict[str, Any]) -> bool:
            pair = ProjectPair(str(item.get("_region") or ""), str(item.get("_project_id") or ""))
            port_id = str(item.get("_resource_id") or item.get("_port_id") or "")
            label = f"[{item.get('_region_label')}] 虚拟IP {item.get('_name') or port_id}"
            if not port_id:
                self.after(0, self.log, f"{label} 缺少端口ID，无法删除。")
                return False
            try:
                try:
                    public_ips = client.list_public_ips(pair, timeout=30, retry_attempts=2)
                except Exception as exc:
                    public_ips = []
                    self.after(0, self.log, f"{label} 查询绑定EIP提示: {exc}")
                for eip in public_ips:
                    if str(eip.get("port_id") or "") != port_id:
                        continue
                    eip_id = str(eip.get("id") or eip.get("publicip_id") or "")
                    if not eip_id:
                        continue
                    try:
                        client.unbind_public_ip(pair, eip_id)
                        client.wait_public_ip_unbound(pair, eip_id, timeout=60)
                        self.after(0, self.log, f"{label} 已解绑绑定EIP {public_ip_address(eip) or eip_id}。")
                    except Exception as exc:
                        self.after(0, self.log, f"{label} 解绑EIP {public_ip_address(eip) or eip_id} 提示: {exc}")
                client.update_port(pair, port_id, {"port": {"allowed_address_pairs": []}})
            except Exception:
                pass
            try:
                client.delete_port(pair, port_id)
                self.after(0, self.log, f"{label} 删除成功。")
                return True
            except CloudApiError as exc:
                if exc.status == 404 or "not found" in str(exc).lower():
                    self.after(0, self.log, f"{label} 已不存在，按成功处理。")
                    return True
                self.after(0, self.log, f"{label} 删除失败: {exc}")
                return False
            except Exception as exc:
                self.after(0, self.log, f"{label} 删除失败: {exc}")
                return False

        virtual_ip_items = [item for item in items if item.get("_operation_kind") == "virtual_ip_delete"]
        if virtual_ip_items:
            workers = min(8, len(virtual_ip_items))
            self.after(0, self.log, f"虚拟IP并发删除启动：{len(virtual_ip_items)} 条，并发 {workers}。")
            with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
                future_map = {executor.submit(cleanup_virtual_ip_item, item): item for item in virtual_ip_items}
                for future in concurrent.futures.as_completed(future_map):
                    item = future_map[future]
                    try:
                        if future.result():
                            success += 1
                        else:
                            failed += 1
                    except Exception as exc:
                        failed += 1
                        self.after(0, self.log, f"虚拟IP删除线程异常: {exc}")
                    finally:
                        mark_items_finished([item], "虚拟IP删除")

        def is_default_security_group_item(item: Dict[str, Any]) -> bool:
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
            name = str(item.get("_name") or raw.get("name") or "").strip().lower()
            return bool(raw.get("default")) or name in {"default", "sys-default", "default-sg"}

        deleted_security_group_ids: Set[str] = set()
        deleted_security_group_lock = threading.Lock()

        def cleanup_security_group_item(item: Dict[str, Any]) -> bool:
            pair = ProjectPair(str(item.get("_region") or ""), str(item.get("_project_id") or ""))
            resource_id = str(item.get("_resource_id") or "")
            label = f"[{item.get('_region_label')}] {item.get('_kind_label')} {item.get('_name') or resource_id}"
            if resource_id:
                with deleted_security_group_lock:
                    if resource_id in deleted_security_group_ids:
                        return True
            if is_default_security_group_item(item):
                self.after(0, self.log, f"{label} 默认安全组不可删除，已跳过。")
                return True
            try:
                raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else {}
                for rule in raw.get("security_group_rules") or raw.get("rules") or []:
                    if isinstance(rule, dict) and rule.get("id"):
                        try:
                            client.delete_security_group_rule(pair, str(rule["id"]))
                        except Exception as rule_exc:
                            self.after(0, self.log, f"{label} 规则删除提示: {rule_exc}")
                client.delete_security_group(pair, resource_id)
                if client.wait_security_group_deleted(pair, resource_id, lambda message: self.after(0, self.log, f"{label} {message}")):
                    if resource_id:
                        with deleted_security_group_lock:
                            deleted_security_group_ids.add(resource_id)
                    self.after(0, self.log, f"{label} 处理成功。")
                    return True
                self.after(0, self.log, f"{label} 删除已提交，但未在等待时间内确认消失。")
                return False
            except CloudApiError as exc:
                if exc.status == 404 or "not found" in str(exc).lower():
                    if resource_id:
                        with deleted_security_group_lock:
                            deleted_security_group_ids.add(resource_id)
                    self.after(0, self.log, f"{label} 已不存在，按成功处理。")
                    return True
                self.after(0, self.log, f"{label} 处理失败: {exc}")
                return False
            except Exception as exc:
                self.after(0, self.log, f"{label} 处理失败: {exc}")
                return False

        def cleanup_vpc_item(item: Dict[str, Any]) -> bool:
            pair = ProjectPair(str(item.get("_region") or ""), str(item.get("_project_id") or ""))
            resource_id = str(item.get("_resource_id") or "")
            subnet_ids = [str(subnet_id) for subnet_id in (item.get("_subnet_ids") or []) if subnet_id]
            label = f"[{item.get('_region_label')}] {item.get('_kind_label')} {item.get('_name') or resource_id}"

            def cloud_error_detail(exc: Exception) -> str:
                return f"{exc} {getattr(exc, 'body', '')}".lower()

            def is_already_deleted(exc: Exception) -> bool:
                detail = cloud_error_detail(exc)
                return (
                    isinstance(exc, CloudApiError)
                    and exc.status == 404
                    or "not found" in detail
                    or "resource could not be found" in detail
                    or "sys.0404" in detail
                    or "vpc.0216" in detail
                )

            def is_vpc_busy(exc: Exception) -> bool:
                detail = cloud_error_detail(exc)
                return (
                    isinstance(exc, CloudApiError)
                    and exc.status == 409
                    or "routerinuse" in detail
                    or "router is used" in detail
                    or "vpc.0108" in detail
                    or "in use" in detail
                    or "占用" in detail
                )

            def cleanup_leftover_ports() -> None:
                try:
                    interfaces = client.list_sub_network_interfaces(pair, timeout=30, retry_attempts=2)
                except Exception as exc:
                    self.after(0, self.log, f"{label} 辅助网卡查询提示: {exc}")
                    interfaces = []
                for interface in interfaces:
                    interface_id = str(interface.get("id") or "")
                    virsubnet_id = str(interface.get("virsubnet_id") or interface.get("subnet_id") or "")
                    if not interface_id or (subnet_ids and virsubnet_id not in subnet_ids):
                        continue
                    try:
                        client.delete_sub_network_interface(pair, interface_id)
                        self.after(0, self.log, f"{label} 已删除残留辅助网卡 {interface_id}。")
                    except Exception as exc:
                        self.after(0, self.log, f"{label} 残留辅助网卡 {interface_id} 删除提示: {exc}")

                try:
                    ports = client.list_ports(pair, timeout=30, retry_attempts=2)
                except Exception as exc:
                    self.after(0, self.log, f"{label} 端口查询提示: {exc}")
                    return
                for port in ports:
                    port_id = str(port.get("id") or "")
                    port_vpc_id = str(port.get("vpc_id") or "")
                    fixed_ips = port.get("fixed_ips") or []
                    port_subnets = {
                        str(row.get("subnet_id") or "")
                        for row in fixed_ips
                        if isinstance(row, dict) and row.get("subnet_id")
                    }
                    if not port_id:
                        continue
                    port_text = json.dumps(port, ensure_ascii=False)
                    matches_vpc = (port_vpc_id == resource_id) or (resource_id and resource_id in port_text)
                    matches_subnet = bool(subnet_ids and (port_subnets & set(subnet_ids) or any(subnet_id in port_text for subnet_id in subnet_ids)))
                    if not matches_vpc and not matches_subnet:
                        continue
                    owner = str(port.get("device_owner") or "").lower()
                    if "router" in owner:
                        continue
                    if "vip" in owner:
                        cleanup_virtual_ip_item(
                            {
                                "_kind": "virtual_ip",
                                "_kind_label": "虚拟IP",
                                "_region": item.get("_region"),
                                "_region_label": item.get("_region_label"),
                                "_project_id": item.get("_project_id"),
                                "_name": next(iter(self._port_fixed_ip_addresses(port)), port_id),
                                "_resource_id": port_id,
                                "_port_id": port_id,
                                "_raw": port,
                            }
                        )
                        continue
                    try:
                        client.delete_port(pair, port_id)
                        self.after(0, self.log, f"{label} 已删除残留端口 {port_id}。")
                    except Exception as exc:
                        self.after(0, self.log, f"{label} 残留端口 {port_id} 删除提示: {exc}")

            def cleanup_security_groups_for_vpc() -> None:
                try:
                    groups = client.list_security_groups(pair, timeout=30, retry_attempts=2)
                except Exception as exc:
                    self.after(0, self.log, f"{label} 安全组补充查询提示: {exc}")
                    return
                for group in groups:
                    if not isinstance(group, dict):
                        continue
                    group_id = str(group.get("id") or "")
                    group_name = str(group.get("name") or group_id)
                    if not group_id:
                        continue
                    if bool(group.get("default")) or group_name.strip().lower() in {"default", "sys-default", "default-sg"}:
                        continue
                    group_vpc_id = str(group.get("vpc_id") or "")
                    group_text = json.dumps(group, ensure_ascii=False)
                    if group_vpc_id and group_vpc_id != resource_id:
                        continue
                    if not group_vpc_id and resource_id not in group_text and "wawacloud" not in group_name.lower():
                        continue
                    cleanup_security_group_item(
                        {
                            "_kind": "security_group",
                            "_kind_label": "安全组",
                            "_region": item.get("_region"),
                            "_region_label": item.get("_region_label"),
                            "_project_id": item.get("_project_id"),
                            "_name": group_name,
                            "_resource_id": group_id,
                            "_raw": group,
                        }
                    )

            def delete_subnet_with_retry(subnet_id: str) -> None:
                for attempt in range(1, 4):
                    try:
                        if attempt > 1:
                            cleanup_leftover_ports()
                            cleanup_security_groups_for_vpc()
                        client.delete_subnet(pair, subnet_id)
                        if not client.wait_subnet_deleted(pair, subnet_id, lambda message: self.after(0, self.log, f"{label} 子网: {message}")):
                            raise CloudApiError(f"子网 {subnet_id} 删除等待超时")
                        self.after(0, self.log, f"{label} 子网 {subnet_id} 删除成功。")
                        return
                    except Exception as exc:
                        if is_already_deleted(exc):
                            self.after(0, self.log, f"{label} 子网 {subnet_id} 已不存在，按删除成功处理。")
                            return
                        if attempt >= 3:
                            raise
                        self.after(0, self.log, f"{label} 子网 {subnet_id} 第 {attempt} 次删除提示: {exc}，清理残留端口后重试。")
                        cleanup_leftover_ports()
                        cleanup_security_groups_for_vpc()
                        time.sleep(4 * attempt)

            def delete_vpc_with_retry() -> None:
                for attempt in range(1, 5):
                    try:
                        if attempt > 1:
                            cleanup_leftover_ports()
                            cleanup_security_groups_for_vpc()
                        client.delete_vpc(pair, resource_id)
                        if not client.wait_vpc_deleted(pair, resource_id, lambda message: self.after(0, self.log, f"{label} {message}")):
                            raise CloudApiError(f"VPC {resource_id} 删除等待超时")
                        return
                    except Exception as exc:
                        if is_already_deleted(exc):
                            self.after(0, self.log, f"{label} VPC已不存在，按删除成功处理。")
                            return
                        if attempt >= 4:
                            raise
                        wait_seconds = 8 + attempt * 6
                        if is_vpc_busy(exc):
                            self.after(0, self.log, f"{label} VPC仍被云路由或残留端口占用，第 {attempt} 次提示: {exc}，等待 {wait_seconds} 秒后补删。")
                        else:
                            self.after(0, self.log, f"{label} VPC第 {attempt} 次删除提示: {exc}，等待 {wait_seconds} 秒后重试。")
                        cleanup_leftover_ports()
                        cleanup_security_groups_for_vpc()
                        time.sleep(wait_seconds)

            try:
                cleanup_leftover_ports()
                cleanup_security_groups_for_vpc()
                for subnet_id in subnet_ids:
                    delete_subnet_with_retry(subnet_id)
                cleanup_security_groups_for_vpc()
                delete_vpc_with_retry()
                self.after(0, self.log, f"{label} 处理成功。")
                return True
            except Exception as exc:
                self.after(0, self.log, f"{label} 处理失败: {exc}")
                return False

        def run_cleanup_phase(
            title: str,
            phase_items: Sequence[Dict[str, Any]],
            cleanup: Callable[[Dict[str, Any]], bool],
            *,
            workers: int,
        ) -> Tuple[int, List[Dict[str, Any]]]:
            if not phase_items:
                return 0, []
            ok_count = 0
            failed_items: List[Dict[str, Any]] = []
            self.after(0, self.log, f"{title}并发处理启动：{len(phase_items)} 项，并发 {workers}。")
            with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, min(workers, len(phase_items)))) as executor:
                future_map = {executor.submit(cleanup, item): item for item in phase_items}
                for future in concurrent.futures.as_completed(future_map):
                    item = future_map[future]
                    try:
                        if future.result():
                            ok_count += 1
                        else:
                            failed_items.append(item)
                    except Exception as exc:
                        failed_items.append(item)
                        self.after(0, self.log, f"{title}线程异常: {exc}")
            return ok_count, failed_items

        security_group_items = [item for item in items if item.get("_operation_kind") == "security_group_delete"]
        vpc_items = [item for item in items if item.get("_operation_kind") == "vpc_delete"]
        vpc_ok, failed_vpc_items = run_cleanup_phase("VPC", vpc_items, cleanup_vpc_item, workers=3)
        success += vpc_ok
        if failed_vpc_items:
            self.after(0, self.log, f"VPC首轮仍有 {len(failed_vpc_items)} 项被占用，等待 10 秒后补删一次。")
            time.sleep(10)
            retry_ok, failed_vpc_items = run_cleanup_phase("VPC补删", failed_vpc_items, cleanup_vpc_item, workers=2)
            success += retry_ok
        failed += len(failed_vpc_items)
        mark_items_finished(vpc_items, "VPC处理")

        remaining_security_group_items = []
        for item in security_group_items:
            resource_id = str(item.get("_resource_id") or "")
            if resource_id:
                with deleted_security_group_lock:
                    if resource_id in deleted_security_group_ids:
                        continue
            remaining_security_group_items.append(item)
        sg_ok, failed_sg_items = run_cleanup_phase("安全组", remaining_security_group_items, cleanup_security_group_item, workers=5)
        success += sg_ok
        if failed_sg_items:
            self.after(0, self.log, f"安全组首轮仍有 {len(failed_sg_items)} 项被占用，等待 10 秒后补删一次。")
            time.sleep(10)
            retry_ok, failed_sg_items = run_cleanup_phase("安全组补删", failed_sg_items, cleanup_security_group_item, workers=4)
            success += retry_ok
        failed += len(failed_sg_items)
        mark_items_finished(security_group_items, "安全组处理")

        return success, failed

    def unsubscribe_selected_manage_resources(self) -> None:
        items = [item for item in self.selected_manage_items() if item.get("_operation_kind") not in {"", "none", None}]
        if not items:
            self.show_info("请先选择可退订/可删除/可释放的资源。")
            return
        confirm_message = action_confirm_message(
            f"确认退订/删除/释放 {len(items)} 项资源",
            regions=sorted({str(item.get("_region_label") or "") for item in items if item.get("_region_label")}),
            resources=[f"{item.get('_kind_label') or item.get('_kind')}: {item.get('_name') or item.get('_resource_id')}" for item in items],
            warning=DESTRUCTIVE_CONFIRM_TEXT,
        )
        release_ip_groups = self._manage_release_ip_groups(items)
        release_action = self._ask_cleanup_release_action(
            confirm_message,
            allow_used_ip_import=bool(release_ip_groups),
        )
        if release_action == "cancel":
            return
        if release_action == "confirm_and_import":
            try:
                self._import_manage_release_ip_groups(release_ip_groups)
            except Exception as exc:
                message = f"导入本次待释放IP失败，已停止释放资源：{exc}"
                self.log(message, "manage")
                self.show_info(message)
                return
        try:
            client = self.get_client()
        except Exception as exc:
            self.show_error(str(exc))
            return
        self._set_log_context("manage")
        affected_regions = sorted({str(item.get("_region") or "") for item in items if item.get("_region")})
        affected_plans = [plan for plan in REGION_PLANS if plan.region in affected_regions]

        def worker() -> None:
            success, failed = self._process_manage_items(client, items)
            if success > 0 and affected_regions:
                self.after(
                    0,
                    lambda regions=list(affected_regions): self._clear_results_for_regions(
                        regions,
                        context="manage",
                    ),
                )
            try:
                refreshed = self._collect_manage_resources(client, affected_plans or list(REGION_PLANS), self.manage_only_main_var.get(), full=True)
                refresh_regions = [plan.region for plan in affected_plans]
                self.after(0, self.apply_manage_resource_refresh, refreshed, refresh_regions)
                self.after(0, self.log, f"资源处理后已自动刷新所选地区资源，共 {len(refreshed)} 条。")
            except Exception as exc:
                self.after(0, self.log, f"资源处理后自动刷新所选地区资源失败: {exc}")
            final_popup_message = self._queue_result_message_or_popup(f"资源处理完成，成功 {success}，失败 {failed}。")
            if final_popup_message:
                self.after(250, self.show_info, final_popup_message)

        self.run_task("退订/删除资源", worker, context="manage", regions=[str(item.get("_region") or "") for item in items])

    def _used_ip_table_account_label(self) -> str:
        username = self._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("HUAWEI") / "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")
        source_pending_path = self._used_ip_pending_path(source_path)
        target_pending_path = self._used_ip_pending_path(target_path)
        with _used_ip_table_file_lock(target_path):
            source_rows = [
                *self._read_used_ip_csv_rows(source_path),
                *self._read_used_ip_csv_rows(source_pending_path),
            ]
            existing_rows = [
                *self._read_used_ip_csv_rows(target_path),
                *self._read_used_ip_csv_rows(target_pending_path),
            ]
            failed_rows = self._read_failed_ip_rows(target_path)
            merged_rows = self._dedupe_used_ip_csv_rows([*existing_rows, *source_rows])
            self._write_used_ip_csv_rows_unlocked(target_path, merged_rows, failed_rows=failed_rows)
            source_pending_path.unlink(missing_ok=True)
            target_pending_path.unlink(missing_ok=True)
        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("HUAWEI") / "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")

    @staticmethod
    def _used_ip_csv_template_path() -> Path:
        return Path(__file__).resolve().parent / "templates" / USED_IP_CSV_TEMPLATE_NAME

    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"
        options: List[Any] = [
            command,
            "-parent",
            str(parent),
            "-title",
            title,
            "-filetypes",
            (("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")),
        ]
        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_error(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)
        self._apply_dialog_icon(win)
        win.transient(self.winfo_toplevel())
        win.grab_set()
        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

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

        actions = ttk.Frame(body)
        actions.grid(row=2, column=0, columnspan=2, pady=(2, 0))

        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)
            if path.suffix.lower() not in {".csv", ".xlsx"}:
                status_var.set("保存失败：旧IP库必须为 .csv 或 .xlsx 文件。")
                return
            try:
                self._sync_current_account_ips_to_used_ip_table()
                source_path = self._used_ip_table_path()
                source_rows = self._read_used_ip_table_rows(source_path)
                source_failed_rows = self._read_failed_ip_rows(source_path)
                target_rows = self._read_used_ip_table_rows(path)
                target_failed_rows = self._read_failed_ip_rows(path)
                if path.suffix.lower() == ".csv":
                    companion_xlsx = path.with_suffix(".xlsx")
                    target_rows = self._dedupe_used_ip_csv_rows(
                        [*target_rows, *self._read_used_ip_table_rows(companion_xlsx)]
                    )
                    target_failed_rows = self._dedupe_used_ip_csv_rows(
                        [*target_failed_rows, *self._read_failed_ip_rows(companion_xlsx)]
                    )
                target_rows = self._dedupe_used_ip_csv_rows(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])
                original_count = len(target_rows)
                merged_count = max(0, len(rows) - original_count)
                self._write_used_ip_csv_rows(path, rows, failed_rows=failed_rows)
                if path.suffix.lower() == ".csv":
                    path = self._migrate_used_ip_csv_to_xlsx(path, update_link=True)
                    self._write_used_ip_csv_rows(path, rows, failed_rows=failed_rows)
                else:
                    self._set_linked_used_ip_table_path(path)
                if path.suffix.lower() == ".xlsx":
                    for companion in (path.with_suffix(".csv"), path.with_name(f"{path.with_suffix('.csv').name}.lock")):
                        try:
                            if companion != Path(target):
                                companion.unlink(missing_ok=True)
                        except OSError:
                            pass
                else:
                    try:
                        path.with_name(f"{path.name}.lock").unlink(missing_ok=True)
                    except OSError:
                        pass
            except Exception as exc:
                status_var.set(f"保存失败：{exc}")
                return
            path_var.set(str(path))
            status_var.set(
                f"已合并保存并链接：{path}，原有 {original_count} 条，新增 {merged_count} 条，共 {len(rows)} 条。"
            )

        def link_existing_table() -> None:
            target = ask_csv_path(
                mode="open",
                title="链接旧IP库",
            )
            if not target:
                return
            path = Path(target)
            if path.suffix.lower() not in {".csv", ".xlsx"}:
                status_var.set("链接失败：旧IP库必须为 .csv 或 .xlsx 文件。")
                return
            try:
                if not path.exists():
                    raise FileNotFoundError(f"未找到已用IP表: {path}")
                active_path = self._migrate_used_ip_csv_to_xlsx(path, update_link=True) if path.suffix.lower() == ".csv" else path
                rows = self._read_used_ip_table_rows(active_path)
                if active_path.suffix.lower() == ".xlsx" and not active_path.exists():
                    self._write_used_ip_csv_rows(active_path, rows)
                self._set_linked_used_ip_table_path(active_path)
            except Exception as exc:
                status_var.set(f"链接失败：{exc}")
                return
            path_var.set(str(active_path))
            migrated_text = "，CSV已自动升级为Excel并保留原文件" if path.suffix.lower() == ".csv" else ""
            status_var.set(f"已链接旧IP库，读取到 {len(rows)} 条IP{migrated_text}。")

        ttk.Button(actions, text="保存当前旧IP库(模板)", width=28, command=save_current_template).pack(side=tk.LEFT, padx=(0, 12))
        ttk.Button(actions, text="链接旧IP库", width=14, command=link_existing_table).pack(side=tk.LEFT)
        ttk.Label(body, textvariable=status_var).grid(row=3, column=0, columnspan=2, sticky="w", pady=(10, 0))

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

    def _used_ip_table_account_value(self) -> str:
        username = self._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:
            ipaddress.IPv4Address(text)
        except Exception:
            return ""
        return text

    @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)

    @staticmethod
    def _parse_used_ip_timestamp(value: Any) -> Optional[datetime]:
        if isinstance(value, datetime):
            return value
        if isinstance(value, date):
            return datetime.combine(value, datetime.min.time())
        text = str(value or "").strip()
        if not text:
            return None
        iso_text = text[:-1] + "+00:00" if text.endswith(("Z", "z")) else text
        try:
            return datetime.fromisoformat(iso_text)
        except ValueError:
            pass
        for pattern in (
            "%Y/%m/%d %H:%M:%S",
            "%Y/%m/%d %H:%M",
            "%Y/%m/%d",
            "%Y.%m.%d %H:%M:%S",
            "%Y.%m.%d %H:%M",
            "%Y.%m.%d",
        ):
            try:
                return datetime.strptime(text, pattern)
            except ValueError:
                continue
        chinese_match = re.fullmatch(
            r"\s*(\d{4})年(\d{1,2})月(\d{1,2})日"
            r"(?:\s*(\d{1,2})(?:时|:)(\d{1,2})(?:(?:分|:)(\d{1,2})秒?)?)?\s*",
            text,
        )
        if chinese_match:
            parts = [int(value or 0) for value in chinese_match.groups()]
            try:
                return datetime(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5])
            except ValueError:
                return None
        try:
            excel_serial = float(text)
            if 1 <= excel_serial <= 2958465:
                return datetime(1899, 12, 30) + timedelta(days=excel_serial)
        except (OverflowError, ValueError):
            pass
        return None

    def _retain_recent_used_ip_rows(
        self,
        rows: Sequence[Dict[str, str]],
        *,
        today: Optional[date] = None,
    ) -> List[Dict[str, str]]:
        current_date = today or datetime.now().date()
        cutoff_date = current_date - timedelta(days=max(0, USED_IP_RETENTION_DAYS - 1))
        retained: List[Dict[str, str]] = []
        for row in rows:
            timestamp = self._parse_used_ip_timestamp(row.get(USED_IP_CSV_HEADERS[0]))
            if timestamp is None or not cutoff_date <= timestamp.date() <= current_date:
                continue
            retained.append(row)
        return retained

    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
            existing_timestamp = self._parse_used_ip_timestamp(existing_row.get(USED_IP_CSV_HEADERS[0]))
            normalized_timestamp = self._parse_used_ip_timestamp(normalized_row.get(USED_IP_CSV_HEADERS[0]))
            if existing_timestamp is not None and existing_timestamp.tzinfo is not None:
                existing_timestamp = existing_timestamp.astimezone().replace(tzinfo=None)
            if normalized_timestamp is not None and normalized_timestamp.tzinfo is not None:
                normalized_timestamp = normalized_timestamp.astimezone().replace(tzinfo=None)
            if normalized_timestamp is not None and (
                existing_timestamp is None or normalized_timestamp > existing_timestamp
            ):
                for header in USED_IP_CSV_HEADERS:
                    existing_row[header] = normalized_row.get(header, "")
                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 _write_used_ip_csv_rows_unlocked(
        self,
        path: Path,
        rows: Sequence[Dict[str, str]],
        *,
        failed_rows: Optional[Sequence[Dict[str, str]]] = None,
    ) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        normalized = self._retain_recent_used_ip_rows(
            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
            )
            temp_path = path.with_name(
                f".{path.stem}.{os.getpid()}.{threading.get_ident()}.{time.time_ns()}.tmp.xlsx"
            )
            try:
                written = write_excel_view_file(
                    temp_path,
                    USED_IP_CSV_HEADERS,
                    normalized,
                    column_widths=USED_IP_TABLE_COLUMN_WIDTHS,
                    target_path=temp_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,
                )
                if written is None:
                    raise OSError("已用IP表 Excel 临时文件写入失败。")
                os.replace(temp_path, path)
            finally:
                try:
                    temp_path.unlink(missing_ok=True)
                except OSError:
                    pass
            return
        temp_path = path.with_name(
            f".{path.name}.{os.getpid()}.{threading.get_ident()}.{time.time_ns()}.tmp"
        )
        try:
            with temp_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)
                handle.flush()
                os.fsync(handle.fileno())
            os.replace(temp_path, path)
            try:
                path.with_suffix(".xlsx").unlink(missing_ok=True)
            except OSError:
                pass
        finally:
            try:
                temp_path.unlink(missing_ok=True)
            except OSError:
                pass
    def _write_used_ip_csv_rows(
        self,
        path: Path,
        rows: Sequence[Dict[str, str]],
        *,
        failed_rows: Optional[Sequence[Dict[str, str]]] = None,
    ) -> None:
        with _used_ip_table_file_lock(path):
            self._write_used_ip_csv_rows_unlocked(path, rows, failed_rows=failed_rows)

    def _create_used_ip_csv_table(self, path: Path) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        template_path = self._used_ip_csv_template_path()
        if template_path.exists() and not path.exists():
            self._write_used_ip_csv_rows(path, self._read_used_ip_csv_rows(template_path))
            return
        self._write_used_ip_csv_rows(path, [])

    @staticmethod
    def _used_ip_pending_path(path: Path) -> Path:
        return path.with_name(f"{path.name}.pending.csv")

    def _read_used_ip_table_rows_unlocked(self, path: Path) -> List[Dict[str, str]]:
        pending_path = self._used_ip_pending_path(path)
        return self._dedupe_used_ip_csv_rows(
            [*self._read_used_ip_csv_rows(path), *self._read_used_ip_csv_rows(pending_path)]
        )

    def _merge_pending_used_ip_rows_unlocked(self, path: Path) -> bool:
        pending_path = self._used_ip_pending_path(path)
        if not pending_path.exists():
            return False
        pending_rows = self._read_used_ip_csv_rows(pending_path)
        if not pending_rows:
            pending_path.unlink(missing_ok=True)
            return False
        rows = self._dedupe_used_ip_csv_rows(
            [*self._read_used_ip_csv_rows(path), *pending_rows]
        )
        self._write_used_ip_csv_rows_unlocked(path, rows)
        pending_path.unlink(missing_ok=True)
        return True

    def _schedule_used_ip_pending_merge(
        self,
        path: Path,
        *,
        retention_cleanup: bool = False,
    ) -> None:
        target = Path(path)
        pending_path = self._used_ip_pending_path(target)
        key = os.path.normcase(str(target.resolve(strict=False)))
        with _USED_IP_TABLE_PENDING_WORKERS_LOCK:
            if key in _USED_IP_TABLE_PENDING_WORKERS:
                return
            _USED_IP_TABLE_PENDING_WORKERS.add(key)

        def worker() -> None:
            merged = False
            retention_cleaned = False
            try:
                for _attempt in range(USED_IP_TABLE_PENDING_RETRY_ATTEMPTS):
                    if not pending_path.exists() and not retention_cleanup:
                        break
                    time.sleep(USED_IP_TABLE_PENDING_RETRY_SECONDS)
                    try:
                        with _used_ip_table_file_lock(target):
                            if pending_path.exists():
                                merged = self._merge_pending_used_ip_rows_unlocked(target)
                            if retention_cleanup and not merged:
                                raw_rows = self._read_used_ip_table_rows_unlocked(target)
                                retained_rows = self._retain_recent_used_ip_rows(raw_rows)
                                if len(retained_rows) != len(raw_rows):
                                    self._write_used_ip_csv_rows_unlocked(target, retained_rows)
                                    pending_path.unlink(missing_ok=True)
                                    retention_cleaned = True
                        if merged or retention_cleaned or not pending_path.exists():
                            break
                    except Exception:
                        continue
                if merged or retention_cleaned:
                    try:
                        self.after(0, self.log, f"已用IP库文档已自动写入: {target}")
                    except Exception:
                        pass
            finally:
                with _USED_IP_TABLE_PENDING_WORKERS_LOCK:
                    _USED_IP_TABLE_PENDING_WORKERS.discard(key)

        threading.Thread(
            target=worker,
            daemon=True,
            name="huawei-used-ip-pending-merge",
        ).start()

    def _read_used_ip_table_rows(self, path: Path) -> List[Dict[str, str]]:
        merge_deferred = False
        retention_deferred = False
        with _used_ip_table_file_lock(path):
            try:
                self._merge_pending_used_ip_rows_unlocked(path)
            except OSError:
                merge_deferred = self._used_ip_pending_path(path).exists()
            raw_rows = self._read_used_ip_table_rows_unlocked(path)
            rows = self._retain_recent_used_ip_rows(raw_rows)
            if len(rows) != len(raw_rows):
                try:
                    self._write_used_ip_csv_rows_unlocked(path, rows)
                    self._used_ip_pending_path(path).unlink(missing_ok=True)
                except OSError:
                    retention_deferred = True
        if merge_deferred or retention_deferred:
            self._schedule_used_ip_pending_merge(
                path,
                retention_cleanup=retention_deferred,
            )
        return rows

    def _load_used_ip_table(self) -> Set[str]:
        return {row[USED_IP_CSV_HEADERS[3]] for row in self._read_used_ip_table_rows(self._used_ip_table_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

    @staticmethod
    def _dedupe_public_ip_values(values: Sequence[Any]) -> List[str]:
        cleaned: List[str] = []
        seen: Set[str] = set()
        for value in values:
            text = str(value or "").strip()
            if not text or text in seen:
                continue
            try:
                ipaddress.IPv4Address(text)
            except Exception:
                continue
            if not is_public_ip(text):
                continue
            seen.add(text)
            cleaned.append(text)
        return cleaned

    @staticmethod
    def _huawei_known_network_candidate_ips(
        address: str,
        seed_addresses: Sequence[str] = (),
        excluded_addresses: Sequence[str] = (),
        limit: int = 32,
    ) -> List[str]:
        try:
            original = tuple(
                int(part)
                for part in str(ipaddress.IPv4Address(str(address or "").strip())).split(".")
            )
        except Exception:
            return []
        excluded: Set[str] = set()
        for value in (address, *excluded_addresses):
            try:
                excluded.add(str(ipaddress.IPv4Address(str(value or "").strip())))
            except Exception:
                continue
        networks: List[Tuple[Tuple[int, int, int], int]] = []
        network_seen: Set[Tuple[int, int, int]] = set()
        for value in (*seed_addresses, address):
            try:
                parts = tuple(
                    int(part)
                    for part in str(ipaddress.IPv4Address(str(value or "").strip())).split(".")
                )
            except Exception:
                continue
            network = parts[:3]
            if network in network_seen:
                continue
            network_seen.add(network)
            networks.append((network, parts[3]))
        if not networks:
            return []
        rotation = sum((index + 1) * value for index, value in enumerate(original)) % len(networks)
        networks = networks[rotation:] + networks[:rotation]
        ordered: List[str] = []
        seen = set(excluded)
        target_limit = max(1, int(limit))
        for distance in range(0, 255):
            for host_mode in range(4):
                for network, seed_host in networks:
                    host = (
                        original[3] + distance,
                        original[3] - distance,
                        seed_host + distance,
                        seed_host - distance,
                    )[host_mode]
                    if not 1 <= host <= 254:
                        continue
                    candidate = ".".join(str(part) for part in (*network, host))
                    if candidate in seen:
                        continue
                    seen.add(candidate)
                    if not is_public_ip(candidate):
                        continue
                    ordered.append(candidate)
                    if len(ordered) >= target_limit:
                        return ordered
        return ordered

    def _huawei_eip_candidate_rounds(
        self,
        plan: RegionPlan,
        eip_type: str,
        seed_addresses: Sequence[str],
        *,
        excluded_addresses: Sequence[str] = (),
        anchor_ip: str = "",
        rotation_index: int = 1,
    ) -> List[List[str]]:
        clean_type = str(eip_type or EIP_TYPE)
        excluded = set(self._dedupe_public_ip_values(excluded_addresses))
        excluded.update(self._dedupe_public_ip_values((anchor_ip,)))
        seeds = self._dedupe_public_ip_values(seed_addresses)
        network_seeds: List[Tuple[str, str]] = []
        network_seen: Set[str] = set()
        for value in seeds:
            try:
                network = str(ipaddress.IPv4Network(f"{value}/24", strict=False))
            except Exception:
                continue
            if network in network_seen:
                continue
            network_seen.add(network)
            network_seeds.append((network, value))
        if not network_seeds:
            return []

        randomizer = secrets.SystemRandom()
        randomizer.shuffle(network_seeds)
        rotation = (max(1, int(rotation_index)) - 1) % len(network_seeds)
        network_seeds = network_seeds[rotation:] + network_seeds[:rotation]

        candidates_by_network: List[List[str]] = []
        for network, seed_value in network_seeds:
            try:
                parsed_network = ipaddress.IPv4Network(network)
                seed_host = int(str(ipaddress.IPv4Address(seed_value)).split(".")[3])
            except Exception:
                continue
            host_values = list(range(1, 255))
            randomizer.shuffle(host_values)
            network_candidates: List[str] = []
            for host in host_values:
                if host == seed_host:
                    continue
                candidate = str(parsed_network.network_address + host)
                if candidate in excluded or not is_public_ip(candidate):
                    continue
                network_candidates.append(candidate)
            if network_candidates:
                candidates_by_network.append(network_candidates)
        if not candidates_by_network:
            return []

        candidate_rounds: List[List[str]] = []
        batch_size = HUAWEI_EIP_REPLACE_CANDIDATES_PER_SEGMENT
        max_candidates = max(len(values) for values in candidates_by_network)
        for offset in range(0, max_candidates, batch_size):
            network_rotation = (offset // batch_size) % len(candidates_by_network)
            ordered_networks = (
                candidates_by_network[network_rotation:]
                + candidates_by_network[:network_rotation]
            )
            round_candidates: List[str] = []
            for network_candidates in ordered_networks:
                round_candidates.extend(network_candidates[offset : offset + batch_size])
            if round_candidates:
                candidate_rounds.append(round_candidates)
        return candidate_rounds

    def _allocate_huawei_new_eip(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        *,
        eip_types: Sequence[str],
        seed_addresses_by_type: Dict[str, Sequence[str]],
        excluded_ips: Set[str],
        reserved_candidate_ips: Set[str],
        attempted_candidate_keys: Set[Tuple[str, str, str]],
        successful_segment_seeds: Dict[Tuple[str, str], List[str]],
        state_lock: Any,
        anchor_ip: str,
        rotation_index: int,
        resource_name_prefix: str,
        discard_eip: Callable[[Dict[str, Any], str], bool],
        emit: Callable[[str], None],
        note_api_error: Optional[Callable[[Exception], None]] = None,
        terminal_error: Optional[Callable[[Exception], bool]] = None,
        reference_ip: str = "",
    ) -> Tuple[Dict[str, Any], str]:
        clean_types: List[str] = []
        for value in eip_types:
            clean_type = str(value or EIP_TYPE)
            if clean_type not in clean_types:
                clean_types.append(clean_type)
        if not clean_types:
            clean_types = [EIP_TYPE]

        def notify_error(exc: Exception) -> None:
            if note_api_error is not None:
                note_api_error(exc)

        def should_stop(exc: Exception) -> bool:
            return bool(terminal_error is not None and terminal_error(exc))

        candidate_rounds_by_type: Dict[str, List[List[str]]] = {}
        for eip_type in clean_types:
            success_key = (plan.region, eip_type)
            with state_lock:
                preferred_seeds = list(successful_segment_seeds.get(success_key, []))
                excluded_candidates = tuple(
                    excluded_ips
                    | reserved_candidate_ips
                    | {
                        candidate_ip
                        for candidate_region, candidate_type, candidate_ip in attempted_candidate_keys
                        if candidate_region == plan.region and candidate_type == eip_type
                    }
                )
            seed_addresses = self._dedupe_public_ip_values(
                [
                    *preferred_seeds,
                    *seed_addresses_by_type.get(eip_type, ()),
                ]
            )
            candidate_rounds_by_type[eip_type] = self._huawei_eip_candidate_rounds(
                plan,
                eip_type,
                seed_addresses,
                excluded_addresses=excluded_candidates,
                anchor_ip=anchor_ip,
                rotation_index=rotation_index,
            )

        specified_round_count = max(
            (len(rounds) for rounds in candidate_rounds_by_type.values()),
            default=0,
        )
        specified_attempts: List[Tuple[int, str, str]] = []
        for round_index in range(specified_round_count):
            for eip_type in clean_types:
                rounds = candidate_rounds_by_type.get(eip_type, [])
                if round_index >= len(rounds):
                    continue
                specified_attempts.extend(
                    (round_index + 1, eip_type, candidate_ip)
                    for candidate_ip in rounds[round_index]
                )

        specified_errors = 0
        specified_skipped = 0
        last_specified_error = ""
        for round_index, eip_type, candidate_ip in specified_attempts:
            success_key = (plan.region, eip_type)
            candidate_key = (plan.region, eip_type, candidate_ip)
            with state_lock:
                if (
                    candidate_ip in excluded_ips
                    or candidate_ip in reserved_candidate_ips
                    or candidate_key in attempted_candidate_keys
                ):
                    specified_skipped += 1
                    continue
                reserved_candidate_ips.add(candidate_ip)
                attempted_candidate_keys.add(candidate_key)
            try:
                item = client.create_public_ip(
                    pair,
                    timestamp_name(f"{resource_name_prefix}-r{round_index}", plan),
                    eip_type=eip_type,
                    ip_address=candidate_ip,
                    timeout=20,
                )
            except Exception as exc:
                notify_error(exc)
                with state_lock:
                    reserved_candidate_ips.discard(candidate_ip)
                if should_stop(exc):
                    raise
                specified_errors += 1
                last_specified_error = str(exc)
                continue

            new_ip = public_ip_address(item)
            with state_lock:
                reserved_candidate_ips.discard(candidate_ip)
                duplicate_ip = bool(
                    new_ip
                    and (new_ip in excluded_ips or new_ip in reserved_candidate_ips)
                )
                if new_ip and not duplicate_ip:
                    excluded_ips.add(new_ip)
                    successful_values = successful_segment_seeds.setdefault(success_key, [])
                    if new_ip in successful_values:
                        successful_values.remove(new_ip)
                    successful_values.insert(0, new_ip)
                    del successful_values[16:]
            if duplicate_ip:
                if not discard_eip(item, new_ip):
                    return item, new_ip
                specified_skipped += 1
                continue
            if not new_ip:
                if not discard_eip(item, candidate_ip):
                    raise RuntimeError("未返回公网IP地址且临时EIP释放状态未确认")
                specified_errors += 1
                last_specified_error = "云平台未返回公网IP地址"
                continue
            emit(
                f"指定申请第{round_index}轮新EIP成功: "
                f"{reference_ip or candidate_ip} -> {new_ip}"
            )
            return item, ""

        if not specified_attempts:
            raise RuntimeError("新EIP申请失败: 服务器历史IP段没有可用候选。")
        raise RuntimeError(
            f"新EIP申请失败: 服务器历史IP段候选已全部尝试，"
            f"共{specified_round_count}轮、{len(specified_attempts)}条候选，"
            f"实际申请未成功{specified_errors}次，跳过已用/已尝试{specified_skipped}条"
            f"{f'，参考旧IP={reference_ip}' if reference_ip else ''}。"
            f"最后提示: {last_specified_error or '未取得可用新IP'}"
        )

    @staticmethod
    def _ip_range_cache_dir() -> Path:
        return cloud_data_dir("HUAWEI") / "ip_ranges"

    def _read_online_ip_range_text(self, url: str, cache_name: str, *, force_refresh: bool = False) -> str:
        clean_url = str(url or "").strip()
        if not clean_url:
            return ""
        cache_dir = self._ip_range_cache_dir()
        cache_path = cache_dir / safe_log_name(cache_name)
        try:
            if (
                not force_refresh
                and cache_path.exists()
                and time.time() - cache_path.stat().st_mtime < HUAWEI_IP_RANGE_CACHE_SECONDS
            ):
                return cache_path.read_text(encoding="utf-8-sig")
        except Exception:
            pass
        try:
            request = urllib.request.Request(clean_url, headers={"User-Agent": "YunTools-HUAWEI/1.0"})
            with urllib.request.urlopen(request, timeout=6) as response:
                text = response.read().decode("utf-8-sig")
            cache_dir.mkdir(parents=True, exist_ok=True)
            cache_path.write_text(text, encoding="utf-8")
            return text
        except (OSError, urllib.error.URLError, UnicodeError):
            try:
                if cache_path.exists():
                    return cache_path.read_text(encoding="utf-8-sig")
            except Exception:
                pass
        return ""

    def _load_online_ip_range_manifest(self, *, force_refresh: bool = False) -> Dict[str, Any]:
        text = self._read_online_ip_range_text(
            HUAWEI_IP_RANGE_MANIFEST_URL,
            "manifest.json",
            force_refresh=force_refresh,
        )
        if not text:
            return {}
        try:
            payload = json.loads(text)
        except Exception:
            return {}
        return payload if isinstance(payload, dict) else {}

    @staticmethod
    def _normalize_huawei_ip_segment_type(value: Any) -> str:
        text = str(value or "").strip().lower().replace("-", "_")
        compact = text.replace("_", "").replace(" ", "")
        if text in {"5_sbgp", "static_bgp"} or "sbgp" in compact or "static" in compact or "静态" in text:
            return STATIC_BGP_EIP_TYPE
        if text in {"5_bgp", "dynamic_bgp"} or "bgp" in compact or "dynamic" in compact or "动态" in text:
            return EIP_TYPE
        return "unknown"

    @classmethod
    def _huawei_ip_segment_type_from_eip(cls, eip: Dict[str, Any]) -> str:
        label = cls._eip_actual_type_label(eip)
        if label:
            return cls._normalize_huawei_ip_segment_type(label)
        for key in (
            "type",
            "publicip_type",
            "public_ip_type",
            "publicipType",
            "publicIpType",
            "ip_type",
            "ipType",
            "network_type",
            "networkType",
            "line",
            "line_id",
            "lineId",
        ):
            value = eip.get(key)
            if value not in (None, ""):
                normalized = cls._normalize_huawei_ip_segment_type(value)
                if normalized != "unknown":
                    return normalized
        bandwidth = eip.get("bandwidth")
        if isinstance(bandwidth, dict):
            for key in ("type", "line", "line_id", "lineId", "publicip_type", "public_ip_type"):
                normalized = cls._normalize_huawei_ip_segment_type(bandwidth.get(key))
                if normalized != "unknown":
                    return normalized
        return "unknown"

    @staticmethod
    def _huawei_ip_segment_key(region_id: Any, eip_type: Any, cidr: Any) -> str:
        return "|".join(
            (
                str(region_id or "").strip(),
                str(eip_type or "unknown").strip().lower() or "unknown",
                str(cidr or "").strip(),
            )
        )

    @staticmethod
    def _huawei_ip_segment_key_parts(key: Any) -> Tuple[str, str, str]:
        parts = str(key or "").split("|", 2)
        if len(parts) != 3:
            return "", "", ""
        return parts[0], parts[1], parts[2]

    @classmethod
    def _huawei_ip_segment_is_known(cls, key: str, known_keys: Set[str]) -> bool:
        region_id, eip_type, cidr = cls._huawei_ip_segment_key_parts(key)
        if not region_id or not cidr:
            return False
        if key in known_keys or cls._huawei_ip_segment_key(region_id, "unknown", cidr) in known_keys:
            return True
        if eip_type == "unknown":
            return any(
                known_region == region_id and known_cidr == cidr
                for known_region, _known_type, known_cidr in (
                    cls._huawei_ip_segment_key_parts(item) for item in known_keys
                )
            )
        return False

    @classmethod
    def _huawei_ip_segment_record(cls, plan: RegionPlan, eip: Dict[str, Any]) -> Optional[Dict[str, str]]:
        value = str(public_ip_address(eip) or "").strip()
        try:
            address = ipaddress.IPv4Address(value)
        except Exception:
            return None
        if not is_public_ip(str(address)):
            return None
        cidr = str(ipaddress.IPv4Network(f"{address}/24", strict=False))
        eip_type = cls._huawei_ip_segment_type_from_eip(eip)
        return {
            "key": cls._huawei_ip_segment_key(plan.region, eip_type, cidr),
            "region_id": plan.region,
            "region_name": plan.label,
            "eip_type": eip_type,
            "cidr": cidr,
        }

    @classmethod
    def _read_huawei_ip_segment_state_unlocked(cls, path: Path) -> Tuple[Dict[str, Dict[str, str]], Set[str]]:
        try:
            payload = json.loads(path.read_text(encoding="utf-8-sig"))
        except Exception:
            payload = {}
        if not isinstance(payload, dict):
            payload = {}
        pending: Dict[str, Dict[str, str]] = {}
        for item in payload.get("pending") or []:
            if not isinstance(item, dict):
                continue
            key = str(item.get("key") or "").strip()
            if key:
                pending[key] = {
                    "key": key,
                    "region_id": str(item.get("region_id") or "").strip(),
                    "region_name": str(item.get("region_name") or "").strip(),
                    "eip_type": str(item.get("eip_type") or "unknown").strip().lower() or "unknown",
                    "cidr": str(item.get("cidr") or "").strip(),
                }
        uploaded = {
            str(item or "").strip()
            for item in payload.get("uploaded_keys") or []
            if str(item or "").strip()
        }
        return pending, uploaded

    @staticmethod
    def _write_huawei_ip_segment_state_unlocked(
        path: Path,
        pending: Dict[str, Dict[str, str]],
        uploaded_keys: Set[str],
    ) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        temp_path = path.with_name(
            f".{path.name}.{os.getpid()}.{threading.get_ident()}.{time.time_ns()}.tmp"
        )
        payload = {
            "schema_version": 1,
            "pending": [pending[key] for key in sorted(pending)],
            "uploaded_keys": sorted(uploaded_keys),
        }
        try:
            temp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
            os.replace(temp_path, path)
        finally:
            try:
                temp_path.unlink(missing_ok=True)
            except OSError:
                pass

    @classmethod
    def _huawei_ip_segment_state_path(cls) -> Path:
        return cls._ip_range_cache_dir() / "segment_upload_state.json"

    def _queue_huawei_ip_segments(self, plan: RegionPlan, eips: Sequence[Dict[str, Any]]) -> None:
        if not self.ip_segment_report_auth_code:
            return
        records = {
            record["key"]: record
            for record in (self._huawei_ip_segment_record(plan, item) for item in eips if isinstance(item, dict))
            if record is not None
        }
        if not records:
            return
        path = self._huawei_ip_segment_state_path()
        has_pending = False
        try:
            with _used_ip_table_file_lock(path):
                pending, uploaded_keys = self._read_huawei_ip_segment_state_unlocked(path)
                for key, record in records.items():
                    if self._huawei_ip_segment_is_known(key, uploaded_keys):
                        continue
                    pending.setdefault(key, record)
                has_pending = bool(pending)
                self._write_huawei_ip_segment_state_unlocked(path, pending, uploaded_keys)
        except Exception:
            return
        if has_pending:
            self._schedule_huawei_ip_segment_report(HUAWEI_IP_SEGMENT_REPORT_DELAY_SECONDS)

    def _schedule_huawei_ip_segment_report(self, delay_seconds: float) -> None:
        with self.ip_segment_report_timer_lock:
            timer = self.ip_segment_report_timer
            if timer is not None:
                timer.cancel()
            timer = threading.Timer(max(0.1, float(delay_seconds)), self._flush_huawei_ip_segments)
            timer.daemon = True
            self.ip_segment_report_timer = timer
            timer.start()

    def _huawei_ip_segment_report_package(self) -> str:
        try:
            resolver = getattr(self._root(), "_runtime_update_package_key", None)
            package = str(resolver() if callable(resolver) else "huawei").strip().lower()
        except Exception:
            package = "huawei"
        return package if package in {"desktop", "huawei"} else "desktop"

    def _online_huawei_ip_segment_keys(self, region_ids: Set[str]) -> Set[str]:
        manifest = self._load_online_ip_range_manifest(force_refresh=True)
        regions = manifest.get("regions")
        if not isinstance(regions, list):
            return set()
        known_keys: Set[str] = set()
        for item in regions:
            if not isinstance(item, dict):
                continue
            region_id = str(item.get("region_id") or "").strip()
            if region_id not in region_ids:
                continue
            url = str(item.get("url") or "").strip()
            if not url:
                base_url = str(manifest.get("base_url") or HUAWEI_IP_RANGE_MANIFEST_URL).strip()
                url = urllib.parse.urljoin(base_url, str(item.get("path") or "").strip())
            text = self._read_online_ip_range_text(url, f"{region_id}.csv", force_refresh=True)
            if not text:
                continue
            try:
                rows = csv.DictReader(text.splitlines())
                for row in rows:
                    row_region = str(row.get("region_id") or region_id).strip()
                    if row_region != region_id:
                        continue
                    cidr = str(row.get("cidr") or "").strip()
                    sample_ip = str(row.get("sample_ip") or "").strip()
                    if not cidr and sample_ip:
                        try:
                            cidr = str(ipaddress.IPv4Network(f"{sample_ip}/24", strict=False))
                        except Exception:
                            continue
                    try:
                        network = ipaddress.IPv4Network(cidr, strict=False)
                    except Exception:
                        continue
                    if network.prefixlen != 24:
                        continue
                    eip_type = self._normalize_huawei_ip_segment_type(row.get("eip_type"))
                    known_keys.add(self._huawei_ip_segment_key(region_id, eip_type, str(network)))
            except Exception:
                continue
        return known_keys

    def _flush_huawei_ip_segments(self) -> None:
        with self.ip_segment_report_timer_lock:
            self.ip_segment_report_timer = None
            if self.ip_segment_report_running:
                return
            self.ip_segment_report_running = True
        reschedule_delay: Optional[float] = None
        try:
            path = self._huawei_ip_segment_state_path()
            with _used_ip_table_file_lock(path):
                pending, uploaded_keys = self._read_huawei_ip_segment_state_unlocked(path)
            if not pending:
                return

            region_ids = {record["region_id"] for record in pending.values() if record.get("region_id")}
            online_keys = self._online_huawei_ip_segment_keys(region_ids)
            server_known = {
                key for key in pending if self._huawei_ip_segment_is_known(key, online_keys)
            }
            if server_known:
                with _used_ip_table_file_lock(path):
                    current_pending, current_uploaded = self._read_huawei_ip_segment_state_unlocked(path)
                    for key in server_known:
                        current_pending.pop(key, None)
                        current_uploaded.add(key)
                    self._write_huawei_ip_segment_state_unlocked(path, current_pending, current_uploaded)

            with _used_ip_table_file_lock(path):
                current_pending, current_uploaded = self._read_huawei_ip_segment_state_unlocked(path)
                pending_regions = {
                    str(record.get("region_id") or "").strip()
                    for key, record in current_pending.items()
                    if not self._huawei_ip_segment_is_known(key, current_uploaded)
                    and str(record.get("region_id") or "").strip()
                }
            if not pending_regions:
                return

            client = self.get_client()
            pairs = {
                region_id: pair
                for region_id, pair in dict(self.region_projects).items()
                if region_id in pending_regions
            }
            missing_plans = [
                plan for plan in REGION_PLANS
                if plan.region in pending_regions and plan.region not in pairs
            ]
            if missing_plans:
                pairs.update(client.selected_project_pairs(missing_plans))

            ordered_regions = [
                plan.region for plan in REGION_PLANS if plan.region in pending_regions
            ]
            for region_id in ordered_regions:
                pair = pairs.get(region_id)
                if pair is None:
                    reschedule_delay = HUAWEI_IP_SEGMENT_REPORT_RETRY_SECONDS
                    continue
                proof = client.build_public_ip_list_proof(pair)
                body = json.dumps(
                    {
                        "code": self.ip_segment_report_auth_code,
                        "package": self._huawei_ip_segment_report_package(),
                        "proof": proof,
                    },
                    ensure_ascii=False,
                ).encode("utf-8")
                request = urllib.request.Request(
                    HUAWEI_IP_SEGMENT_REPORT_URL,
                    data=body,
                    method="POST",
                    headers={
                        "Content-Type": "application/json; charset=utf-8",
                        "Accept": "application/json",
                        "User-Agent": "YunTools-HUAWEI/1.0",
                    },
                )
                try:
                    with urllib.request.urlopen(
                        request,
                        timeout=HUAWEI_IP_SEGMENT_REPORT_TIMEOUT_SECONDS,
                    ) as response:
                        result = json.loads(response.read().decode("utf-8-sig"))
                except Exception:
                    reschedule_delay = HUAWEI_IP_SEGMENT_REPORT_RETRY_SECONDS
                    continue
                if not isinstance(result, dict) or not result.get("ok"):
                    reschedule_delay = HUAWEI_IP_SEGMENT_REPORT_RETRY_SECONDS
                    continue
                if not result.get("valid"):
                    return
                if str(result.get("verified_region_id") or "").strip() != region_id:
                    reschedule_delay = HUAWEI_IP_SEGMENT_REPORT_RETRY_SECONDS
                    continue
                confirmed_keys = {
                    str(item or "").strip()
                    for item in result.get("confirmed_keys") or []
                    if str(item or "").strip()
                }
                with _used_ip_table_file_lock(path):
                    current_pending, current_uploaded = self._read_huawei_ip_segment_state_unlocked(path)
                    for key, record in list(current_pending.items()):
                        if str(record.get("region_id") or "").strip() == region_id:
                            current_pending.pop(key, None)
                    current_uploaded.update(confirmed_keys)
                    self._write_huawei_ip_segment_state_unlocked(path, current_pending, current_uploaded)

            with _used_ip_table_file_lock(path):
                remaining_pending, _uploaded = self._read_huawei_ip_segment_state_unlocked(path)
            if remaining_pending and reschedule_delay is None:
                reschedule_delay = HUAWEI_IP_SEGMENT_REPORT_DELAY_SECONDS
        except Exception:
            reschedule_delay = HUAWEI_IP_SEGMENT_REPORT_RETRY_SECONDS
        finally:
            with self.ip_segment_report_timer_lock:
                self.ip_segment_report_running = False
            if reschedule_delay is not None:
                self._schedule_huawei_ip_segment_report(reschedule_delay)

    @staticmethod
    def _cidr_seed_ips(cidr: Any, *, limit: int = 8) -> List[str]:
        text = str(cidr or "").strip()
        if not text:
            return []
        try:
            network = ipaddress.IPv4Network(text, strict=False)
        except Exception:
            return []
        if network.version != 4:
            return []
        if network.prefixlen == 32:
            value = str(network.network_address)
            return [value] if is_public_ip(value) else []
        offsets = [1, 2, 3, 10, 20, 50, 100, 150, 200, network.num_addresses - 2]
        values: List[str] = []
        seen: Set[str] = set()
        for offset in offsets:
            if offset <= 0 or offset >= network.num_addresses - 1:
                continue
            value = str(network.network_address + offset)
            if value in seen or not is_public_ip(value):
                continue
            seen.add(value)
            values.append(value)
            if len(values) >= limit:
                break
        return values

    def _online_ip_range_seed_values_for_region(
        self,
        plan: RegionPlan,
        *,
        manifest: Optional[Dict[str, Any]] = None,
        eip_type: str = "",
        prefer_verified: bool = False,
    ) -> List[str]:
        manifest = manifest if isinstance(manifest, dict) else self._load_online_ip_range_manifest()
        regions = manifest.get("regions")
        if not isinstance(regions, list):
            return []
        target: Dict[str, Any] = {}
        for item in regions:
            if not isinstance(item, dict):
                continue
            region_id = str(item.get("region_id") or "").strip()
            region_name = str(item.get("region_name") or "").strip()
            if region_id == plan.region or region_name == plan.label:
                target = item
                break
        if not target:
            return []
        url = str(target.get("url") or "").strip()
        if not url:
            base_url = str(manifest.get("base_url") or HUAWEI_IP_RANGE_MANIFEST_URL).strip()
            path = str(target.get("path") or "").strip()
            url = urllib.parse.urljoin(base_url, path)
        if not url:
            return []
        cache_name = f"{plan.region}.csv"
        text = self._read_online_ip_range_text(url, cache_name)
        if not text:
            return []
        values: List[str] = []
        ranked_values: List[Tuple[int, int, str]] = []
        requested_type = self._normalize_huawei_ip_segment_type(eip_type) if eip_type else ""
        try:
            for row_index, row in enumerate(csv.DictReader(text.splitlines())):
                enabled = str(row.get("enabled") or "1").strip().lower()
                if enabled in {"0", "false", "no", "off", "否"}:
                    continue
                row_region_id = str(row.get("region_id") or "").strip()
                row_region_name = str(row.get("region_name") or "").strip()
                if row_region_id and row_region_id != plan.region:
                    continue
                if row_region_name and row_region_name != plan.label:
                    continue
                row_type = self._normalize_huawei_ip_segment_type(row.get("eip_type"))
                if requested_type and row_type not in {requested_type, "unknown"}:
                    continue
                row_values = [
                    str(row.get("sample_ip") or "").strip(),
                    *self._cidr_seed_ips(row.get("cidr")),
                ]
                if not prefer_verified:
                    values.extend(row_values)
                    continue
                source = str(row.get("source") or "").strip().lower()
                confidence = str(row.get("confidence") or "").strip().lower()
                rank = 0 if (
                    source == "huawei_signed_list_publicips_v3"
                    and row_type == requested_type
                ) else 1 if confidence in {"verified", "high"} else 2
                ranked_values.extend(
                    (rank, row_index, value)
                    for value in row_values
                    if value
                )
        except Exception:
            return []
        if prefer_verified:
            ranked_values.sort(key=lambda item: (item[0], item[1]))
            values = [value for _rank, _row_index, value in ranked_values]
        return self._dedupe_public_ip_values(values)

    def _append_used_ip_table_groups(
        self,
        ip_groups: Dict[str, Sequence[str]],
        *,
        reason: str,
    ) -> Tuple[Path, int, Set[str], List[Dict[str, str]], Optional[Path]]:
        del reason
        path = self._used_ip_table_path()
        pending_path = self._used_ip_pending_path(path)
        cleaned_items: List[Tuple[str, str]] = []
        cleaned_seen: Set[str] = set()
        for region_label, ips in ip_groups.items():
            clean_region = str(region_label or "").strip()
            for ip in ips:
                value = self._normalize_used_ip_value(ip)
                if not value or value in cleaned_seen:
                    continue
                cleaned_seen.add(value)
                cleaned_items.append((clean_region, value))
        with _used_ip_table_file_lock(path):
            target_rows = self._read_used_ip_csv_rows(path)
            pending_rows = self._read_used_ip_csv_rows(pending_path)
            combined_rows = self._dedupe_used_ip_csv_rows([*target_rows, *pending_rows])
            existing_rows = self._retain_recent_used_ip_rows(combined_rows)
            retention_changed = len(existing_rows) != len(combined_rows)
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            account_value = self._used_ip_table_account_value()
            rows_by_ip = {
                row[USED_IP_CSV_HEADERS[3]]: row
                for row in existing_rows
            }
            added_count = 0
            changed_rows: List[Dict[str, str]] = []
            for region, ip in cleaned_items:
                refreshed_row = self._used_ip_csv_row(
                    timestamp,
                    account_value,
                    region,
                    ip,
                )
                existing_row = rows_by_ip.get(ip)
                if existing_row is None:
                    existing_rows.append(refreshed_row)
                    rows_by_ip[ip] = refreshed_row
                    added_count += 1
                else:
                    for header in USED_IP_CSV_HEADERS:
                        existing_row[header] = refreshed_row[header]
                changed_rows.append(refreshed_row)
            rows = self._retain_recent_used_ip_rows(
                self._dedupe_used_ip_csv_rows(existing_rows)
            )
            needs_write = bool(
                cleaned_items
                or pending_rows
                or retention_changed
            )
            if not needs_write:
                current_ips = {row[USED_IP_CSV_HEADERS[3]] for row in rows}
                return path, 0, current_ips, rows, None
            try:
                self._write_used_ip_csv_rows_unlocked(path, rows)
                pending_path.unlink(missing_ok=True)
                queued_path: Optional[Path] = None
            except OSError as target_exc:
                try:
                    self._write_used_ip_csv_rows_unlocked(
                        pending_path,
                        self._dedupe_used_ip_csv_rows([*pending_rows, *changed_rows]),
                    )
                except OSError as pending_exc:
                    raise OSError(
                        f"已用IP表和待合并文件均写入失败: {pending_exc}"
                    ) from target_exc
                queued_path = pending_path
            if queued_path is not None:
                self._schedule_used_ip_pending_merge(
                    path,
                    retention_cleanup=retention_changed,
                )
            current_ips = {row[USED_IP_CSV_HEADERS[3]] for row in rows}
            return path, added_count, current_ips, rows, queued_path

    def _append_used_ip_table(
        self,
        ips: Sequence[str],
        *,
        region_label: str,
        reason: str,
    ) -> Tuple[Path, int, Set[str], Optional[Path]]:
        path, added_count, used_ips, _rows, pending_path = self._append_used_ip_table_groups(
            {region_label: ips},
            reason=reason,
        )
        return path, added_count, used_ips, pending_path

    def _update_failed_ip_table_groups(
        self,
        *,
        add_ip_groups: Dict[str, Sequence[str]],
        remove_ips: Sequence[str] = (),
    ) -> Tuple[int, int, bool]:
        path = self._used_ip_table_path()
        if path.suffix.lower() != ".xlsx":
            return 0, 0, False
        cleaned_add: List[Tuple[str, str]] = []
        add_seen: Set[str] = set()
        for region_label, ips in add_ip_groups.items():
            clean_region = str(region_label or "").strip()
            for ip in ips:
                value = self._normalize_used_ip_value(ip)
                if not value or value in add_seen:
                    continue
                add_seen.add(value)
                cleaned_add.append((clean_region, value))
        cleaned_remove = {
            value
            for value in (self._normalize_used_ip_value(ip) for ip in remove_ips)
            if value
        }
        with _used_ip_table_file_lock(path):
            failed_rows = self._read_failed_ip_rows(path)
            before_ips = {row[USED_IP_CSV_HEADERS[3]] for row in failed_rows}
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            account_value = self._used_ip_table_account_value()
            rows_by_ip = {row[USED_IP_CSV_HEADERS[3]]: row for row in failed_rows}
            added_count = 0
            for region_label, ip in cleaned_add:
                if ip in rows_by_ip:
                    continue
                row = self._used_ip_csv_row(timestamp, account_value, region_label, ip)
                failed_rows.append(row)
                rows_by_ip[ip] = row
                added_count += 1
            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_unlocked(path, main_rows, failed_rows=failed_rows)
        return added_count, removed_count, True

    def _update_failed_ip_table(
        self,
        *,
        add_ips: Sequence[str] = (),
        remove_ips: Sequence[str] = (),
        region_label: str = "",
    ) -> Tuple[int, int, bool]:
        return self._update_failed_ip_table_groups(
            add_ip_groups={region_label: add_ips},
            remove_ips=remove_ips,
        )

    def _current_account_public_ip_groups_for_used_table(self) -> Dict[str, List[str]]:
        groups: Dict[str, List[str]] = {}
        for item in self.manage_resource_items:
            if bool(item.get("_displayOnly")):
                continue
            if str(item.get("_kind") or "").lower() not in {"ecs", "eip", "virtual_ip"}:
                continue
            region_label = str(
                item.get("_region_label")
                or display_region(str(item.get("_region") or ""))
                or item.get("_region")
                or "当前账号"
            ).strip() or "当前账号"
            raw_values = [
                item.get("_public_ip"),
                item.get("_name"),
                public_ip_address(item.get("_raw") if isinstance(item.get("_raw"), dict) else item),
            ]
            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 or not is_public_ip(ip):
                        continue
                    bucket = groups.setdefault(region_label, [])
                    if ip not in bucket:
                        bucket.append(ip)
        return groups

    def _manage_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 item
            public_ip = self._normalize_used_ip_value(
                item.get("_public_ip") or item.get("_name") or public_ip_address(raw)
            )
            if not public_ip or not is_public_ip(public_ip):
                continue
            region_label = str(
                item.get("_region_label")
                or display_region(str(item.get("_region") or ""))
                or item.get("_region")
                or "未识别地区"
            ).strip() or "未识别地区"
            bucket = groups.setdefault(region_label, [])
            if public_ip not in bucket:
                bucket.append(public_ip)
        return groups

    def _import_manage_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, pending_path = self._append_used_ip_table(
                ips,
                region_label=region_label,
                reason="删除EIP导入旧IP库",
            )
            imported_count += added_count
            target_text = f"，待关闭文档后合并：{pending_path}" if pending_path else f"：{path}"
            self.log(
                f"[{region_label}] 释放IP已合并导入旧IP库 {len(ips)} 条，本次新增 {added_count} 条{target_text}",
                "manage",
            )
        self.log(f"确认并导入旧IP库完成：本次新增 {imported_count} 条，随后执行资源释放。", "manage")
        return imported_count

    def _sync_current_account_ips_to_used_ip_table(self) -> int:
        added_total = 0
        for region_label, ips in self._current_account_public_ip_groups_for_used_table().items():
            _path, added_count, _used_ip_table, _pending_path = self._append_used_ip_table(
                ips,
                region_label=region_label,
                reason="保存旧IP库",
            )
            added_total += added_count
        return added_total

    @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

    @classmethod
    def _parse_huawei_result_blocks(
        cls,
        text: str,
        *,
        kind: str,
        allowed_regions: Optional[Set[str]] = None,
    ) -> Dict[str, List[str]]:
        section = "socks" if str(kind or "").lower().startswith("socks") else "l2tp"
        region_aliases: Dict[str, str] = {}
        for region_plan in REGION_PLANS:
            for alias in (region_plan.label, region_plan.region, region_plan.key):
                value = str(alias or "").strip()
                if value:
                    region_aliases[value.casefold()] = region_plan.region
        allowed = set(allowed_regions or ())
        default_region = next(iter(allowed)) if len(allowed) == 1 else ""
        current_region = default_region
        records: Dict[str, List[str]] = {}
        for raw_line in str(text or "").replace("\r", "\n").splitlines():
            value = cls._clean_script_output_line(raw_line)
            if not value:
                continue
            label_value = re.sub(r"^地区[:：]\s*", "", value).strip()
            region = region_aliases.get(label_value.casefold(), "")
            if region:
                current_region = region if not allowed or region in allowed else ""
                continue
            row = cls._script_result_data_line(value, section)
            if not row or not current_region or (allowed and current_region not in allowed):
                continue
            rows = records.setdefault(current_region, [])
            if row not in rows:
                rows.append(row)
        return records

    @classmethod
    def _format_huawei_result_blocks(
        cls,
        records: Dict[str, List[str]],
        *,
        kind: str,
        mark: str,
    ) -> str:
        socks = str(kind or "").lower().startswith("socks")
        header = SOCKS5_RESULT_HEADER if socks else L2TP_RESULT_HEADER
        blocks: List[str] = []
        for region_plan in REGION_PLANS:
            rows = [str(row or "").strip() for row in records.get(region_plan.region, []) if str(row or "").strip()]
            if rows:
                blocks.append("\n".join([region_plan.label, header, *rows]))
        return append_result_footer("\n".join(blocks), "Socks5" if socks else "L2TP", mark)

    def _display_replaced_huawei_result_blocks(
        self,
        l2tp_records: Dict[str, List[str]],
        socks_records: Dict[str, List[str]],
        display_regions: Set[str],
    ) -> None:
        mark = self.mark_var.get()
        display_l2tp = {
            region: list(rows)
            for region, rows in l2tp_records.items()
            if region in display_regions and rows
        }
        display_socks = {
            region: list(rows)
            for region, rows in socks_records.items()
            if region in display_regions and rows
        }
        l2tp_output = self._format_huawei_result_blocks(display_l2tp, kind="l2tp", mark=mark)
        socks_output = self._format_huawei_result_blocks(display_socks, kind="socks", mark=mark)
        if self.l2tp_text is not None:
            self.l2tp_text.delete("1.0", tk.END)
            if l2tp_output:
                self.l2tp_text.insert(tk.END, l2tp_output + "\n")
        if self.socks_text is not None:
            self.socks_text.delete("1.0", tk.END)
            if socks_output:
                self.socks_text.insert(tk.END, socks_output + "\n")

        cached_l2tp = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_l2tp.txt"),
            kind="l2tp",
        )
        cached_socks = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_socks5.txt"),
            kind="socks",
        )
        for region in display_regions:
            if display_l2tp.get(region):
                cached_l2tp[region] = list(display_l2tp[region])
            else:
                cached_l2tp.pop(region, None)
            if display_socks.get(region):
                cached_socks[region] = list(display_socks[region])
            else:
                cached_socks.pop(region, None)
        cached_l2tp_output = self._format_huawei_result_blocks(cached_l2tp, kind="l2tp", mark=mark)
        cached_socks_output = self._format_huawei_result_blocks(cached_socks, kind="socks", mark=mark)
        self._save_latest_result_outputs(cached_l2tp_output, cached_socks_output)

    def _recover_huawei_result_templates_from_logs(self, plan: RegionPlan) -> 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 [], []

        region_aliases: Dict[str, str] = {}
        for region_plan in REGION_PLANS:
            for alias in (region_plan.label, region_plan.region, region_plan.key):
                value = str(alias or "").strip()
                if value:
                    region_aliases[value.casefold()] = region_plan.region
        best_l2tp: List[str] = []
        best_socks: List[str] = []
        best_score = 0
        for path in log_paths:
            try:
                log_text = path.read_text(encoding="utf-8", errors="replace")
            except OSError:
                continue
            active = False
            selected_lines: List[str] = []
            for raw_line in log_text.splitlines():
                line = str(raw_line or "").strip()
                timestamp_match = re.match(r"^\[\d{2}:\d{2}:\d{2}\]\s*(.*)$", line)
                if timestamp_match:
                    line = timestamp_match.group(1).strip()
                region_match = re.match(r"^\[([^\]]+)\]\s*(.*)$", line)
                if region_match:
                    region_token, remainder = region_match.groups()
                    region = region_aliases.get(region_token.strip().casefold(), "")
                    if region:
                        active = region == plan.region
                        if active and remainder.strip():
                            selected_lines.append(remainder.strip())
                        continue
                if active:
                    selected_lines.append(line)
            candidate_text = "\n".join(selected_lines)
            if not candidate_text and plan.label in log_text:
                candidate_text = log_text
            l2tp_output, socks_output = self._script_result_sections(candidate_text)
            l2tp_rows = self._parse_huawei_result_blocks(
                f"{plan.label}\n{l2tp_output}",
                kind="l2tp",
                allowed_regions={plan.region},
            ).get(plan.region, [])
            socks_rows = self._parse_huawei_result_blocks(
                f"{plan.label}\n{socks_output}",
                kind="socks",
                allowed_regions={plan.region},
            ).get(plan.region, [])
            score = len(l2tp_rows) + len(socks_rows)
            if score > best_score:
                best_l2tp = list(l2tp_rows)
                best_socks = list(socks_rows)
                best_score = score
            if l2tp_rows and socks_rows:
                break
        return best_l2tp, best_socks

    @staticmethod
    def _indexed_huawei_l2tp_username(value: str, index: int, source_index: Optional[int] = None) -> str:
        username = str(value or "").strip() or f"user{index}"
        source_suffix = str(source_index if source_index is not None else index)
        if source_suffix and username.endswith(source_suffix) and len(username) > len(source_suffix):
            return f"{username[:-len(source_suffix)]}{index}"
        match = re.match(r"^(.*?)(\d+)$", username)
        return f"{match.group(1)}{index}" if match else username

    @classmethod
    def _rebuild_huawei_result_rows(
        cls,
        l2tp_rows: Sequence[str],
        socks_rows: Sequence[str],
        current_public_ips: Sequence[str],
    ) -> Tuple[List[str], List[str]]:
        current_ips: List[str] = []
        for ip_addr in current_public_ips:
            value = str(ip_addr or "").strip()
            if is_public_ip(value) and value not in current_ips:
                current_ips.append(value)
        if not current_ips:
            return list(l2tp_rows), list(socks_rows)

        valid_socks = [line.split("/", 3) for line in socks_rows if len(line.split("/", 3)) == 4]
        rebuilt_socks: List[str] = []
        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 l2tp_rows if len(line.split("/")) >= 5]
        rebuilt_l2tp: List[str] = []
        entry_ip = current_ips[0]
        for index, current_ip in enumerate(current_ips, start=1):
            if not valid_l2tp:
                break
            source_index = index if index - 1 < len(valid_l2tp) else 1
            source = valid_l2tp[source_index - 1]
            username = cls._indexed_huawei_l2tp_username(source[1], index, source_index)
            rebuilt_l2tp.append(f"{entry_ip}/{username}/{source[2]}/{source[3]}/{current_ip}")
        return rebuilt_l2tp, rebuilt_socks

    def _upsert_huawei_result_model(
        self,
        plan: RegionPlan,
        l2tp_rows: Sequence[str],
        socks_rows: Sequence[str],
        current_public_ips: Sequence[str],
    ) -> Optional[RegionBuildResult]:
        current_ips = self._dedupe_public_ip_values(current_public_ips)
        clean_l2tp = [str(row or "").strip() for row in l2tp_rows if str(row or "").strip()]
        clean_socks = [str(row or "").strip() for row in socks_rows if str(row or "").strip()]
        if not current_ips or not (clean_l2tp or clean_socks):
            return None

        server_id = next(
            (
                str(item.get("_server_id") or item.get("_resource_id") or "").strip()
                for item in self.manage_resource_items
                if item.get("_region") == plan.region and item.get("_kind") == "ecs"
            ),
            "",
        )
        result = next(
            (item for item in self.last_results if item.plan.region == plan.region),
            None,
        )
        if result is None:
            result = RegionBuildResult(
                plan=plan,
                server_id=server_id,
                server_ip=current_ips[0],
                password=self.password_var.get().strip() or DEFAULT_PASSWORD,
                ip_txt="",
                public_ips=list(current_ips),
                root_volume_type=ROOT_VOLUME_TYPE,
                script_exit_code=0,
            )
            self.last_results.append(result)
        else:
            if server_id:
                result.server_id = server_id
            result.server_ip = current_ips[0]
            result.public_ips = list(current_ips)

        if clean_l2tp:
            setattr(
                result,
                "_huawei_script_l2tp_output",
                "\n".join([L2TP_RESULT_HEADER, *clean_l2tp]),
            )
        if clean_socks:
            setattr(
                result,
                "_huawei_script_socks_output",
                "\n".join([SOCKS5_RESULT_HEADER, *clean_socks]),
            )
        setattr(result, "_huawei_require_script_result_output", True)
        return result

    def _restore_huawei_result_models_from_local_records(self) -> None:
        existing_regions = {result.plan.region for result in self.last_results}
        resource_regions = {
            str(item.get("_region") or "").strip()
            for item in self.manage_resource_items
            if item.get("_kind") == "ecs" and str(item.get("_region") or "").strip()
        }
        missing_regions = resource_regions - existing_regions
        if not missing_regions:
            return

        cached_l2tp = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_l2tp.txt"),
            kind="l2tp",
        )
        cached_socks = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_socks5.txt"),
            kind="socks",
        )
        for plan in REGION_PLANS:
            if plan.region not in missing_regions:
                continue
            region_items = [
                item
                for item in self.manage_resource_items
                if item.get("_region") == plan.region and item.get("_kind") == "eip"
            ]
            ordered_items = [
                item
                for _index, item in sorted(
                    enumerate(region_items),
                    key=lambda pair: (
                        self._ipv4_sort_tuple(str(pair[1].get("_eip_bound_private_ip") or ""))
                        if str(pair[1].get("_eip_bound_private_ip") or "")
                        else (999, 999, 999, 999),
                        pair[0],
                    ),
                )
            ]
            current_ips = self._dedupe_public_ip_values(
                [
                    str(
                        item.get("_public_ip")
                        or item.get("_name")
                        or public_ip_address(item.get("_raw") if isinstance(item.get("_raw"), dict) else item)
                        or ""
                    ).strip()
                    for item in ordered_items
                ]
            )
            if not current_ips:
                continue

            template_l2tp = list(cached_l2tp.get(plan.region, []))
            template_socks = list(cached_socks.get(plan.region, []))
            if not template_l2tp or not template_socks:
                log_l2tp, log_socks = self._recover_huawei_result_templates_from_logs(plan)
                if not template_l2tp:
                    template_l2tp = log_l2tp
                if not template_socks:
                    template_socks = log_socks
            rebuilt_l2tp, rebuilt_socks = self._rebuild_huawei_result_rows(
                template_l2tp,
                template_socks,
                current_ips,
            )
            self._upsert_huawei_result_model(
                plan,
                rebuilt_l2tp,
                rebuilt_socks,
                current_ips,
            )

    def _sync_results_after_new_ip_purchase(
        self,
        plan: RegionPlan,
        current_public_ips: Sequence[str],
        context: str = "purchase",
    ) -> bool:
        current_ips = self._dedupe_public_ip_values(current_public_ips)
        if not current_ips:
            return False
        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 ""
        l2tp_rows = self._parse_huawei_result_blocks(
            current_l2tp,
            kind="l2tp",
            allowed_regions={plan.region},
        ).get(plan.region, [])
        socks_rows = self._parse_huawei_result_blocks(
            current_socks,
            kind="socks",
            allowed_regions={plan.region},
        ).get(plan.region, [])

        target_results = [result for result in self.last_results if result.plan.region == plan.region]
        mark = self.mark_var.get()
        if target_results and not l2tp_rows:
            l2tp_rows = self._parse_huawei_result_blocks(
                self._format_l2tp_output_for_display(target_results, mark),
                kind="l2tp",
                allowed_regions={plan.region},
            ).get(plan.region, [])
        if target_results and not socks_rows:
            socks_rows = self._parse_huawei_result_blocks(
                self._format_socks5_output_for_display(self._socks5_results_for_display(target_results), mark),
                kind="socks",
                allowed_regions={plan.region},
            ).get(plan.region, [])
        if not l2tp_rows:
            l2tp_rows = self._parse_huawei_result_blocks(
                self._read_latest_result_file("last_l2tp.txt"),
                kind="l2tp",
                allowed_regions={plan.region},
            ).get(plan.region, [])
        if not socks_rows:
            socks_rows = self._parse_huawei_result_blocks(
                self._read_latest_result_file("last_socks5.txt"),
                kind="socks",
                allowed_regions={plan.region},
            ).get(plan.region, [])
        if not l2tp_rows or not socks_rows:
            log_l2tp, log_socks = self._recover_huawei_result_templates_from_logs(plan)
            if not l2tp_rows:
                l2tp_rows = log_l2tp
            if not socks_rows:
                socks_rows = log_socks

        rebuilt_l2tp, rebuilt_socks = self._rebuild_huawei_result_rows(
            l2tp_rows,
            socks_rows,
            current_ips,
        )
        if not rebuilt_l2tp and not rebuilt_socks:
            warning = "未在本地找到L2TP/Socks5结果记录，请重装云服务器后重新搭建即可显示结果。"
            self._huawei_new_ip_result_warning = warning
            self.log(f"[{plan.label}] {warning}", context)
            return False
        self._upsert_huawei_result_model(plan, rebuilt_l2tp, rebuilt_socks, current_ips)
        self._display_replaced_huawei_result_blocks(
            {plan.region: rebuilt_l2tp},
            {plan.region: rebuilt_socks},
            {plan.region},
        )
        self.log(f"[{plan.label}] 购买新IP后结果框已按当前 {len(current_ips)} 条公网IP同步。", context)
        return True

    def _sync_result_ips_after_eip_replace(
        self,
        plan: RegionPlan,
        replacements: Dict[str, str],
        current_public_ips: Optional[Sequence[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, "_huawei_replace_result_regions", set()) or set())
        display_regions.add(plan.region)
        self._huawei_replace_result_regions = display_regions

        structured_records_found = False
        structured_changed = False
        for result in self.last_results:
            if result.plan.region != plan.region:
                continue
            structured_records_found = True
            old_server_ip = result.server_ip
            result.server_ip = clean_replacements.get(result.server_ip, result.server_ip)
            if result.server_ip != old_server_ip:
                structured_changed = True
            old_public_ips = list(result.public_ips)
            result.public_ips = [clean_replacements.get(ip, ip) for ip in result.public_ips]
            if result.public_ips != old_public_ips:
                structured_changed = True
            old_ip_txt = result.ip_txt
            result.ip_txt = self._replace_public_ips_in_text(result.ip_txt, clean_replacements)
            if result.ip_txt != old_ip_txt:
                structured_changed = True
            for attr_name in ("_huawei_script_l2tp_output", "_huawei_script_socks_output"):
                old_text = str(getattr(result, attr_name, "") or "")
                if not old_text:
                    continue
                new_text = self._replace_public_ips_in_text(old_text, clean_replacements)
                if new_text != old_text:
                    setattr(result, attr_name, new_text)
                    structured_changed = True

        if structured_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 ""
            l2tp_records = {
                region: rows
                for region, rows in self._parse_huawei_result_blocks(current_l2tp, kind="l2tp").items()
                if region in display_regions
            }
            socks_records = {
                region: rows
                for region, rows in self._parse_huawei_result_blocks(current_socks, kind="socks").items()
                if region in display_regions
            }
            display_results = [
                result for result in self.last_results if result.plan.region in display_regions
            ]
            mark = self.mark_var.get()
            structured_l2tp = self._parse_huawei_result_blocks(
                self._format_l2tp_output_for_display(display_results, mark),
                kind="l2tp",
            )
            structured_socks = self._parse_huawei_result_blocks(
                self._format_socks5_output_for_display(
                    self._socks5_results_for_display(display_results),
                    mark,
                ),
                kind="socks",
            )
            l2tp_records.update(structured_l2tp)
            socks_records.update(structured_socks)
            self._display_replaced_huawei_result_blocks(
                l2tp_records,
                socks_records,
                display_regions,
            )
            pairs = ", ".join(f"{old_ip}->{new_ip}" for old_ip, new_ip in clean_replacements.items())
            self.log(f"[{plan.label}] 结果框IP已同步更新: {pairs}", "manage")
            return

        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 ""
        l2tp_records = {
            region: rows
            for region, rows in self._parse_huawei_result_blocks(current_l2tp, kind="l2tp").items()
            if region in display_regions
        }
        socks_records = {
            region: rows
            for region, rows in self._parse_huawei_result_blocks(current_socks, kind="socks").items()
            if region in display_regions
        }
        restored_from_memory = False
        if structured_records_found:
            target_results = [result for result in self.last_results if result.plan.region == plan.region]
            mark = self.mark_var.get()
            memory_l2tp = self._parse_huawei_result_blocks(
                self._format_l2tp_output_for_display(target_results, mark),
                kind="l2tp",
                allowed_regions={plan.region},
            ).get(plan.region, [])
            memory_socks = self._parse_huawei_result_blocks(
                self._format_socks5_output_for_display(
                    self._socks5_results_for_display(target_results),
                    mark,
                ),
                kind="socks",
                allowed_regions={plan.region},
            ).get(plan.region, [])
            if not l2tp_records.get(plan.region) and memory_l2tp:
                l2tp_records[plan.region] = list(memory_l2tp)
                restored_from_memory = True
            if not socks_records.get(plan.region) and memory_socks:
                socks_records[plan.region] = list(memory_socks)
                restored_from_memory = True
        restored_from_cache = False
        if not l2tp_records.get(plan.region):
            saved_l2tp = self._parse_huawei_result_blocks(
                self._read_latest_result_file("last_l2tp.txt"),
                kind="l2tp",
                allowed_regions={plan.region},
            ).get(plan.region, [])
            if saved_l2tp:
                l2tp_records[plan.region] = list(saved_l2tp)
                restored_from_cache = True
        if not socks_records.get(plan.region):
            saved_socks = self._parse_huawei_result_blocks(
                self._read_latest_result_file("last_socks5.txt"),
                kind="socks",
                allowed_regions={plan.region},
            ).get(plan.region, [])
            if saved_socks:
                socks_records[plan.region] = list(saved_socks)
                restored_from_cache = True

        recovered_from_logs = False
        if not l2tp_records.get(plan.region) or not socks_records.get(plan.region):
            template_l2tp, template_socks = self._recover_huawei_result_templates_from_logs(plan)
            rebuilt_l2tp, rebuilt_socks = self._rebuild_huawei_result_rows(
                template_l2tp,
                template_socks,
                current_public_ips or (),
            )
            if not l2tp_records.get(plan.region) and rebuilt_l2tp:
                l2tp_records[plan.region] = rebuilt_l2tp
                recovered_from_logs = True
            if not socks_records.get(plan.region) and rebuilt_socks:
                socks_records[plan.region] = rebuilt_socks
                recovered_from_logs = True

        region_records_found = structured_records_found or bool(
            l2tp_records.get(plan.region) or socks_records.get(plan.region)
        )
        old_l2tp_rows = list(l2tp_records.get(plan.region, []))
        old_socks_rows = list(socks_records.get(plan.region, []))
        new_l2tp_rows = [self._replace_public_ips_in_text(line, clean_replacements) for line in old_l2tp_rows]
        new_socks_rows = [self._replace_public_ips_in_text(line, clean_replacements) for line in old_socks_rows]

        template_count = max(len(new_l2tp_rows), len(new_socks_rows))
        current_ips = [ip for ip in (current_public_ips or ()) if is_public_ip(str(ip or "").strip())]
        if template_count and len(current_ips) == template_count and (restored_from_cache or recovered_from_logs):
            new_l2tp_rows, new_socks_rows = self._rebuild_huawei_result_rows(
                new_l2tp_rows,
                new_socks_rows,
                current_ips,
            )

        if new_l2tp_rows:
            l2tp_records[plan.region] = new_l2tp_rows
        if new_socks_rows:
            socks_records[plan.region] = new_socks_rows
        changed = (
            restored_from_memory
            or restored_from_cache
            or recovered_from_logs
            or new_l2tp_rows != old_l2tp_rows
            or new_socks_rows != old_socks_rows
        )

        if region_records_found and (new_l2tp_rows or new_socks_rows):
            self._upsert_huawei_result_model(
                plan,
                new_l2tp_rows,
                new_socks_rows,
                current_ips,
            )

        if changed and region_records_found:
            self._display_replaced_huawei_result_blocks(
                l2tp_records,
                socks_records,
                display_regions,
            )

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

        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")
            elif restored_from_cache:
                self.log(f"[{plan.label}] 原结果为空，已从本地最新结果缓存恢复连接配置。", "manage")
            self.log(f"[{plan.label}] 结果框IP已同步更新: {pairs}", "manage")

    def replace_selected_region_ips(self) -> None:
        selected_items = self.selected_manage_items()
        if not selected_items:
            self.show_info("请先在资源管理里选择要更换IP的地区或资源。")
            return
        region_order = {plan.region: index for index, plan in enumerate(REGION_PLANS)}
        selected_regions = sorted(
            {str(item.get("_region") or "") for item in selected_items if item.get("_region")},
            key=lambda region: region_order.get(region, 999),
        )
        plans = [plan for plan in REGION_PLANS if plan.region in selected_regions]
        if not plans:
            self.show_info("未识别到可更换IP的地区。")
            return
        confirm_lines = [
            "确认更换新IP",
            "",
            f"选择地区：{compact_name_list([plan.label for plan in plans])}",
            "",
            "会先把本次旧IP写入已用IP表，再解绑/释放旧EIP。",
            "释放旧EIP后申请新EIP，并绑定新IP到原端口。",
            "仅处理已绑定端口的EIP；未绑定IP不会参与替换。",
            "",
            "该操作会创建云资源并产生费用。",
            "",
            "该操作会造成已使用IP的设备断网，请确认执行任务？",
        ]
        if not self.ask_confirm_cancel("\n".join(confirm_lines)):
            return
        self._huawei_replace_result_warning = ""
        self._huawei_replace_result_regions: Set[str] = set()
        replacement_account_key = self._used_ip_table_account_label()
        replacement_task_token = secrets.token_hex(8)

        def eip_id(item: Dict[str, Any]) -> str:
            return str(item.get("_resource_id") or item.get("id") or item.get("publicip_id") or "").strip()

        def eip_port(item: Dict[str, Any]) -> str:
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else item
            return str(item.get("_port_id") or raw.get("port_id") or raw.get("portId") or "").strip()

        def eip_addr(item: Dict[str, Any]) -> str:
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else item
            return str(item.get("_public_ip") or item.get("_name") or public_ip_address(raw) or "").strip()

        def eip_type(item: Dict[str, Any]) -> str:
            raw = item.get("_raw") if isinstance(item.get("_raw"), dict) else item
            return STATIC_BGP_EIP_TYPE if self._eip_actual_type_label(raw) == "静态BGP" else EIP_TYPE

        def worker() -> None:
            try:
                client = self.get_client()
                self._wait_region_project_warmup()
                pairs = client.selected_project_pairs(plans)
            except Exception as exc:
                self.after(0, self.show_error, str(exc))
                return
            client.configure_request_pacing(
                HUAWEI_EIP_REPLACE_REQUEST_INTERVAL_SECONDS,
                rate_limit_cooldown_seconds=HUAWEI_EIP_REPLACE_RATE_LIMIT_COOLDOWN_SECONDS,
            )
            total_success = 0
            total_failed = 0
            selected_by_region: Dict[str, List[Dict[str, Any]]] = {}
            for item in selected_items:
                selected_by_region.setdefault(str(item.get("_region") or ""), []).append(item)

            def prepare_plan(plan: RegionPlan) -> Optional[Tuple[Any, List[Dict[str, Any]]]]:
                pair = pairs.get(plan.region)
                if not pair:
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] 未打通该地区，无法更换新IP，{REGION_ACCESS_TUTORIAL_GUIDANCE}",
                    )
                    return None
                old_items = [
                    item
                    for item in selected_by_region.get(plan.region, [])
                    if item.get("_kind") == "eip" and eip_id(item) and eip_port(item)
                ]
                if not old_items:
                    try:
                        old_items = [
                            {
                                "_raw": item,
                                "_resource_id": str(item.get("id") or item.get("publicip_id") or ""),
                                "_port_id": str(item.get("port_id") or ""),
                                "_public_ip": public_ip_address(item),
                            }
                            for item in client.list_public_ips(pair, timeout=30, retry_attempts=2)
                            if str(item.get("id") or item.get("publicip_id") or "") and str(item.get("port_id") or "")
                        ]
                    except Exception as exc:
                        self.after(0, self.log, f"[{plan.label}] 查询旧EIP失败: {exc}")
                        return None
                if not old_items:
                    self.after(0, self.log, f"[{plan.label}] 未找到已绑定端口的旧EIP，无法更换。")
                    return None
                self.after(0, self.log, f"[{plan.label}] 准备安全更换 {len(old_items)} 条EIP。")
                return pair, old_items

            prepared_plans: Dict[str, Tuple[Any, List[Dict[str, Any]]]] = {}
            with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(plans), MAX_WORKERS)) as prepare_executor:
                prepare_futures = {prepare_executor.submit(prepare_plan, plan): plan for plan in plans}
                for future in concurrent.futures.as_completed(prepare_futures):
                    plan = prepare_futures[future]
                    try:
                        prepared = future.result()
                    except Exception as exc:
                        prepared = None
                        self.after(0, self.log, f"[{plan.label}] 准备更换IP失败: {exc}")
                    if prepared is None:
                        total_failed += 1
                    else:
                        prepared_plans[plan.region] = prepared

            old_ip_groups = {
                plan.label: [
                    eip_addr(item)
                    for item in prepared_plans[plan.region][1]
                    if eip_addr(item)
                ]
                for plan in plans
                if plan.region in prepared_plans
            }
            all_old_ip_values = self._dedupe_public_ip_values(
                [ip for ips in old_ip_groups.values() for ip in ips]
            )
            used_ip_rows: List[Dict[str, str]] = []
            used_ip_table: Set[str] = set(all_old_ip_values)
            if old_ip_groups:
                try:
                    (
                        used_ip_path,
                        added_count,
                        used_ip_table,
                        used_ip_rows,
                        pending_used_ip_path,
                    ) = self._append_used_ip_table_groups(
                        old_ip_groups,
                        reason="更换新IP释放旧IP",
                    )
                    if pending_used_ip_path is not None:
                        used_ip_message = (
                            f"已用IP表已加载 {len(used_ip_table)} 条，本次新增 {added_count} 条；"
                            f"正式表当前无法直接写入（通常为 Excel 正在占用），新增内容已安全写入待合并文件，"
                            f"请关闭文档，关闭后约5秒将自动写入已用IP库文档: {pending_used_ip_path}"
                        )
                    else:
                        used_ip_message = (
                            f"已用IP表已加载 {len(used_ip_table)} 条，本次新增 {added_count} 条: {used_ip_path}"
                        )
                    self.after(0, self.log, used_ip_message)
                except Exception as exc:
                    used_ip_path = self._used_ip_table_path()
                    try:
                        used_ip_rows = self._read_used_ip_table_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(all_old_ip_values)
                        self.after(
                            0,
                            self.log,
                            f"已用IP表写入失败，但已读取 {len(used_ip_rows)} 条用于避开；本次旧IP仅使用内存记录: {exc}",
                        )
                    except Exception as read_exc:
                        used_ip_table = set(all_old_ip_values)
                        self.after(
                            0,
                            self.log,
                            f"已用IP表读取/写入失败，本次仅使用内存记录: {exc}; 读取提示: {read_exc}",
                        )

            region_row_values: Dict[str, List[str]] = {}
            region_row_seen: Dict[str, Set[str]] = {}
            blank_region_values: List[str] = []
            blank_region_seen: Set[str] = set()
            for row in used_ip_rows:
                ip = self._normalize_used_ip_value(row.get(USED_IP_CSV_HEADERS[3]))
                if not ip:
                    continue
                region_label = str(row.get(USED_IP_CSV_HEADERS[2]) or "").strip()
                if not region_label:
                    if ip not in blank_region_seen:
                        blank_region_seen.add(ip)
                        blank_region_values.append(ip)
                    continue
                seen = region_row_seen.setdefault(region_label, set())
                if ip not in seen:
                    seen.add(ip)
                    region_row_values.setdefault(region_label, []).append(ip)
            region_seed_values = {
                plan.region: list(region_row_values.get(plan.label) or blank_region_values)
                for plan in plans
                if plan.region in prepared_plans
            }

            manifest = self._load_online_ip_range_manifest()
            online_seed_values: Dict[Tuple[str, str], List[str]] = {}
            online_requests: List[Tuple[RegionPlan, str]] = []

            def replacement_types_for_item(_plan: RegionPlan, _item: Dict[str, Any]) -> List[str]:
                return [EIP_TYPE]

            for plan in plans:
                prepared = prepared_plans.get(plan.region)
                if prepared is None:
                    continue
                replacement_types = {
                    replacement_type
                    for item in prepared[1]
                    for replacement_type in replacement_types_for_item(plan, item)
                }
                for replacement_type in sorted(replacement_types):
                    online_requests.append((plan, replacement_type or EIP_TYPE))

            def load_online_seeds(request_item: Tuple[RegionPlan, str]) -> Tuple[Tuple[str, str], List[str]]:
                plan, replacement_type = request_item
                values = self._online_ip_range_seed_values_for_region(
                    plan,
                    manifest=manifest,
                    eip_type=replacement_type,
                    prefer_verified=True,
                )
                return (plan.region, replacement_type), values

            if online_requests:
                with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(online_requests))) as seed_executor:
                    seed_futures = [seed_executor.submit(load_online_seeds, item) for item in online_requests]
                    for future in concurrent.futures.as_completed(seed_futures):
                        try:
                            key, values = future.result()
                        except Exception:
                            continue
                        online_seed_values[key] = values

            def rotate_segment_seed_values(values: Sequence[str], rotation_key: str) -> List[str]:
                network_values: List[str] = []
                network_seen: Set[str] = set()
                for value in self._dedupe_public_ip_values(values):
                    network = str(ipaddress.IPv4Network(f"{value}/24", strict=False))
                    if network in network_seen:
                        continue
                    network_seen.add(network)
                    network_values.append(value)
                if len(network_values) <= 1:
                    return network_values
                rotation_window = min(len(network_values), 32)
                score = sum(
                    (index + 1) * ord(character)
                    for index, character in enumerate(str(rotation_key or ""))
                )
                rotation = score % rotation_window
                return network_values[rotation:] + network_values[:rotation]

            for plan in plans:
                prepared = prepared_plans.get(plan.region)
                if prepared is None:
                    continue
                if not any(values for (region, _eip_type), values in online_seed_values.items() if region == plan.region):
                    self.after(0, self.log, f"[{plan.label}] 服务器辅助IP段暂无可用样本，继续使用旧IP库。")

            replace_state_lock = threading.Lock()
            task_failed_ip_groups: Dict[str, Set[str]] = {}
            task_succeeded_old_ips: Set[str] = set()

            replace_limit_condition = threading.Condition()
            replace_active_count = 0
            replace_active_limit = HUAWEI_EIP_REPLACE_ACTIVE_LIMIT
            replace_success_streak = 0
            restore_limit_condition = threading.Condition()
            restore_active_count = 0
            restore_active_limit = HUAWEI_EIP_RESTORE_ACTIVE_LIMIT

            def acquire_replace_slot() -> None:
                nonlocal replace_active_count
                with replace_limit_condition:
                    while replace_active_count >= replace_active_limit:
                        replace_limit_condition.wait()
                    replace_active_count += 1

            def release_replace_slot() -> None:
                nonlocal replace_active_count
                with replace_limit_condition:
                    replace_active_count = max(0, replace_active_count - 1)
                    replace_limit_condition.notify_all()

            def acquire_restore_slot() -> None:
                nonlocal restore_active_count
                with restore_limit_condition:
                    while restore_active_count >= restore_active_limit:
                        restore_limit_condition.wait()
                    restore_active_count += 1

            def release_restore_slot() -> None:
                nonlocal restore_active_count
                with restore_limit_condition:
                    restore_active_count = max(0, restore_active_count - 1)
                    restore_limit_condition.notify_all()

            def reduce_restore_limit() -> Tuple[int, int]:
                nonlocal restore_active_limit
                with restore_limit_condition:
                    previous_limit = restore_active_limit
                    restore_active_limit = max(
                        HUAWEI_EIP_RESTORE_MIN_ACTIVE_LIMIT,
                        restore_active_limit - 1,
                    )
                    restore_limit_condition.notify_all()
                    return previous_limit, restore_active_limit

            def current_restore_limit() -> int:
                with restore_limit_condition:
                    return restore_active_limit

            def is_rate_limit_error(exc: Exception) -> bool:
                if str(getattr(exc, "status", "") or "").strip() == "429":
                    return True
                text = str(exc or "").lower()
                return "http 429" in text or "throttl" in text or "rate limit" in text

            def note_api_error(exc: Exception) -> None:
                nonlocal replace_active_limit, replace_success_streak
                if not is_rate_limit_error(exc):
                    return
                with replace_limit_condition:
                    replace_active_limit = HUAWEI_EIP_REPLACE_RATE_LIMIT_ACTIVE_LIMIT
                    replace_success_streak = 0
                    replace_limit_condition.notify_all()

            def note_replace_success() -> None:
                nonlocal replace_active_limit, replace_success_streak
                with replace_limit_condition:
                    replace_success_streak += 1
                    if (
                        replace_active_limit == HUAWEI_EIP_REPLACE_RATE_LIMIT_ACTIVE_LIMIT
                        and replace_success_streak >= HUAWEI_EIP_REPLACE_RATE_LIMIT_RECOVERY_SUCCESSES
                    ):
                        replace_active_limit = HUAWEI_EIP_REPLACE_ACTIVE_LIMIT
                        replace_success_streak = 0
                    elif (
                        replace_active_limit == HUAWEI_EIP_REPLACE_ACTIVE_LIMIT
                        and replace_success_streak >= HUAWEI_EIP_REPLACE_CONCURRENCY_RAISE_SUCCESSES
                    ):
                        replace_active_limit = HUAWEI_EIP_REPLACE_MAX_ACTIVE_LIMIT
                        replace_success_streak = 0
                    replace_limit_condition.notify_all()

            def process_plan(plan: RegionPlan) -> Tuple[int, int]:
                prepared = prepared_plans.get(plan.region)
                if prepared is None:
                    return 0, 0
                pair, old_items = prepared
                old_ip_values = [eip_addr(item) for item in old_items if eip_addr(item)]
                region_seed_ip_values = region_seed_values.get(plan.region, [])
                rotated_online_seed_values = rotate_segment_seed_values(
                    online_seed_values.get((plan.region, EIP_TYPE), []),
                    f"{replacement_account_key}|{replacement_task_token}|{plan.region}",
                )
                plan_stop_event = threading.Event()
                plan_reserved_candidate_ips: Set[str] = set()
                plan_attempted_candidate_keys: Set[Tuple[str, str, str]] = set()
                plan_successful_segment_seeds: Dict[Tuple[str, str], List[str]] = {}

                def refresh_retry_item(
                    old_item: Dict[str, Any],
                    resource: Dict[str, Any],
                    port_id: str,
                    old_ip: str,
                ) -> None:
                    resource_id = str(
                        resource.get("id") or resource.get("publicip_id") or ""
                    ).strip()
                    old_item["_raw"] = resource
                    old_item["_resource_id"] = resource_id
                    old_item["_port_id"] = port_id
                    old_item["_public_ip"] = old_ip
                    old_item["_name"] = old_ip

                def release_old_eip(old_item: Dict[str, Any]) -> str:
                    old_id = eip_id(old_item)
                    old_ip = eip_addr(old_item)
                    port_id = eip_port(old_item)
                    if not old_id:
                        self.after(0, self.log, f"[{plan.label}] 旧EIP参数缺失，无法释放: {old_ip or '未知IP'}")
                        return "failed"
                    try:
                        client.unbind_public_ip(pair, old_id)
                        client.wait_public_ip_unbound(pair, old_id, timeout=45, interval=2.0)
                        client.delete_public_ip(pair, old_id)
                        deleted = client.wait_public_ip_deleted(
                            pair,
                            old_id,
                            lambda message: self.after(0, self.log, f"[{plan.label}] {message}"),
                            timeout=90,
                            interval=2.0,
                        )
                        if not deleted:
                            raise RuntimeError("旧EIP释放状态未确认")
                        self.after(0, self.log, f"[{plan.label}] 旧EIP已解绑并释放: {old_ip or old_id}")
                        return "released"
                    except Exception as exc:
                        note_api_error(exc)
                        retained = False
                        try:
                            current_items = client.list_public_ips(pair, timeout=30, retry_attempts=2)
                            current_item = next(
                                (
                                    item
                                    for item in current_items
                                    if str(item.get("id") or item.get("publicip_id") or "").strip() == old_id
                                ),
                                None,
                            )
                            if current_item is not None:
                                current_port_id = str(current_item.get("port_id") or "").strip()
                                if current_port_id == port_id:
                                    retained = True
                                elif port_id:
                                    client.bind_public_ip(pair, old_id, port_id)
                                    retained = True
                                if retained:
                                    refresh_retry_item(old_item, current_item, port_id, old_ip)
                        except Exception:
                            retained = False
                        if not retained and port_id:
                            try:
                                client.bind_public_ip(pair, old_id, port_id)
                                retained = True
                            except Exception:
                                pass
                        self.after(
                            0,
                            self.log,
                            f"[{plan.label}] 旧EIP解绑/释放失败 {old_ip or old_id}: {exc}；"
                            f"{'原资源已保留并恢复绑定' if retained else '原资源状态未能恢复确认'}。",
                        )
                        return "retained" if retained else "failed"

                def ensure_new_eip_bound(new_item: Dict[str, Any], port_id: str) -> bool:
                    new_id = str(new_item.get("id") or new_item.get("publicip_id") or "").strip()
                    new_ip = public_ip_address(new_item)
                    if not new_id or not port_id:
                        self.after(0, self.log, f"[{plan.label}] 新EIP绑定参数缺失 new={new_id} port={port_id}")
                        return False
                    try:
                        if str(new_item.get("port_id") or "").strip() != port_id:
                            client.bind_public_ip(pair, new_id, port_id)
                        client.wait_public_ip_bound(pair, new_id, port_id, timeout=45, interval=2.0)
                        return True
                    except Exception as exc:
                        note_api_error(exc)
                        try:
                            client.delete_public_ip(pair, new_id)
                        except Exception as cleanup_exc:
                            self.after(0, self.log, f"[{plan.label}] 新EIP {new_ip or new_id} 绑定失败后释放提示: {cleanup_exc}")
                        self.after(0, self.log, f"[{plan.label}] 新EIP绑定原端口失败 {new_ip or new_id}: {exc}")
                        return False

                def bind_retained_old_eip(old_resource: Dict[str, Any], port_id: str, old_ip: str) -> bool:
                    retained_id = str(
                        old_resource.get("id") or old_resource.get("publicip_id") or ""
                    ).strip()
                    if not retained_id or not port_id:
                        self.after(0, self.log, f"[{plan.label}] 暂留原IP绑定参数缺失: {old_ip or retained_id}")
                        return False
                    try:
                        if str(old_resource.get("port_id") or "").strip() != port_id:
                            client.bind_public_ip(pair, retained_id, port_id)
                        client.wait_public_ip_bound(pair, retained_id, port_id, timeout=45, interval=2.0)
                        return True
                    except Exception as exc:
                        note_api_error(exc)
                        self.after(0, self.log, f"[{plan.label}] 暂留原IP重新绑定失败 {old_ip or retained_id}: {exc}")
                        return False

                def discard_rejected_eip(item: Dict[str, Any], value: str) -> bool:
                    item_id = str(item.get("id") or item.get("publicip_id") or "").strip()
                    if not item_id:
                        self.after(0, self.log, f"[{plan.label}] 待释放EIP参数缺失: {value or '未知IP'}")
                        return False
                    try:
                        client.delete_public_ip(pair, item_id)
                        deleted = client.wait_public_ip_deleted(
                            pair,
                            item_id,
                            lambda _message: None,
                            timeout=45,
                            interval=1.0,
                        )
                        if not deleted:
                            raise RuntimeError("EIP释放状态未确认")
                        return True
                    except Exception as exc:
                        note_api_error(exc)
                        self.after(0, self.log, f"[{plan.label}] 待释放EIP {value or item_id} 释放失败: {exc}")
                        return False

                def attempt_restore_old_eip(old_item: Dict[str, Any], index: int) -> str:
                    old_ip = eip_addr(old_item)
                    port_id = eip_port(old_item)
                    if not old_ip or not port_id:
                        self.after(0, self.log, f"[{plan.label}] 原IP恢复参数缺失: {old_ip or '未知IP'}")
                        return "failed"
                    pending_resource = old_item.get("_pending_restore_resource")
                    restored_item = pending_resource if isinstance(pending_resource, dict) else None
                    restore_types = [eip_type(old_item) or EIP_TYPE]
                    if restored_item is None:
                        restore_type = restore_types[0]
                        try:
                            restored_item = client.create_public_ip(
                                pair,
                                timestamp_name(f"wawacloud-restore-eip-{index}", plan),
                                eip_type=restore_type,
                                ip_address=old_ip,
                                timeout=30,
                            )
                        except Exception as exc:
                            note_api_error(exc)
                            if is_rate_limit_error(exc):
                                return "rate_limited"
                            self.after(0, self.log, f"[{plan.label}] 原IP恢复申请失败 {old_ip}: {exc}")
                            return "failed"
                        restored_ip = public_ip_address(restored_item)
                        if restored_ip != old_ip:
                            restored_id = str(
                                restored_item.get("id") or restored_item.get("publicip_id") or ""
                            ).strip()
                            if restored_id:
                                try:
                                    client.delete_public_ip(pair, restored_id)
                                except Exception:
                                    pass
                            self.after(
                                0,
                                self.log,
                                f"[{plan.label}] 原IP恢复失败 {old_ip}: 云平台返回了其他IP {restored_ip or '未知'}",
                            )
                            return "failed"
                        old_item["_pending_restore_resource"] = restored_item
                    restored_id = str(
                        restored_item.get("id") or restored_item.get("publicip_id") or ""
                    ).strip()
                    try:
                        if str(restored_item.get("port_id") or "").strip() != port_id:
                            client.bind_public_ip(pair, restored_id, port_id)
                        client.wait_public_ip_bound(pair, restored_id, port_id, timeout=45, interval=2.0)
                    except Exception as exc:
                        note_api_error(exc)
                        if is_rate_limit_error(exc):
                            return "rate_limited"
                        self.after(0, self.log, f"[{plan.label}] 原IP已申请但重新绑定失败 {old_ip}: {exc}")
                        return "failed"
                    old_item.pop("_pending_restore_resource", None)
                    refresh_retry_item(old_item, restored_item, port_id, old_ip)
                    self.after(0, self.log, f"[{plan.label}] 换新失败，原IP已恢复并重新绑定: {old_ip}")
                    return "restored"

                def restore_old_eip_with_retry(old_item: Dict[str, Any], index: int) -> bool:
                    for _attempt in range(1, HUAWEI_EIP_RESTORE_MAX_ATTEMPTS + 1):
                        acquire_restore_slot()
                        try:
                            restore_status = attempt_restore_old_eip(old_item, index)
                        finally:
                            release_restore_slot()
                        if restore_status == "restored":
                            return True
                        if restore_status != "rate_limited":
                            return False
                        previous_limit, next_limit = reduce_restore_limit()
                        if next_limit != previous_limit:
                            self.after(
                                0,
                                self.log,
                                f"[{plan.label}] 旧IP恢复检测到接口限流，并发由 {previous_limit} 降至 {next_limit}。",
                            )
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] 原IP恢复连续受到接口限流，已完成 {HUAWEI_EIP_RESTORE_MAX_ATTEMPTS} 次重试: "
                        f"{eip_addr(old_item) or '未知IP'}",
                    )
                    return False

                def create_replacement_eip(
                    old_item: Dict[str, Any],
                    index: int,
                ) -> Tuple[Dict[str, Any], bool, Optional[Dict[str, Any]]]:
                    old_ip = eip_addr(old_item)
                    replacement_type = replacement_types_for_item(plan, old_item)[0]
                    success_key = (plan.region, replacement_type)
                    with replace_state_lock:
                        preferred_seeds = list(plan_successful_segment_seeds.get(success_key, []))
                    seed_values = self._dedupe_public_ip_values(
                        [
                            *preferred_seeds,
                            *rotated_online_seed_values,
                            *old_ip_values,
                            *region_seed_ip_values,
                        ]
                    )

                    def terminal_error(exc: Exception) -> bool:
                        return bool(
                            self._is_confirmed_eip_sold_out(exc)
                            or self._is_eip_quota_insufficient(exc)
                        )

                    item, retained_rejected_ip = self._allocate_huawei_new_eip(
                        client,
                        pair,
                        plan,
                        eip_types=(replacement_type,),
                        seed_addresses_by_type={replacement_type: seed_values},
                        excluded_ips=used_ip_table,
                        reserved_candidate_ips=plan_reserved_candidate_ips,
                        attempted_candidate_keys=plan_attempted_candidate_keys,
                        successful_segment_seeds=plan_successful_segment_seeds,
                        state_lock=replace_state_lock,
                        anchor_ip=old_ip,
                        rotation_index=index,
                        resource_name_prefix=f"wawacloud-replace-eip-{index}",
                        discard_eip=discard_rejected_eip,
                        emit=lambda message: self.after(
                            0,
                            self.log,
                            f"[{plan.label}] {message}",
                        ),
                        note_api_error=note_api_error,
                        terminal_error=terminal_error,
                        reference_ip=old_ip,
                    )
                    if retained_rejected_ip:
                        if retained_rejected_ip == old_ip:
                            return item, True, None
                        raise RuntimeError(
                            f"重复EIP释放状态未确认: {retained_rejected_ip}"
                        )
                    return item, False, None

                result_ip_replacements: Dict[str, str] = {}
                failed_old_ips: Set[str] = set()
                succeeded_old_ips: Set[str] = set()

                def replace_one(old_item: Dict[str, Any], index: int) -> str:
                    old_id = eip_id(old_item)
                    port_id = eip_port(old_item)
                    old_ip = eip_addr(old_item)
                    if not old_id or not port_id:
                        self.after(0, self.log, f"[{plan.label}] 换IP参数缺失 old={old_id} port={port_id}")
                        plan_stop_event.set()
                        return "failed"
                    replace_slot_acquired = False
                    try:
                        acquire_replace_slot()
                        replace_slot_acquired = True
                        if plan_stop_event.is_set() or self.stop_event.is_set():
                            return "skipped"
                        release_status = release_old_eip(old_item)
                        if release_status != "released":
                            if release_status == "retained":
                                return "restored"
                            restored = restore_old_eip_with_retry(old_item, index)
                            if not restored:
                                plan_stop_event.set()
                            return "restored" if restored else "failed"
                        try:
                            new_item, restored_old_ip, retained_old_item = create_replacement_eip(old_item, index)
                        except Exception as exc:
                            if str(exc).startswith("新EIP申请失败: 服务器历史IP段"):
                                self.after(
                                    0,
                                    self.log,
                                    f"[{plan.label}] 新EIP申请失败 {old_ip or old_id}: {exc}；"
                                    "已加入本地区末尾统一恢复队列。",
                                )
                                return "deferred_restore"
                            restored = restore_old_eip_with_retry(old_item, index)
                            if not restored:
                                plan_stop_event.set()
                            status = "restored" if restored else "failed"
                            self.after(
                                0,
                                self.log,
                                f"[{plan.label}] 新EIP申请失败 {old_ip or old_id}: {exc}；"
                                f"{'原IP已恢复，继续处理其他EIP' if restored else '原IP恢复失败，停止该地区后续更换'}。",
                            )
                            return status
                        new_id = str(new_item.get("id") or new_item.get("publicip_id") or "").strip()
                        new_ip = public_ip_address(new_item)
                        if restored_old_ip:
                            restored = bind_retained_old_eip(new_item, port_id, old_ip)
                            if restored:
                                refresh_retry_item(old_item, new_item, port_id, old_ip)
                                self.after(0, self.log, f"[{plan.label}] 未取得不同新IP，原IP已重新绑定: {old_ip}；继续处理其他EIP。")
                            else:
                                plan_stop_event.set()
                            return "restored" if restored else "failed"
                        if not ensure_new_eip_bound(new_item, port_id):
                            restored = (
                                bind_retained_old_eip(retained_old_item, port_id, old_ip)
                                if retained_old_item is not None
                                else restore_old_eip_with_retry(old_item, index)
                            )
                            if restored and retained_old_item is not None:
                                refresh_retry_item(old_item, retained_old_item, port_id, old_ip)
                            if not restored:
                                plan_stop_event.set()
                            return "restored" if restored else "failed"
                        if retained_old_item is not None:
                            retained_id = str(
                                retained_old_item.get("id") or retained_old_item.get("publicip_id") or ""
                            ).strip()
                            if retained_id:
                                try:
                                    client.delete_public_ip(pair, retained_id)
                                    client.wait_public_ip_deleted(
                                        pair,
                                        retained_id,
                                        lambda message: self.after(0, self.log, f"[{plan.label}] {message}"),
                                        timeout=60,
                                        interval=2.0,
                                    )
                                except Exception as exc:
                                    self.after(0, self.log, f"[{plan.label}] 新IP已绑定，暂留原IP释放提示 {old_ip}: {exc}")
                        if old_ip and new_ip:
                            with replace_state_lock:
                                result_ip_replacements[old_ip] = new_ip
                        self.after(0, self.log, f"[{plan.label}] EIP已更换 {old_ip or old_id} -> {new_ip or new_id}")
                        note_replace_success()
                        return "success"
                    finally:
                        if replace_slot_acquired:
                            release_replace_slot()

                ok = 0
                fail = 0
                skipped = 0
                indexed_old_items = list(enumerate(old_items, 1))
                pending_items: List[Tuple[int, Dict[str, Any]]] = []
                skipped_items: List[Tuple[int, Dict[str, Any]]] = []
                deferred_restore_items: List[Tuple[int, Dict[str, Any]]] = []
                try:
                    completed_items: List[Tuple[int, Dict[str, Any], str]] = []
                    if indexed_old_items:
                        with concurrent.futures.ThreadPoolExecutor(
                            max_workers=min(
                                len(indexed_old_items),
                                HUAWEI_EIP_REPLACE_MAX_ACTIVE_LIMIT,
                            )
                        ) as replace_executor:
                            replace_futures = {
                                replace_executor.submit(replace_one, old_item, index): (index, old_item)
                                for index, old_item in indexed_old_items
                            }
                            for replace_future in concurrent.futures.as_completed(replace_futures):
                                index, old_item = replace_futures[replace_future]
                                try:
                                    outcome = str(replace_future.result() or "failed")
                                except Exception as exc:
                                    outcome = "failed"
                                    plan_stop_event.set()
                                    self.after(
                                        0,
                                        self.log,
                                        f"[{plan.label}] EIP更换异常 "
                                        f"{eip_addr(old_item) or eip_id(old_item)}: {exc}",
                                    )
                                completed_items.append((index, old_item, outcome))

                    for index, old_item, outcome in sorted(
                        completed_items,
                        key=lambda value: value[0],
                    ):
                        old_ip = eip_addr(old_item)
                        if outcome == "success":
                            ok += 1
                            if old_ip:
                                with replace_state_lock:
                                    succeeded_old_ips.add(old_ip)
                                    failed_old_ips.discard(old_ip)
                            continue
                        pending_items.append((index, old_item))
                        if outcome == "deferred_restore":
                            deferred_restore_items.append((index, old_item))
                        elif outcome in {"failed", "skipped"}:
                            skipped_items.append((index, old_item))
                        if outcome == "failed":
                            plan_stop_event.set()
                finally:
                    if deferred_restore_items:
                        initial_restore_limit = min(
                            current_restore_limit(),
                            HUAWEI_EIP_RESTORE_ACTIVE_LIMIT,
                            len(deferred_restore_items),
                        )
                        self.after(
                            0,
                            self.log,
                            f"[{plan.label}] 换新尝试结束，开始统一恢复 {len(deferred_restore_items)} 条旧IP，"
                            f"当前恢复并发 {initial_restore_limit}。",
                        )
                        restore_success_count = 0
                        restore_failed_items: List[Tuple[int, Dict[str, Any]]] = []
                        with concurrent.futures.ThreadPoolExecutor(
                            max_workers=min(
                                HUAWEI_EIP_RESTORE_ACTIVE_LIMIT,
                                len(deferred_restore_items),
                            )
                        ) as restore_executor:
                            restore_futures = {
                                restore_executor.submit(
                                    restore_old_eip_with_retry,
                                    old_item,
                                    index,
                                ): (index, old_item)
                                for index, old_item in deferred_restore_items
                            }
                            for restore_future in concurrent.futures.as_completed(restore_futures):
                                restore_item = restore_futures[restore_future]
                                try:
                                    restored = bool(restore_future.result())
                                except Exception as exc:
                                    restored = False
                                    self.after(
                                        0,
                                        self.log,
                                        f"[{plan.label}] 原IP统一恢复异常 "
                                        f"{eip_addr(restore_item[1]) or '未知IP'}: {exc}",
                                    )
                                if restored:
                                    restore_success_count += 1
                                else:
                                    restore_failed_items.append(restore_item)
                        if restore_failed_items:
                            plan_stop_event.set()
                            skipped_items.extend(restore_failed_items)
                        self.after(
                            0,
                            self.log,
                            f"[{plan.label}] 旧IP统一恢复完成: 成功 {restore_success_count}/"
                            f"{len(deferred_restore_items)}，失败 {len(restore_failed_items)}。",
                        )

                if pending_items:
                    fail = len(pending_items)
                    skipped = len(skipped_items)
                    with replace_state_lock:
                        failed_old_ips.update(
                            old_ip
                            for _index, old_item in pending_items
                            if (old_ip := eip_addr(old_item))
                        )
                if skipped:
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] 任务已停止或资源状态无法安全恢复确认，保留未完成旧EIP {skipped} 条。",
                    )
                with replace_state_lock:
                    if failed_old_ips:
                        task_failed_ip_groups.setdefault(plan.label, set()).update(failed_old_ips)
                    task_succeeded_old_ips.update(succeeded_old_ips)
                if result_ip_replacements:
                    region_result_items = [
                        item
                        for item in self.manage_resource_items
                        if item.get("_region") == plan.region
                        and item.get("_kind") == "eip"
                        and eip_addr(item)
                        and eip_port(item)
                    ]
                    if not region_result_items:
                        region_result_items = list(old_items)
                    ordered_items = [
                        item
                        for _index, item in sorted(
                            enumerate(region_result_items),
                            key=lambda pair: (
                                self._ipv4_sort_tuple(str(pair[1].get("_eip_bound_private_ip") or ""))
                                if str(pair[1].get("_eip_bound_private_ip") or "")
                                else (999, 999, 999, 999),
                                pair[0],
                            ),
                        )
                    ]
                    current_result_ips: List[str] = []
                    for item in ordered_items:
                        old_ip = eip_addr(item)
                        current_ip = str(result_ip_replacements.get(old_ip) or old_ip).strip()
                        if is_public_ip(current_ip) and current_ip not in current_result_ips:
                            current_result_ips.append(current_ip)
                    for new_ip in result_ip_replacements.values():
                        if is_public_ip(new_ip) and new_ip not in current_result_ips:
                            current_result_ips.append(new_ip)
                    self.after(
                        0,
                        self._sync_result_ips_after_eip_replace,
                        plan,
                        dict(result_ip_replacements),
                        current_result_ips,
                    )
                return ok, fail

            runnable_plans = [plan for plan in plans if plan.region in prepared_plans]
            if runnable_plans:
                def process_plan_with_cleanup(plan: RegionPlan) -> Tuple[int, int]:
                    region_lock = _huawei_eip_region_lock(plan.region)
                    region_lock.acquire()
                    try:
                        return process_plan(plan)
                    finally:
                        region_lock.release()

                with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(runnable_plans), MAX_WORKERS)) as executor:
                    futures = {
                        executor.submit(process_plan_with_cleanup, plan): plan
                        for plan in runnable_plans
                    }
                    for future in concurrent.futures.as_completed(futures):
                        try:
                            ok, fail = future.result()
                        except Exception as exc:
                            plan = futures[future]
                            ok, fail = 0, 1
                            self.after(0, self.log, f"[{plan.label}] 更换IP失败: {exc}")
                        total_success += ok
                        total_failed += fail
            if task_failed_ip_groups or task_succeeded_old_ips:
                try:
                    added_failed, removed_failed, failure_sheet_ready = self._update_failed_ip_table_groups(
                        add_ip_groups={
                            region_label: sorted(ips)
                            for region_label, ips in task_failed_ip_groups.items()
                        },
                        remove_ips=sorted(task_succeeded_old_ips),
                    )
                    if failure_sheet_ready and (added_failed or removed_failed):
                        self.after(
                            0,
                            self.log,
                            f"更换失败IP库已更新：新增 {added_failed} 条，移除 {removed_failed} 条。",
                            "manage",
                        )
                    elif not failure_sheet_ready:
                        self.after(0, self.log, "当前旧IP库不是Excel文件，未能更新更换失败IP工作表。", "manage")
                except Exception as exc:
                    self.after(0, self.log, f"更新更换失败IP工作表失败: {exc}", "manage")
            final_popup = f"更换新IP完成，成功 {total_success} 条，失败 {total_failed} 条。"

            def finish_replace() -> None:
                warning = str(getattr(self, "_huawei_replace_result_warning", "") or "").strip()
                message = f"{final_popup}\n\n{warning}" if warning else final_popup
                final_popup_message = self._queue_result_message_or_popup(message)
                self.refresh_resource_views_for_plans(
                    list(plans),
                    context="manage",
                    final_popup_message=final_popup_message,
                )

            self.after(100, finish_replace)

        self.run_task("更换新IP", worker, exclusive=False, context="manage", regions=[plan.region for plan in plans])

    def clear_actionable_manage_resources(self) -> None:
        if self._standalone_limited_features() or self._customer_manage_eip_only():
            return
        confirm_message = action_confirm_message(
            "确认一键清空账号资源",
            regions=[plan.label for plan in REGION_PLANS],
            details=["将自动处理全部地区可退订、可删除、可释放资源。"],
            warning=DESTRUCTIVE_CONFIRM_TEXT,
        )
        cached_actionable = [
            item
            for item in self.manage_resource_items
            if item.get("_operation_kind") not in {"", "none", None}
        ]
        release_ip_groups = self._manage_release_ip_groups(cached_actionable)
        release_action = self._ask_cleanup_release_action(
            confirm_message,
            allow_used_ip_import=bool(release_ip_groups),
        )
        if release_action == "cancel":
            return
        if release_action == "confirm_and_import":
            try:
                self._import_manage_release_ip_groups(release_ip_groups)
            except Exception as exc:
                message = f"导入本次待释放IP失败，已停止一键清空账号资源：{exc}"
                self.log(message, "manage")
                self.show_info(message)
                return
        try:
            client = self.get_client()
        except Exception as exc:
            self.show_error(str(exc))
            return
        self._set_log_context("manage")
        plans = list(REGION_PLANS)
        only_main = self.manage_only_main_var.get()

        def worker() -> None:
            all_items = self._collect_manage_resources(client, plans, only_main, full=True)
            action_items = self._manage_display_items(all_items)
            actionable = [item for item in action_items if item.get("_operation_kind") not in {"", "none", None}]
            self.after(0, self.populate_manage_tree, all_items)
            blocked_subscription_ecs = [
                item
                for item in action_items
                if item.get("_kind") == "ecs"
                and str(item.get("_billing") or "") == "包周期"
                and item.get("_operation_kind") != "subscription_unsubscribe"
            ]
            if blocked_subscription_ecs:
                names = "、".join(str(item.get("_region_label") or item.get("_name") or "未知地区") for item in blocked_subscription_ecs[:5])
                self.after(0, self.log, f"检测到包周期ECS但未拿到可退订资源ID，已停止一键清空，避免先删EIP/VPC导致失败: {names}")
                self.after(0, self.show_info, "包周期云服务器退订信息未读取完整，已停止一键清空。请重新读取账号资源后再执行。")
                return
            if not actionable:
                self.after(0, self.show_info, "当前账号没有可退订/可删除/可释放的资源。")
                return
            success, failed = self._process_manage_items(client, actionable)
            try:
                refreshed = self._collect_manage_resources(client, plans, only_main, full=True)
                self.after(0, self.populate_manage_tree, refreshed)
                self.after(0, self.log, f"一键清空后已自动刷新账号资源，共 {len(refreshed)} 条。")
            except Exception as exc:
                self.after(0, self.log, f"一键清空后自动刷新账号资源失败: {exc}")
            final_popup_message = self._queue_result_message_or_popup(f"一键清空账号资源完成，成功 {success}，失败 {failed}。")
            if final_popup_message:
                self.after(250, self.show_info, final_popup_message)

        self.run_task("一键清空账号资源", worker, context="manage", regions=[plan.region for plan in plans])

    def unsubscribe_selected_region_resources(self) -> None:
        self.unsubscribe_selected_manage_resources()

    def start_purchase_selected_regions(self) -> None:
        self._start_purchase_task(build_after_purchase=False)

    def purchase_selected_region_new_ips(self) -> None:
        try:
            client = self.get_client()
            plans = self.selected_plans()
            password = self.password_var.get()
            prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
            flavor_mode = self._selected_flavor_mode_or_warn()
            x1_flavor_enabled = bool(self.x1_flavor_var.get())
            x1_ten_ip_enabled = bool(
                flavor_mode in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED}
                and self._x1_ten_ip_purchase_enabled(x1_flavor_enabled)
            )
            static_bgp_enabled = self._static_bgp_purchase_enabled()
        except Exception as exc:
            self.show_error(str(exc))
            return
        if not flavor_mode:
            return
        plans = [
            self._effective_plan_for_flavor_mode(
                plan,
                flavor_mode,
                x1_ten_ip_enabled=x1_ten_ip_enabled,
            )
            for plan in plans
        ]
        if not plans:
            self.show_info("请先选择要购买新IP的地区。")
            return
        used_ip_path = self._used_ip_table_path()
        try:
            used_ip_rows = self._read_used_ip_table_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, "purchase")
            self.show_info(message)
            return
        if not used_ip_filter:
            message = "旧IP库为空，无法过滤已使用IP；请先在更换新IP右键配置中保存或链接旧IP库。"
            self.log(message, "purchase")
            self.show_info(message)
            return
        confirm_details = [
            "购买信息：",
            "购买选项：按所选地区已有云服务器补齐公网IP",
            "仅补齐已有云服务器缺少的网卡和公网IP，不创建新云服务器。",
            f"新购公网IP将过滤当前旧IP库 {len(used_ip_filter)} 条记录。",
            f"EIP类型：{self._huawei_eip_bgp_label(plans, static_bgp_enabled)}",
        ]
        if not self.ask_yes_no(
            self._purchase_confirm_message(
                "购买选择地区新IP",
                plans,
                details=confirm_details,
            )
        ):
            return
        self._ensure_result_scope_for_current_credentials()
        self._huawei_new_ip_result_warning = ""
        action_name = "购买选择地区新IP"
        self._set_log_context("purchase")

        def worker() -> None:
            self._mark_one_time_x1_ip_count_reset(x1_ten_ip_enabled=x1_ten_ip_enabled)
            self.after(0, self.log, f"步骤: {action_name}启动 地区={[plan.label for plan in plans]}")
            self.after(0, self.log, f"{action_name}：旧IP库过滤 {len(used_ip_filter)} 条，仅复用已有云服务器。")
            self._wait_region_project_warmup()
            pairs = client.selected_project_pairs(plans)
            self.region_projects = pairs
            online_ip_range_manifest = self._load_online_ip_range_manifest()
            success_count = 0
            failures: Dict[str, str] = {}

            def process_plan(plan: RegionPlan) -> RegionBuildResult:
                pair = pairs.get(plan.region)
                if pair is None:
                    raise CloudApiError(f"未打通该地区，无法购买新IP，{REGION_ACCESS_TUTORIAL_GUIDANCE}")
                self.call_ui(self.update_region_row, plan, status="购买新IP中")
                emit = lambda message, p=plan: self.after(0, self.log, f"[{p.label}] {localize_region_tokens(message)}")
                result = self._find_reusable_region_result(
                    client,
                    plan,
                    pair,
                    prefix,
                    password,
                    emit,
                    strict_aux_ips=False,
                    allow_create_missing=True,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                    excluded_public_ips=sorted(used_ip_filter),
                    new_ip_seed_addresses=self._used_ip_seed_values_for_region(
                        used_ip_rows,
                        plan.label,
                    ),
                    online_ip_range_manifest=online_ip_range_manifest,
                )
                if result is None:
                    raise CloudApiError(
                        "所选地区没有可复用的云服务器；购买选择地区新IP只补齐已有云服务器，不会创建新云服务器。"
                    )
                return result

            with concurrent.futures.ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(plans))) as executor:
                future_map = {executor.submit(process_plan, plan): plan for plan in plans}
                for future in concurrent.futures.as_completed(future_map):
                    plan = future_map[future]
                    try:
                        result = future.result()
                    except Exception as exc:
                        reason = localize_region_tokens(str(exc))
                        failures[plan.key] = reason
                        self.call_ui(self.update_region_row, plan, status=f"新IP购买失败: {reason}")
                        self.after(0, self.log, f"[{plan.label}] 购买新IP失败: {reason}")
                        continue
                    success_count += 1
                    current_ips = self._dedupe_public_ip_values(result.public_ips)
                    try:
                        _path, added_count, _all_used_ips, pending_path = self._append_used_ip_table(
                            current_ips,
                            region_label=plan.label,
                            reason="购买选择地区新IP",
                        )
                        pending_note = "，新增记录已进入待合并文件" if pending_path is not None else ""
                        self.after(0, self.log, f"[{plan.label}] 新IP购买绑定完成，旧IP库新增 {added_count} 条{pending_note}。")
                    except Exception as exc:
                        self.after(0, self.log, f"[{plan.label}] 新IP已购买绑定，但同步旧IP库失败: {exc}")
                    self.call_ui(
                        self._sync_results_after_new_ip_purchase,
                        plan,
                        current_ips,
                        "purchase",
                    )
                    self.call_ui(
                        self.update_region_row,
                        plan,
                        status="新IP购买完成",
                        server_ip=result.server_ip,
                        server_id=result.server_id,
                    )

            failed_count = len(failures)
            self.after(
                0,
                self.log,
                f"步骤: {action_name}任务结束，成功 {success_count}/{len(plans)}，失败 {failed_count}",
            )
            final_popup = f"{action_name}任务结束，成功 {success_count}/{len(plans)} 个地区，失败 {failed_count}。"
            if failures:
                final_popup += "\n\n失败原因：\n" + "\n".join(
                    f"{next((item.label for item in plans if item.key == key), key)}：{reason}"
                    for key, reason in failures.items()
                )
            warning = str(getattr(self, "_huawei_new_ip_result_warning", "") or "").strip()
            if warning:
                final_popup += f"\n\n{warning}"
            final_popup_message = self._queue_result_message_or_popup(final_popup)
            self.after(
                100,
                lambda p=list(plans), message=final_popup_message: self.refresh_resource_views_for_plans(
                    p,
                    context="purchase",
                    final_popup_message=message,
                ),
            )

        self.run_task(action_name, worker, context="purchase", regions=[plan.region for plan in plans])

    def start_purchase_and_build(self) -> None:
        self._start_purchase_task(build_after_purchase=True)

    @staticmethod
    def _huawei_purchase_spec_label(flavor_mode: str, x1_flavor_enabled: bool = False) -> str:
        if flavor_mode == FLAVOR_MODE_WAWACLOUD_LOW:
            label = "1vCPUs|1GiB"
        elif flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            label = "20(1vCPUs|1GiB)"
        else:
            label = "2vCPUs|2GiB"
        return f"{label}|X1" if x1_flavor_enabled else label

    @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 _huawei_purchase_billing_label(on_demand_billing_enabled: Optional[bool] = None) -> str:
        enabled = is_on_demand_debug_enabled("huawei") if on_demand_billing_enabled is None else bool(on_demand_billing_enabled)
        if enabled:
            return "ECS按需计费"
        return f"ECS包年包月{BILLING_PERIOD_NUM}个月，自动续费，自动支付"

    def _huawei_eip_bgp_label(self, plans: Sequence[RegionPlan], static_bgp_enabled: bool) -> str:
        static_labels = [
            plan.label
            for plan in plans
            if self._static_bgp_enabled_for_plan(plan, static_bgp_enabled)
        ]
        if not static_labels:
            return "全动态BGP"
        if len(static_labels) == len(plans):
            return "静态BGP"
        return f"静态BGP({'、'.join(static_labels)})，其余动态BGP"

    def _huawei_build_spec_label(self, *, build_enabled: bool, ros_build_enabled: bool) -> str:
        if not build_enabled:
            return "未搭建"
        mode = self.build_mode_var.get().strip() or BUILD_MODE_RANDOM
        connection_label = "默认连接账密(右键自定义)" if mode == BUILD_MODE_DEFAULT else "随机连接账密"
        if ros_build_enabled:
            return f"Ros镜像(右键自定义|L2TP)，{connection_label}"
        return connection_label

    def _huawei_ros_config_confirm_details(self, ros_config: RosBuildConfig) -> List[str]:
        config_source = self._ros_connection_config_source()
        if config_source == "ros_custom":
            source_label = "Ros镜像右键配置"
            effect_label = "生效1次" if self.ros_config_once_var.get() else "本次软件运行期间保持"
        elif config_source == "default_custom":
            source_label = "默认连接右键配置（用于ROS）"
            effect_label = "生效1次" if self.default_conn_once_var.get() else "本次软件运行期间保持"
        elif config_source == "random":
            source_label = "ROS随机连接账密"
            effect_label = "本次任务"
        else:
            source_label = "ROS默认连接配置"
            effect_label = "默认值"
        username = ros_l2tp_username(ros_config)
        psk = ros_l2tp_psk(ros_config)
        return [
            f"{source_label}：{effect_label}",
            f"L2TP连接用户名：{username}1、{username}2...",
            f"L2TP连接密码：{ros_config.ppp_password}",
            f"L2TP连接密钥：{psk}",
            "ROS许可证：从服务器动态获取",
        ]

    def _huawei_build_confirm_details(
        self,
        *,
        build_enabled: bool,
        ros_build_enabled: bool,
        ros_config: Optional[RosBuildConfig] = None,
    ) -> List[str]:
        if not build_enabled:
            return ["搭建信息：未搭建"]
        details = [
            "搭建信息：",
            f"搭建选项：{self._huawei_build_spec_label(build_enabled=True, ros_build_enabled=ros_build_enabled)}",
        ]
        if ros_build_enabled and ros_config is not None:
            details.extend(self._huawei_ros_config_confirm_details(ros_config))
        else:
            details.extend(self._default_connection_confirm_details())
        return details

    def _huawei_task_popup_details(
        self,
        plans: Sequence[RegionPlan],
        *,
        flavor_mode: str,
        x1_flavor_enabled: bool = False,
        static_bgp_enabled: bool = False,
        on_demand_billing_enabled: bool = False,
        purchase_enabled: bool,
        build_enabled: bool,
        ros_build_enabled: bool = False,
    ) -> str:
        build_spec = self._huawei_build_spec_label(build_enabled=build_enabled, ros_build_enabled=ros_build_enabled)
        lines: List[str] = []
        for plan in plans:
            if purchase_enabled:
                purchase_spec = self._huawei_purchase_spec_label(flavor_mode, x1_flavor_enabled)
                bgp_label = "静态BGP" if self._static_bgp_enabled_for_plan(plan, static_bgp_enabled) else "动态BGP"
                billing_label = self._huawei_purchase_billing_label(on_demand_billing_enabled)
                purchase_spec = f"{purchase_spec}，{bgp_label}，{billing_label}"
            else:
                purchase_spec = "未购买，复用已有资源"
            lines.append(f"{plan.label}|购买规格:{purchase_spec}|搭建规格:{build_spec}")
        return "\n\n明细:\n" + "\n".join(lines) if lines else ""

    def _start_purchase_task(self, build_after_purchase: bool) -> None:
        try:
            client = self.get_client()
            plans = self.selected_plans()
            ssh_port = self.parse_ssh_port()
            ssh_user = self.ssh_user_var.get().strip() or DEFAULT_SSH_USER
            password = self.password_var.get()
            flavor_mode = self._selected_flavor_mode_or_warn()
            x1_flavor_enabled = bool(self.x1_flavor_var.get())
            configured_x1_ten_ip_enabled = bool(
                flavor_mode in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED}
                and self._x1_ten_ip_purchase_enabled(x1_flavor_enabled)
            )
            purchase_eip_quota = self._purchase_eip_quota_for_task(flavor_mode)
            if purchase_eip_quota:
                x1_flavor_enabled = True
            x1_ten_ip_enabled = bool(configured_x1_ten_ip_enabled or purchase_eip_quota)
            static_bgp_enabled = self._static_bgp_purchase_enabled()
            on_demand_billing_enabled = self._on_demand_billing_enabled()
            ros_build_enabled = self._ros_build_enabled()
            ros_config = self._current_ros_config()
        except Exception as exc:
            self.show_error(str(exc))
            return
        if not flavor_mode:
            return
        if ros_build_enabled and flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            self.show_info("Ros镜像(右键自定义|L2TP)暂不支持20规格，请选择1vCPUs | 1GiB或2vCPUs | 2GiB。")
            return
        plans = [
            self._effective_plan_for_flavor_mode(
                plan,
                flavor_mode,
                x1_ten_ip_enabled=x1_ten_ip_enabled,
            )
            for plan in plans
        ]
        target_set_counts = {
            plan.key: self._resource_set_count_for_eip_quota(plan, purchase_eip_quota)
            for plan in plans
        }
        if not plans:
            self.show_info("请先选择一个或多个地区。")
            return
        if not password:
            self.show_info("请填写云服务器登录密码。")
            return
        do_build_after_purchase = bool(build_after_purchase)
        task_connection_env = (
            self._task_connection_env()
            if do_build_after_purchase and not ros_build_enabled
            else {}
        )
        script_path = resource_path(SCRIPT_FILENAME)
        self._set_log_context("purchase")
        action_name = "购买并搭建" if do_build_after_purchase else "购买"
        confirm_details = [
            "购买信息：",
            f"购买规格：{self._huawei_purchase_spec_label(flavor_mode, x1_flavor_enabled)}",
            f"计费方式：{self._huawei_purchase_billing_label(on_demand_billing_enabled)}",
            f"EIP类型：{self._huawei_eip_bgp_label(plans, static_bgp_enabled)}",
            *(
                [
                    f"EIP配额数量：{purchase_eip_quota}",
                    *[
                        (
                            f"{plan.label}：{target_set_counts[plan.key]}套ECS，"
                            f"每套{expected_public_ip_count(plan)}IP，"
                            f"计划使用{target_set_counts[plan.key] * expected_public_ip_count(plan)}IP"
                        )
                        for plan in plans
                    ],
                ]
                if purchase_eip_quota
                else []
            ),
            *(
                ["已有完整ECS仅计入配额，不重复执行搭建脚本；仅搭建本次新购ECS。"]
                if purchase_eip_quota and do_build_after_purchase
                else []
            ),
            *self._huawei_build_confirm_details(
                build_enabled=do_build_after_purchase,
                ros_build_enabled=ros_build_enabled,
                ros_config=ros_config,
            ),
            self._huawei_task_popup_details(
                plans,
                flavor_mode=flavor_mode,
                x1_flavor_enabled=x1_flavor_enabled,
                static_bgp_enabled=static_bgp_enabled,
                on_demand_billing_enabled=on_demand_billing_enabled,
                purchase_enabled=True,
                build_enabled=do_build_after_purchase,
                ros_build_enabled=ros_build_enabled,
            ),
        ]
        if not self.ask_yes_no(
            self._purchase_confirm_message(
                action_name,
                plans,
                details=confirm_details,
            )
        ):
            return
        self._ensure_result_scope_for_current_credentials()

        def worker() -> None:
            self._mark_one_time_x1_ip_count_reset(
                x1_ten_ip_enabled=configured_x1_ten_ip_enabled
            )
            self._mark_one_time_purchase_eip_quota_reset(eip_quota=purchase_eip_quota)
            self._mark_one_time_build_config_reset(build_used=do_build_after_purchase, ros_build_enabled=ros_build_enabled)
            if do_build_after_purchase and ros_build_enabled:
                self._start_ros_chr_prefetch()
            self.after(0, self.log, f"步骤: 接口启动 地区={[plan.label for plan in plans]}")
            self.after(0, self.log, f"自动购买映射: {HUAWEI_AUTOBUY_SOURCE_MODES.get(flavor_mode, '华为云自动购买')}")
            self._wait_region_project_warmup()
            pairs = client.selected_project_pairs(plans)
            self.region_projects = pairs
            missing = [plan for plan in plans if plan.region not in pairs]
            for plan in missing:
                self.call_ui(self.update_region_row, plan, status="地区初始化失败")
                self.after(
                    0,
                    self.log,
                    f"[{plan.label}] 未打通该地区，无法执行任务，{REGION_ACCESS_TUTORIAL_GUIDANCE}",
                )
            runnable = [plan for plan in plans if plan.region in pairs]
            if not runnable:
                missing_desc = "、".join(f"{plan.label}({plan.region})" for plan in missing) or "所选地区"
                raise ValueError(
                    "所选地区项目仍未识别："
                    f"{missing_desc}。{REGION_ACCESS_TUTORIAL_GUIDANCE}"
                )
            if purchase_eip_quota:
                self._run_purchase_eip_quota_task(
                    client,
                    runnable,
                    pairs,
                    target_set_counts,
                    password=password,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    x1_ten_ip_enabled=x1_ten_ip_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                    on_demand_billing_enabled=on_demand_billing_enabled,
                    do_build_after_purchase=do_build_after_purchase,
                    ssh_port=ssh_port,
                    ssh_user=ssh_user,
                    script_path=script_path,
                    ros_build_enabled=ros_build_enabled,
                    ros_config=ros_config,
                    task_connection_env=task_connection_env,
                    action_name=action_name,
                )
                return
            purchase_results: Dict[str, RegionBuildResult] = {}
            purchase_success_count = 0
            pipeline_build_success_count = 0
            purchase_failures: Dict[str, str] = {}
            build_failures: Dict[str, str] = {}
            unavailable_ip_count = 0

            def build_region(result: RegionBuildResult) -> Tuple[RegionPlan, Optional[RegionBuildResult], str, str]:
                if not do_build_after_purchase:
                    return result.plan, result, "purchase", ""
                if self.stop_event.is_set():
                    return result.plan, None, "build", "用户请求停止，跳过后续步骤。"
                try:
                    built = self._build_existing_region_with_retry(
                        result,
                        ssh_port,
                        ssh_user,
                        password,
                        script_path,
                        flavor_mode,
                        ros_build_enabled,
                        ros_config,
                        task_connection_env=task_connection_env,
                    )
                    return result.plan, built, "build", ""
                except Exception as exc:
                    return result.plan, None, "build", localize_region_tokens(str(exc))

            def retry_region(plan: RegionPlan, first_error: str) -> Tuple[RegionPlan, Optional[RegionBuildResult], str, str]:
                last_error = first_error or "未知错误"
                last_signature = normalize_failure_signature(last_error)
                repeat_count = 1 if last_signature else 0
                if self._is_confirmed_eip_sold_out_text(last_error):
                    return (
                        plan,
                        None,
                        "purchase",
                        f"{last_error}（EIP已确认售罄，保留已有资源，停止重复购买/重试；公网IP恢复后会复用已有资源并继续该地区补齐/搭建。）",
                    )
                if self._is_subeni_quota_text(last_error):
                    return (
                        plan,
                        None,
                        "purchase",
                        f"{last_error}（辅助弹性网卡配额已达当前ECS规格上限，停止重复创建网卡和重复反查；已有网卡会在下次直接复用并继续EIP绑定/搭建。）",
                    )
                for attempt in range(2, REGION_RETRY_ATTEMPTS + 1):
                    if self.stop_event.is_set():
                        return plan, None, "purchase", "用户请求停止，跳过后续步骤。"
                    if repeat_count >= 2 and not huawei_retryable_reconcile_error(last_error):
                        return plan, None, "purchase", f"{last_error}（同类失败已连续2次，停止重试）"
                    if "待支付" in last_error or "PENDING_PAYMENT" in last_error:
                        return plan, None, "purchase", last_error
                    self.call_ui(self.update_region_row, plan, status=f"失败后独立重试 {attempt}/{REGION_RETRY_ATTEMPTS}")
                    self.after(0, self.log, f"[{plan.label}] 失败后独立重试 {attempt}/{REGION_RETRY_ATTEMPTS}，不等待其他地区。")
                    time.sleep(REGION_RETRY_DELAY_SECONDS)
                    try:
                        round_results, round_failures = self._run_huawei_wwwroot_purchase_batch(
                            client,
                            [plan],
                            pairs,
                            password,
                            flavor_mode,
                            x1_flavor_enabled=x1_flavor_enabled,
                            x1_ten_ip_enabled=x1_ten_ip_enabled,
                            static_bgp_enabled=static_bgp_enabled,
                            on_demand_billing_enabled=on_demand_billing_enabled,
                        )
                    except Exception as exc:
                        round_results = {}
                        round_failures = {plan.key: str(exc)}
                    result = round_results.get(plan.key)
                    if result:
                        return build_region(result)
                    last_error = round_failures.get(plan.key, "未知错误")
                    if self._is_confirmed_eip_sold_out_text(last_error):
                        return (
                            plan,
                            None,
                            "purchase",
                            f"{last_error}（EIP已确认售罄，保留已有资源，停止重复购买/重试；公网IP恢复后会复用已有资源并继续该地区补齐/搭建。）",
                        )
                    if self._is_subeni_quota_text(last_error):
                        return (
                            plan,
                            None,
                            "purchase",
                            f"{last_error}（辅助弹性网卡配额已达当前ECS规格上限，停止重复创建网卡和重复反查；已有网卡会在下次直接复用并继续EIP绑定/搭建。）",
                        )
                    signature = normalize_failure_signature(last_error)
                    repeat_count = repeat_count + 1 if signature and signature == last_signature else 1
                    last_signature = signature
                    self.call_ui(self.update_region_row, plan, status=f"失败: {last_error}")
                    self.after(0, self.log, f"[{plan.label}] 第 {attempt}/{REGION_RETRY_ATTEMPTS} 次失败: {last_error}")
                return plan, None, "purchase", last_error

            self.after(0, self.log, f"步骤: 首轮调用自动购买多地区批处理 地区={[plan.label for plan in runnable]}")
            try:
                initial_results, initial_failures = self._run_huawei_wwwroot_purchase_batch(
                    client,
                    runnable,
                    pairs,
                    password,
                    flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    x1_ten_ip_enabled=x1_ten_ip_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                    on_demand_billing_enabled=on_demand_billing_enabled,
                )
            except Exception as exc:
                initial_results = {}
                initial_failures = {plan.key: str(exc) for plan in runnable}

            futures: Dict[concurrent.futures.Future[Tuple[RegionPlan, Optional[RegionBuildResult], str, str]], RegionPlan] = {}
            retry_plans = [plan for plan in runnable if plan.key not in initial_results]
            worker_count = max(1, min(MAX_WORKERS, len(initial_results) + len(retry_plans)))
            with concurrent.futures.ThreadPoolExecutor(max_workers=worker_count) as executor:
                for result in initial_results.values():
                    futures[executor.submit(build_region, result)] = result.plan
                for plan in retry_plans:
                    error_text = initial_failures.get(plan.key, "未知错误")
                    purchase_failures[plan.key] = error_text
                    self.call_ui(self.update_region_row, plan, status=f"失败，准备独立重试: {error_text}")
                    futures[executor.submit(retry_region, plan, error_text)] = plan

                for future in concurrent.futures.as_completed(futures):
                    plan = futures[future]
                    try:
                        plan, result, stage, error_text = future.result()
                    except Exception as exc:
                        result = None
                        stage = "build" if do_build_after_purchase else "purchase"
                        error_text = localize_region_tokens(str(exc))
                    if result:
                        purchase_success_count += 1
                        purchase_failures.pop(plan.key, None)
                        build_failures.pop(plan.key, None)
                        purchase_results[plan.key] = result
                        if not do_build_after_purchase:
                            # 纯购买只产生云资源，不代表 L2TP/Socks5 已搭建成功。
                            # 保留资源模型供后续“搭建选择地区”复用，但禁止按默认值伪造连接结果。
                            setattr(result, "_huawei_require_script_result_output", True)
                        self.last_results = [item for item in self.last_results if item.plan.key != result.plan.key]
                        self.last_results.append(result)
                        if do_build_after_purchase:
                            pipeline_build_success_count += 1
                            unavailable_ip_count += len(getattr(result, "unavailable_public_ips", []))
                            self.call_ui(self.update_region_row, plan, status="购买并搭建完成", server_ip=result.server_ip, server_id=result.server_id)
                        else:
                            self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=result.server_ip, server_id=result.server_id)
                        self.after(0, self.append_result_blocks, result)
                    else:
                        if stage == "build":
                            build_failures[plan.key] = error_text
                            self.call_ui(self.update_region_row, plan, status=f"搭建失败: {error_text}")
                        else:
                            purchase_failures[plan.key] = error_text
                            self.call_ui(self.update_region_row, plan, status=f"失败: {error_text}")
                        self.after(0, self.log, f"[{plan.label}] 失败: {error_text}")

            self.after(0, self.log, f"步骤: 购买阶段完成 成功 {purchase_success_count}/{len(runnable)}")

            build_success_count = pipeline_build_success_count if do_build_after_purchase else 0
            if do_build_after_purchase:
                self.after(0, self.log, f"步骤: 流水线购买并搭建完成 成功 {build_success_count}/{len(runnable)}")

            success_count = build_success_count if do_build_after_purchase else purchase_success_count
            total_count = len(runnable)
            fail_count = len(purchase_failures) + len(build_failures)
            fail_note = f"，失败 {fail_count} 个" if fail_count else ""
            unavailable_note = f"，SOCKS5不可用IP {unavailable_ip_count} 个" if unavailable_ip_count else ""
            final_log = f"步骤: {action_name}任务结束，成功 {success_count}/{total_count}"
            if fail_count:
                final_log += f"，失败 {fail_count}"
            if unavailable_ip_count:
                final_log += f"，SOCKS5不可用IP {unavailable_ip_count}"
            self.after(0, self.log, final_log)
            final_popup = (
                f"{action_name}任务结束，成功 {success_count}/{total_count} 个地区{fail_note}{unavailable_note}。"
                + self._huawei_task_popup_details(
                    plans,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                    on_demand_billing_enabled=on_demand_billing_enabled,
                    purchase_enabled=True,
                    build_enabled=do_build_after_purchase,
                    ros_build_enabled=ros_build_enabled,
                )
            )
            final_popup_message = self._queue_result_message_or_popup(final_popup)
            self.after(
                100,
                lambda p=list(plans), message=final_popup_message: self.refresh_resource_views_for_plans(
                    p,
                    context="purchase",
                    final_popup_message=message,
                ),
            )

        self.run_task(action_name, worker, context="purchase", regions=[plan.region for plan in plans])

    def _parse_saved_results(self) -> Dict[str, RegionBuildResult]:
        parsed: Dict[str, RegionBuildResult] = {}
        legacy_text = getattr(self, "result_text", None)
        text = legacy_text.get("1.0", tk.END) if legacy_text is not None else ""
        label_pattern = "|".join(re.escape(plan.label) for plan in REGION_PLANS)
        for block in re.split(rf"(?=^(?:地区:)?(?:{label_pattern})\s*$|^地区:)", text, flags=re.MULTILINE):
            block = block.strip()
            if not block:
                continue

            def field(label: str) -> str:
                match = re.search(rf"^{re.escape(label)}:(.*)$", block, flags=re.MULTILINE)
                return match.group(1).strip() if match else ""

            label = field("地区")
            if not label:
                first_line = next((line.strip() for line in block.splitlines() if line.strip()), "")
                if first_line in {plan.label for plan in REGION_PLANS}:
                    label = first_line
            plan = next((item for item in REGION_PLANS if item.label == label or item.key == label), None)
            if not plan:
                continue
            server_ip = field("服务器IP")
            server_id = field("服务器ID")
            password = field("服务器密码") or self.password_var.get()
            volume_match = re.search(r"^系统盘:.*\(([A-Z0-9]+)\)", block, flags=re.MULTILINE)
            root_volume_type = volume_match.group(1) if volume_match else ROOT_VOLUME_TYPE
            ip_txt_match = re.search(r"cat > ip\.txt <<'EOF'\s*\n(.*?)\nEOF", block, flags=re.DOTALL)
            ip_txt = ip_txt_match.group(1).strip() + "\n" if ip_txt_match else ""

            public_ips: List[str] = []
            in_socks = False
            for line in block.splitlines():
                stripped = line.strip()
                if stripped in {"SK5:", "IP/端口/用户名/密码", "IP/端口/账号/密码"}:
                    in_socks = True
                    continue
                if stripped.startswith(("服务器ID:", "服务器IP:", "服务器密码:", "系统盘:", "cat > ip.txt")):
                    in_socks = False
                if not in_socks:
                    continue
                public_ip = stripped.split("/", 1)[0].strip()
                if is_public_ip(public_ip) and public_ip not in public_ips:
                    public_ips.append(public_ip)
            if not public_ips and is_public_ip(server_ip):
                public_ips.append(server_ip)
            if server_ip and ip_txt:
                parsed[plan.key] = RegionBuildResult(
                    plan=plan,
                    server_id=server_id,
                    server_ip=server_ip,
                    password=password,
                    ip_txt=ip_txt,
                    public_ips=public_ips,
                    root_volume_type=root_volume_type,
                    script_exit_code=-1,
                )
        return parsed

    @staticmethod
    def _result_from_autobuy_ready_summary(
        plan: RegionPlan,
        summary: Optional[Dict[str, str]],
        fallback_password: str,
    ) -> Optional[RegionBuildResult]:
        if not summary:
            return None
        server_ip = str(summary.get("server_ip") or "").strip()
        ip_txt = str(summary.get("ip_txt") or "")
        if not server_ip or server_ip.upper() == "N/A" or not ip_txt.strip():
            return None
        public_ips = [server_ip] if is_public_ip(server_ip) else []
        return RegionBuildResult(
            plan=plan,
            server_id=str(summary.get("server_id") or "").strip(),
            server_ip=server_ip,
            password=str(summary.get("password") or "").strip() or fallback_password,
            ip_txt=ip_txt,
            public_ips=public_ips,
            root_volume_type=ROOT_VOLUME_TYPE,
            script_exit_code=-1,
        )

    @classmethod
    def _public_ips_from_remote_output(cls, output_text: str) -> List[str]:
        public_ips: List[str] = []
        l2tp_output, socks_output = cls._script_result_sections(output_text)
        for line in socks_output.splitlines()[1:]:
            parts = line.split("/", 3)
            if len(parts) == 4 and is_public_ip(parts[0]) and parts[0] not in public_ips:
                public_ips.append(parts[0])
        for line in l2tp_output.splitlines()[1:]:
            parts = line.split("/")
            if len(parts) >= 5 and is_public_ip(parts[4]) and parts[4] not in public_ips:
                public_ips.append(parts[4])
        return public_ips

    @staticmethod
    def _clean_script_output_line(line: str) -> str:
        text = str(line or "").replace("\r", "").strip()
        return re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)

    @staticmethod
    def _script_result_data_line(line: str, section: str) -> str:
        text = HuaweiAutoBuyApp._clean_script_output_line(line)
        if not text:
            return ""
        if text.startswith("hid_"):
            return ""
        if set(text) <= {"-", "="}:
            return ""
        if any(
            token in text
            for token in (
                "免责声明",
                "切勿用于违法行为",
                "仅可作为技术交流",
                "一切行为造成的后果",
                "如有需要更改",
            )
        ):
            return ""
        text = text.replace("对应出口IP:", "").replace("对应出口IP：", "")
        text = text.replace("出口IP:", "").replace("出口IP：", "")
        parts = [part.strip() for part in text.split("/")]
        if section == "l2tp":
            if len(parts) < 5:
                return ""
            try:
                entry_ip = ipaddress.ip_address(parts[0])
                exit_ip = ipaddress.ip_address(parts[4])
            except ValueError:
                return ""
            if entry_ip.version != 4 or exit_ip.version != 4:
                return ""
            return "/".join([str(entry_ip), parts[1], parts[2], parts[3], str(exit_ip)])
        if section == "socks":
            if len(parts) < 4:
                return ""
            try:
                endpoint_ip = ipaddress.ip_address(parts[0])
            except ValueError:
                return ""
            if endpoint_ip.version != 4:
                return ""
            if not parts[1].isdigit():
                return ""
            return "/".join([str(endpoint_ip), *parts[1:4]])
        return ""

    @classmethod
    def _sanitize_script_result_section(cls, selected: Sequence[str], section: str) -> str:
        header = L2TP_RESULT_HEADER if section == "l2tp" else SOCKS5_RESULT_HEADER
        rows: List[str] = []
        for line in selected:
            row = cls._script_result_data_line(line, section)
            if row:
                rows.append(row)
        if not rows:
            return ""
        return "\n".join([header, *rows])

    @classmethod
    def _script_result_sections(cls, output_text: str) -> Tuple[str, str]:
        lines = [
            cls._clean_script_output_line(line)
            for line in str(output_text or "").replace("\r", "\n").splitlines()
        ]
        lines = [line for line in lines if line]
        l2tp_output = cls._sanitize_script_result_section(lines, "l2tp")
        socks_output = cls._sanitize_script_result_section(lines, "socks")
        return l2tp_output, socks_output

    @classmethod
    def _apply_script_result_outputs(cls, result: RegionBuildResult, output_text: str) -> None:
        l2tp_output, socks_output = cls._script_result_sections(output_text)
        l2tp_rows = l2tp_output.splitlines()[1:] if l2tp_output else []
        socks_rows = socks_output.splitlines()[1:] if socks_output else []
        if result.public_ips and (l2tp_rows or socks_rows):
            rebuilt_l2tp, rebuilt_socks = cls._rebuild_huawei_result_rows(
                l2tp_rows,
                socks_rows,
                result.public_ips,
            )
            if rebuilt_l2tp:
                l2tp_output = "\n".join([L2TP_RESULT_HEADER, *rebuilt_l2tp])
            if rebuilt_socks:
                socks_output = "\n".join([SOCKS5_RESULT_HEADER, *rebuilt_socks])
        if l2tp_output:
            setattr(result, "_huawei_script_l2tp_output", l2tp_output)
        if socks_output:
            setattr(result, "_huawei_script_socks_output", socks_output)

    @staticmethod
    def _reinstall_job_log_text(message: str) -> str:
        progress_match = re.search(r"进度\s*([0-9]+)", str(message or ""))
        if progress_match:
            return f"重装中，进度 {progress_match.group(1)}。"
        return "重装中。"

    def _available_results_by_key(self) -> Dict[str, RegionBuildResult]:
        results = self._parse_saved_results()
        for result in self.last_results:
            results[result.plan.key] = result
        return results

    def start_build_selected_regions(self) -> None:
        try:
            plans = self.selected_plans()
            selected_server_items = self.selected_region_server_items()
            if selected_server_items:
                selected_regions = {
                    str(item.get("region") or "")
                    for item in selected_server_items
                    if str(item.get("region") or "")
                }
                plans = [plan for plan in REGION_PLANS if plan.region in selected_regions]
            ssh_port = self.parse_ssh_port()
            ssh_user = self.ssh_user_var.get().strip() or DEFAULT_SSH_USER
            fallback_password = self.password_var.get()
            flavor_mode = self._selected_flavor_mode_or_warn()
            x1_flavor_enabled = bool(self.x1_flavor_var.get())
            configured_x1_ten_ip_enabled = bool(
                flavor_mode in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED}
                and self._x1_ten_ip_purchase_enabled(x1_flavor_enabled)
            )
            purchase_eip_quota = self._purchase_eip_quota_for_task(flavor_mode)
            if selected_server_items:
                purchase_eip_quota = 0
            if purchase_eip_quota:
                x1_flavor_enabled = True
            x1_ten_ip_enabled = bool(configured_x1_ten_ip_enabled or purchase_eip_quota)
            ros_build_enabled = self._ros_build_enabled()
            ros_config = self._current_ros_config()
            task_connection_env = {} if ros_build_enabled else self._task_connection_env()
            if flavor_mode:
                plans = [
                    self._effective_plan_for_flavor_mode(
                        plan,
                        flavor_mode,
                        x1_ten_ip_enabled=x1_ten_ip_enabled,
                    )
                    for plan in plans
                ]
            target_set_counts = {
                plan.key: self._resource_set_count_for_eip_quota(plan, purchase_eip_quota)
                for plan in plans
            }
            self._ensure_result_scope_for_current_credentials()
            initial_results_by_key = self._available_results_by_key()
            missing_plans = [] if selected_server_items else [
                plan for plan in plans if plan.key not in initial_results_by_key
            ]
            client = self.get_client() if selected_server_items or missing_plans or purchase_eip_quota else None
        except Exception as exc:
            self.show_error(str(exc))
            return
        if not flavor_mode:
            return
        if ros_build_enabled and flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            self.show_info("Ros镜像(右键自定义|L2TP)暂不支持20规格，请选择1vCPUs | 1GiB或2vCPUs | 2GiB。")
            return
        if not plans:
            self.show_info("请先选择一个或多个地区。")
            return
        script_path = resource_path(SCRIPT_FILENAME)
        missing_note = ""
        if selected_server_items:
            missing_note = f"\n\n将仅扫描并搭建已勾选的 {len(selected_server_items)} 台云服务器。"
        elif purchase_eip_quota:
            missing_note = (
                "\n\n将按云上实际资源扫描并搭建："
                + "，".join(
                    f"{plan.label}{target_set_counts[plan.key]}套ECS"
                    for plan in plans
                )
            )
        elif missing_plans:
            missing_note = f"\n\n本地无购买结果的地区会先扫描云上已有资源：{', '.join(plan.label for plan in missing_plans)}"
        _remote_command, command_label, _connection_env = self._remote_command_for_flavor_mode(
            flavor_mode,
            task_connection_env,
        )
        if ros_build_enabled:
            ssh_action = "只会 SSH 上传CHR镜像，等待RouterOS启动后自动上传并导入rsc，不会新建云资源。"
        else:
            ssh_action = f"只会 SSH 上传 /root/ip.txt 并执行{command_label}，不会新建云资源。"
        selected_ecs_counts: Dict[str, int] = {}
        for item in selected_server_items:
            region = str(item.get("region") or "").strip()
            if region:
                selected_ecs_counts[region] = selected_ecs_counts.get(region, 0) + 1
        selected_ecs_region_labels = [
            f"{plan.label}（{selected_ecs_counts[plan.region]}台ECS）"
            for plan in plans
            if selected_ecs_counts.get(plan.region)
        ]
        if not self.ask_yes_no(
            action_confirm_message(
                (
                    f"确认开始搭建 {len(selected_server_items)} 台云服务器"
                    if selected_server_items
                    else f"确认开始搭建 {len(plans)} 个选择地区"
                ),
                regions=(
                    selected_ecs_region_labels
                    if selected_server_items
                    else [plan.label for plan in plans]
                ),
                details=[
                    *self._huawei_build_confirm_details(
                        build_enabled=True,
                        ros_build_enabled=ros_build_enabled,
                        ros_config=ros_config,
                    ),
                    self._huawei_task_popup_details(
                        plans,
                        flavor_mode=flavor_mode,
                        purchase_enabled=False,
                        build_enabled=True,
                        ros_build_enabled=ros_build_enabled,
                    ),
                    ssh_action,
                    missing_note.strip(),
                ],
            )
        ):
            return
        self._set_log_context("purchase")

        def worker() -> None:
            self._mark_one_time_build_config_reset(build_used=True, ros_build_enabled=ros_build_enabled)
            if ros_build_enabled:
                self._start_ros_chr_prefetch()
            if selected_server_items:
                assert client is not None
                self._run_build_selected_server_items(
                    client,
                    selected_server_items,
                    plans,
                    fallback_password=fallback_password,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    ssh_port=ssh_port,
                    ssh_user=ssh_user,
                    script_path=script_path,
                    ros_build_enabled=ros_build_enabled,
                    ros_config=ros_config,
                    task_connection_env=task_connection_env,
                )
                return
            if purchase_eip_quota:
                assert client is not None
                self._run_build_eip_quota_task(
                    client,
                    plans,
                    target_set_counts,
                    fallback_password=fallback_password,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    ssh_port=ssh_port,
                    ssh_user=ssh_user,
                    script_path=script_path,
                    ros_build_enabled=ros_build_enabled,
                    ros_config=ros_config,
                    task_connection_env=task_connection_env,
                )
                return
            results_by_key = dict(initial_results_by_key)
            discovery_failures: Dict[str, str] = {}
            self.after(0, self.log, f"步骤: 搭建启动 地区={[plan.label for plan in plans]}")
            if missing_plans:
                assert client is not None
                self.after(0, self.log, f"步骤: 本地无结果地区开始扫描云上已有资源 {[plan.label for plan in missing_plans]}")
                prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
                self._wait_region_project_warmup()
                pairs = client.selected_project_pairs(missing_plans)
                self.region_projects.update(pairs)

                def discover_existing(plan: RegionPlan) -> Tuple[RegionPlan, Optional[RegionBuildResult], str]:
                    pair = pairs.get(plan.region)
                    if not pair:
                        return plan, None, "未找到项目ID"

                    def emit(message: str, current_plan: RegionPlan = plan) -> None:
                        self.after(0, self.log, f"[{current_plan.label}] {localize_region_tokens(message)}")

                    self.call_ui(self.update_region_row, plan, project_id=pair.project_id, status="扫描旧资源")
                    try:
                        result = self._find_reusable_region_result(
                            client,
                            plan,
                            pair,
                            prefix,
                            fallback_password,
                            emit,
                            strict_aux_ips=False,
                            flavor_mode=flavor_mode,
                            x1_flavor_enabled=x1_flavor_enabled,
                        )
                    except Exception as exc:
                        return plan, None, str(exc)
                    if result:
                        return plan, result, ""
                    return plan, None, "云上未发现满足搭建条件的已有资源"

                scan_workers = min(MAX_WORKERS, len(missing_plans))
                self.after(0, self.log, f"步骤: 并发扫描已有资源 并发数={scan_workers} 地区={[plan.label for plan in missing_plans]}")
                with concurrent.futures.ThreadPoolExecutor(max_workers=scan_workers) as executor:
                    future_map = {executor.submit(discover_existing, plan): plan for plan in missing_plans}
                    for future in concurrent.futures.as_completed(future_map):
                        plan, result, reason = future.result()
                        if result:
                            results_by_key[plan.key] = result
                        else:
                            discovery_failures[plan.key] = reason
                            self.call_ui(self.update_region_row, plan, status=reason)
                            self.after(0, self.log, f"[{plan.label}] {reason}")

            runnable = [plan for plan in plans if plan.key in results_by_key]
            if not runnable:
                raise ValueError("选择地区没有可搭建资源：输出框无结果，云上也未发现满足条件的已有资源。")
            success_count = 0
            failures: Dict[str, str] = dict(discovery_failures)
            unavailable_ip_count = 0
            build_workers = min(MAX_WORKERS, len(runnable))
            self.after(0, self.log, f"步骤: 开始并发SSH搭建 并发数={build_workers} 地区={[plan.label for plan in runnable]}")
            with concurrent.futures.ThreadPoolExecutor(max_workers=build_workers) as executor:
                future_map = {
                    executor.submit(
                        self._build_existing_region_with_retry,
                        results_by_key[plan.key],
                        ssh_port,
                        ssh_user,
                        fallback_password,
                        script_path,
                        flavor_mode,
                        ros_build_enabled,
                        ros_config,
                        task_connection_env=task_connection_env,
                    ): plan
                    for plan in runnable
                }
                for future in concurrent.futures.as_completed(future_map):
                    plan = future_map[future]
                    try:
                        result = future.result()
                        success_count += 1
                        unavailable_ip_count += len(getattr(result, "unavailable_public_ips", []))
                        self.last_results = [item for item in self.last_results if item.plan.key != result.plan.key]
                        self.last_results.append(result)
                        self.after(0, self.append_result_blocks, result)
                    except Exception as exc:
                        failures[plan.key] = str(exc)
                        self.call_ui(self.update_region_row, plan, status=f"搭建失败: {exc}")
                        self.after(0, self.log, f"[{plan.label}] 搭建失败: {exc}")
            fail_note = f"，失败 {len(failures)} 个" if failures else ""
            unavailable_note = f"，SOCKS5不可用IP {unavailable_ip_count} 个" if unavailable_ip_count else ""
            self.after(0, self.log, f"步骤: 搭建完成 成功 {success_count}/{len(plans)}")
            final_log = f"步骤: 搭建任务结束，成功 {success_count}/{len(plans)}"
            if failures:
                final_log += f"，失败 {len(failures)}"
            if unavailable_ip_count:
                final_log += f"，SOCKS5不可用IP {unavailable_ip_count}"
            self.after(0, self.log, final_log)
            final_popup = (
                f"搭建任务结束，成功 {success_count}/{len(plans)} 个地区{fail_note}{unavailable_note}。"
                + self._huawei_task_popup_details(
                    plans,
                    flavor_mode=flavor_mode,
                    purchase_enabled=False,
                    build_enabled=True,
                    ros_build_enabled=ros_build_enabled,
                )
            )
            final_popup_message = self._queue_result_message_or_popup(final_popup)
            self.after(
                100,
                lambda p=list(plans), message=final_popup_message: self.refresh_resource_views_for_plans(
                    p,
                    context="purchase",
                    final_popup_message=message,
                ),
            )

        self.run_task("搭建", worker, context="purchase", regions=[plan.region for plan in plans])

    def _build_existing_region_with_retry(
        self,
        result: RegionBuildResult,
        ssh_port: int,
        ssh_user: str,
        fallback_password: str,
        script_path: str,
        flavor_mode: str = FLAVOR_MODE_FIXED,
        ros_build: bool = False,
        ros_config: Optional[RosBuildConfig] = None,
        task_connection_env: Optional[Dict[str, str]] = None,
    ) -> RegionBuildResult:
        plan = result.plan
        last_error: Optional[BaseException] = None
        max_attempts = 1 if ros_build else BUILD_REGION_RETRY_ATTEMPTS
        # 中文注释：上海首次新购后偶发 ECS 已绑定公网 IP，但 SSH/密码初始化比其他地区更慢。
        # 只对本轮自动购买出来的上海常规5IP资源追加一次就绪等待，避免影响已有资源复建、ROS、20IP和其他地区。
        shanghai_new_purchase_retry = (
            not ros_build
            and flavor_mode != FLAVOR_MODE_WAWACLOUD_20
            and plan.key == "shanghai"
            and bool(getattr(result, "_huawei_autobuy_new_purchase", False))
        )
        if shanghai_new_purchase_retry:
            max_attempts = max(max_attempts, SHANGHAI_NEW_PURCHASE_BUILD_RETRY_ATTEMPTS)
        for attempt in range(1, max_attempts + 1):
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")
            if attempt > 1:
                self.after(0, self.log, f"[{plan.label}] 步骤: 搭建第 {attempt}/{max_attempts} 次重试")
                self.call_ui(self.update_region_row, plan, status=f"搭建重试 {attempt}/{max_attempts}")
                retry_delay = (
                    SHANGHAI_NEW_PURCHASE_BUILD_RETRY_DELAY_SECONDS
                    if shanghai_new_purchase_retry and attempt > BUILD_REGION_RETRY_ATTEMPTS
                    else BUILD_REGION_RETRY_DELAY_SECONDS
                )
                if retry_delay > BUILD_REGION_RETRY_DELAY_SECONDS:
                    self.after(0, self.log, f"[{plan.label}] 新购资源SSH暂未稳定，延迟 {retry_delay}s 后追加重试。")
                time.sleep(retry_delay)
            try:
                return self._build_existing_region(
                    result,
                    ssh_port,
                    ssh_user,
                    fallback_password,
                    script_path,
                    flavor_mode,
                    ros_build,
                    ros_config,
                    task_connection_env=task_connection_env,
                )
            except Exception as exc:
                last_error = exc
                if attempt >= max_attempts or self.stop_event.is_set():
                    raise
                if (
                    shanghai_new_purchase_retry
                    and attempt >= BUILD_REGION_RETRY_ATTEMPTS
                    and not self._is_new_purchase_ssh_not_ready(exc)
                ):
                    raise
                self.after(0, self.log, f"[{plan.label}] 搭建第 {attempt}/{max_attempts} 次失败，准备重试: {localize_region_tokens(str(exc))}")
                self.call_ui(self.update_region_row, plan, status=f"搭建失败待重试: {exc}")
        raise RuntimeError(f"搭建失败: {last_error}")

    def _build_existing_region(
        self,
        result: RegionBuildResult,
        ssh_port: int,
        ssh_user: str,
        fallback_password: str,
        script_path: str,
        flavor_mode: str = FLAVOR_MODE_FIXED,
        ros_build: bool = False,
        ros_config: Optional[RosBuildConfig] = None,
        task_connection_env: Optional[Dict[str, str]] = None,
    ) -> RegionBuildResult:
        plan = result.plan

        def emit(message: str) -> None:
            self.after(0, self.log, f"[{plan.label}] {localize_region_tokens(message)}")

        if self.stop_event.is_set():
            raise RuntimeError("用户请求停止，跳过后续步骤。")
        password = result.password or fallback_password
        if not password:
            raise ValueError("缺少服务器登录密码。")
        self.region_stage[plan.key] = "ssh"
        self.call_ui(self.update_region_row, plan, status="等待SSH", server_ip=result.server_ip, server_id=result.server_id)
        emit("步骤: 等待SSH")
        if ros_build:
            if flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
                raise RuntimeError("Ros镜像(右键自定义|L2TP)暂不支持20规格。")
            return self._build_existing_region_ros(result, ssh_port, ssh_user, password, emit, ros_config or self._current_ros_config())
        remote_command, command_label, connection_env = self._remote_command_for_flavor_mode(
            flavor_mode,
            task_connection_env,
        )
        emit("步骤: 搭建方式=" + command_label)
        ssh_host, used_password, exit_code, output_text = self._ssh_upload_and_run_any_host(
            [result.server_ip, *result.public_ips],
            ssh_port,
            ssh_user,
            password,
            result.ip_txt,
            script_path,
            emit,
            remote_command,
        )
        if used_password != password:
            result.password = used_password
        emit(f"步骤: SSH搭建入口 {ssh_host}")
        result.script_exit_code = exit_code
        self._apply_connection_env_to_result(result, connection_env)
        script_public_ips = self._public_ips_from_remote_output(output_text)
        if script_public_ips:
            result.public_ips = script_public_ips
            if not result.server_ip or result.server_ip not in script_public_ips:
                result.server_ip = script_public_ips[0]
        self._apply_script_result_outputs(result, output_text)
        if self._remote_command_requires_result_markers(remote_command):
            setattr(result, "_huawei_require_script_result_output", True)
        result.available_public_ips = list(result.public_ips)
        result.unavailable_public_ips = []
        emit("步骤: 已按远程脚本退出结果完成搭建，不再执行最后 SOCKS5 外部检测。")
        status = "搭建完成"
        self.region_stage[plan.key] = "done"
        self.call_ui(self.update_region_row, plan, status=status, server_ip=result.server_ip, server_id=result.server_id)
        emit(f"步骤: 完成地区 {plan.key}")
        return result

    def _build_existing_region_ros(
        self,
        result: RegionBuildResult,
        ssh_port: int,
        ssh_user: str,
        password: str,
        emit: Callable[[str], None],
        ros_config: RosBuildConfig,
    ) -> RegionBuildResult:
        plan = result.plan
        # 中文注释：热更新时 HUAWEI.app 可能先于 ros_builder 重载，这里再强制重载一次，
        # 避免继续使用旧模板生成逻辑，把未替换 VLAN/MAC 的原始 rsc 上传到 RouterOS。
        ros_builder = importlib.reload(importlib.import_module("HUAWEI.ros_builder"))
        l2tp_psk = ros_builder.ros_l2tp_psk(ros_config)
        chr_path = ros_builder.ensure_chr_image(emit)
        rsc_name, rsc_text = ros_builder.render_ros_rsc(plan, result.ip_txt, ros_config)
        self._validate_ros_rsc_text_before_upload(plan, result.ip_txt, rsc_name, rsc_text, ros_config, ros_builder)
        emit(f"步骤: ROS镜像搭建(L2TP)，镜像={chr_path.name}，模板={rsc_name}")
        emit(f"步骤: ROS rsc已生成并校验: {rsc_name}，连接密码={ros_config.ppp_password}，L2TP密钥={l2tp_psk}")
        public_hosts = [result.server_ip, *result.public_ips]
        ros_host = self._detect_existing_routeros_host(public_hosts, emit)
        if ros_host:
            emit(f"步骤: 检测到现有RouterOS，跳过CHR写盘，继续导入rsc，入口={ros_host}")
        else:
            linux_host, used_password = self._install_ros_image_any_host(
                public_hosts,
                ssh_port,
                ssh_user,
                password,
                chr_path,
                emit,
                # 中文注释：认证失败就是密码不匹配，不能把同一个错误密码连试24次；SSH端口等待已在 wait_for_ssh_hosts 里处理。
                auth_retry_attempts=2,
            )
            if used_password != password:
                result.password = used_password
            emit(f"步骤: CHR镜像写入已触发，原SSH入口={linux_host}，等待RouterOS启动")
            ros_host = self._wait_for_routeros_host([linux_host, *public_hosts], emit)
        emit(f"步骤: RouterOS已就绪，入口={ros_host}")
        try:
            self._upload_routeros_rsc(ros_host, rsc_name, rsc_text, emit, expected_l2tp_psk=l2tp_psk)
            import_output = self._run_routeros_import(ros_host, rsc_name, emit)
            if self._is_routeros_access_problem(import_output):
                raise RouterOsAccessError(self._routeros_access_problem_message(ros_host, import_output))
        except RouterOsAccessError as exc:
            # 中文注释：CHR刚暴露admin/admin时可能被公网扫描改权限，必须重装回Debian后重新写盘。
            emit(f"步骤: RouterOS管理权限异常，准备自动重装系统后重新执行ROS搭建: {exc}")
            self._recover_routeros_by_reinstall(result, password, emit)
            linux_host, used_password = self._install_ros_image_any_host(
                public_hosts,
                ssh_port,
                ssh_user,
                password,
                chr_path,
                emit,
            )
            if used_password != password:
                result.password = used_password
            emit(f"步骤: 重装后已重新写入CHR镜像，原SSH入口={linux_host}，等待RouterOS启动")
            ros_host = self._wait_for_routeros_host([linux_host, *public_hosts], emit)
            emit(f"步骤: RouterOS已重新就绪，入口={ros_host}")
            self._upload_routeros_rsc(ros_host, rsc_name, rsc_text, emit, expected_l2tp_psk=l2tp_psk)
            import_output = self._run_routeros_import(ros_host, rsc_name, emit)
            if self._is_routeros_access_problem(import_output):
                raise RouterOsAccessError(self._routeros_access_problem_message(ros_host, import_output))
        if self._routeros_import_output_has_failure(import_output):
            raise RuntimeError("RouterOS导入rsc返回异常，请查看实时日志。")
        self._validate_routeros_l2tp_import(import_output, rsc_name, emit)
        self._wait_for_tcp_port(ros_host, ROS_WINBOX_PORT, 60, 3, emit, "Winbox")
        emit(f"步骤: RouterOS导入完成，Winbox端口={ROS_WINBOX_PORT}，用户=cao521，密码=cao520，L2TP密钥={l2tp_psk}")
        result.script_exit_code = 0
        result.available_public_ips = list(result.public_ips)
        result.unavailable_public_ips = []
        # 中文注释：ROS镜像模式只配置L2TP/RouterOS，不生成SOCKS5服务；结果渲染时禁止写入SK5框。
        setattr(result, "_huawei_ros_l2tp_only", True)
        # 中文注释：ROS模板里的 PPP 连接密码可由右键配置临时改动，结果框必须同步显示本次真实写入 RouterOS 的密码。
        setattr(result, "_huawei_l2tp_username", ros_l2tp_username(ros_config))
        setattr(result, "_huawei_l2tp_password", ros_config.ppp_password or ROS_DEFAULT_PPP_PASSWORD)
        setattr(result, "_huawei_l2tp_psk", l2tp_psk)
        status = "ROS(L2TP)搭建完成"
        self.region_stage[plan.key] = "done"
        self.call_ui(self.update_region_row, plan, status=status, server_ip=result.server_ip, server_id=result.server_id)
        emit(f"步骤: 完成地区 {plan.key}")
        return result

    def _recover_routeros_by_reinstall(
        self,
        result: RegionBuildResult,
        password: str,
        emit: Callable[[str], None],
    ) -> None:
        plan = result.plan
        if not result.server_id:
            raise RouterOsAccessError(f"{plan.label} RouterOS权限异常，但缺少ECS ID，无法自动重装。")
        client = self.get_client()
        pair = self.region_projects.get(plan.region)
        if not pair:
            self._wait_region_project_warmup()
            pairs = client.selected_project_pairs([plan])
            pair = pairs.get(plan.region)
        if not pair:
            raise RouterOsAccessError(f"{plan.label} RouterOS权限异常，但未找到华为云项目ID，无法自动重装。")
        emit("检测到RouterOS疑似被扫/权限异常，自动提交重装系统。")
        reinstall_result = client.reinstall_server_with_cloud_init(pair, result.server_id, password)
        job_id = HuaweiCloudClient.job_id_from_create_response(reinstall_result)
        if not job_id:
            emit("重装任务已提交但未返回job_id，继续等待SSH恢复。")
        else:
            emit("重装任务已提交，重装中。")
            client.wait_job(pair, job_id, lambda message: emit(self._reinstall_job_log_text(message)), timeout=1200, interval=8)
        reinstall_hosts = unique_public_hosts([result.server_ip, *result.public_ips])
        self._wait_for_debian_after_routeros_reinstall(reinstall_hosts[:1], emit)
        emit("重装系统已确认恢复Debian/OpenSSH，继续ROS镜像写盘。")

    @staticmethod
    def _validate_ros_rsc_text_before_upload(
        plan: RegionPlan,
        ip_txt: str,
        filename: str,
        content: str,
        ros_config: RosBuildConfig,
        ros_builder: Any,
    ) -> None:
        # 中文注释：上传前做强校验，防止热更新或模板查找异常时误传桌面原始 rsc。
        rows = ros_builder.parse_ip_txt(ip_txt)
        expected_rows = 8 if filename == ros_builder.ROS_TEN_REGION_TEMPLATE else 4
        rows = rows[:expected_rows]
        expected_vid = ":local vid {" + ";".join(vlan_id for vlan_id, _mac, _ip in rows) + "}"
        expected_vmac = ":local vmac {" + ";".join(f'"{mac}"' for _vlan_id, mac, _ip in rows) + "}"
        required_markers = {
            "当前地区VLAN": expected_vid,
            "当前地区MAC": expected_vmac,
            "L2TP自检": "ROS_CHECK_EXPECTED_USERS=",
            "L2TP预共享密钥": f"ipsec-secret={ros_builder.ros_l2tp_psk(ros_config)}",
            "本次连接密码": f"password={ros_config.ppp_password} remote-address=",
        }
        missing = [label for label, marker in required_markers.items() if marker not in content]
        if missing:
            raise RuntimeError(
                f"{plan.label} ROS rsc生成异常，未通过上传前校验：{', '.join(missing)}；"
                "已停止上传，避免把旧模板导入RouterOS。"
            )

    def _install_ros_image_any_host(
        self,
        hosts: Sequence[str],
        port: int,
        username: str,
        password: str,
        chr_path: Path,
        emit: Callable[[str], None],
        *,
        auth_retry_attempts: int = 2,
    ) -> Tuple[str, str]:
        unique_hosts = unique_public_hosts(hosts)
        if not unique_hosts:
            raise TimeoutError("没有可用于ROS镜像写入的公网IP。")
        ready_hosts = wait_for_ssh_hosts(unique_hosts, port, emit, timeout=HUAWEI_SSH_PORT_WAIT_TIMEOUT)
        errors: List[str] = []
        for host in ready_hosts:
            for password_label, candidate_password in self._ssh_password_candidates(password):
                if self.stop_event.is_set():
                    raise RuntimeError("用户请求停止，跳过后续步骤。")
                ssh: Optional[paramiko.SSHClient] = None
                try:
                    emit(f"ROS镜像: SSH认证尝试 {host}:{port} ({password_label})")
                    ssh = self._ssh_connect_with_retry(
                        host,
                        port,
                        username,
                        candidate_password,
                        emit,
                        # 中文注释：这里每个密码最多试2次，认证失败马上换下一个密码；只把连接未就绪当成短重试。
                        max_attempts=max(1, min(2, auth_retry_attempts)),
                        retry_auth=False,
                    )
                    if self.stop_event.is_set():
                        raise RuntimeError("用户请求停止，跳过后续步骤。")
                    self._ssh_upload_local_file(ssh, chr_path, f"/root/{ROS_CHR_FILENAME}", emit)
                    if self.stop_event.is_set():
                        raise RuntimeError("用户请求停止，跳过后续步骤。")
                    command = (
                        f"cd /root && rm -f chr.img && "
                        f"gunzip -c {ROS_CHR_FILENAME} > chr.img && "
                        "sync && echo u > /proc/sysrq-trigger && "
                        # 中文注释：CHR写入后原系统盘已被覆盖，不能再依赖 reboot 二进制存在，直接用内核 sysrq 触发重启。
                        "dd if=chr.img bs=1024 of=/dev/vda; rc=$?; "
                        "sync; echo ROS_FLASH_DONE:$rc; "
                        "[ \"$rc\" -eq 0 ] && echo b > /proc/sysrq-trigger; "
                        "exit \"$rc\""
                    )
                    emit("ROS镜像: 开始写入 /dev/vda 并通过sysrq重启，SSH中断属于正常现象。")
                    try:
                        exit_code = self._ssh_exec_ros_flash_reboot(ssh, command, emit)
                    except Exception as exc:
                        emit(f"ROS镜像: SSH连接已中断，继续等待RouterOS启动: {exc}")
                        exit_code = 0
                    if exit_code != 0:
                        raise RuntimeError(f"CHR镜像写入命令退出码 {exit_code}")
                    return host, candidate_password
                except Exception as exc:
                    errors.append(f"{host}/{password_label}: {exc}")
                    if self._is_ssh_auth_failure(exc):
                        emit(f"ROS镜像: {host}:{port} ({password_label})认证失败，立即切换下一个密码/入口。")
                        continue
                    if self._is_ssh_connect_failure(exc):
                        continue
                    raise
                finally:
                    if ssh is not None:
                        try:
                            ssh.close()
                        except Exception:
                            pass
        raise RuntimeError("ROS镜像写入SSH失败: " + "；".join(errors[:8]))

    def _ssh_exec_ros_flash_reboot(self, ssh: paramiko.SSHClient, command: str, emit: Callable[[str], None]) -> int:
        _, stdout, stderr = ssh.exec_command(command, get_pty=True)
        channel = stdout.channel
        with self.ssh_channel_lock:
            self.ssh_channel = channel
        pending: List[str] = []
        last_flush = time.monotonic()
        output_tail = ""
        deadline = time.monotonic() + 180

        def flush(force: bool = False) -> None:
            nonlocal last_flush
            if not pending:
                return
            if not force and len(pending) < 8 and time.monotonic() - last_flush < 0.4:
                return
            emit("\n".join(pending))
            pending.clear()
            last_flush = time.monotonic()

        def append_output(data: str) -> Optional[int]:
            nonlocal output_tail
            if not data:
                return None
            text = data.replace("\r", "\n").rstrip()
            if len(text) > 3000:
                text = "...输出过长，已截取末尾...\n" + text[-3000:]
            pending.append(text)
            output_tail = (output_tail + "\n" + text)[-12000:]
            flush()
            match = re.search(r"ROS_FLASH_DONE:(\d+)", output_tail)
            if match:
                return int(match.group(1))
            return None

        try:
            while time.monotonic() < deadline:
                if self.stop_event.is_set():
                    raise RuntimeError("用户请求停止，跳过后续步骤。")
                received = False
                if channel.recv_ready():
                    received = True
                    code = append_output(channel.recv(4096).decode("utf-8", errors="replace"))
                    if code is not None:
                        flush(force=True)
                        try:
                            channel.close()
                        except Exception:
                            pass
                        return code
                if channel.recv_stderr_ready():
                    received = True
                    code = append_output(channel.recv_stderr(4096).decode("utf-8", errors="replace"))
                    if code is not None:
                        flush(force=True)
                        try:
                            channel.close()
                        except Exception:
                            pass
                        return code
                if channel.exit_status_ready() and not channel.recv_ready() and not channel.recv_stderr_ready():
                    flush(force=True)
                    return channel.recv_exit_status()
                if channel.closed and not channel.recv_ready() and not channel.recv_stderr_ready():
                    flush(force=True)
                    if "records out" in output_tail and "copied" in output_tail:
                        emit("ROS镜像: SSH已断开，按CHR写盘完成处理，继续等待RouterOS启动。")
                        return 0
                    break
                if not received:
                    time.sleep(0.2)
            flush(force=True)
            if "records out" in output_tail and "copied" in output_tail:
                emit("ROS镜像: 已看到dd写盘完成但未收到退出码，继续等待RouterOS启动。")
                return 0
            raise TimeoutError("等待CHR写盘命令完成超时。")
        finally:
            with self.ssh_channel_lock:
                if self.ssh_channel is channel:
                    self.ssh_channel = None

    @staticmethod
    def _ssh_upload_local_file(
        ssh: paramiko.SSHClient,
        local_path: Path,
        remote_path: str,
        emit: Callable[[str], None],
    ) -> None:
        size_mb = local_path.stat().st_size / 1024 / 1024
        emit(f"上传文件: {local_path.name} -> {remote_path} ({size_mb:.1f}MB)")
        sftp = ssh.open_sftp()
        try:
            sftp.put(str(local_path), remote_path)
        finally:
            sftp.close()
        emit(f"上传完成: {local_path.name}")

    @staticmethod
    def _tcp_port_open(host: str, port: int, timeout: float = 3.0) -> bool:
        try:
            with socket.create_connection((host, port), timeout=timeout):
                return True
        except OSError:
            return False

    @staticmethod
    def _tcp_initial_banner(host: str, port: int, timeout: float = 2.0) -> str:
        try:
            with socket.create_connection((host, port), timeout=timeout) as sock:
                sock.settimeout(timeout)
                return sock.recv(128).decode("utf-8", errors="replace")
        except OSError:
            return ""

    def _wait_for_tcp_port(
        self,
        host: str,
        port: int,
        timeout_seconds: int,
        interval_seconds: int,
        emit: Callable[[str], None],
        label: str,
    ) -> bool:
        deadline = time.time() + timeout_seconds
        while time.time() < deadline:
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")
            if self._tcp_port_open(host, port):
                emit(f"{label}端口已就绪: {host}:{port}")
                return True
            time.sleep(interval_seconds)
        raise TimeoutError(f"等待{label}端口超时: {host}:{port}")

    @staticmethod
    def _ros_check_value(output: str, key: str) -> Optional[str]:
        matches = re.findall(rf"{re.escape(key)}=([^\s\x1b]+)", output)
        return matches[-1].strip().strip('";') if matches else None

    @staticmethod
    def _routeros_import_output_has_failure(output: str) -> bool:
        # 中文注释：RouterOS终端会夹带 system,error 登录失败日志，不能把这类系统日志误判为rsc导入失败。
        failure_patterns = (
            "failure:",
            "bad command name",
            "expected end of command",
            "syntax error",
            "input does not match",
            "no such item",
            "invalid value",
            "invalid command",
            "not permitted",
            "permission denied",
        )
        for raw_line in output.splitlines():
            line = raw_line.strip().lower()
            if not line:
                continue
            if "system,error" in line or "login failure" in line:
                continue
            if any(pattern in line for pattern in failure_patterns):
                return True
        return False

    def _validate_routeros_l2tp_import(self, output: str, filename: str, emit: Callable[[str], None]) -> None:
        expected_users = 10 if filename == "hw10L2.rsc" else 5
        expected_text = self._ros_check_value(output, "ROS_CHECK_EXPECTED_USERS")
        l2tp_enabled = (self._ros_check_value(output, "ROS_CHECK_L2TP_ENABLED") or "").lower()
        use_ipsec = (self._ros_check_value(output, "ROS_CHECK_L2TP_USE_IPSEC") or "").lower()
        ppp_count_text = self._ros_check_value(output, "ROS_CHECK_PPP_COUNT")
        nat_count_text = self._ros_check_value(output, "ROS_CHECK_SRCNAT_COUNT")
        if not all([expected_text, l2tp_enabled, use_ipsec, ppp_count_text, nat_count_text]):
            raise RuntimeError("RouterOS导入后未返回完整L2TP自检输出，不能确认自动导入成功。")
        try:
            expected_from_rsc = int(expected_text)
            ppp_count = int(ppp_count_text)
            nat_count = int(nat_count_text)
        except ValueError as exc:
            raise RuntimeError("RouterOS L2TP自检输出格式异常，请查看实时日志。") from exc
        expected_users = max(expected_users, expected_from_rsc)
        problems: List[str] = []
        if l2tp_enabled not in {"true", "yes"}:
            problems.append(f"L2TP服务未开启({l2tp_enabled})")
        if use_ipsec in {"", "false", "no", "none"}:
            problems.append(f"L2TP/IPsec未开启({use_ipsec or '空'})")
        if ppp_count < expected_users:
            problems.append(f"PPP用户数不足({ppp_count}/{expected_users})")
        if nat_count < expected_users:
            problems.append(f"NAT规则数不足({nat_count}/{expected_users})")
        if problems:
            raise RuntimeError("RouterOS L2TP自检失败: " + "；".join(problems))
        # 中文注释：ROS模式只有L2TP、PPP用户和NAT均确认后才允许进入“搭建完成”。
        emit(f"RouterOS L2TP自检通过: 用户={ppp_count}/{expected_users}，NAT={nat_count}/{expected_users}，IPsec={use_ipsec}")

    def _wait_for_routeros_host(self, hosts: Sequence[str], emit: Callable[[str], None]) -> str:
        unique_hosts = unique_public_hosts(hosts)
        ready_ports = [ROS_SSH_PORT, 21, 23, ROS_WINBOX_PORT, 8291, 80, 8728, 8729]
        deadline = time.time() + ROS_READY_TIMEOUT_SECONDS
        while time.time() < deadline:
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")
            for host in unique_hosts:
                open_ports = [port for port in ready_ports if self._tcp_port_open(host, port, timeout=1.5)]
                if open_ports:
                    ssh_banner = self._tcp_initial_banner(host, ROS_SSH_PORT, timeout=1.2).lower()
                    if self._is_linux_ssh_banner(ssh_banner):
                        # 中文注释：CHR写盘触发后，Linux服务可能还没完全重启，不能把旧Linux端口当RouterOS就绪。
                        emit(f"检测到Linux SSH仍在，继续等待RouterOS启动: {host} banner={ssh_banner.strip()[:80]}")
                        continue
                    emit(f"RouterOS端口已就绪: {host} ports={open_ports}")
                    return host
            emit("等待RouterOS启动中...")
            time.sleep(ROS_READY_INTERVAL_SECONDS)
        raise TimeoutError("等待RouterOS启动超时，未探测到RouterOS管理端口。")

    @staticmethod
    def _is_linux_ssh_banner(banner: str) -> bool:
        banner = (banner or "").lower()
        return "openssh" in banner or "debian" in banner or "ubuntu" in banner

    def _routeros_reinstall_host(self, hosts: Sequence[str]) -> str:
        unique_hosts = unique_public_hosts(hosts)
        for host in unique_hosts:
            banner = self._tcp_initial_banner(host, ROS_SSH_PORT, timeout=1.2)
            if self._is_linux_ssh_banner(banner):
                return ""
        for host in unique_hosts:
            if self._tcp_port_open(host, ROS_WINBOX_PORT, timeout=1.2) or self._tcp_port_open(host, 8291, timeout=1.2):
                return host
        return ""

    def _wait_for_debian_after_routeros_reinstall(
        self,
        hosts: Sequence[str],
        emit: Callable[[str], None],
        *,
        timeout_seconds: float = 180,
        interval_seconds: float = 3,
    ) -> str:
        unique_hosts = unique_public_hosts(hosts)
        if not unique_hosts:
            raise RuntimeError("重装Job已完成，但缺少公网IP，无法确认Debian/OpenSSH是否恢复。")
        emit("重装Job已完成，正在确认Debian/OpenSSH恢复。")
        deadline = time.monotonic() + max(0.1, timeout_seconds)
        last_wait_log = 0.0
        while time.monotonic() < deadline:
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，未继续ROS搭建。")
            for host in unique_hosts:
                banner = self._tcp_initial_banner(host, ROS_SSH_PORT, timeout=1.5)
                if self._is_linux_ssh_banner(banner):
                    emit(f"已确认Debian/OpenSSH恢复: {host} banner={banner.strip()[:80]}")
                    return host
            now = time.monotonic()
            if now - last_wait_log >= 15:
                emit("等待Debian/OpenSSH启动中...")
                last_wait_log = now
            time.sleep(min(max(0.1, interval_seconds), max(0.1, deadline - now)))
        routeros_host = self._routeros_reinstall_host(unique_hosts)
        if routeros_host:
            raise RuntimeError(
                f"重装Job已完成，但 {routeros_host} 仍检测到RouterOS/Winbox，"
                "未恢复Debian/OpenSSH，已停止后续ROS搭建。"
            )
        raise TimeoutError("重装Job已完成，但未检测到Debian/OpenSSH，已停止后续ROS搭建。")

    def _detect_existing_routeros_host(self, hosts: Sequence[str], emit: Callable[[str], None]) -> str:
        unique_hosts = unique_public_hosts(hosts)
        routeros_ports = [21, 23, ROS_WINBOX_PORT, 8291, 80, 8728, 8729]
        for host in unique_hosts:
            ssh_banner = self._tcp_initial_banner(host, ROS_SSH_PORT, timeout=1.2).lower()
            if self._is_linux_ssh_banner(ssh_banner):
                # 中文注释：只要已有入口是Debian，说明当前应写CHR；其它出口IP的残留端口不能抢判断。
                emit(f"检测到Linux SSH，按待写CHR处理: {host} banner={ssh_banner.strip()[:80]}")
                return ""
        for host in unique_hosts:
            open_ports = [port for port in routeros_ports if self._tcp_port_open(host, port, timeout=1.2)]
            if open_ports:
                # 中文注释：ROS重试时云服务器可能已被写成CHR，不能再按Debian SSH重复写盘。
                emit(f"检测到现有RouterOS端口: {host} ports={open_ports}")
                return host
        return ""

    def _upload_routeros_rsc(
        self,
        host: str,
        filename: str,
        content: str,
        emit: Callable[[str], None],
        *,
        expected_l2tp_psk: str = ROS_L2TP_PSK,
    ) -> None:
        try:
            ssh = self._routeros_ssh_connect(host, emit)
            try:
                sftp = ssh.open_sftp()
                try:
                    with sftp.file(filename, "w") as remote_file:
                        remote_file.write(content)
                    with sftp.file(filename, "r") as remote_file:
                        raw = remote_file.read()
                finally:
                    sftp.close()
                remote_text = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else str(raw)
                self._verify_uploaded_routeros_rsc(filename, remote_text, expected_l2tp_psk)
                emit(f"RouterOS已通过SFTP上传并校验: {filename}")
                return
            finally:
                ssh.close()
        except RouterOsAccessError:
            raise
        except Exception as exc:
            if self._is_routeros_access_problem(exc):
                raise RouterOsAccessError(self._routeros_access_problem_message(host, exc)) from exc
            emit(f"RouterOS SFTP上传失败，改用FTP: {exc}")
        try:
            with ftplib.FTP() as ftp:
                ftp.connect(host, 21, timeout=20)
                ftp.login(ROS_ADMIN_USER, ROS_ADMIN_PASSWORD)
                from io import BytesIO

                ftp.storbinary(f"STOR {filename}", BytesIO(content.encode("utf-8")))
                verify = BytesIO()
                ftp.retrbinary(f"RETR {filename}", verify.write)
        except Exception as exc:
            if self._is_routeros_access_problem(exc):
                raise RouterOsAccessError(self._routeros_access_problem_message(host, exc)) from exc
            raise
        self._verify_uploaded_routeros_rsc(filename, verify.getvalue().decode("utf-8", errors="replace"), expected_l2tp_psk)
        emit(f"RouterOS已通过FTP上传并校验: {filename}")

    @staticmethod
    def _verify_uploaded_routeros_rsc(filename: str, content: str, expected_l2tp_psk: str = ROS_L2TP_PSK) -> None:
        # 中文注释：RouterOS文件服务偶发看似成功但保留旧文件，回读关键标记可立即发现。
        required = ("ROS_CHECK_EXPECTED_USERS=", f"ipsec-secret={expected_l2tp_psk or ROS_L2TP_PSK}")
        missing = [marker for marker in required if marker not in content]
        if missing:
            raise RuntimeError(f"{filename} 上传后回读校验失败，缺少: {', '.join(missing)}")

    @staticmethod
    def _is_routeros_access_problem(exc_or_text: Any) -> bool:
        text = str(exc_or_text).lower()
        markers = (
            "authentication failed",
            "530 login incorrect",
            "not permitted",
            "permission denied",
            "unable to authenticate",
            "access denied",
        )
        return isinstance(exc_or_text, RouterOsAccessError) or any(marker in text for marker in markers)

    @staticmethod
    def _routeros_access_problem_message(host: str, detail: Any) -> str:
        return (
            f"{host} RouterOS管理账号或终端权限异常，admin/admin 已不能通过SSH/FTP写入；"
            "WinBox提示 Terminal not permitted 也属于该类。"
            "通常是CHR暴露admin/admin后被公网扫描或权限被改，需重装系统恢复干净Debian后重新写入CHR。"
            f"原始原因: {detail}"
        )

    def _routeros_ssh_connect(self, host: str, emit: Callable[[str], None]) -> paramiko.SSHClient:
        last_error: Optional[BaseException] = None
        for attempt in range(1, 9):
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            try:
                ssh.connect(
                    host,
                    port=ROS_SSH_PORT,
                    username=ROS_ADMIN_USER,
                    password=ROS_ADMIN_PASSWORD,
                    timeout=8,
                    banner_timeout=12,
                    auth_timeout=12,
                    look_for_keys=False,
                    allow_agent=False,
                )
                return ssh
            except Exception as exc:
                last_error = exc
                try:
                    ssh.close()
                except Exception:
                    pass
                if self._is_routeros_access_problem(exc):
                    message = self._routeros_access_problem_message(host, exc)
                    emit(f"RouterOS SSH管理认证/权限异常: {message}")
                    raise RouterOsAccessError(message) from exc
                emit(f"RouterOS SSH登录等待 {attempt}/8: {exc}")
                time.sleep(ROS_READY_INTERVAL_SECONDS)
        raise RuntimeError(f"RouterOS SSH登录失败: {last_error}")

    def _run_routeros_import(self, host: str, filename: str, emit: Callable[[str], None]) -> str:
        try:
            return self._run_routeros_import_ssh(host, filename, emit)
        except RouterOsAccessError:
            raise
        except Exception as exc:
            emit(f"RouterOS SSH导入不可用，改用Telnet终端: {exc}")
            return self._run_routeros_import_telnet(host, filename, emit)

    def _run_routeros_import_ssh(self, host: str, filename: str, emit: Callable[[str], None]) -> str:
        ssh = self._routeros_ssh_connect(host, emit)
        output_parts: List[str] = []
        channel = None
        try:
            transport = ssh.get_transport()
            if transport is None:
                raise RuntimeError("RouterOS SSH传输未建立。")
            channel = transport.open_session(timeout=15)
            with self.ssh_channel_lock:
                self.ssh_channel = channel

            def append_output(raw: bytes) -> None:
                text = raw.decode("utf-8", errors="replace").replace("\r", "\n").strip()
                if text:
                    output_parts.append(text)
                    emit(text)

            command = f"/import file-name={filename}"
            emit(f"RouterOS执行: {command}")
            channel.exec_command(command)
            deadline = time.time() + 120
            # 中文注释：导入成功后脚本会关闭SSH服务，通道可能没有正常exit-status，按输出和关闭状态收尾。
            while time.time() < deadline:
                received = False
                if channel.recv_ready():
                    received = True
                    append_output(channel.recv(4096))
                if channel.recv_stderr_ready():
                    received = True
                    append_output(channel.recv_stderr(4096))
                if channel.exit_status_ready() and not channel.recv_ready() and not channel.recv_stderr_ready():
                    break
                if channel.closed and not channel.recv_ready() and not channel.recv_stderr_ready():
                    break
                if not received:
                    time.sleep(0.2)
            return "\n".join(output_parts)
        finally:
            with self.ssh_channel_lock:
                if channel is not None and self.ssh_channel is channel:
                    self.ssh_channel = None
            try:
                if channel is not None:
                    channel.close()
            except Exception:
                pass
            try:
                ssh.close()
            except Exception:
                pass

    def _run_routeros_import_telnet(self, host: str, filename: str, emit: Callable[[str], None]) -> str:
        import telnetlib

        output_parts: List[str] = []

        def read_some(tn: telnetlib.Telnet, seconds: float) -> str:
            end = time.time() + seconds
            chunks: List[str] = []
            while time.time() < end:
                try:
                    data = tn.read_very_eager()
                except EOFError:
                    break
                if data:
                    text = data.decode("utf-8", errors="replace")
                    chunks.append(text)
                    end = time.time() + 1.0
                else:
                    time.sleep(0.2)
            text = "".join(chunks).replace("\r", "\n").strip()
            if text:
                output_parts.append(text)
                emit(text)
            return text

        last_error: Optional[BaseException] = None
        for password in (ROS_ADMIN_PASSWORD, ""):
            try:
                emit(f"RouterOS Telnet登录: {host}:23 admin/{'空密码' if password == '' else 'admin'}")
                tn = telnetlib.Telnet(host, 23, timeout=15)
                try:
                    first = tn.read_until(b"Login:", timeout=8)
                    if first:
                        text = first.decode("utf-8", errors="replace")
                        output_parts.append(text)
                        emit(text.strip())
                    tn.write(ROS_ADMIN_USER.encode("ascii") + b"\n")
                    second = tn.read_until(b"Password:", timeout=8)
                    if second:
                        text = second.decode("utf-8", errors="replace")
                        output_parts.append(text)
                        emit(text.strip())
                    tn.write(password.encode("utf-8") + b"\n")
                    read_some(tn, 5)
                    # 中文注释：RouterOS首次终端通常会询问是否查看许可，按用户手动流程先发送 n。
                    tn.write(b"n\n")
                    read_some(tn, 2)
                    command = f"/import file-name={filename}\n"
                    emit(f"RouterOS Telnet执行: {command.strip()}")
                    tn.write(command.encode("utf-8"))
                    read_some(tn, 90)
                    return "\n".join(output_parts)
                finally:
                    try:
                        tn.close()
                    except Exception:
                        pass
            except Exception as exc:
                last_error = exc
                emit(f"RouterOS Telnet登录/导入失败: {exc}")
        raise RuntimeError(f"RouterOS Telnet导入失败: {last_error}")

    @staticmethod
    def _first_security_group_id(
        server: Dict[str, Any],
        aux_interfaces: Sequence[Dict[str, Any]],
        primary_interface: Optional[Dict[str, Any]] = None,
    ) -> str:
        for item in list(aux_interfaces) + ([primary_interface] if primary_interface else []):
            if not isinstance(item, dict):
                continue
            groups = item.get("security_groups") or []
            if isinstance(groups, list):
                for group in groups:
                    if isinstance(group, dict) and group.get("id"):
                        return str(group["id"])
                    if isinstance(group, str) and group:
                        return group
        groups = server.get("security_groups") or []
        if isinstance(groups, list):
            for group in groups:
                if isinstance(group, dict) and group.get("id"):
                    return str(group["id"])
                if isinstance(group, str) and group:
                    return group
        return ""

    @staticmethod
    def _first_virsubnet_id(
        aux_interfaces: Sequence[Dict[str, Any]],
        primary_interface: Optional[Dict[str, Any]] = None,
    ) -> str:
        for item in list(aux_interfaces) + ([primary_interface] if primary_interface else []):
            if not isinstance(item, dict):
                continue
            subnet_id = (
                item.get("virsubnet_id")
                or item.get("subnet_id")
                or item.get("subnetId")
                or item.get("net_id")
                or item.get("network_id")
                or item.get("networkId")
                or item.get("neutron_network_id")
            )
            if subnet_id:
                return str(subnet_id)
            fixed_ips = item.get("fixed_ips") or item.get("fixedIps") or []
            if isinstance(fixed_ips, list):
                for fixed_ip in fixed_ips:
                    if isinstance(fixed_ip, dict):
                        subnet_id = fixed_ip.get("subnet_id") or fixed_ip.get("subnetId")
                        if subnet_id:
                            return str(subnet_id)
        return ""

    @staticmethod
    def _private_ips_from_server(server: Dict[str, Any], interfaces: Sequence[Dict[str, Any]]) -> List[str]:
        ips: List[str] = []
        addresses = server.get("addresses") or {}
        if isinstance(addresses, dict):
            for items in addresses.values():
                if not isinstance(items, list):
                    continue
                for address in items:
                    if not isinstance(address, dict):
                        continue
                    ip_text = str(address.get("addr") or "")
                    if ip_text and not is_public_ip(ip_text) and ip_text not in ips:
                        ips.append(ip_text)
        for item in interfaces:
            fixed_ips = item.get("fixed_ips") or item.get("fixedIps") or []
            if isinstance(fixed_ips, list):
                for fixed_ip in fixed_ips:
                    if isinstance(fixed_ip, dict):
                        ip_text = str(fixed_ip.get("ip_address") or fixed_ip.get("ipAddress") or "")
                        if ip_text and not is_public_ip(ip_text) and ip_text not in ips:
                            ips.append(ip_text)
            ip_text = str(item.get("fixed_ip") or item.get("private_ip") or item.get("private_ip_address") or "")
            if ip_text and not is_public_ip(ip_text) and ip_text not in ips:
                ips.append(ip_text)
        return ips

    def _find_subnet_id_for_old_server(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        server: Dict[str, Any],
        interfaces: Sequence[Dict[str, Any]],
        primary_interface: Optional[Dict[str, Any]],
        emit: Callable[[str], None],
    ) -> str:
        direct_id = self._first_virsubnet_id([], primary_interface)
        if direct_id:
            return direct_id
        network_ids = set()
        for item in ([primary_interface] if primary_interface else []) + list(interfaces):
            if not isinstance(item, dict):
                continue
            for key in ("net_id", "network_id", "networkId", "neutron_network_id"):
                value = item.get(key)
                if value:
                    network_ids.add(str(value))
        private_ips = self._private_ips_from_server(server, interfaces)
        try:
            subnets = client.list_subnets(pair)
        except Exception as exc:
            emit(f"旧ECS子网反查失败: {exc}")
            return ""
        for subnet in subnets:
            if not isinstance(subnet, dict):
                continue
            subnet_id = str(subnet.get("id") or "")
            neutron_id = str(subnet.get("neutron_network_id") or subnet.get("network_id") or "")
            if subnet_id and (subnet_id in network_ids or neutron_id in network_ids):
                return subnet_id
        for subnet in subnets:
            cidr = str(subnet.get("cidr") or "")
            subnet_id = str(subnet.get("id") or "")
            if not cidr or not subnet_id:
                continue
            try:
                network = ipaddress.ip_network(cidr, strict=False)
                if any(ipaddress.ip_address(ip_text) in network for ip_text in private_ips):
                    return subnet_id
            except Exception:
                continue
        return ""

    def _find_security_group_for_old_server(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        server: Dict[str, Any],
        aux_interfaces: Sequence[Dict[str, Any]],
        primary_interface: Optional[Dict[str, Any]],
        subnet_id: str,
        emit: Callable[[str], None],
    ) -> str:
        security_group_id = self._first_security_group_id(server, aux_interfaces, primary_interface)
        if security_group_id:
            return security_group_id
        vpc_id = ""
        if subnet_id:
            try:
                subnet = client.show_subnet(pair, subnet_id)
                vpc_id = str(subnet.get("vpc_id") or "")
            except Exception as exc:
                emit(f"旧ECS安全组反查子网失败: {exc}")
        groups = client.list_security_groups(pair)
        for group in groups:
            if not isinstance(group, dict) or not group.get("id"):
                continue
            group_vpc = str(group.get("vpc_id") or group.get("vpcId") or "")
            if vpc_id and group_vpc and group_vpc != vpc_id:
                continue
            name = str(group.get("name") or "").lower()
            if "wawacloud" in name or "wtdiy" in name or name == "default":
                return str(group["id"])
        for group in groups:
            if isinstance(group, dict) and group.get("id"):
                group_vpc = str(group.get("vpc_id") or group.get("vpcId") or "")
                if not vpc_id or not group_vpc or group_vpc == vpc_id:
                    return str(group["id"])
        return ""

    def _standard_interface_rows(
        self,
        client: HuaweiCloudClient,
        server: Dict[str, Any],
        interfaces: Sequence[Dict[str, Any]],
        plan: RegionPlan,
    ) -> List[Dict[str, Any]]:
        records: List[Dict[str, Any]] = []
        seen_ports = set()
        for interface in interfaces:
            port_id = client.interface_port_id(interface)
            private_ip = client.interface_private_ip(interface)
            if not port_id or not private_ip or is_public_ip(private_ip):
                continue
            seen_ports.add(port_id)
            records.append({"port_id": port_id, "private_ip": private_ip, "interface": interface})
        addresses = server.get("addresses") or {}
        if isinstance(addresses, dict):
            for items in addresses.values():
                if not isinstance(items, list):
                    continue
                for address in items:
                    if not isinstance(address, dict) or address.get("OS-EXT-IPS:type") != "fixed":
                        continue
                    port_id = str(address.get("OS-EXT-IPS:port_id") or address.get("port_id") or "")
                    private_ip = str(address.get("addr") or "")
                    if not port_id or not private_ip or port_id in seen_ports or is_public_ip(private_ip):
                        continue
                    seen_ports.add(port_id)
                    records.append({"port_id": port_id, "private_ip": private_ip, "interface": address})
        selected: List[Dict[str, Any]] = []
        used_ports = set()
        by_ip = {row["private_ip"]: row for row in records}
        for private_ip in standard_private_ips(plan):
            row = by_ip.get(private_ip)
            if row and row["port_id"] not in used_ports:
                used_ports.add(row["port_id"])
                selected.append(dict(row))
        for row in sorted(records, key=lambda item: sort_ipv4(item["private_ip"])):
            if len(selected) >= plan.standard_nic_count:
                break
            if row["port_id"] in used_ports:
                continue
            used_ports.add(row["port_id"])
            selected.append(dict(row))
        for index, row in enumerate(selected):
            row["iface"] = parent_iface_for_index(index)
        return selected

    def _subnet_network_id(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        subnet_id: str,
    ) -> str:
        if not subnet_id:
            return ""
        try:
            subnet = client.show_subnet(pair, subnet_id)
            return str(subnet.get("neutron_network_id") or subnet.get("network_id") or subnet_id)
        except Exception:
            return subnet_id

    def _ensure_standard_interfaces(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        server_id: str,
        server: Dict[str, Any],
        interfaces: Sequence[Dict[str, Any]],
        subnet_id: str,
        subnet_network_id: str,
        security_group_id: str,
        emit: Callable[[str], None],
    ) -> List[Dict[str, Any]]:
        rows = self._standard_interface_rows(client, server, interfaces, plan)
        existing_ips = {row["private_ip"] for row in rows}
        missing_ips = [private_ip for private_ip in standard_private_ips(plan) if private_ip not in existing_ips]
        for private_ip in missing_ips:
            if len(rows) >= plan.standard_nic_count:
                break
            if aux_ip_range_is_hard(plan):
                blocking_aux = self._find_existing_aux_by_ip(
                    client,
                    pair,
                    subnet_id,
                    private_ip,
                    parent_iface_for_index(0),
                )
                if blocking_aux:
                    aux_id = str(blocking_aux.get("id") or blocking_aux.get("port_id") or "未知ID")
                    raise CloudApiError(
                        f"{plan.label} 固定网卡规划冲突：{private_ip} 必须是标准弹性网卡，"
                        f"但当前云上已存在同 IP 的辅助弹性网卡 {aux_id}。"
                        "本次不会把硬性配置改成辅助弹性网卡，也不会继续漂移到其它 IP；"
                        "请先在资源管理中释放这条错误辅助弹性网卡后重试。"
                    )
            if not subnet_network_id or not security_group_id:
                raise CloudApiError(f"旧ECS缺少第 {len(rows) + 1}/{plan.standard_nic_count} 个弹性网卡，但无法读取子网/安全组用于补齐。")
            client.create_server_interface(pair, server_id, subnet_network_id, security_group_id, private_ip)
            emit(f"步骤: 为旧ECS补齐弹性网卡 {private_ip}")
            for _attempt in range(12):
                time.sleep(5)
                refreshed_server = client.show_server(pair, server_id)
                refreshed_interfaces = client.list_server_interfaces(pair, server_id)
                rows = self._standard_interface_rows(client, refreshed_server, refreshed_interfaces, plan)
                if any(row["private_ip"] == private_ip for row in rows):
                    break
        if len(rows) < plan.standard_nic_count:
            raise CloudApiError(f"弹性网卡数量不足 {len(rows)}/{plan.standard_nic_count}。")
        return rows[: plan.standard_nic_count]

    @staticmethod
    def _aux_parent_index(plan: RegionPlan, aux_index: int) -> int:
        if aux_should_use_primary_parent(plan):
            return 0
        if plan.standard_nic_count <= 1:
            return 0
        per_parent = max(1, (plan.aux_count + plan.standard_nic_count - 1) // plan.standard_nic_count)
        return min(aux_index // per_parent, plan.standard_nic_count - 1)

    def _annotate_aux_interface(
        self,
        aux: Dict[str, Any],
        parent_iface: str,
        parent_port_id: str = "",
    ) -> Dict[str, Any]:
        item = dict(aux)
        item["_parent_iface"] = parent_iface
        item["_parent_port_id"] = parent_port_id or str(aux.get("parent_id") or aux.get("parent_port_id") or "")
        return item

    @staticmethod
    def _aux_candidate_ips(plan: RegionPlan, *, strict_aux_ips: bool) -> List[str]:
        base_ips = list(aux_private_ips(plan))
        if strict_aux_ips or aux_ip_range_is_hard(plan):
            return base_ips
        start = plan.aux_start_octet + plan.aux_count
        extra_limit = min(254, start + max(8, plan.aux_count * 2))
        fallback_ips = [f"192.168.0.{octet}" for octet in range(start, extra_limit)]
        return list(dict.fromkeys(base_ips + fallback_ips))

    @staticmethod
    def _port_fixed_ip_addresses(port: Dict[str, Any]) -> List[str]:
        values: List[str] = []
        for item in port.get("fixed_ips") or port.get("fixedIps") or []:
            if not isinstance(item, dict):
                continue
            ip_addr = str(item.get("ip_address") or item.get("ipAddress") or item.get("addr") or "").strip()
            if ip_addr:
                values.append(ip_addr)
        for key in ("private_ip_address", "privateIpAddress", "ip_address", "ipAddress"):
            ip_addr = str(port.get(key) or "").strip()
            if ip_addr:
                values.append(ip_addr)
        return list(dict.fromkeys(values))

    @staticmethod
    def _first_private_ip(item: Dict[str, Any]) -> str:
        for key in ("private_ip", "private_ip_address", "privateIpAddress", "ip_address", "ipAddress"):
            value = str(item.get(key) or "").strip()
            if value and not is_public_ip(value):
                return value
        fixed_ips = item.get("fixed_ips") or item.get("fixedIps") or []
        if isinstance(fixed_ips, list):
            for fixed_ip in fixed_ips:
                if not isinstance(fixed_ip, dict):
                    continue
                value = str(fixed_ip.get("ip_address") or fixed_ip.get("ipAddress") or fixed_ip.get("addr") or "").strip()
                if value and not is_public_ip(value):
                    return value
        return ""

    @staticmethod
    def _port_target_label(kind: str, private_ip: str) -> str:
        clean_ip = str(private_ip or "").strip()
        if kind == "aux":
            return f"辅助弹性网卡{clean_ip}" if clean_ip else "辅助弹性网卡"
        if kind == "virtual":
            return f"虚拟IP{clean_ip}" if clean_ip else "虚拟IP"
        return f"弹性网卡{clean_ip}" if clean_ip else "弹性网卡"

    @classmethod
    def _standard_port_display_label(cls, row: Dict[str, Any]) -> str:
        return cls._port_target_label("standard", str(row.get("private_ip") or ""))

    @classmethod
    def _virtual_ip_display_label(cls, item: Dict[str, Any]) -> str:
        return cls._port_target_label("virtual", cls._first_private_ip(item))

    @classmethod
    def _aux_port_display_label(cls, item: Dict[str, Any]) -> str:
        return cls._port_target_label("aux", cls._first_private_ip(item))

    @staticmethod
    def _old_ecs_display_label(name: str) -> str:
        clean_name = str(name or "").strip() or "未命名"
        return f"发现旧ECS ({clean_name})"

    @staticmethod
    def _server_name_for_log(server: Dict[str, Any]) -> str:
        return str(server.get("name") or server.get("hostname") or "未命名").strip() or "未命名"

    @classmethod
    def _server_attribute_display_summary(cls, server: Dict[str, Any]) -> str:
        if not isinstance(server, dict):
            return "name=未命名"
        flavor = server_flavor_label(server)
        parts = [
            f"name={cls._server_name_for_log(server)}",
            f"status={server_status_text(server) or str(server.get('status') or '未知')}",
        ]
        if flavor:
            parts.append(f"flavor={flavor}")
        flavor_info = server.get("flavor") or {}
        if isinstance(flavor_info, dict):
            vcpus = flavor_info.get("vcpus")
            ram = flavor_info.get("ram")
            if vcpus not in (None, ""):
                parts.append(f"vcpus={vcpus}")
            if ram not in (None, ""):
                parts.append(f"ram={ram}")
        az = str(server.get("OS-EXT-AZ:availability_zone") or server.get("availability_zone") or "")
        if az:
            parts.append(f"az={az}")
        public_ip = HuaweiCloudClient.public_ip_from_server(server)
        if public_ip:
            parts.append(f"public_ip={public_ip}")
        charging = server_charging_text(server)
        if charging:
            parts.append(f"charging={charging}")
        return " ".join(parts)

    @classmethod
    def _eip_port_display_maps(
        cls,
        ports: Sequence[Dict[str, Any]],
        sub_network_interfaces: Sequence[Dict[str, Any]],
    ) -> Tuple[Dict[str, str], Dict[str, str]]:
        labels: Dict[str, str] = {}
        private_ips: Dict[str, str] = {}
        for port in ports:
            port_id = str(port.get("id") or port.get("port_id") or port.get("portId") or "").strip()
            if not port_id:
                continue
            private_ip = cls._first_private_ip(port)
            owner = str(port.get("device_owner") or "").lower()
            kind = "virtual" if "vip" in owner else "standard"
            labels[port_id] = cls._port_target_label(kind, private_ip)
            private_ips[port_id] = private_ip
        for aux in sub_network_interfaces:
            private_ip = cls._first_private_ip(aux)
            for key in ("id", "port_id", "portId"):
                port_id = str(aux.get(key) or "").strip()
                if not port_id:
                    continue
                # 辅助弹性网卡来自独立接口，覆盖普通端口映射，避免资源库把 .11/.12 误显示成内部端口ID。
                labels[port_id] = cls._port_target_label("aux", private_ip)
                private_ips[port_id] = private_ip
        return labels, private_ips

    @staticmethod
    def _ipv4_sort_tuple(ip_addr: str) -> Tuple[int, int, int, int]:
        try:
            parts = [int(part) for part in str(ip_addr or "").split(".")]
            if len(parts) == 4 and all(0 <= part <= 255 for part in parts):
                return parts[0], parts[1], parts[2], parts[3]
        except Exception:
            pass
        return 999, 999, 999, 999

    @staticmethod
    def _port_vlan_id(port: Dict[str, Any]) -> str:
        for key in ("vlan_id", "vlanId", "vlan", "segmentation_id", "segmentationId"):
            value = port.get(key)
            if value not in (None, ""):
                return str(value)
        details = port.get("binding:vif_details") or port.get("binding_vif_details") or {}
        if isinstance(details, dict):
            for key in ("vlan", "vlan_id", "vlanId", "segmentation_id", "segmentationId"):
                value = details.get(key)
                if value not in (None, ""):
                    return str(value)
        profile = port.get("binding:profile") or port.get("binding_profile") or {}
        if isinstance(profile, dict):
            for key in ("vlan", "vlan_id", "vlanId", "segmentation_id", "segmentationId"):
                value = profile.get(key)
                if value not in (None, ""):
                    return str(value)
        return ""

    def _port_as_aux_interface(
        self,
        port: Dict[str, Any],
        private_ip: str,
        parent_iface: str,
    ) -> Optional[Dict[str, Any]]:
        port_id = str(port.get("id") or port.get("port_id") or port.get("portId") or "")
        mac = str(port.get("mac_address") or port.get("macAddress") or "").strip()
        vlan_id = self._port_vlan_id(port)
        if not port_id or not mac or not vlan_id:
            return None
        return {
            "id": port_id,
            "port_id": port_id,
            "vlan_id": vlan_id,
            "mac_address": mac,
            "private_ip_address": private_ip,
            "_parent_iface": parent_iface,
            "_source_kind": "port",
        }

    @staticmethod
    def _select_aux_by_candidates(
        aux_by_ip: Dict[str, Dict[str, Any]],
        candidates: Sequence[str],
        plan: RegionPlan,
    ) -> List[Dict[str, Any]]:
        selected: List[Dict[str, Any]] = []
        seen_ids: Set[str] = set()
        for private_ip in candidates:
            aux = aux_by_ip.get(private_ip)
            if not aux:
                continue
            if aux_should_use_primary_parent(plan) and str(aux.get("_parent_iface") or "") != parent_iface_for_index(0):
                continue
            aux_id = str(aux.get("id") or aux.get("port_id") or private_ip)
            if aux_id in seen_ids:
                continue
            seen_ids.add(aux_id)
            selected.append(aux)
            if len(selected) >= plan.aux_count:
                break
        return selected

    def _find_existing_aux_by_ip(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        subnet_id: str,
        private_ip: str,
        parent_iface: str,
        parent_port_id: str = "",
    ) -> Optional[Dict[str, Any]]:
        try:
            interfaces = client.list_sub_network_interfaces(pair, timeout=30, retry_attempts=2)
        except Exception:
            return None
        for aux in interfaces:
            aux_subnet_id = str(aux.get("virsubnet_id") or aux.get("subnet_id") or aux.get("neutron_subnet_id") or "")
            if subnet_id and aux_subnet_id and aux_subnet_id != subnet_id:
                continue
            if str(aux.get("private_ip_address") or "") == private_ip:
                aux_parent_id = str(aux.get("parent_id") or aux.get("parent_port_id") or "")
                if parent_port_id and aux_parent_id and aux_parent_id != parent_port_id:
                    continue
                return self._annotate_aux_interface(aux, parent_iface, parent_port_id)
        return None

    @staticmethod
    def _port_allowed_address_pairs(port: Dict[str, Any]) -> List[Dict[str, Any]]:
        pairs = port.get("allowed_address_pairs") or port.get("allowedAddressPairs") or []
        return [dict(item) for item in pairs if isinstance(item, dict)]

    @classmethod
    def _merge_allowed_address_pairs(
        cls,
        port: Dict[str, Any],
        additions: Sequence[Dict[str, str]],
    ) -> List[Dict[str, Any]]:
        merged: List[Dict[str, Any]] = []
        seen = set()
        for item in [*cls._port_allowed_address_pairs(port), *additions]:
            ip_address = str(item.get("ip_address") or item.get("ipAddress") or "").strip()
            mac_address = str(item.get("mac_address") or item.get("macAddress") or "").strip()
            if not ip_address:
                continue
            # 20 模式会反复复用旧 VIP/主网卡；同一个 IP 的 allowed_address_pairs 只保留一条，避免云端 DuplicateAddressPair。
            key = ip_address.lower()
            if key in seen:
                continue
            seen.add(key)
            row: Dict[str, Any] = {"ip_address": ip_address}
            if mac_address:
                row["mac_address"] = mac_address
            merged.append(row)
        return merged

    @staticmethod
    def _allowed_address_pair_signature(pairs: Sequence[Dict[str, Any]]) -> Set[Tuple[str, str]]:
        signature: Set[Tuple[str, str]] = set()
        for item in pairs or []:
            if not isinstance(item, dict):
                continue
            ip_address = str(item.get("ip_address") or item.get("ipAddress") or "").strip().lower()
            mac_address = str(item.get("mac_address") or item.get("macAddress") or "").strip().lower()
            if ip_address:
                signature.add((ip_address, mac_address))
        return signature

    @classmethod
    def _allowed_address_pair_ips(cls, port: Dict[str, Any]) -> Set[str]:
        return {ip for ip, _mac in cls._allowed_address_pair_signature(cls._port_allowed_address_pairs(port))}

    @staticmethod
    def _is_duplicate_allowed_address_pair_error(exc: BaseException) -> bool:
        text = str(exc).lower()
        return (
            "duplicateaddresspair" in text
            or "duplicate address pair" in text
            or "request contains duplicate address pair" in text
        )

    def _refresh_port_by_id(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        port_id: str,
    ) -> Optional[Dict[str, Any]]:
        for port in client.list_ports(pair, timeout=30, retry_attempts=2):
            current_id = str(port.get("id") or port.get("port_id") or port.get("portId") or "")
            if current_id == port_id:
                return port
        return None

    def _update_allowed_address_pairs_if_needed(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        port_id: str,
        port: Dict[str, Any],
        pairs: Sequence[Dict[str, Any]],
        required_ips: Sequence[str],
        emit: Callable[[str], None],
        label: str,
    ) -> Tuple[Dict[str, Any], bool]:
        current_pairs = self._port_allowed_address_pairs(port)
        if self._allowed_address_pair_signature(current_pairs) == self._allowed_address_pair_signature(pairs):
            return port, False
        try:
            response = client.update_port(pair, port_id, {"port": {"allowed_address_pairs": list(pairs)}})
            updated_port = response.get("port") if isinstance(response, dict) and isinstance(response.get("port"), dict) else port
            return updated_port, True
        except Exception as exc:
            if self._is_duplicate_allowed_address_pair_error(exc):
                refreshed = self._refresh_port_by_id(client, pair, port_id)
                required = {str(ip).strip().lower() for ip in required_ips if str(ip).strip()}
                if refreshed and required.issubset(self._allowed_address_pair_ips(refreshed)):
                    emit(f"步骤: {label} allowed_address_pairs 已存在，跳过重复更新。")
                    return refreshed, False
            raise

    def _virtual_ip_as_interface(
        self,
        port: Dict[str, Any],
        private_ip: str,
        parent_iface: str,
        parent_port_id: str,
    ) -> Dict[str, Any]:
        port_id = str(port.get("id") or port.get("port_id") or port.get("portId") or "")
        return {
            "id": port_id,
            "port_id": port_id,
            "private_ip_address": private_ip,
            "_parent_iface": parent_iface,
            "_parent_port_id": parent_port_id,
            "_source_kind": "virtual_ip",
        }

    def _ensure_virtual_ip_ports(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        subnet_network_id: str,
        security_group_id: str,
        standard_rows: Sequence[Dict[str, Any]],
        ports: Sequence[Dict[str, Any]],
        prefix: str,
        emit: Callable[[str], None],
        *,
        allow_create_missing: bool,
    ) -> List[Dict[str, Any]]:
        expected_ips = virtual_private_ips(plan)
        if not expected_ips:
            return []
        if not standard_rows:
            raise CloudApiError("20规格需要先读取主网卡 192.168.0.10，才能绑定虚拟IP。")
        parent_row = standard_rows[0]
        parent_port_id = str(parent_row.get("port_id") or "")
        parent_iface = str(parent_row.get("iface") or parent_iface_for_index(0))
        parent_private_ip = str(parent_row.get("private_ip") or standard_private_ips(plan)[0])
        ports_by_id = {str(item.get("id") or item.get("port_id") or item.get("portId") or ""): item for item in ports if isinstance(item, dict)}
        vip_by_ip: Dict[str, Dict[str, Any]] = {}
        occupied_by_other: List[str] = []
        for port in ports:
            if not isinstance(port, dict):
                continue
            port_id = str(port.get("id") or port.get("port_id") or port.get("portId") or "")
            owner = str(port.get("device_owner") or port.get("deviceOwner") or "").lower()
            device_id = str(port.get("device_id") or port.get("deviceId") or "")
            for private_ip in self._port_fixed_ip_addresses(port):
                if private_ip not in expected_ips:
                    continue
                if "vip" in owner or not device_id:
                    vip_by_ip[private_ip] = port
                else:
                    occupied_by_other.append(f"{private_ip}({port_id})")
        if occupied_by_other:
            raise CloudApiError("20规格虚拟IP被非VIP端口占用: " + ", ".join(occupied_by_other))

        selected: List[Dict[str, Any]] = []
        for index, private_ip in enumerate(expected_ips, start=1):
            port = vip_by_ip.get(private_ip)
            if not port and allow_create_missing:
                if not subnet_network_id:
                    raise CloudApiError(f"无法创建虚拟IP {private_ip}: 缺少 neutron_network_id。")
                # 虚拟 IP 用 neutron:VIP_PORT 创建，随后写 allowed_address_pairs；这里不影响旧 5IP/10IP 模式。
                port = client.create_virtual_ip_port(
                    pair,
                    subnet_network_id,
                    private_ip,
                    timestamp_name(prefix_with_suffix(prefix, f"vip-{index}"), plan),
                    security_group_id,
                )
                emit(f"步骤: 创建虚拟IP {private_ip}")
                vip_by_ip[private_ip] = port
                ports_by_id[str(port.get("id") or port.get("port_id") or port.get("portId") or "")] = port
            if not port:
                emit(f"虚拟IP {private_ip} 未发现，继续检查其他旧资源。")
                continue
            port_id = str(port.get("id") or port.get("port_id") or port.get("portId") or "")
            vip_pairs = self._merge_allowed_address_pairs(port, [{"ip_address": parent_private_ip}])
            port, changed = self._update_allowed_address_pairs_if_needed(
                client,
                pair,
                port_id,
                port,
                vip_pairs,
                [parent_private_ip],
                emit,
                f"虚拟IP {private_ip}",
            )
            if changed:
                emit(f"步骤: 虚拟IP {private_ip} 已绑定到主网卡内网IP {parent_private_ip}")
            selected.append(self._virtual_ip_as_interface(port, private_ip, parent_iface, parent_port_id))

        parent_port = ports_by_id.get(parent_port_id, {"id": parent_port_id})
        source_check_pairs = self._merge_allowed_address_pairs(parent_port, [{"ip_address": "1.1.1.1/0"}])
        if parent_port_id:
            # 华为云虚拟 IP 通过 EIP 出公网时，主网卡需要关闭源/目的检查，API 表现为 allowed_address_pairs=1.1.1.1/0。
            _parent_port, changed = self._update_allowed_address_pairs_if_needed(
                client,
                pair,
                parent_port_id,
                parent_port,
                source_check_pairs,
                ["1.1.1.1/0"],
                emit,
                "主网卡源目检查",
            )
            if changed:
                emit("步骤: 主网卡已设置 allowed_address_pairs=1.1.1.1/0 以承载虚拟IP出公网。")
        return selected

    def _ensure_aux_interfaces(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        subnet_id: str,
        security_group_id: str,
        standard_rows: Sequence[Dict[str, Any]],
        aux_by_ip: Dict[str, Dict[str, Any]],
        emit: Callable[[str], None],
        *,
        strict_aux_ips: bool,
    ) -> List[Dict[str, Any]]:
        candidates = self._aux_candidate_ips(plan, strict_aux_ips=strict_aux_ips)
        hard_aux_range = aux_ip_range_is_hard(plan)
        selected = self._select_aux_by_candidates(aux_by_ip, candidates, plan)
        if len(selected) >= plan.aux_count:
            return selected
        if not subnet_id or not security_group_id:
            raise CloudApiError("无法读取子网/安全组用于补齐辅助弹性网卡。")
        while len(selected) < plan.aux_count:
            private_ip = next((item for item in candidates if item not in aux_by_ip), "")
            if not private_ip:
                raise CloudApiError(f"辅助弹性网卡不足 {len(selected)}/{plan.aux_count}，且没有可用内网IP候选。")
            aux_index = len(selected)
            parent_index = self._aux_parent_index(plan, aux_index)
            parent_row = standard_rows[parent_index]
            try:
                item = client.create_sub_network_interface(
                    pair,
                    subnet_id,
                    parent_row["port_id"],
                    security_group_id,
                    private_ip,
                )
                annotated = self._annotate_aux_interface(item, parent_row["iface"], parent_row["port_id"])
                aux_by_ip[private_ip] = annotated
                emit(f"步骤: 补齐辅助弹性网卡 {parent_row['iface']} {item.get('private_ip_address') or private_ip}")
            except Exception as exc:
                text = str(exc)
                if "ip address" not in text.lower() and "in use" not in text.lower() and "VPC.9902" not in text:
                    raise
                if not strict_aux_ips and not hard_aux_range:
                    aux_by_ip[private_ip] = {}
                    emit(f"固定辅助网卡IP {private_ip} 已被占用，非严格模式下直接跳过并使用后续候选IP。")
                    selected = self._select_aux_by_candidates(aux_by_ip, candidates, plan)
                    continue
                reused_aux = self._find_existing_aux_by_ip(
                    client,
                    pair,
                    subnet_id,
                    private_ip,
                    parent_row["iface"],
                    parent_row["port_id"],
                )
                if reused_aux:
                    aux_by_ip[private_ip] = reused_aux
                    emit(f"步骤: 辅助弹性网卡IP {private_ip} 已存在，按旧资源直接复用。")
                else:
                    try:
                        ports = client.list_ports(pair, timeout=30, retry_attempts=2)
                    except Exception:
                        ports = []
                    owner = ""
                    port_id = ""
                    reusable_port: Optional[Dict[str, Any]] = None
                    for port in ports:
                        if private_ip in self._port_fixed_ip_addresses(port):
                            owner = str(port.get("device_owner") or "")
                            port_id = str(port.get("id") or "")
                            reusable_port = port
                            break
                    if reusable_port:
                        port_aux = self._port_as_aux_interface(reusable_port, private_ip, parent_row["iface"])
                        if port_aux:
                            aux_by_ip[private_ip] = port_aux
                            emit(
                                f"步骤: 固定IP {private_ip} 已存在于端口 {port_id} "
                                f"owner={owner or '未知'}，带VLAN/MAC，按旧端口直接复用。"
                            )
                            selected = self._select_aux_by_candidates(aux_by_ip, candidates, plan)
                            continue
                        if not strict_aux_ips and not hard_aux_range:
                            aux_by_ip[private_ip] = {}
                            emit(
                                f"固定辅助网卡IP {private_ip} 已被端口 {port_id or '未知'} "
                                f"owner={owner or '未知'} 占用；该端口未返回VLAN/MAC，"
                                "不能直接写入 huawei3 的 ip.txt，改用下一个可用辅助IP。"
                            )
                            selected = self._select_aux_by_candidates(aux_by_ip, candidates, plan)
                            continue
                    message = f"固定辅助网卡IP {private_ip} 已被占用，但未找到可复用辅助网卡"
                    if port_id:
                        message += f"；占用端口={port_id} owner={owner or '未知'}"
                    raise CloudApiError(message) from exc
            selected = self._select_aux_by_candidates(aux_by_ip, candidates, plan)
        return selected

    @staticmethod
    def _is_resource_active(item: Dict[str, Any]) -> bool:
        status = str(item.get("status") or item.get("state") or "").upper()
        return status in {"", "ACTIVE", "NORMAL", "RUNNING", "OK"}

    @staticmethod
    def _cidr_contains(parent_cidr: str, child_cidr: str) -> bool:
        try:
            return ipaddress.ip_network(child_cidr, strict=False).subnet_of(ipaddress.ip_network(parent_cidr, strict=False))
        except Exception:
            return False

    @staticmethod
    def _name_score(name: str, prefix: str, plan: RegionPlan) -> int:
        lowered = name.lower()
        score = 0
        if prefix and lowered.startswith(f"{prefix.lower()}-{plan.key}"):
            score += 80
        if plan.key.lower() in lowered:
            score += 40
        if "wawacloud" in lowered or "wtdiy" in lowered:
            score += 20
        return score

    def _choose_existing_vpc(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        prefix: str,
        emit: Callable[[str], None],
    ) -> Optional[Dict[str, Any]]:
        usable: List[Dict[str, Any]] = []
        for vpc in client.list_vpcs(pair):
            if not isinstance(vpc, dict) or not self._is_resource_active(vpc):
                continue
            cidr = str(vpc.get("cidr") or "")
            if cidr and not self._cidr_contains(cidr, SUBNET_CIDR):
                continue
            usable.append(vpc)
        if not usable:
            emit("旧网络检查: 未发现可复用VPC。")
            return None
        usable.sort(
            key=lambda item: (
                self._name_score(str(item.get("name") or ""), prefix, plan),
                str(item.get("updated_at") or item.get("created_at") or item.get("created") or ""),
            ),
            reverse=True,
        )
        selected = usable[0]
        emit(f"复用旧VPC ({selected.get('name') or '未命名'})。")
        return selected

    def _ensure_vpc(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        prefix: str,
        emit: Callable[[str], None],
    ) -> Tuple[str, bool]:
        existing = self._choose_existing_vpc(client, pair, plan, prefix, emit)
        if existing and existing.get("id"):
            return str(existing["id"]), True
        vpc_name = timestamp_name(prefix, plan)
        try:
            vpc = client.create_vpc(pair, vpc_name)
        except CloudApiError as exc:
            text = f"{exc} {exc.body}".lower()
            if "quota exceeded" in text and "router" in text:
                raise CloudApiError(
                    "VPC/路由配额已满，且未找到可复用旧VPC。请先清理该地区失败重试留下的空VPC/路由，或保留一个可用旧VPC后重试。",
                    status=exc.status,
                    body=exc.body,
                ) from exc
            raise
        vpc_id = str(vpc.get("id") or "")
        if not vpc_id:
            raise CloudApiError(f"创建VPC后未返回ID: {vpc}")
        emit(f"步骤: 创建VPC {vpc_name}")
        return vpc_id, False

    def _choose_existing_subnet(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        vpc_id: str,
        availability_zone: str,
        prefix: str,
        plan: RegionPlan,
        emit: Callable[[str], None],
    ) -> Optional[Dict[str, Any]]:
        usable: List[Dict[str, Any]] = []
        for subnet in client.list_subnets(pair):
            if not isinstance(subnet, dict) or not self._is_resource_active(subnet):
                continue
            if str(subnet.get("vpc_id") or "") != vpc_id:
                continue
            cidr = str(subnet.get("cidr") or "")
            if cidr and cidr != SUBNET_CIDR:
                continue
            gateway = str(subnet.get("gateway_ip") or subnet.get("gateway") or "")
            if gateway and gateway != SUBNET_GATEWAY:
                continue
            subnet_az = str(subnet.get("availability_zone") or subnet.get("availabilityZone") or "")
            if availability_zone and subnet_az and subnet_az != availability_zone:
                continue
            usable.append(subnet)
        if not usable:
            emit(f"旧网络检查: VPC {vpc_id} 下没有可复用子网。")
            return None
        usable.sort(
            key=lambda item: (
                self._name_score(str(item.get("name") or ""), prefix, plan),
                str(item.get("updated_at") or item.get("created_at") or item.get("created") or ""),
            ),
            reverse=True,
        )
        selected = usable[0]
        emit(f"复用旧子网 ({selected.get('name') or '未命名'})。")
        return selected

    def _ensure_subnet(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        vpc_id: str,
        availability_zone: str,
        prefix: str,
        emit: Callable[[str], None],
    ) -> Tuple[str, str, bool]:
        existing = self._choose_existing_subnet(client, pair, vpc_id, availability_zone, prefix, plan, emit)
        if existing and existing.get("id"):
            subnet_id = str(existing["id"])
            subnet = client.wait_subnet_active(pair, subnet_id, emit)
            subnet_network_id = str(subnet.get("neutron_network_id") or existing.get("neutron_network_id") or subnet_id)
            return subnet_id, subnet_network_id, True
        subnet_name = timestamp_name(prefix_with_suffix(prefix, "subnet"), plan)
        subnet = client.create_subnet(pair, vpc_id, subnet_name, availability_zone)
        subnet_id = str(subnet.get("id") or "")
        if not subnet_id:
            raise CloudApiError(f"创建子网后未返回ID: {subnet}")
        emit(f"步骤: 创建子网 {subnet_name}")
        subnet = client.wait_subnet_active(pair, subnet_id, emit)
        subnet_network_id = str(subnet.get("neutron_network_id") or subnet_id)
        return subnet_id, subnet_network_id, False

    def _choose_existing_security_group(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        vpc_id: str,
        prefix: str,
        plan: RegionPlan,
        emit: Callable[[str], None],
    ) -> Optional[Dict[str, Any]]:
        usable: List[Dict[str, Any]] = []
        for group in client.list_security_groups(pair):
            if not isinstance(group, dict):
                continue
            group_vpc = str(group.get("vpc_id") or group.get("vpcId") or "")
            if group_vpc and group_vpc != vpc_id:
                continue
            usable.append(group)
        if not usable:
            emit(f"旧网络检查: VPC {vpc_id} 下没有可复用安全组。")
            return None
        usable.sort(
            key=lambda item: (
                self._name_score(str(item.get("name") or ""), prefix, plan),
                0 if str(item.get("name") or "").lower() == "default" else 1,
                str(item.get("updated_at") or item.get("created_at") or item.get("created") or ""),
            ),
            reverse=True,
        )
        selected = usable[0]
        emit(f"复用旧安全组 ({selected.get('name') or '未命名'})。")
        return selected

    def _ensure_security_group(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        vpc_id: str,
        prefix: str,
        emit: Callable[[str], None],
    ) -> Tuple[str, bool]:
        existing = self._choose_existing_security_group(client, pair, vpc_id, prefix, plan, emit)
        if existing and existing.get("id"):
            security_group_id = str(existing["id"])
            client.open_security_group(pair, security_group_id, emit)
            return security_group_id, True
        security_group_name = timestamp_name(prefix_with_suffix(prefix, "sg"), plan)
        security_group = client.create_security_group(pair, vpc_id, security_group_name)
        security_group_id = str(security_group.get("id") or "")
        if not security_group_id:
            raise CloudApiError(f"创建安全组后未返回ID: {security_group}")
        emit(f"步骤: 创建安全组 {security_group_name}")
        client.open_security_group(pair, security_group_id, emit)
        return security_group_id, False

    def _ensure_eips_for_ports(
        self,
        client: HuaweiCloudClient,
        pair: ProjectPair,
        plan: RegionPlan,
        ordered_port_ids: Sequence[str],
        prefix: str,
        emit: Callable[[str], None],
        static_bgp_enabled: bool = False,
        port_labels: Optional[Dict[str, str]] = None,
        excluded_public_ips: Optional[Sequence[str]] = None,
        candidate_seed_ips: Optional[Sequence[str]] = None,
        online_ip_range_manifest: Optional[Dict[str, Any]] = None,
    ) -> List[str]:
        ordered_port_ids = [str(port_id) for port_id in ordered_port_ids if port_id]
        if not ordered_port_ids:
            raise CloudApiError("EIP绑定目标为空。")
        display_labels = {str(key): str(value) for key, value in (port_labels or {}).items() if key and value}
        filter_new_ips = excluded_public_ips is not None
        excluded_ips = {
            value
            for value in (self._normalize_used_ip_value(ip) for ip in (excluded_public_ips or ()))
            if value
        }
        candidate_seed_values = self._dedupe_public_ip_values(candidate_seed_ips or ())
        candidate_manifest = (
            online_ip_range_manifest
            if isinstance(online_ip_range_manifest, dict)
            else self._load_online_ip_range_manifest()
            if filter_new_ips
            else {}
        )
        candidate_state_lock = threading.Lock()
        candidate_reserved_ips: Set[str] = set()
        candidate_attempted_keys: Set[Tuple[str, str, str]] = set()
        successful_candidate_seeds: Dict[Tuple[str, str], List[str]] = {}

        def target_label(port_id: str) -> str:
            return display_labels.get(str(port_id) or "", "绑定目标")

        def discard_created_eip(item: Dict[str, Any], value: str) -> bool:
            item_id = str(item.get("id") or item.get("publicip_id") or "").strip()
            if not item_id:
                raise CloudApiError(f"待释放EIP参数缺失: {value or '未知IP'}")
            try:
                client.delete_public_ip(pair, item_id)
                deleted = client.wait_public_ip_deleted(
                    pair,
                    item_id,
                    lambda _message: None,
                    timeout=45,
                    interval=1.0,
                )
                if not deleted:
                    raise CloudApiError("EIP释放状态未确认")
                return True
            except Exception as exc:
                raise CloudApiError(f"待释放EIP {value or item_id} 释放失败: {exc}") from exc

        def create_filtered_eip(
            target_index: int,
            eip_type: str,
            current_ips: Sequence[str],
        ) -> Dict[str, Any]:
            resource_name = timestamp_name(prefix_with_suffix(prefix, f"eip-{target_index}"), plan)
            if not filter_new_ips:
                item = client.create_public_ip(pair, resource_name, eip_type=eip_type)
                self._queue_huawei_ip_segments(plan, [item])
                return item

            clean_type = str(eip_type or EIP_TYPE)
            online_seeds = self._online_ip_range_seed_values_for_region(
                plan,
                manifest=candidate_manifest,
                eip_type=clean_type,
            )
            clean_current_ips = self._dedupe_public_ip_values(current_ips)
            excluded_ips.update(clean_current_ips)
            anchor_ip = ""
            if clean_current_ips:
                anchor_ip = clean_current_ips[(max(1, target_index) - 1) % len(clean_current_ips)]
            seed_values = self._dedupe_public_ip_values(
                [
                    *online_seeds,
                    *candidate_seed_values,
                    *clean_current_ips,
                ]
            )
            if not anchor_ip and seed_values:
                anchor_ip = seed_values[0]

            def terminal_error(exc: Exception) -> bool:
                return bool(
                    self._is_confirmed_eip_sold_out(exc)
                    or self._is_confirmed_static_bgp_sold_out(exc)
                    or self._is_eip_quota_insufficient(exc)
                )

            item, retained_rejected_ip = self._allocate_huawei_new_eip(
                client,
                pair,
                plan,
                eip_types=(clean_type,),
                seed_addresses_by_type={clean_type: seed_values},
                excluded_ips=excluded_ips,
                reserved_candidate_ips=candidate_reserved_ips,
                attempted_candidate_keys=candidate_attempted_keys,
                successful_segment_seeds=successful_candidate_seeds,
                state_lock=candidate_state_lock,
                anchor_ip=anchor_ip,
                rotation_index=target_index,
                resource_name_prefix=prefix_with_suffix(prefix, f"eip-{target_index}"),
                discard_eip=discard_created_eip,
                emit=lambda message: emit(f"步骤: {message}"),
                terminal_error=terminal_error,
            )
            if retained_rejected_ip:
                raise CloudApiError(
                    f"重复EIP释放状态未确认: {retained_rejected_ip}"
                )
            return item

        static_bgp_active = bool(static_bgp_enabled)
        static_sold_out_prompted = False
        quota_prompted = False
        for round_index in range(1, 4):
            by_port = client.public_ips_by_port(pair)
            missing_ports = [port_id for port_id in ordered_port_ids if not by_port.get(port_id)]
            if not missing_ports:
                ips = client.public_ips_for_ports(pair, ordered_port_ids)
                if len(ips) >= len(ordered_port_ids):
                    return ips[: len(ordered_port_ids)]
            unbound_eips: List[Dict[str, Any]] = []
            for item in client.list_public_ips(pair, timeout=30, retry_attempts=2):
                item_id = str(item.get("id") or item.get("publicip_id") or "").strip()
                if not item_id or str(item.get("port_id") or "").strip():
                    continue
                item_ip = public_ip_address(item)
                if filter_new_ips and item_ip and item_ip in excluded_ips:
                    try:
                        client.delete_public_ip(pair, item_id)
                        client.wait_public_ip_deleted(pair, item_id, emit, timeout=90, interval=2.0)
                    except Exception as exc:
                        raise CloudApiError(f"未绑定EIP {item_ip} 已命中旧IP库，但释放失败: {exc}") from exc
                    emit(f"步骤: 未绑定EIP {item_ip} 已命中旧IP库，释放后重新购买。")
                    continue
                unbound_eips.append(item)
            for port_id in missing_ports:
                target_index = ordered_port_ids.index(port_id) + 1
                last_error: Optional[Exception] = None
                eip_id = ""
                eip_addr = ""
                if unbound_eips:
                    reusable_eip = unbound_eips.pop(0)
                    eip_id = str(reusable_eip.get("id") or reusable_eip.get("publicip_id") or "")
                    eip_addr = public_ip_address(reusable_eip)
                    if eip_addr:
                        excluded_ips.add(eip_addr)
                    emit(f"步骤: 复用未绑定EIP {target_index}/{len(ordered_port_ids)} {eip_addr or eip_id}")
                for attempt in range(1, REQUEST_RETRY_ATTEMPTS + 1):
                    try:
                        already_ips = client.public_ips_for_ports(pair, [port_id])
                        if already_ips:
                            emit(f"步骤: EIP已绑定 {target_index}/{len(ordered_port_ids)} {already_ips[0]} -> {target_label(port_id)}")
                            break
                        if not eip_id:
                            eip_type = self._eip_type_for_plan(plan, static_bgp_active)
                            current_ips = client.public_ips_for_ports(pair, ordered_port_ids)
                            eip = create_filtered_eip(target_index, eip_type, current_ips)
                            eip_id = str(eip.get("id") or eip.get("publicip_id") or "")
                            eip_addr = str(eip.get("public_ip_address") or eip.get("public_ip") or "")
                            if not eip_id:
                                raise CloudApiError(f"申请EIP后未返回ID: {eip}")
                        client.bind_public_ip(pair, eip_id, port_id)
                        emit(f"步骤: 补齐/绑定按流量300M EIP {target_index}/{len(ordered_port_ids)} {eip_addr or eip_id} -> {target_label(port_id)}")
                        break
                    except Exception as exc:
                        last_error = exc
                        confirmed_eip_sold_out = self._is_confirmed_eip_sold_out(exc)
                        quota_insufficient = len(ordered_port_ids) >= 20 and self._is_eip_quota_insufficient(exc)
                        if quota_insufficient:
                            message = self._eip_quota_message(plan, len(ordered_port_ids), len(missing_ports), str(exc))
                            if not quota_prompted:
                                quota_prompted = True
                                self.call_ui_sync(self.show_error, message)
                            raise CloudApiError(message) from exc
                        # 华为云明确返回 EIP 地址池售罄时，继续多层重试只会拖慢任务；保留已有资源，等公网 IP 恢复后再补齐本地区。
                        if (
                            static_bgp_active
                            and self._static_bgp_enabled_for_plan(plan, static_bgp_enabled)
                            and not static_sold_out_prompted
                            and (confirmed_eip_sold_out or self._is_confirmed_static_bgp_sold_out(exc))
                        ):
                            static_sold_out_prompted = True
                            text = str(exc)
                            keep_going = bool(
                                self.call_ui_sync(
                                    self.ask_yes_no,
                                    f"{plan.label} 静态BGP EIP 已确认售罄：{text}\n\n继续将改用全动态BGP购买该地区剩余EIP；取消将停止本次任务，请重新选择地区。",
                                )
                            )
                            if keep_going:
                                emit(f"{plan.label} 静态BGP已确认售罄，已按确认改用全动态BGP继续购买剩余EIP。")
                                static_bgp_active = False
                                eip_id = ""
                                eip_addr = ""
                                continue
                            raise CloudApiError(f"{plan.label} 静态BGP EIP售罄，用户选择停止任务。") from exc
                        if confirmed_eip_sold_out:
                            raise CloudApiError(
                                f"{plan.label} 弹性公网IP地址池已确认售罄，已保留已有ECS/网卡并停止重复购买；"
                                "待该地区公网IP恢复后重新执行，会复用已有资源继续补齐/搭建。"
                            ) from exc
                        if filter_new_ips and not eip_id:
                            raise CloudApiError(f"EIP补齐失败 {target_label(port_id)}: {exc}") from exc
                        if attempt >= REQUEST_RETRY_ATTEMPTS:
                            raise CloudApiError(f"EIP补齐失败 {target_label(port_id)}: {exc}") from exc
                        emit(f"EIP绑定重试 {attempt}/{REQUEST_RETRY_ATTEMPTS}: {target_label(port_id)} eip={eip_addr or eip_id or '未申请'} {exc}")
                        time.sleep(min(2 * attempt, 6))
            time.sleep(3 * round_index)
        ips = client.public_ips_for_ports(pair, ordered_port_ids)
        if len(ips) < len(ordered_port_ids):
            raise CloudApiError(f"EIP绑定校验失败：只读取到 {len(ips)}/{len(ordered_port_ids)} 个绑定到实例网卡的公网IP。")
        return ips[: len(ordered_port_ids)]

    def _find_reusable_region_result(
        self,
        client: HuaweiCloudClient,
        plan: RegionPlan,
        pair: ProjectPair,
        prefix: str,
        password: str,
        emit: Callable[[str], None],
        *,
        strict_aux_ips: bool = True,
        allow_create_missing: bool = False,
        flavor_mode: str = FLAVOR_MODE_FIXED,
        x1_flavor_enabled: bool = False,
        preferred_server_ids: Optional[Sequence[str]] = None,
        required_server_ids: Optional[Sequence[str]] = None,
        excluded_server_ids: Optional[Sequence[str]] = None,
        static_bgp_enabled: bool = False,
        excluded_public_ips: Optional[Sequence[str]] = None,
        new_ip_seed_addresses: Optional[Sequence[str]] = None,
        online_ip_range_manifest: Optional[Dict[str, Any]] = None,
        infer_selected_ten_ip: bool = False,
    ) -> Optional[RegionBuildResult]:
        expected_eip_count = expected_public_ip_count(plan)
        action_text = "继续新建" if strict_aux_ips else "继续检查其他旧资源"
        servers: List[Dict[str, Any]] = []
        detail_cache: Dict[str, Dict[str, Any]] = {}
        try:
            servers = client.list_cloud_servers(pair)
        except Exception as exc:
            emit(f"旧资源列表检查失败，尝试按自动购买返回的服务器继续兜底反查: {exc}")
            if not preferred_server_ids:
                return None
        seen_server_ids = {str(server.get("id") or "") for server in servers if str(server.get("id") or "")}
        for preferred_id in preferred_server_ids or []:
            server_id = str(preferred_id or "").strip()
            if not server_id or server_id in seen_server_ids:
                continue
            try:
                detail = client.show_server(pair, server_id)
                detail.setdefault("id", server_id)
                detail["_autobuy_preferred"] = True
                detail_cache[server_id] = detail
                servers.insert(0, detail)
                seen_server_ids.add(server_id)
                emit(f"按自动购买返回服务器兜底反查: {self._server_name_for_log(detail)} status={detail.get('status') or '-'}")
            except Exception as exc:
                emit(f"自动购买返回服务器暂未可查询: {exc}")
        prefix_matches: List[Dict[str, Any]] = []
        other_matches: List[Dict[str, Any]] = []
        skipped_flavors: List[str] = []
        preferred_server_id_set = {
            str(server_id or "").strip()
            for server_id in preferred_server_ids or []
            if str(server_id or "").strip()
        }
        required_server_id_set = {
            str(server_id or "").strip()
            for server_id in required_server_ids or []
            if str(server_id or "").strip()
        }
        excluded_server_id_set = {
            str(server_id or "").strip()
            for server_id in excluded_server_ids or []
            if str(server_id or "").strip()
        }
        require_x1_1c1g = x1_flavor_enabled and flavor_mode in {
            FLAVOR_MODE_WAWACLOUD_LOW,
            FLAVOR_MODE_WAWACLOUD_20,
        }

        def matches_purchase_config(server: Dict[str, Any]) -> bool:
            if required_server_id_set:
                flavor_id = client.server_flavor_id(server).casefold()
                if (
                    flavor_mode == FLAVOR_MODE_WAWACLOUD_LOW
                    and flavor_id == HUAWEI_X1_1C1G_FLAVOR_ID.casefold()
                ):
                    return True
                return client.server_matches_purchase_config(server, flavor_mode, pair.region)
            if require_x1_1c1g:
                return client.server_flavor_id(server).casefold() == HUAWEI_X1_1C1G_FLAVOR_ID.casefold()
            return client.server_matches_purchase_config(server, flavor_mode, pair.region)

        for server in servers:
            name = str(server.get("name") or "")
            status = str(server.get("status") or "").upper()
            server_id = str(server.get("id") or "")
            if (
                not server_id
                or server_id in excluded_server_id_set
                or (required_server_id_set and server_id not in required_server_id_set)
                or status not in {"ACTIVE", "RUNNING"}
            ):
                continue
            matches_config = matches_purchase_config(server)
            if not matches_config:
                try:
                    detail = client.show_server(pair, server_id)
                    detail_cache[server_id] = detail
                    matches_config = matches_purchase_config(detail)
                except Exception as exc:
                    skipped_flavors.append(f"{name or '未命名'}: 详情读取失败 {exc}")
            if not matches_config:
                skipped_flavors.append(
                    f"{name or '未命名'}: 规格不匹配/接口字段不完整 flavor={client.server_flavor_id(server) or '空'}"
                )
                continue
            if prefix and (name.startswith(f"{prefix}-{plan.key}-") or name.startswith(f"{prefix}-ecs-{plan.key}-")):
                prefix_matches.append(server)
            else:
                other_matches.append(server)
        def reusable_sort_key(item: Dict[str, Any]) -> Tuple[int, int, int, int, int, str]:
            item_name = str(item.get("name") or "")
            item_id = str(item.get("id") or "")
            preferred_id = 1 if item_id in preferred_server_id_set else 0
            prefix_match = 1 if item in prefix_matches else 0
            set_match = re.search(r"-set-(\d+)$", item_name, flags=re.IGNORECASE)
            unnumbered_resource = 1 if set_match is None else 0
            set_order = -int(set_match.group(1)) if set_match else 0
            preferred_name = 1 if item_name == LOW_CONFIG_ECS_NAME else 0
            created_at = str(item.get("created") or item.get("updated") or "")
            return (
                preferred_id,
                prefix_match,
                unnumbered_resource,
                set_order,
                preferred_name,
                created_at,
            )

        candidates = sorted(
            [*prefix_matches, *other_matches],
            key=reusable_sort_key,
            reverse=True,
        )
        if not candidates:
            emit(f"旧资源检查: 未发现可复用的 {flavor_mode_label(flavor_mode)} ACTIVE 云服务器。")
            if skipped_flavors:
                emit("旧资源检查跳过明细: " + " | ".join(skipped_flavors[:5]))
        reusable_errors: List[str] = []
        for server in candidates:
            name = str(server.get("name") or "")
            server_id = str(server.get("id") or "")
            ecs_label = self._old_ecs_display_label(name)
            name_matched = server in prefix_matches
            try:
                detail = detail_cache.get(server_id) or client.show_server(pair, server_id)
                ecs_name = self._server_name_for_log(detail)
                emit(f"旧ECS属性: {self._server_attribute_display_summary(detail)}")
                interfaces = client.list_server_interfaces(pair, server_id)
                all_aux_interfaces: Optional[List[Dict[str, Any]]] = None
                public_ip_rows: Optional[List[Dict[str, Any]]] = None
                if (
                    infer_selected_ten_ip
                    and required_server_id_set
                    and flavor_mode in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED}
                    and expected_eip_count < 10
                ):
                    try:
                        bound_port_ids = {
                            client.interface_port_id(interface)
                            for interface in interfaces
                            if client.interface_port_id(interface)
                        }
                        all_aux_interfaces = client.list_sub_network_interfaces(pair)
                        for aux in all_aux_interfaces:
                            parent_port_id = str(aux.get("parent_id") or aux.get("parent_port_id") or "")
                            if parent_port_id not in bound_port_ids:
                                continue
                            aux_port_id = str(aux.get("id") or aux.get("port_id") or aux.get("portId") or "")
                            if aux_port_id:
                                bound_port_ids.add(aux_port_id)
                        public_ip_rows = client.list_public_ips(pair)
                        bound_eip_keys = {
                            str(
                                item.get("id")
                                or item.get("publicip_id")
                                or item.get("public_ip_address")
                                or item.get("public_ip")
                                or ""
                            )
                            for item in public_ip_rows
                            if str(item.get("port_id") or "") in bound_port_ids
                        }
                        bound_eip_count = len({value for value in bound_eip_keys if value})
                        if bound_eip_count >= 10:
                            plan = self._effective_plan_for_flavor_mode(
                                plan,
                                flavor_mode,
                                x1_ten_ip_enabled=True,
                            )
                            expected_eip_count = expected_public_ip_count(plan)
                            emit(
                                f"所选ECS实际绑定公网IP {bound_eip_count} 条，"
                                f"按{expected_eip_count}-IP拓扑扫描并搭建。"
                            )
                    except Exception as exc:
                        emit(f"所选ECS实际EIP拓扑识别失败，按当前地区拓扑继续检查: {exc}")
                primary_port_id = client.primary_port_id_from_server(detail, interfaces)
                if not primary_port_id:
                    reason = f"{ecs_label}，但未读取到主网卡端口。"
                    reusable_errors.append(reason)
                    emit(f"{reason}{action_text}。")
                    continue
                primary_interface = next(
                    (
                        item
                        for item in interfaces
                        if str(item.get("port_id") or item.get("id") or item.get("portId") or "") == primary_port_id
                    ),
                    None,
                )
                subnet_id_for_old = self._find_subnet_id_for_old_server(client, pair, detail, interfaces, primary_interface, emit)
                subnet_network_id_for_old = self._subnet_network_id(client, pair, subnet_id_for_old)
                security_group_id_for_old = self._find_security_group_for_old_server(
                    client,
                    pair,
                    detail,
                    [],
                    primary_interface,
                    subnet_id_for_old,
                    emit,
                )
                standard_rows = self._standard_interface_rows(client, detail, interfaces, plan)
                if len(standard_rows) < plan.standard_nic_count:
                    if not allow_create_missing:
                        emit(
                            f"{ecs_label}，但弹性网卡不足 "
                            f"{len(standard_rows)}/{plan.standard_nic_count}，继续检查其他旧资源。"
                        )
                        continue
                    emit(
                        f"{ecs_label}，弹性网卡不足 "
                        f"{len(standard_rows)}/{plan.standard_nic_count}，将尝试补齐。"
                    )
                    standard_rows = self._ensure_standard_interfaces(
                        client,
                        pair,
                        plan,
                        server_id,
                        detail,
                        interfaces,
                        subnet_id_for_old,
                        subnet_network_id_for_old,
                        security_group_id_for_old,
                        emit,
                    )
                    detail = client.show_server(pair, server_id)
                    interfaces = client.list_server_interfaces(pair, server_id)
                else:
                    emit(
                        f"{ecs_label} 弹性网卡数量满足 "
                        f"{len(standard_rows)}/{plan.standard_nic_count}。"
                    )
                standard_port_ids = [row["port_id"] for row in standard_rows]
                parent_iface_by_port = {row["port_id"]: row["iface"] for row in standard_rows}
                standard_port_id_set = set(standard_port_ids)
                virtual_ip_ports: List[Dict[str, Any]] = []
                if virtual_private_ips(plan):
                    all_ports = client.list_ports(pair, timeout=30, retry_attempts=2)
                    virtual_ip_ports = self._ensure_virtual_ip_ports(
                        client,
                        pair,
                        plan,
                        subnet_network_id_for_old,
                        security_group_id_for_old,
                        standard_rows,
                        all_ports,
                        prefix,
                        emit,
                        allow_create_missing=allow_create_missing,
                    )
                if len(virtual_ip_ports) < int(getattr(plan, "virtual_ip_count", 0)):
                    reason = (
                        f"{ecs_label}，但虚拟IP不足 "
                        f"{len(virtual_ip_ports)}/{getattr(plan, 'virtual_ip_count', 0)}。"
                    )
                    reusable_errors.append(reason)
                    emit(f"{reason}继续检查其他旧资源。")
                    continue
                if all_aux_interfaces is None:
                    all_aux_interfaces = client.list_sub_network_interfaces(pair)
                aux_interfaces = [
                    item
                    for item in all_aux_interfaces
                    if str(item.get("parent_id") or item.get("parent_port_id") or "") in standard_port_id_set
                ]
                if public_ip_rows is None:
                    public_ip_rows = client.list_public_ips(pair)
                eips_by_port: Dict[str, List[str]] = {}
                eip_ids_by_port: Dict[str, List[str]] = {}
                for item in public_ip_rows:
                    port_id = str(item.get("port_id") or "")
                    ip_addr = str(item.get("public_ip_address") or item.get("public_ip") or "")
                    if port_id and is_public_ip(ip_addr):
                        eips_by_port.setdefault(port_id, [])
                        if ip_addr not in eips_by_port[port_id]:
                            eips_by_port[port_id].append(ip_addr)
                        eip_id = str(item.get("id") or item.get("publicip_id") or "")
                        if eip_id:
                            eip_ids_by_port.setdefault(port_id, [])
                            if eip_id not in eip_ids_by_port[port_id]:
                                eip_ids_by_port[port_id].append(eip_id)
                aux_by_ip: Dict[str, Dict[str, Any]] = {}
                for aux in all_aux_interfaces:
                    aux_subnet_id = str(
                        aux.get("virsubnet_id")
                        or aux.get("subnet_id")
                        or aux.get("neutron_subnet_id")
                        or ""
                    )
                    if subnet_id_for_old and aux_subnet_id and aux_subnet_id != subnet_id_for_old:
                        continue
                    private_ip = str(aux.get("private_ip_address") or "")
                    parent_port_id = str(aux.get("parent_id") or aux.get("parent_port_id") or "")
                    parent_iface = parent_iface_by_port.get(parent_port_id, parent_iface_for_index(0))
                    if private_ip:
                        if (
                            aux_should_use_primary_parent(plan)
                            and standard_rows
                            and parent_port_id
                            and parent_port_id != standard_rows[0]["port_id"]
                        ):
                            emit(f"辅助弹性网卡 {private_ip} 所属弹性网卡不是 192.168.0.10，按规则不用于绑定/搭建。")
                            continue
                        aux_by_ip[private_ip] = self._annotate_aux_interface(aux, parent_iface, parent_port_id)
                expected_aux_ips = aux_private_ips(plan)
                aux_candidates = self._aux_candidate_ips(plan, strict_aux_ips=strict_aux_ips)
                partial_aux_allowed = False
                misplaced_aux: List[Tuple[str, Dict[str, Any], Dict[str, Any]]] = []
                for aux_index, private_ip in enumerate(expected_aux_ips):
                    aux = aux_by_ip.get(private_ip)
                    if not aux:
                        continue
                    expected_parent = standard_rows[self._aux_parent_index(plan, aux_index)]
                    current_parent_id = str(aux.get("parent_id") or aux.get("parent_port_id") or "")
                    if current_parent_id and current_parent_id != expected_parent["port_id"]:
                        misplaced_aux.append((private_ip, aux, expected_parent))
                if misplaced_aux:
                    if aux_should_use_primary_parent(plan):
                        emit(
                            f"发现旧辅助弹性网卡父网卡不是 {parent_iface_for_index(0)} 共 {len(misplaced_aux)} 条，"
                            "本轮不使用这些错误父级网卡，改用主网卡下可用辅助IP。"
                        )
                    else:
                        emit(
                            f"发现旧辅助弹性网卡父网卡顺序不一致 {len(misplaced_aux)} 条，"
                            "按现有资源直接复用，不删除、不重建。"
                        )
                selected_aux = self._select_aux_by_candidates(aux_by_ip, aux_candidates, plan)
                if len(selected_aux) < plan.aux_count:
                    if not allow_create_missing:
                        missing_aux_ips = [ip for ip in expected_aux_ips if ip not in aux_by_ip]
                        emit(
                            f"{ecs_label}，但辅助弹性网卡数量不足 "
                            f"{len(selected_aux)}/{plan.aux_count}，"
                            f"缺少 {', '.join(missing_aux_ips[:8]) or '可用辅助网卡'}，继续检查其他旧资源。"
                        )
                        continue
                    if not subnet_id_for_old or not security_group_id_for_old:
                        reason = f"{ecs_label}，但无法读取子网/安全组用于补齐辅助弹性网卡。"
                        reusable_errors.append(reason)
                        emit(f"{reason}继续检查其他旧资源。")
                        continue
                    client.open_security_group(pair, security_group_id_for_old, emit)
                    try:
                        selected_aux = self._ensure_aux_interfaces(
                            client,
                            pair,
                            plan,
                            subnet_id_for_old,
                            security_group_id_for_old,
                            standard_rows,
                            aux_by_ip,
                            emit,
                            strict_aux_ips=strict_aux_ips,
                        )
                    except Exception as exc:
                        selected_aux = self._select_aux_by_candidates(aux_by_ip, aux_candidates, plan)
                        if (
                            allow_create_missing
                            and not strict_aux_ips
                            and flavor_mode != FLAVOR_MODE_WAWACLOUD_20
                            and not aux_ip_range_is_hard(plan)
                            and selected_aux
                            and self._is_subeni_quota_error(exc)
                        ):
                            partial_aux_allowed = True
                            emit(
                                f"辅助弹性网卡已达到当前ECS规格配额上限，保留已有 {len(selected_aux)}/{plan.aux_count} 条，"
                                "不再反复创建网卡，继续按现有网卡补齐/绑定EIP并进入搭建。"
                            )
                        else:
                            raise
                if len(selected_aux) < plan.aux_count and not partial_aux_allowed:
                    reason = f"{ecs_label}，但辅助弹性网卡不足 {len(selected_aux)}/{plan.aux_count}。"
                    reusable_errors.append(reason)
                    emit(f"{reason}继续检查其他旧资源。")
                    continue
                emit(
                    f"{ecs_label} 网卡满足："
                    f"弹性网卡 {len(standard_rows)}/{plan.standard_nic_count}，"
                    f"虚拟IP {len(virtual_ip_ports)}/{getattr(plan, 'virtual_ip_count', 0)}，"
                    f"辅助弹性网卡 {len(selected_aux)}/{plan.aux_count}。"
                )
                ordered_port_ids = (
                    [row["port_id"] for row in standard_rows]
                    + [str(item.get("id") or "") for item in virtual_ip_ports]
                    + [str(item.get("id") or "") for item in selected_aux]
                )
                port_labels = {
                    **{str(row["port_id"]): self._standard_port_display_label(row) for row in standard_rows},
                    **{str(item.get("id") or ""): self._virtual_ip_display_label(item) for item in virtual_ip_ports},
                    **{str(item.get("id") or ""): self._aux_port_display_label(item) for item in selected_aux},
                }
                effective_eip_count = len(ordered_port_ids) if partial_aux_allowed else expected_eip_count
                if allow_create_missing:
                    eips = self._ensure_eips_for_ports(
                        client,
                        pair,
                        plan,
                        ordered_port_ids,
                        prefix,
                        emit,
                        static_bgp_enabled=static_bgp_enabled,
                        port_labels=port_labels,
                        excluded_public_ips=excluded_public_ips,
                        candidate_seed_ips=new_ip_seed_addresses,
                        online_ip_range_manifest=online_ip_range_manifest,
                    )
                else:
                    eips = []
                    for port_id in ordered_port_ids:
                        for ip_addr in eips_by_port.get(port_id, []):
                            if ip_addr not in eips:
                                eips.append(ip_addr)
                primary_public_ip = eips[0] if eips else client.public_ip_from_server(detail)
                if len(eips) < effective_eip_count:
                    reason = f"{ecs_label}，但绑定到该实例网卡的公网IP不足 {len(eips)}/{effective_eip_count}。"
                    reusable_errors.append(reason)
                    emit(f"{reason}继续检查其他旧资源。")
                    continue
                ip_txt = format_ip_txt(selected_aux)
                if not ip_txt.strip():
                    reason = f"{ecs_label}，但未生成有效 ip.txt。"
                    reusable_errors.append(reason)
                    emit(f"{reason}{action_text}。")
                    continue
                source_note = "同前缀" if name_matched else "非当前前缀"
                if flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
                    emit(f"发现已有资源满足20规格购买拓扑({source_note})：ECS={ecs_name}，公网IP={len(eips)}/{expected_eip_count}。")
                elif partial_aux_allowed:
                    emit(
                        f"发现旧资源达到当前ECS规格可用上限({source_note})：ECS={ecs_name}，"
                        f"辅助弹性网卡={len(selected_aux)}/{plan.aux_count}，公网IP={len(eips)}/{effective_eip_count}，"
                        "直接继续搭建。"
                    )
                elif strict_aux_ips:
                    emit(f"发现旧资源满足规格({source_note})：ECS={ecs_name}，公网IP={len(eips)}/{expected_eip_count}，该地区复用已有资源继续后续任务。")
                else:
                    emit(f"发现已有资源满足搭建条件({source_note})：ECS={ecs_name}，公网IP={len(eips)}/{expected_eip_count}，直接搭建。")
                self.call_ui(
                    self.update_region_row,
                    plan,
                    status="复用已有资源，继续后续",
                    server_ip=primary_public_ip or eips[0],
                    server_id=server_id,
                )
                result = RegionBuildResult(
                    plan=plan,
                    server_id=server_id,
                    server_ip=primary_public_ip or eips[0],
                    password=password,
                    ip_txt=ip_txt,
                    public_ips=eips[:effective_eip_count],
                    root_volume_type=ROOT_VOLUME_TYPE,
                    script_exit_code=-1,
                )
                setattr(result, "_huawei_cloud_server_name", ecs_name)
                return result
            except Exception as exc:
                reason = f"旧ECS {name or '未命名'} 检查失败: {exc}"
                reusable_errors.append(reason)
                emit(f"{reason}，{action_text}")
        if allow_create_missing and candidates:
            raise CloudApiError(
                "已发现同地区已有满足配置的运行中云服务器，但未能完成网卡/EIP补齐；为避免重复购买ECS，本地区保留已有ECS并继续按缺失资源补齐。"
                f"请查看日志处理旧资源问题。失败明细: {' | '.join(reusable_errors[:5]) or '无详细原因'}"
            )
        return None

    @staticmethod
    def _huawei_worker_source_runner() -> Optional[Path]:
        """优先使用外置源码启动华为云自动购买，避开打包 exe 子进程缺少 pyexpat/Tcl 目录的问题。"""
        candidates: List[Path] = []
        env_source = os.environ.get("WAWA_HOT_UPDATE_SOURCE_DIR", "").strip()
        if env_source:
            candidates.append(Path(env_source))
        try:
            candidates.append(Path(__file__).resolve().parent.parent)
        except OSError:
            pass
        if getattr(sys, "frozen", False):
            try:
                candidates.append(Path(sys.executable).resolve().parent / "source")
            except OSError:
                pass
        seen: Set[str] = set()
        for candidate in candidates:
            try:
                source_root = candidate.resolve()
            except OSError:
                continue
            source_text = str(source_root).lower()
            if source_text in seen:
                continue
            seen.add(source_text)
            runner = source_root / "HUAWEI" / "source_runner.py"
            if runner.exists() and (source_root / "HUAWEI" / "api" / "main.py").exists():
                return runner
        return None

    def _huawei_wwwroot_worker_command(self) -> List[str]:
        source_runner = self._huawei_worker_source_runner()
        if getattr(sys, "frozen", False):
            # 便携版默认必须自带可运行 worker，不依赖用户电脑安装 python/py；外部 Python 仅保留为手动排障开关。
            if os.environ.get("WAWA_HUAWEI_WORKER_EXTERNAL_PYTHON") == "1" and source_runner:
                python_exe = shutil.which("python") or shutil.which("python3")
                if python_exe:
                    return [python_exe, "-B", "-u", str(source_runner)]
                py_launcher = shutil.which("py")
                if py_launcher:
                    return [py_launcher, "-3", "-B", "-u", str(source_runner)]
            return [sys.executable, "--huawei-wwwroot-worker"]
        if source_runner:
            return [sys.executable, "-B", "-u", str(source_runner)]
        return [sys.executable, "-u", os.path.abspath(__file__), "--huawei-wwwroot-worker"]

    @staticmethod
    def _decode_worker_line(raw: bytes) -> str:
        if not raw:
            return ""
        candidates = []
        for encoding in ("utf-8-sig", "utf-8", "gb18030", "cp936", "mbcs"):
            try:
                text = raw.decode(encoding, errors="replace")
            except LookupError:
                continue
            score = text.count("\ufffd") + text.count("锟")
            candidates.append((score, text))
        if candidates:
            candidates.sort(key=lambda item: item[0])
            return candidates[0][1]
        return raw.decode("utf-8", errors="replace")

    @staticmethod
    def _hide_resource_ids_in_log_text(text: str) -> str:
        cleaned = str(text or "")
        uuid = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
        # 中文注释：日志面向用户阅读，云资源 UUID 只保留在内存元数据里用于 API 操作，不直接写入实时日志/导出日志。
        cleaned = re.sub(r"(步骤:\s*创建VPC)\s+" + uuid, r"\1完成", cleaned, flags=re.I)
        cleaned = re.sub(r"(步骤:\s*创建子网)\s+" + uuid, r"\1完成", cleaned, flags=re.I)
        cleaned = re.sub(r"(步骤:\s*创建安全组)\s+" + uuid, r"\1完成", cleaned, flags=re.I)
        cleaned = re.sub(r"(步骤:\s*购买云服务器)\s+" + uuid, r"\1", cleaned, flags=re.I)
        cleaned = re.sub(r"(步骤:\s*创建辅助网卡)\s+" + uuid, r"\1", cleaned, flags=re.I)
        cleaned = re.sub(r"(步骤:\s*创建虚拟IP\s+\d{1,3}(?:\.\d{1,3}){3})\s+port=" + uuid, r"\1", cleaned, flags=re.I)
        cleaned = re.sub(r"步骤:\s*云服务器ID\s+" + uuid, "步骤: 云服务器已创建", cleaned, flags=re.I)
        cleaned = re.sub(r"服务器ID列表=\[[^\]]+\]", "服务器已创建", cleaned, flags=re.I)
        cleaned = re.sub(r"服务器ID[:：]\s*" + uuid, "服务器已创建", cleaned, flags=re.I)
        cleaned = re.sub(r"云服务器ID\s+" + uuid, "云服务器已创建", cleaned, flags=re.I)
        cleaned = re.sub(r"(步骤:\s*购买云服务器)\s+job_id=\S+", r"\1已提交", cleaned, flags=re.I)
        cleaned = re.sub(r"任务ID=\S+\s+订单ID=\S+", "云服务器任务已提交", cleaned, flags=re.I)
        cleaned = re.sub(r"归属ECS\s+" + uuid + r"\(([^)]*)\)", r"归属ECS(\1)", cleaned, flags=re.I)
        cleaned = re.sub(r"归属服务器ID:" + uuid + r"\s+name=([^\s，,；;]+)", r"归属ECS(\1)", cleaned, flags=re.I)
        cleaned = re.sub(r"\bid=" + uuid, "", cleaned, flags=re.I)
        cleaned = re.sub(r"\bimage=" + uuid, "", cleaned, flags=re.I)
        cleaned = re.sub(r"\b(?:server_id|server id|resource_id|publicip_id|port_id|job_id|order_id)=" + uuid, "", cleaned, flags=re.I)
        cleaned = re.sub(uuid, "", cleaned, flags=re.I)
        cleaned = cleaned.replace("内部ID=已隐藏", "").replace("内部ID已隐藏", "")
        cleaned = re.sub(r"[ \t]{2,}", " ", cleaned)
        cleaned = re.sub(r"\s+([。,.，；;:：])", r"\1", cleaned)
        return cleaned

    @classmethod
    def _sanitize_autobuy_output(cls, text: str) -> str:
        cleaned = text.replace("wwwroot", "自动购买").replace("WWWROOT", "自动购买")
        return localize_region_tokens(cls._hide_resource_ids_in_log_text(cleaned))

    @staticmethod
    def _is_autobuy_immediate_progress(text: str) -> bool:
        # 中文注释：这些是用户最容易误以为卡死的阶段，收到后必须立即刷新到实时日志。
        return any(
            token in text
            for token in (
                "自动购买预检",
                "检查地区项目ID",
                "自动购买地区并发",
                "开始地区",
                "创建VPC",
                "购买云服务器",
                "申请EIP",
                "绑定EIP",
            )
        )

    def _run_huawei_autobuy_process(
        self,
        client: HuaweiCloudClient,
        region_keys: Sequence[str],
        flavor_mode: str,
        emit: Callable[[str], None],
        pairs: Optional[Dict[str, ProjectPair]] = None,
        x1_flavor_enabled: bool = False,
        x1_ten_ip_enabled: bool = False,
        static_bgp_enabled: bool = False,
        on_demand_billing_enabled: bool = False,
        resource_set_index: int = 0,
    ) -> List[str]:
        clean_region_keys = [key for key in region_keys if key]
        if not clean_region_keys:
            return []
        source_dir = huawei_wwwroot_source_dir()
        env = os.environ.copy()
        env["PYTHONIOENCODING"] = "utf-8"
        env["PYTHONUTF8"] = "1"
        # 子进程不需要 Tk 界面，清掉打包 exe 传下来的 Tcl/Tk 路径，避免热更新源码 worker 继承失效的 _MEI 临时目录。
        for tk_env_key in ("TCL_LIBRARY", "TK_LIBRARY", "TCLLIBPATH"):
            env.pop(tk_env_key, None)
        env["HUAWEI_AK"] = client.ak
        env["HUAWEI_SK"] = client.sk
        env["WAWACLOUD_HUAWEI_AK"] = client.ak
        env["WAWACLOUD_HUAWEI_SK"] = client.sk
        env["HUAWEI_REGIONS"] = ",".join(clean_region_keys)
        env["HUAWEI_REGION_WORKERS"] = str(HUAWEI_AUTOBUY_REGION_WORKERS)
        project_map: Dict[str, str] = {}
        static_region_ids: List[str] = []
        if pairs:
            for key in clean_region_keys:
                plan = REGION_BY_KEY.get(key) or REGION_BY_CODE.get(key)
                if not plan:
                    continue
                pair = pairs.get(plan.region) or pairs.get(plan.key) or pairs.get(key)
                if not pair or not pair.project_id:
                    continue
                project_map[plan.region] = pair.project_id
                project_map[plan.key] = pair.project_id
                if self._static_bgp_enabled_for_plan(plan, static_bgp_enabled):
                    static_region_ids.append(plan.region)
            if project_map:
                env["HUAWEI_PROJECT_IDS_JSON"] = json.dumps(project_map, ensure_ascii=False)
        env["HUAWEI_STATIC_BGP_REGION_IDS"] = ",".join(static_region_ids)
        if resource_set_index > 0:
            env["HUAWEI_RESOURCE_SET_INDEX"] = str(resource_set_index)
        else:
            env.pop("HUAWEI_RESOURCE_SET_INDEX", None)
        if x1_flavor_enabled and flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            env["HUAWEI_PURCHASE_MODE"] = "x1.20"
        elif x1_flavor_enabled and flavor_mode == FLAVOR_MODE_WAWACLOUD_LOW:
            env["HUAWEI_PURCHASE_MODE"] = HUAWEI_X1_1C1G_FLAVOR_ID
        elif flavor_mode == FLAVOR_MODE_FIXED:
            env["HUAWEI_PURCHASE_MODE"] = "delta"
        elif flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            env["HUAWEI_PURCHASE_MODE"] = "20"
        else:
            env.pop("HUAWEI_PURCHASE_MODE", None)
        if (
            x1_ten_ip_enabled
            and x1_flavor_enabled
            and flavor_mode in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED}
        ):
            env["HUAWEI_X1_IP_COUNT"] = "10"
        else:
            env.pop("HUAWEI_X1_IP_COUNT", None)
        if on_demand_billing_enabled:
            env["WAWA_ON_DEMAND_DEBUG_HUAWEI"] = "1"
            emit("步骤: 华为云按需计费已开启，自动购买ECS将使用按需计费。")
        else:
            env["WAWA_ON_DEMAND_DEBUG_HUAWEI"] = "0"
        env["WAWA_PROJECT_ROOT"] = os.path.dirname(os.path.dirname(source_dir))
        env["WAWA_WWWROOT"] = os.path.dirname(os.path.dirname(source_dir))

        command = self._huawei_wwwroot_worker_command()
        if flavor_mode == FLAVOR_MODE_FIXED:
            mode_text = "2vCPUs | 2GiB"
        elif flavor_mode == FLAVOR_MODE_WAWACLOUD_20:
            mode_text = "20"
        else:
            mode_text = "1vCPUs | 1GiB"
        if x1_flavor_enabled:
            mode_text += " | X1"
        if x1_ten_ip_enabled and flavor_mode in {FLAVOR_MODE_WAWACLOUD_LOW, FLAVOR_MODE_FIXED}:
            mode_text += " | 10IP"
        display_regions = [(REGION_BY_KEY.get(key) or REGION_BY_CODE.get(key)).label if (REGION_BY_KEY.get(key) or REGION_BY_CODE.get(key)) else key for key in clean_region_keys]
        emit(f"步骤: 调用自动购买流程 mode={mode_text} 地区={display_regions}")
        for key in clean_region_keys:
            plan = REGION_BY_KEY.get(key)
            if plan:
                self.call_ui(self.update_region_row, plan, status="自动购买中")
                self.region_stage[plan.key] = "autobuy"
        emit("步骤: 自动购买子进程启动中，正在加载华为云购买模块。")
        output_lines: List[str] = []
        pending_lines: List[str] = []
        last_flush = time.monotonic()

        def flush_pending(force: bool = False) -> None:
            nonlocal last_flush
            if not pending_lines:
                return
            if not force and len(pending_lines) < 12 and time.monotonic() - last_flush < 0.35:
                return
            emit("\n".join(pending_lines))
            pending_lines.clear()
            last_flush = time.monotonic()

        process = subprocess.Popen(
            command,
            cwd=source_dir,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=False,
            bufsize=0,
            creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
        )
        emit("步骤: 自动购买子进程已启动，进入项目预检。")
        emit("步骤: 正在检查 AK/SK、地区项目ID和购买环境，请稍候。")
        self.child_processes.add(process)
        assert process.stdout is not None
        try:
            for raw_line in process.stdout:
                if self.stop_event.is_set():
                    process.terminate()
                    raise RuntimeError("用户请求停止，已终止自动购买流程。")
                line = self._decode_worker_line(raw_line).rstrip()
                if not line:
                    continue
                output_lines.append(line)
                sanitized_line = self._sanitize_autobuy_output(line)
                pending_lines.append(sanitized_line)
                flush_pending(force=self._is_autobuy_immediate_progress(sanitized_line))
        finally:
            flush_pending(force=True)
            try:
                process.stdout.close()
            except Exception:
                pass
            self.child_processes.discard(process)
        return_code = process.wait()
        if return_code != 0:
            tail = "\n".join(self._sanitize_autobuy_output(line) for line in output_lines[-30:])
            if huawei_autobuy_created_server(output_lines):
                emit(
                    "步骤: 自动购买子流程返回异常，但已返回云服务器；"
                    "为避免重复购买，直接进入云上反查/补齐。"
                )
                if tail:
                    emit("自动购买异常尾部日志:\n" + tail)
            else:
                if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 and self._is_eip_quota_insufficient_text(tail):
                    message = self._eip_quota_message(
                        REGION_BY_KEY.get(clean_region_keys[0]) or REGION_BY_CODE.get(clean_region_keys[0]) or REGION_PLANS[0],
                        20,
                        20,
                        tail,
                    )
                    self.call_ui_sync(self.show_error, message)
                    raise CloudApiError(message)
                raise CloudApiError(f"自动购买失败，退出码 {return_code}。\n{tail}")
        emit("步骤: 自动购买流程完成/已有云服务器，开始按云上资源反查 ECS/网卡/EIP。")
        return output_lines

    @staticmethod
    def _mark_autobuy_new_purchase_result(result: RegionBuildResult) -> RegionBuildResult:
        # 中文注释：RegionBuildResult 来自公共模型，不新增字段，避免影响三云共享结构；
        # 仅在华为云本轮自动购买链路内加运行时标记，用于后续上海 SSH 就绪补偿。
        setattr(result, "_huawei_autobuy_new_purchase", True)
        return result

    @staticmethod
    def _autobuy_confirm_window(
        plan: RegionPlan,
        flavor_mode: str,
        has_created_server_id: bool,
    ) -> Tuple[int, int, bool]:
        # 中文注释：上海新购 ECS 偶发 server_id 已返回但 ECS 查询详情仍 404；
        # 仅在确认已创建服务器 ID 时延长反查，避免售罄/未创建资源时空等，也不影响 20IP 拓扑。
        extended = (
            plan.key == "shanghai"
            and flavor_mode != FLAVOR_MODE_WAWACLOUD_20
            and has_created_server_id
        )
        if extended:
            return (
                SHANGHAI_NEW_PURCHASE_CONFIRM_ATTEMPTS,
                SHANGHAI_NEW_PURCHASE_CONFIRM_DELAY_SECONDS,
                True,
            )
        return HUAWEI_AUTOBUY_CONFIRM_ATTEMPTS, HUAWEI_AUTOBUY_CONFIRM_DELAY_SECONDS, False

    def _run_huawei_wwwroot_purchase_region(
        self,
        client: HuaweiCloudClient,
        plan: RegionPlan,
        pair: ProjectPair,
        password: str,
        flavor_mode: str,
        emit: Callable[[str], None],
        x1_flavor_enabled: bool = False,
        x1_ten_ip_enabled: bool = False,
        static_bgp_enabled: bool = False,
        on_demand_billing_enabled: bool = False,
        resource_set_index: int = 0,
        excluded_server_ids: Optional[Sequence[str]] = None,
    ) -> RegionBuildResult:
        output_lines = self._run_huawei_autobuy_process(
            client,
            [plan.key],
            flavor_mode,
            emit,
            pairs={pair.region: pair, plan.key: pair},
            x1_flavor_enabled=x1_flavor_enabled,
            x1_ten_ip_enabled=x1_ten_ip_enabled,
            static_bgp_enabled=static_bgp_enabled,
            on_demand_billing_enabled=on_demand_billing_enabled,
            resource_set_index=resource_set_index,
        )
        ready_result = self._result_from_autobuy_ready_summary(
            plan,
            huawei_autobuy_ready_summaries_by_plan(output_lines).get(plan.key),
            password,
        )
        preferred_server_ids = huawei_autobuy_server_ids_by_plan(output_lines).get(plan.key, [])
        prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
        last_reason = "未开始反查"
        last_signature = ""
        same_failure_count = 0
        confirm_attempts, confirm_delay, extended_confirm = self._autobuy_confirm_window(
            plan,
            flavor_mode,
            bool(preferred_server_ids),
        )
        if extended_confirm:
            emit(f"上海新购资源已返回服务器，延长云端同步反查至 {confirm_attempts} 次。")
        for check_index in range(1, confirm_attempts + 1):
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")
            if check_index > 1:
                time.sleep(confirm_delay)
            try:
                result = self._find_reusable_region_result(
                    client,
                    plan,
                    pair,
                    prefix,
                    password,
                    emit,
                    strict_aux_ips=False,
                    allow_create_missing=True,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    preferred_server_ids=preferred_server_ids,
                    excluded_server_ids=excluded_server_ids,
                    static_bgp_enabled=static_bgp_enabled,
                )
            except Exception as exc:
                last_reason = str(exc)
                if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 and self._is_eip_quota_insufficient_text(last_reason):
                    raise CloudApiError(last_reason) from exc
                if self._is_confirmed_eip_sold_out_text(last_reason):
                    raise CloudApiError(
                        f"{plan.label} 弹性公网IP地址池已确认售罄，已保留已有ECS/网卡并停止自动购买后反查；"
                        "待该地区公网IP恢复后重新执行，会复用已有资源继续补齐/搭建。"
                    ) from exc
                signature = normalize_failure_signature(last_reason)
                same_failure_count = same_failure_count + 1 if signature and signature == last_signature else 1
                last_signature = signature
                emit(f"自动购买后反查第 {check_index}/{confirm_attempts} 次失败: {exc}")
                if same_failure_count >= 2 and not huawei_retryable_reconcile_error(last_reason):
                    if ready_result:
                        emit("自动购买已返回服务器IP和ip.txt，云端短反查未确认，保留该结果进入后续搭建。")
                        self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=ready_result.server_ip, server_id=ready_result.server_id)
                        return self._mark_autobuy_new_purchase_result(ready_result)
                    raise CloudApiError(f"自动购买后反查连续2次同类失败，停止重试: {last_reason}") from exc
                continue
            if result:
                emit("步骤: 已确认自动购买资源满足20规格购买拓扑。" if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 else "步骤: 已确认自动购买资源满足搭建条件。")
                self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=result.server_ip, server_id=result.server_id)
                return self._mark_autobuy_new_purchase_result(result)
            last_reason = "云上暂未发现满足20规格购买拓扑的 ECS/网卡/虚拟IP/EIP" if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 else "云上暂未发现满足搭建条件的 ECS/网卡/EIP"
            emit(f"自动购买后反查第 {check_index}/{confirm_attempts} 次未就绪，继续等待云端同步。")
        if ready_result:
            emit("自动购买已返回服务器IP和ip.txt，云端短反查未确认，保留该结果进入后续搭建。")
            self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=ready_result.server_ip, server_id=ready_result.server_id)
            return self._mark_autobuy_new_purchase_result(ready_result)
        raise CloudApiError(f"自动购买已结束，但资源反查未确认完成: {last_reason}")

    @staticmethod
    def _tag_huawei_resource_set_result(
        result: RegionBuildResult,
        set_index: int,
        set_total: int,
    ) -> RegionBuildResult:
        setattr(result, "_huawei_resource_set_index", set_index)
        setattr(result, "_huawei_resource_set_total", set_total)
        return result

    def _upsert_last_result_by_server(self, result: RegionBuildResult) -> None:
        server_id = str(result.server_id or "").strip()
        server_ip = str(result.server_ip or "").strip()

        def keep(old: RegionBuildResult) -> bool:
            if old.plan.region != result.plan.region:
                return True
            old_server_id = str(old.server_id or "").strip()
            old_server_ip = str(old.server_ip or "").strip()
            if server_id and old_server_id:
                return old_server_id != server_id
            return not server_ip or old_server_ip != server_ip

        self.last_results = [old for old in self.last_results if keep(old)]
        self.last_results.append(result)

    def _ensure_region_resource_sets(
        self,
        client: HuaweiCloudClient,
        plan: RegionPlan,
        pair: ProjectPair,
        password: str,
        flavor_mode: str,
        target_set_count: int,
        *,
        x1_flavor_enabled: bool = False,
        x1_ten_ip_enabled: bool = False,
        static_bgp_enabled: bool = False,
        on_demand_billing_enabled: bool = False,
        on_result: Optional[Callable[[RegionBuildResult], None]] = None,
    ) -> List[RegionBuildResult]:
        target_set_count = max(1, int(target_set_count or 1))
        prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
        results: List[RegionBuildResult] = []
        used_server_ids: set[str] = set()
        occupied_resource_set_indexes: set[int] = set()

        def emit(message: str) -> None:
            self.after(0, self.log, f"[{plan.label}] {localize_region_tokens(message)}")

        for set_index in range(1, target_set_count + 1):
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")
            last_error = ""
            last_signature = ""
            repeat_count = 0
            result: Optional[RegionBuildResult] = None
            resource_set_index_for_creation: Optional[int] = None
            for attempt in range(1, REGION_RETRY_ATTEMPTS + 1):
                if self.stop_event.is_set():
                    raise RuntimeError("用户请求停止，跳过后续步骤。")
                try:
                    result = self._find_reusable_region_result(
                        client,
                        plan,
                        pair,
                        prefix,
                        password,
                        emit,
                        strict_aux_ips=False,
                        allow_create_missing=True,
                        flavor_mode=flavor_mode,
                        x1_flavor_enabled=x1_flavor_enabled,
                        excluded_server_ids=used_server_ids,
                        static_bgp_enabled=static_bgp_enabled,
                    )
                    if result is None:
                        if resource_set_index_for_creation is None:
                            if 0 not in occupied_resource_set_indexes:
                                resource_set_index_for_creation = 0
                            else:
                                resource_set_index_for_creation = 2
                                while resource_set_index_for_creation in occupied_resource_set_indexes:
                                    resource_set_index_for_creation += 1
                        self.call_ui(
                            self.update_region_row,
                            plan,
                            status=f"创建第{set_index}/{target_set_count}套资源",
                        )
                        emit(
                            f"未发现可复用的第 {set_index}/{target_set_count} 套完整资源，"
                            "开始创建独立VPC、子网、安全组、ECS和EIP。"
                        )
                        result = self._run_huawei_wwwroot_purchase_region(
                            client,
                            plan,
                            pair,
                            password,
                            flavor_mode,
                            emit,
                            x1_flavor_enabled=x1_flavor_enabled,
                            x1_ten_ip_enabled=x1_ten_ip_enabled,
                            static_bgp_enabled=static_bgp_enabled,
                            on_demand_billing_enabled=on_demand_billing_enabled,
                            resource_set_index=resource_set_index_for_creation,
                            excluded_server_ids=used_server_ids,
                        )
                    server_id = str(result.server_id or "").strip()
                    if not server_id:
                        raise CloudApiError(
                            f"{plan.label}第 {set_index} 套资源未返回ECS ID，"
                            "为避免重复购买，已停止继续创建后续资源组。"
                        )
                    if server_id in used_server_ids:
                        raise CloudApiError(
                            f"{plan.label}第 {set_index} 套资源反查重复命中已有ECS，"
                            "为避免重复绑定网卡，已停止本地区任务。"
                        )
                    used_server_ids.add(server_id)
                    cloud_server_name = str(
                        getattr(result, "_huawei_cloud_server_name", "") or ""
                    ).strip()
                    set_match = re.search(
                        r"-set-(\d+)$",
                        cloud_server_name,
                        flags=re.IGNORECASE,
                    )
                    if set_match:
                        occupied_resource_set_indexes.add(int(set_match.group(1)))
                    elif resource_set_index_for_creation is not None:
                        occupied_resource_set_indexes.add(resource_set_index_for_creation)
                    else:
                        occupied_resource_set_indexes.add(0)
                    self._tag_huawei_resource_set_result(result, set_index, target_set_count)
                    results.append(result)
                    if on_result is not None:
                        on_result(result)
                    emit(
                        f"第 {set_index}/{target_set_count} 套资源已就绪，"
                        f"本套EIP {len(result.public_ips)}/{expected_public_ip_count(plan)}。"
                    )
                    break
                except Exception as exc:
                    last_error = localize_region_tokens(str(exc))
                    if (
                        "待支付" in last_error
                        or "PENDING_PAYMENT" in last_error
                        or self._is_confirmed_eip_sold_out_text(last_error)
                        or self._is_subeni_quota_text(last_error)
                    ):
                        raise
                    signature = normalize_failure_signature(last_error)
                    repeat_count = repeat_count + 1 if signature and signature == last_signature else 1
                    last_signature = signature
                    if attempt >= REGION_RETRY_ATTEMPTS or (
                        repeat_count >= 2 and not huawei_retryable_reconcile_error(last_error)
                    ):
                        raise CloudApiError(
                            f"{plan.label}第 {set_index}/{target_set_count} 套资源处理失败: {last_error}"
                        ) from exc
                    emit(
                        f"第 {set_index}/{target_set_count} 套资源处理失败，"
                        f"{REGION_RETRY_DELAY_SECONDS}秒后重试 {attempt + 1}/{REGION_RETRY_ATTEMPTS}: {last_error}"
                    )
                    time.sleep(REGION_RETRY_DELAY_SECONDS)
            if result is None:
                raise CloudApiError(
                    f"{plan.label}第 {set_index}/{target_set_count} 套资源未完成: {last_error or '未知错误'}"
                )
        return results

    def _run_purchase_eip_quota_task(
        self,
        client: HuaweiCloudClient,
        plans: Sequence[RegionPlan],
        pairs: Dict[str, ProjectPair],
        target_set_counts: Dict[str, int],
        *,
        password: str,
        flavor_mode: str,
        x1_flavor_enabled: bool,
        x1_ten_ip_enabled: bool,
        static_bgp_enabled: bool,
        on_demand_billing_enabled: bool,
        do_build_after_purchase: bool,
        ssh_port: int,
        ssh_user: str,
        script_path: str,
        ros_build_enabled: bool,
        ros_config: RosBuildConfig,
        task_connection_env: Dict[str, str],
        action_name: str,
    ) -> None:
        results_by_region: Dict[str, List[RegionBuildResult]] = {
            plan.region: [] for plan in plans
        }
        purchase_failures: Dict[str, str] = {}
        result_lock = threading.Lock()

        def remember_result(result: RegionBuildResult) -> None:
            with result_lock:
                region_results = results_by_region.setdefault(result.plan.region, [])
                server_id = str(result.server_id or "").strip()
                if server_id and any(
                    str(existing.server_id or "").strip() == server_id
                    for existing in region_results
                ):
                    return
                region_results.append(result)

        def ensure_plan(plan: RegionPlan) -> Tuple[RegionPlan, List[RegionBuildResult], str]:
            target_count = target_set_counts[plan.key]
            pair = pairs[plan.region]
            try:
                results = self._ensure_region_resource_sets(
                    client,
                    plan,
                    pair,
                    password,
                    flavor_mode,
                    target_count,
                    x1_flavor_enabled=x1_flavor_enabled,
                    x1_ten_ip_enabled=x1_ten_ip_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                    on_demand_billing_enabled=on_demand_billing_enabled,
                    on_result=remember_result,
                )
                return plan, results, ""
            except Exception as exc:
                return plan, list(results_by_region.get(plan.region, [])), localize_region_tokens(str(exc))

        region_workers = min(HUAWEI_AUTOBUY_REGION_WORKERS, len(plans))
        self.after(
            0,
            self.log,
            f"步骤: 按EIP配额补齐独立资源组，并发地区数 {region_workers}，"
            f"地区={[plan.label for plan in plans]}",
        )
        with concurrent.futures.ThreadPoolExecutor(max_workers=region_workers) as executor:
            future_map = {executor.submit(ensure_plan, plan): plan for plan in plans}
            for future in concurrent.futures.as_completed(future_map):
                plan, results, error_text = future.result()
                if error_text:
                    purchase_failures[plan.key] = error_text
                    self.call_ui(
                        self.update_region_row,
                        plan,
                        status=f"资源组失败 {len(results)}/{target_set_counts[plan.key]}套",
                    )
                    self.after(0, self.log, f"[{plan.label}] {error_text}")
                else:
                    self.call_ui(
                        self.update_region_row,
                        plan,
                        status=f"购买完成 {len(results)}/{target_set_counts[plan.key]}套",
                        server_ip=results[0].server_ip if results else "",
                        server_id=results[0].server_id if results else "",
                    )

        ordered_results: List[RegionBuildResult] = []
        for plan in plans:
            region_results = results_by_region.get(plan.region, [])
            region_results.sort(
                key=lambda result: int(getattr(result, "_huawei_resource_set_index", 0) or 0)
            )
            ordered_results.extend(region_results)

        new_purchase_results = [
            result
            for result in ordered_results
            if bool(getattr(result, "_huawei_autobuy_new_purchase", False))
        ]
        reused_results = [
            result
            for result in ordered_results
            if not bool(getattr(result, "_huawei_autobuy_new_purchase", False))
        ]
        previous_results = list(self.last_results)

        def previous_result_for(reused: RegionBuildResult) -> Optional[RegionBuildResult]:
            server_id = str(reused.server_id or "").strip()
            server_ip = str(reused.server_ip or "").strip()
            for previous in previous_results:
                if previous.plan.region != reused.plan.region:
                    continue
                previous_server_id = str(previous.server_id or "").strip()
                previous_server_ip = str(previous.server_ip or "").strip()
                if server_id and previous_server_id and server_id == previous_server_id:
                    return previous
                if server_ip and previous_server_ip and server_ip == previous_server_ip:
                    return previous
            return None

        selected_regions_with_results = {
            result.plan.region for result in ordered_results
        }
        if selected_regions_with_results:
            self.last_results = [
                old
                for old in self.last_results
                if old.plan.region not in selected_regions_with_results
            ]
        for result in ordered_results:
            if bool(getattr(result, "_huawei_autobuy_new_purchase", False)):
                setattr(result, "_huawei_require_script_result_output", True)
                self._upsert_last_result_by_server(result)
                continue

            previous = previous_result_for(result)
            if previous is not None:
                self._tag_huawei_resource_set_result(
                    previous,
                    int(getattr(result, "_huawei_resource_set_index", 0) or 0),
                    int(getattr(result, "_huawei_resource_set_total", 0) or 0),
                )
                setattr(previous, "_huawei_autobuy_new_purchase", False)
                setattr(previous, "_huawei_reused_existing_resource", True)
                self._upsert_last_result_by_server(previous)
                continue

            setattr(result, "_huawei_reused_existing_resource", True)
            setattr(result, "_huawei_require_script_result_output", True)
            self._upsert_last_result_by_server(result)

        total_set_count = sum(target_set_counts[plan.key] for plan in plans)
        purchased_set_count = len(ordered_results)
        build_failures: Dict[str, str] = {}
        built_results: List[RegionBuildResult] = []
        unavailable_ip_count = 0

        if do_build_after_purchase:
            for plan in plans:
                reused_count = sum(
                    1 for result in reused_results if result.plan.region == plan.region
                )
                new_count = sum(
                    1 for result in new_purchase_results if result.plan.region == plan.region
                )
                if reused_count:
                    self.after(
                        0,
                        self.log,
                        f"[{plan.label}] 已复用旧资源 {reused_count} 套，本次仅搭建新购 {new_count} 套；"
                        "旧ECS不重复执行搭建脚本。",
                    )

            if new_purchase_results:
                build_workers = min(MAX_WORKERS, len(new_purchase_results))
                self.after(
                    0,
                    self.log,
                    f"步骤: 独立资源组购买阶段完成，开始并发搭建本次新购的 "
                    f"{len(new_purchase_results)} 套ECS，并发数 {build_workers}。",
                )
                with concurrent.futures.ThreadPoolExecutor(max_workers=build_workers) as executor:
                    future_map = {
                        executor.submit(
                            self._build_existing_region_with_retry,
                            result,
                            ssh_port,
                            ssh_user,
                            password,
                            script_path,
                            flavor_mode,
                            ros_build_enabled,
                            ros_config,
                            task_connection_env=task_connection_env,
                        ): result
                        for result in new_purchase_results
                    }
                    for future in concurrent.futures.as_completed(future_map):
                        original = future_map[future]
                        set_index = int(getattr(original, "_huawei_resource_set_index", 0) or 0)
                        try:
                            built = future.result()
                            self._tag_huawei_resource_set_result(
                                built,
                                set_index,
                                int(getattr(original, "_huawei_resource_set_total", 0) or 0),
                            )
                            built_results.append(built)
                            unavailable_ip_count += len(getattr(built, "unavailable_public_ips", []))
                            self._upsert_last_result_by_server(built)
                            self.after(0, self.append_result_blocks, built)
                        except Exception as exc:
                            failure_key = f"{original.plan.key}:{set_index}"
                            build_failures[failure_key] = localize_region_tokens(str(exc))
                            self.after(
                                0,
                                self.log,
                                f"[{original.plan.label}] 第 {set_index} 套搭建失败: {exc}",
                            )
            else:
                self.after(
                    0,
                    self.log,
                    "步骤: 所选地区已由旧资源满足目标配额，未重复执行搭建脚本。",
                )
                self.after(0, lambda: self.render_results(restore_local=False))

            for plan in plans:
                target = target_set_counts[plan.key]
                ready_count = sum(
                    1 for result in ordered_results if result.plan.region == plan.region
                )
                reused_count = sum(
                    1 for result in reused_results if result.plan.region == plan.region
                )
                new_count = sum(
                    1 for result in new_purchase_results if result.plan.region == plan.region
                )
                built_count = sum(
                    1 for result in built_results if result.plan.region == plan.region
                )
                completed_count = reused_count + built_count
                if ready_count < target:
                    status = f"资源组失败 {ready_count}/{target}套"
                elif built_count == new_count:
                    status = f"购买并搭建完成 {completed_count}/{target}套"
                    if reused_count:
                        status += f"（复用{reused_count}套）"
                elif built_count:
                    status = f"搭建完成 {completed_count}/{target}套"
                else:
                    status = f"搭建失败 {completed_count}/{target}套"
                self.call_ui(self.update_region_row, plan, status=status)
        else:
            self.after(0, lambda: self.render_results(restore_local=False))

        success_set_count = (
            len(reused_results) + len(built_results)
            if do_build_after_purchase
            else purchased_set_count
        )
        failure_count = len(purchase_failures) + len(build_failures)
        final_log = (
            f"步骤: {action_name}任务结束，成功 {success_set_count}/{total_set_count} 套ECS"
        )
        if do_build_after_purchase and reused_results:
            final_log += (
                f"，复用旧资源 {len(reused_results)} 套未重复搭建，"
                f"新购搭建 {len(built_results)}/{len(new_purchase_results)} 套"
            )
        if failure_count:
            final_log += f"，失败 {failure_count} 项"
        if unavailable_ip_count:
            final_log += f"，SOCKS5不可用IP {unavailable_ip_count} 个"
        self.after(0, self.log, final_log)

        final_popup = (
            f"{action_name}任务结束，成功 {success_set_count}/{total_set_count} 套ECS"
            + (
                f"，复用旧资源 {len(reused_results)} 套未重复搭建，"
                f"新购搭建 {len(built_results)}/{len(new_purchase_results)} 套"
                if do_build_after_purchase and reused_results
                else ""
            )
            + (f"，失败 {failure_count} 项" if failure_count else "")
            + (f"，SOCKS5不可用IP {unavailable_ip_count} 个" if unavailable_ip_count else "")
            + "。"
            + self._huawei_task_popup_details(
                plans,
                flavor_mode=flavor_mode,
                x1_flavor_enabled=x1_flavor_enabled,
                static_bgp_enabled=static_bgp_enabled,
                on_demand_billing_enabled=on_demand_billing_enabled,
                purchase_enabled=True,
                build_enabled=do_build_after_purchase,
                ros_build_enabled=ros_build_enabled,
            )
        )
        final_popup_message = self._queue_result_message_or_popup(final_popup)
        self.after(
            100,
            lambda p=list(plans), message=final_popup_message: self.refresh_resource_views_for_plans(
                p,
                context="purchase",
                final_popup_message=message,
            ),
        )

    def _run_build_selected_server_items(
        self,
        client: HuaweiCloudClient,
        selected_items: Sequence[Dict[str, str]],
        plans: Sequence[RegionPlan],
        *,
        fallback_password: str,
        flavor_mode: str,
        x1_flavor_enabled: bool,
        ssh_port: int,
        ssh_user: str,
        script_path: str,
        ros_build_enabled: bool,
        ros_config: RosBuildConfig,
        task_connection_env: Dict[str, str],
    ) -> None:
        plan_by_region = {plan.region: plan for plan in plans}
        unique_items: List[Dict[str, str]] = []
        seen_targets: Set[Tuple[str, str]] = set()
        for source_item in selected_items:
            item = dict(source_item)
            target = (
                str(item.get("region") or "").strip(),
                str(item.get("server_id") or "").strip(),
            )
            if not all(target) or target in seen_targets or target[0] not in plan_by_region:
                continue
            seen_targets.add(target)
            unique_items.append(item)
        if not unique_items:
            raise ValueError("未读取到可搭建的选定云服务器。")

        missing_pair_regions = {
            str(item.get("region") or "")
            for item in unique_items
            if not str(item.get("project_id") or "").strip()
        }
        missing_pair_plans = [
            plan for plan in plans if plan.region in missing_pair_regions
        ]
        fallback_pairs: Dict[str, ProjectPair] = {}
        if missing_pair_plans:
            self._wait_region_project_warmup()
            fallback_pairs = client.selected_project_pairs(missing_pair_plans)
            self.region_projects.update(fallback_pairs)
        prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
        scan_failures: Dict[Tuple[str, str], str] = {}
        discovered: Dict[Tuple[str, str], RegionBuildResult] = {}

        def scan_item(item: Dict[str, str]) -> Tuple[Tuple[str, str], Optional[RegionBuildResult], str]:
            region = str(item.get("region") or "")
            server_id = str(item.get("server_id") or "")
            target = (region, server_id)
            plan = plan_by_region[region]
            label = str(item.get("label") or plan.label)
            row_iid = str(item.get("row_iid") or "")
            project_id = str(item.get("project_id") or "").strip()
            if not project_id:
                pair = fallback_pairs.get(region)
                project_id = pair.project_id if pair else ""
            if not project_id:
                return target, None, "未找到项目ID"
            pair = ProjectPair(region, project_id)
            self.region_projects[region] = pair

            def emit(message: str) -> None:
                self.after(0, self.log, f"[{label}] {localize_region_tokens(message)}")

            if row_iid:
                self.call_ui(self.update_region_ecs_row, row_iid, status="扫描旧资源")
            try:
                result = self._find_reusable_region_result(
                    client,
                    plan,
                    pair,
                    prefix,
                    fallback_password,
                    emit,
                    strict_aux_ips=False,
                    allow_create_missing=False,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    preferred_server_ids=[server_id],
                    required_server_ids=[server_id],
                    infer_selected_ten_ip=not ros_build_enabled,
                )
            except Exception as exc:
                return target, None, localize_region_tokens(str(exc))
            if result is None:
                return target, None, "所选云服务器未满足当前搭建条件"
            setattr(result, "_huawei_selected_row_iid", row_iid)
            setattr(result, "_huawei_selected_server_label", label)
            return target, result, ""

        scan_workers = min(MAX_WORKERS, len(unique_items))
        self.after(
            0,
            self.log,
            f"步骤: 并发扫描已选云服务器 共{len(unique_items)}台，并发数={scan_workers}",
        )
        with concurrent.futures.ThreadPoolExecutor(max_workers=scan_workers) as executor:
            future_map = {executor.submit(scan_item, item): item for item in unique_items}
            for future in concurrent.futures.as_completed(future_map):
                target, result, reason = future.result()
                item = future_map[future]
                row_iid = str(item.get("row_iid") or "")
                label = str(item.get("label") or target[1])
                if result is not None:
                    discovered[target] = result
                    if row_iid:
                        self.call_ui(
                            self.update_region_ecs_row,
                            row_iid,
                            status="等待搭建",
                            server_ip=result.server_ip,
                        )
                else:
                    scan_failures[target] = reason
                    if row_iid:
                        self.call_ui(self.update_region_ecs_row, row_iid, status=f"不可搭建: {reason}")
                    self.after(0, self.log, f"[{label}] {reason}")

        ordered_results = [
            discovered[target]
            for item in unique_items
            if (target := (
                str(item.get("region") or ""),
                str(item.get("server_id") or ""),
            )) in discovered
        ]
        if not ordered_results:
            raise ValueError("选定云服务器均未满足当前搭建条件，请查看实时日志。")

        built_results: List[RegionBuildResult] = []
        build_failures: Dict[Tuple[str, str], str] = {}
        unavailable_ip_count = 0
        build_workers = min(MAX_WORKERS, len(ordered_results))
        self.after(
            0,
            self.log,
            f"步骤: 开始并发SSH搭建 {len(ordered_results)} 台云服务器，并发数={build_workers}",
        )
        with concurrent.futures.ThreadPoolExecutor(max_workers=build_workers) as executor:
            future_map = {
                executor.submit(
                    self._build_existing_region_with_retry,
                    result,
                    ssh_port,
                    ssh_user,
                    fallback_password,
                    script_path,
                    flavor_mode,
                    ros_build_enabled,
                    ros_config,
                    task_connection_env=task_connection_env,
                ): result
                for result in ordered_results
            }
            for future in concurrent.futures.as_completed(future_map):
                original = future_map[future]
                target = (original.plan.region, str(original.server_id or ""))
                row_iid = str(getattr(original, "_huawei_selected_row_iid", "") or "")
                label = str(
                    getattr(original, "_huawei_selected_server_label", "")
                    or f"{original.plan.label} / {original.server_id}"
                )
                try:
                    built = future.result()
                    built_results.append(built)
                    unavailable_ip_count += len(getattr(built, "unavailable_public_ips", []))
                    self._upsert_last_result_by_server(built)
                    if row_iid:
                        self.call_ui(
                            self.update_region_ecs_row,
                            row_iid,
                            status="搭建完成",
                            server_ip=built.server_ip,
                            server_id=built.server_id,
                        )
                    self.after(0, self.append_result_blocks, built)
                except Exception as exc:
                    reason = localize_region_tokens(str(exc))
                    build_failures[target] = reason
                    if row_iid:
                        self.call_ui(self.update_region_ecs_row, row_iid, status=f"搭建失败: {reason}")
                    self.after(0, self.log, f"[{label}] 搭建失败: {reason}")

        for plan in plans:
            target_count = sum(
                1 for item in unique_items if str(item.get("region") or "") == plan.region
            )
            success_count = sum(
                1 for result in built_results if result.plan.region == plan.region
            )
            self.call_ui(
                self.update_region_row,
                plan,
                status=f"搭建完成 {success_count}/{target_count}台" if success_count else f"搭建失败 0/{target_count}台",
            )

        failure_count = len(scan_failures) + len(build_failures)
        final_log = f"步骤: 搭建任务结束，成功 {len(built_results)}/{len(unique_items)} 台云服务器"
        if failure_count:
            final_log += f"，失败 {failure_count}"
        if unavailable_ip_count:
            final_log += f"，SOCKS5不可用IP {unavailable_ip_count}"
        self.after(0, self.log, final_log)
        final_popup = (
            f"搭建任务结束，成功 {len(built_results)}/{len(unique_items)} 台云服务器"
            + (f"，失败 {failure_count} 台" if failure_count else "")
            + (f"，SOCKS5不可用IP {unavailable_ip_count} 个" if unavailable_ip_count else "")
            + "。"
            + self._huawei_task_popup_details(
                plans,
                flavor_mode=flavor_mode,
                purchase_enabled=False,
                build_enabled=True,
                ros_build_enabled=ros_build_enabled,
            )
        )
        final_popup_message = self._queue_result_message_or_popup(final_popup)
        self.after(
            100,
            lambda p=list(plans), message=final_popup_message: self.refresh_resource_views_for_plans(
                p,
                context="purchase",
                final_popup_message=message,
            ),
        )

    def _run_build_eip_quota_task(
        self,
        client: HuaweiCloudClient,
        plans: Sequence[RegionPlan],
        target_set_counts: Dict[str, int],
        *,
        fallback_password: str,
        flavor_mode: str,
        x1_flavor_enabled: bool,
        ssh_port: int,
        ssh_user: str,
        script_path: str,
        ros_build_enabled: bool,
        ros_config: RosBuildConfig,
        task_connection_env: Dict[str, str],
    ) -> None:
        self.after(
            0,
            self.log,
            f"步骤: 按EIP配额扫描云上独立资源组，地区={[plan.label for plan in plans]}",
        )
        self._wait_region_project_warmup()
        pairs = client.selected_project_pairs(plans)
        self.region_projects.update(pairs)
        prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
        scan_failures: Dict[str, str] = {}
        results_by_region: Dict[str, List[RegionBuildResult]] = {}

        def scan_plan(plan: RegionPlan) -> Tuple[RegionPlan, List[RegionBuildResult], str]:
            pair = pairs.get(plan.region)
            if pair is None:
                return plan, [], "未找到项目ID"
            target_count = target_set_counts[plan.key]
            found_results: List[RegionBuildResult] = []
            used_server_ids: set[str] = set()

            def emit(message: str) -> None:
                self.after(0, self.log, f"[{plan.label}] {localize_region_tokens(message)}")

            self.call_ui(
                self.update_region_row,
                plan,
                project_id=pair.project_id,
                status=f"扫描资源组 0/{target_count}套",
            )
            try:
                for set_index in range(1, target_count + 1):
                    result = self._find_reusable_region_result(
                        client,
                        plan,
                        pair,
                        prefix,
                        fallback_password,
                        emit,
                        strict_aux_ips=False,
                        allow_create_missing=False,
                        flavor_mode=flavor_mode,
                        x1_flavor_enabled=x1_flavor_enabled,
                        excluded_server_ids=used_server_ids,
                    )
                    if result is None:
                        break
                    server_id = str(result.server_id or "").strip()
                    if not server_id or server_id in used_server_ids:
                        break
                    used_server_ids.add(server_id)
                    self._tag_huawei_resource_set_result(result, set_index, target_count)
                    setattr(result, "_huawei_require_script_result_output", True)
                    found_results.append(result)
                    self.call_ui(
                        self.update_region_row,
                        plan,
                        status=f"扫描资源组 {len(found_results)}/{target_count}套",
                    )
            except Exception as exc:
                return plan, found_results, localize_region_tokens(str(exc))
            if len(found_results) < target_count:
                return (
                    plan,
                    found_results,
                    f"云上仅发现 {len(found_results)}/{target_count} 套满足搭建条件的ECS资源组",
                )
            return plan, found_results, ""

        scan_workers = min(MAX_WORKERS, len(plans))
        with concurrent.futures.ThreadPoolExecutor(max_workers=scan_workers) as executor:
            future_map = {executor.submit(scan_plan, plan): plan for plan in plans}
            for future in concurrent.futures.as_completed(future_map):
                plan, found_results, error_text = future.result()
                results_by_region[plan.region] = found_results
                if error_text:
                    scan_failures[plan.key] = error_text
                    self.after(0, self.log, f"[{plan.label}] {error_text}")

        ordered_results: List[RegionBuildResult] = []
        for plan in plans:
            ordered_results.extend(results_by_region.get(plan.region, []))
        if not ordered_results:
            raise ValueError("选择地区没有可搭建资源：云上未发现满足条件的已有ECS资源组。")

        result_regions = {result.plan.region for result in ordered_results}
        self.last_results = [
            old for old in self.last_results if old.plan.region not in result_regions
        ]
        for result in ordered_results:
            self._upsert_last_result_by_server(result)

        built_results: List[RegionBuildResult] = []
        build_failures: Dict[str, str] = {}
        unavailable_ip_count = 0
        build_workers = min(MAX_WORKERS, len(ordered_results))
        self.after(
            0,
            self.log,
            f"步骤: 开始并发SSH搭建 {len(ordered_results)} 套ECS，并发数={build_workers}",
        )
        with concurrent.futures.ThreadPoolExecutor(max_workers=build_workers) as executor:
            future_map = {
                executor.submit(
                    self._build_existing_region_with_retry,
                    result,
                    ssh_port,
                    ssh_user,
                    fallback_password,
                    script_path,
                    flavor_mode,
                    ros_build_enabled,
                    ros_config,
                    task_connection_env=task_connection_env,
                ): result
                for result in ordered_results
            }
            for future in concurrent.futures.as_completed(future_map):
                original = future_map[future]
                set_index = int(getattr(original, "_huawei_resource_set_index", 0) or 0)
                try:
                    built = future.result()
                    self._tag_huawei_resource_set_result(
                        built,
                        set_index,
                        int(getattr(original, "_huawei_resource_set_total", 0) or 0),
                    )
                    built_results.append(built)
                    unavailable_ip_count += len(getattr(built, "unavailable_public_ips", []))
                    self._upsert_last_result_by_server(built)
                    self.after(0, self.append_result_blocks, built)
                except Exception as exc:
                    build_failures[f"{original.plan.key}:{set_index}"] = localize_region_tokens(str(exc))
                    self.after(
                        0,
                        self.log,
                        f"[{original.plan.label}] 第 {set_index} 套搭建失败: {exc}",
                    )

        for plan in plans:
            target = target_set_counts[plan.key]
            built_count = sum(
                1 for result in built_results if result.plan.region == plan.region
            )
            status = (
                f"搭建完成 {built_count}/{target}套"
                if built_count
                else f"搭建失败 0/{target}套"
            )
            self.call_ui(self.update_region_row, plan, status=status)

        total_set_count = sum(target_set_counts[plan.key] for plan in plans)
        failure_count = len(scan_failures) + len(build_failures)
        final_log = f"步骤: 搭建任务结束，成功 {len(built_results)}/{total_set_count} 套ECS"
        if failure_count:
            final_log += f"，失败 {failure_count} 项"
        if unavailable_ip_count:
            final_log += f"，SOCKS5不可用IP {unavailable_ip_count} 个"
        self.after(0, self.log, final_log)

        final_popup = (
            f"搭建任务结束，成功 {len(built_results)}/{total_set_count} 套ECS"
            + (f"，失败 {failure_count} 项" if failure_count else "")
            + (f"，SOCKS5不可用IP {unavailable_ip_count} 个" if unavailable_ip_count else "")
            + "。"
            + self._huawei_task_popup_details(
                plans,
                flavor_mode=flavor_mode,
                purchase_enabled=False,
                build_enabled=True,
                ros_build_enabled=ros_build_enabled,
            )
        )
        final_popup_message = self._queue_result_message_or_popup(final_popup)
        self.after(
            100,
            lambda p=list(plans), message=final_popup_message: self.refresh_resource_views_for_plans(
                p,
                context="purchase",
                final_popup_message=message,
            ),
        )

    def _run_huawei_wwwroot_purchase_batch(
        self,
        client: HuaweiCloudClient,
        plans: Sequence[RegionPlan],
        pairs: Dict[str, ProjectPair],
        password: str,
        flavor_mode: str,
        x1_flavor_enabled: bool = False,
        x1_ten_ip_enabled: bool = False,
        static_bgp_enabled: bool = False,
        on_demand_billing_enabled: bool = False,
    ) -> Tuple[Dict[str, RegionBuildResult], Dict[str, str]]:
        prefix = safe_optional_name(self.resource_prefix_var.get(), 32)
        results: Dict[str, RegionBuildResult] = {}
        failures: Dict[str, str] = {}
        need_purchase: List[RegionPlan] = []
        need_confirm_only: List[RegionPlan] = []

        def emit_plan(plan: RegionPlan, message: str) -> None:
            self.after(0, self.log, f"[{plan.label}] {localize_region_tokens(message)}")

        for plan in plans:
            pair = pairs[plan.region]
            self.call_ui(self.update_region_row, plan, project_id=pair.project_id, status="开始")
            self.region_stage[plan.key] = "start"
            emit_plan(plan, f"步骤: 开始地区 {plan.label}")
            try:
                reusable = self._find_reusable_region_result(
                    client,
                    plan,
                    pair,
                    prefix,
                    password,
                    lambda message, p=plan: emit_plan(p, message),
                    strict_aux_ips=False,
                    allow_create_missing=True,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                )
            except Exception as exc:
                failures[plan.key] = str(exc)
                if huawei_existing_ecs_guard_error(str(exc)):
                    emit_plan(plan, f"复用资源检查发现已有ECS，本轮不重复购买，转入反查/补齐: {exc}")
                    need_confirm_only.append(plan)
                else:
                    emit_plan(plan, f"复用资源检查失败，转入自动购买补齐流程: {exc}")
                    need_purchase.append(plan)
                continue
            if reusable:
                self.region_stage[plan.key] = "done"
                self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=reusable.server_ip, server_id=reusable.server_id)
                results[plan.key] = reusable
            else:
                need_purchase.append(plan)

        confirm_targets: List[RegionPlan] = []
        for plan in [*need_purchase, *need_confirm_only]:
            if plan.key not in {existing.key for existing in confirm_targets}:
                confirm_targets.append(plan)
        new_purchase_keys = {plan.key for plan in need_purchase}

        if (not need_purchase and not confirm_targets) or self.stop_event.is_set():
            return results, failures

        process_error = ""
        autobuy_server_ids: Dict[str, List[str]] = {}
        autobuy_ready_summaries: Dict[str, Dict[str, str]] = {}
        if need_purchase:
            try:
                output_lines = self._run_huawei_autobuy_process(
                    client,
                    [plan.key for plan in need_purchase],
                    flavor_mode,
                    lambda message: self.after(0, self.log, message),
                    pairs=pairs,
                    x1_flavor_enabled=x1_flavor_enabled,
                    x1_ten_ip_enabled=x1_ten_ip_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                    on_demand_billing_enabled=on_demand_billing_enabled,
                )
                autobuy_server_ids = huawei_autobuy_server_ids_by_plan(output_lines)
                autobuy_ready_summaries = huawei_autobuy_ready_summaries_by_plan(output_lines)
            except Exception as exc:
                process_error = str(exc)
                self.after(0, self.log, f"自动购买流程返回异常，将继续反查已创建资源: {exc}")
        elif confirm_targets:
            self.after(0, self.log, "检测到已有ECS资源，跳过自动购买，直接反查并补齐网卡/EIP。")

        def confirm_plan(plan: RegionPlan) -> RegionBuildResult:
            pair = pairs[plan.region]
            last_reason = process_error or "自动购买后资源未同步"
            last_signature = ""
            same_failure_count = 0
            ready_result = self._result_from_autobuy_ready_summary(plan, autobuy_ready_summaries.get(plan.key), password)
            plan_server_ids = autobuy_server_ids.get(plan.key, [])
            confirm_attempts, confirm_delay, extended_confirm = self._autobuy_confirm_window(
                plan,
                flavor_mode,
                plan.key in new_purchase_keys and bool(plan_server_ids),
            )
            if extended_confirm:
                emit_plan(plan, f"上海新购资源已返回服务器，延长云端同步反查至 {confirm_attempts} 次。")

            def mark_if_new_purchase(result: RegionBuildResult) -> RegionBuildResult:
                if plan.key in new_purchase_keys:
                    self._mark_autobuy_new_purchase_result(result)
                return result

            for check_index in range(1, confirm_attempts + 1):
                if self.stop_event.is_set():
                    raise RuntimeError("用户请求停止，跳过后续步骤。")
                if check_index > 1:
                    time.sleep(confirm_delay)
                try:
                    found = self._find_reusable_region_result(
                        client,
                        plan,
                        pair,
                        prefix,
                        password,
                        lambda message, p=plan: emit_plan(p, message),
                        strict_aux_ips=False,
                        allow_create_missing=True,
                        flavor_mode=flavor_mode,
                        x1_flavor_enabled=x1_flavor_enabled,
                        preferred_server_ids=plan_server_ids,
                        static_bgp_enabled=static_bgp_enabled,
                    )
                except Exception as exc:
                    last_reason = str(exc)
                    if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 and self._is_eip_quota_insufficient_text(last_reason):
                        raise CloudApiError(last_reason) from exc
                    if self._is_confirmed_eip_sold_out_text(last_reason):
                        if ready_result:
                            emit_plan(plan, "自动购买已返回服务器IP和ip.txt，后续异常不覆盖已成功结果，继续进入后续搭建。")
                            self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=ready_result.server_ip, server_id=ready_result.server_id)
                            return mark_if_new_purchase(ready_result)
                        raise CloudApiError(
                            f"{plan.label} 弹性公网IP地址池已确认售罄，已保留已有ECS/网卡并停止自动购买后反查；"
                            "待该地区公网IP恢复后重新执行，会复用已有资源继续补齐/搭建。"
                        ) from exc
                    signature = normalize_failure_signature(last_reason)
                    same_failure_count = same_failure_count + 1 if signature and signature == last_signature else 1
                    last_signature = signature
                    emit_plan(plan, f"自动购买后反查第 {check_index}/{confirm_attempts} 次失败: {exc}")
                    if same_failure_count >= 2 and not huawei_retryable_reconcile_error(last_reason):
                        if ready_result:
                            emit_plan(plan, "自动购买已返回服务器IP和ip.txt，云端短反查未确认，保留该结果进入后续搭建。")
                            self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=ready_result.server_ip, server_id=ready_result.server_id)
                            return mark_if_new_purchase(ready_result)
                        raise CloudApiError(f"自动购买后反查连续2次同类失败，停止重试: {last_reason}") from exc
                    continue
                if found:
                    emit_plan(plan, "步骤: 已确认自动购买资源满足20规格购买拓扑。" if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 else "步骤: 已确认自动购买资源满足搭建条件。")
                    self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=found.server_ip, server_id=found.server_id)
                    self.region_stage[plan.key] = "done"
                    return mark_if_new_purchase(found)
                last_reason = "云上暂未发现满足20规格购买拓扑的 ECS/网卡/虚拟IP/EIP" if flavor_mode == FLAVOR_MODE_WAWACLOUD_20 else "云上暂未发现满足搭建条件的 ECS/网卡/EIP"
                emit_plan(plan, f"自动购买后反查第 {check_index}/{confirm_attempts} 次未就绪，继续等待云端同步。")
            if ready_result:
                emit_plan(plan, "自动购买已返回服务器IP和ip.txt，云端短反查未确认，保留该结果进入后续搭建。")
                self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=ready_result.server_ip, server_id=ready_result.server_id)
                return mark_if_new_purchase(ready_result)
            raise CloudApiError(f"自动购买已结束，但资源反查未确认完成: {last_reason}")

        workers = min(HUAWEI_AUTOBUY_CONFIRM_WORKERS, len(confirm_targets))
        with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
            future_map = {executor.submit(confirm_plan, plan): plan for plan in confirm_targets}
            for future in concurrent.futures.as_completed(future_map):
                plan = future_map[future]
                try:
                    results[plan.key] = future.result()
                    failures.pop(plan.key, None)
                except Exception as exc:
                    failures[plan.key] = str(exc)
                    self.call_ui(self.update_region_row, plan, status=f"失败: {exc}")
                    emit_plan(plan, f"失败: {exc}")
        return results, failures

    def _purchase_and_build_region(
        self,
        client: HuaweiCloudClient,
        plan: RegionPlan,
        pair: ProjectPair,
        ssh_port: int,
        ssh_user: str,
        password: str,
        script_path: str,
        build_after_purchase: bool,
        flavor_mode: str = FLAVOR_MODE_FIXED,
        x1_flavor_enabled: bool = False,
        x1_ten_ip_enabled: bool = False,
        static_bgp_enabled: bool = False,
        on_demand_billing_enabled: bool = False,
    ) -> RegionBuildResult:
        prefix = safe_optional_name(self.resource_prefix_var.get(), 32)

        def emit(message: str) -> None:
            self.after(0, self.log, f"[{plan.label}] {localize_region_tokens(message)}")

        def check_stop() -> None:
            if self.stop_event.is_set():
                raise RuntimeError("用户请求停止，跳过后续步骤。")

        self.call_ui(self.update_region_row, plan, project_id=pair.project_id, status="开始")
        self.region_stage[plan.key] = "start"
        emit(f"步骤: 开始地区 {plan.key}")

        reusable = self._find_reusable_region_result(
            client,
            plan,
            pair,
            prefix,
            password,
            emit,
            strict_aux_ips=False,
            allow_create_missing=True,
            flavor_mode=flavor_mode,
            x1_flavor_enabled=x1_flavor_enabled,
            static_bgp_enabled=static_bgp_enabled,
        )
        if reusable:
            if build_after_purchase:
                return self._build_existing_region_with_retry(
                    reusable,
                    ssh_port,
                    ssh_user,
                    password,
                    script_path,
                    flavor_mode,
                )
            self.region_stage[plan.key] = "done"
            return reusable

        purchased = self._run_huawei_wwwroot_purchase_region(
            client,
            plan,
            pair,
            password,
            flavor_mode,
            emit,
            x1_flavor_enabled=x1_flavor_enabled,
            x1_ten_ip_enabled=x1_ten_ip_enabled,
            static_bgp_enabled=static_bgp_enabled,
            on_demand_billing_enabled=on_demand_billing_enabled,
        )
        if build_after_purchase:
            return self._build_existing_region_with_retry(
                purchased,
                ssh_port,
                ssh_user,
                password,
                script_path,
                flavor_mode,
            )
        self.region_stage[plan.key] = "done"
        return purchased

        check_stop()
        self.region_stage[plan.key] = "az"
        zones = client.list_availability_zones(pair, emit)
        server_candidates = client.zone_flavor_candidates(pair, zones, emit, flavor_mode)
        preview = ", ".join(f"{zone or '自动AZ'}/{flavor}" for zone, flavor in server_candidates[:6])
        if len(server_candidates) > 6:
            preview += f", ... 共{len(server_candidates)}组"
        emit(f"步骤: 可用区/规格候选 {preview}")

        check_stop()
        self.region_stage[plan.key] = "image"
        image_id, image_name = client.find_image_id(pair)
        emit(f"步骤: 使用镜像 {image_name} ({image_id})")

        check_stop()
        self.region_stage[plan.key] = "vpc"
        vpc_id, vpc_reused = self._ensure_vpc(client, pair, plan, prefix, emit)
        self.call_ui(self.update_region_row, plan, status="复用VPC" if vpc_reused else "VPC已创建")

        check_stop()
        self.region_stage[plan.key] = "sg"
        security_group_id, security_group_reused = self._ensure_security_group(client, pair, plan, vpc_id, prefix, emit)
        self.call_ui(self.update_region_row, plan, status="复用安全组" if security_group_reused else "安全组已创建")

        server_name = LOW_CONFIG_ECS_NAME

        def recover_reusable_after_uncertain_create(exc: BaseException) -> Optional[RegionBuildResult]:
            if not is_network_transient_error(exc):
                return None
            emit("创建/查询ECS时出现网络返回异常，开始反查同地区可复用云服务器，避免重复下单。")
            for check_index in range(1, 7):
                if check_index > 1:
                    time.sleep(6)
                recovered = self._find_reusable_region_result(
                    client,
                    plan,
                    pair,
                    prefix,
                    password,
                    emit,
                    strict_aux_ips=False,
                    allow_create_missing=True,
                    flavor_mode=flavor_mode,
                    x1_flavor_enabled=x1_flavor_enabled,
                    static_bgp_enabled=static_bgp_enabled,
                )
                if recovered:
                    emit("已反查到可复用云服务器，停止本轮新购并继续后续步骤。")
                    self.region_stage[plan.key] = "done"
                    return recovered
                emit(f"反查可复用云服务器 {check_index}/6 未发现，继续等待云端状态同步。")
            return None

        subnet_id = ""
        subnet_network_id = ""
        server_response: Dict[str, Any] = {}
        job: Dict[str, Any] = {}
        server_id = ""
        root_volume_type = ROOT_VOLUME_TYPE
        last_server_error: Optional[CloudApiError] = None
        for attempt_index, (availability_zone, flavor_id) in enumerate(server_candidates, start=1):
            check_stop()
            if availability_zone:
                emit(f"步骤: 尝试可用区 {availability_zone} / 规格 {flavor_id} ({attempt_index}/{len(server_candidates)})")
            else:
                emit(f"步骤: 尝试自动AZ / 规格 {flavor_id} ({attempt_index}/{len(server_candidates)})")

            self.region_stage[plan.key] = "subnet"
            subnet_id, subnet_network_id, subnet_reused = self._ensure_subnet(
                client,
                pair,
                plan,
                vpc_id,
                availability_zone,
                prefix,
                emit,
            )
            self.call_ui(self.update_region_row, plan, status="复用子网" if subnet_reused else "子网已创建")

            try:
                for volume_index, (candidate_volume_type, candidate_volume_label) in enumerate(ROOT_VOLUME_CANDIDATES, start=1):
                    check_stop()
                    self.region_stage[plan.key] = "server"
                    emit(
                        f"步骤: 尝试系统盘 {candidate_volume_label}({candidate_volume_type}) "
                        f"{ROOT_VOLUME_SIZE}GiB ({volume_index}/{len(ROOT_VOLUME_CANDIDATES)})"
                    )
                    emit(
                        purchase_config_snapshot(
                            plan=plan,
                            pair=pair,
                            flavor_mode=flavor_mode,
                            server_name=server_name,
                            availability_zone=availability_zone,
                            flavor_id=flavor_id,
                            image_id=image_id,
                            image_name=image_name,
                            vpc_id=vpc_id,
                            subnet_id=subnet_id,
                            subnet_network_id=subnet_network_id,
                            security_group_id=security_group_id,
                            root_volume_type=candidate_volume_type,
                            root_volume_label=candidate_volume_label,
                        )
                    )
                    try:
                        server_response = client.create_server(
                            pair,
                            server_name,
                            availability_zone,
                            flavor_id,
                            candidate_volume_type,
                            image_id,
                            vpc_id,
                            subnet_network_id,
                            security_group_id,
                            password,
                            standard_private_ips(plan),
                            on_demand_billing_enabled,
                        )
                        root_volume_type = candidate_volume_type
                        job_id = str(server_response.get("job_id") or server_response.get("jobId") or "")
                        emit("步骤: 购买云服务器已提交")
                        if job_id:
                            job = client.wait_job(pair, job_id, emit)
                        else:
                            job = {}
                        server_id = client.server_id_from_create_response(server_response, job)
                        if not server_id:
                            raise CloudApiError("未能确认云服务器创建结果，请导出日志发给维护人员。")
                        break
                    except CloudApiError as exc:
                        last_server_error = exc
                        recovered = recover_reusable_after_uncertain_create(exc)
                        if recovered:
                            return recovered
                        if is_volume_unavailable_error(exc) and volume_index < len(ROOT_VOLUME_CANDIDATES):
                            emit(f"系统盘 {candidate_volume_label}({candidate_volume_type}) 售罄或不可用，升一档继续尝试。")
                            continue
                        raise
                if server_id:
                    break
            except CloudApiError as exc:
                last_server_error = exc
                retryable = is_capacity_retry_error(exc) and attempt_index < len(server_candidates)
                if not retryable:
                    raise
                emit(f"当前可用区/规格创建失败: {exc}；切换下一组。")
                if not subnet_reused:
                    try:
                        client.delete_subnet(pair, subnet_id)
                        client.wait_subnet_deleted(pair, subnet_id, emit)
                        emit("步骤: 已清理失败子网")
                    except Exception as cleanup_exc:
                        emit(f"清理失败子网提示: {cleanup_exc}")
                else:
                    emit("步骤: 旧子网为复用资源，不删除。")
                subnet_id = ""
                subnet_network_id = ""

        if not server_id:
            if last_server_error:
                raise last_server_error
            raise CloudApiError("ECS创建失败：没有可用的可用区/规格候选。")
        self.region_stage[plan.key] = "ecs"
        emit("步骤: 云服务器已创建")
        self.call_ui(self.update_region_row, plan, status="ECS已创建", server_id=server_id)

        time.sleep(5)
        server = client.show_server(pair, server_id)
        emit(f"新购ECS属性: {self._server_attribute_display_summary(server)}")
        interfaces = client.list_server_interfaces(pair, server_id)
        standard_rows = self._ensure_standard_interfaces(
            client,
            pair,
            plan,
            server_id,
            server,
            interfaces,
            subnet_id,
            subnet_network_id,
            security_group_id,
            emit,
        )
        emit(
            "步骤: 弹性网卡 "
            + ", ".join(f"{row['iface']}={row['private_ip']}({row['port_id']})" for row in standard_rows)
        )

        expected_eip_count = expected_public_ip_count(plan)
        if expected_eip_count not in {5, 10, 20}:
            raise CloudApiError(f"地区IP数量配置异常: {expected_eip_count}，必须是5条、10条或20条。")
        emit(
            f"步骤: 本地区计划绑定按流量EIP数量={expected_eip_count} "
            f"(弹性网卡 {plan.standard_nic_count} + 虚拟IP {getattr(plan, 'virtual_ip_count', 0)} + 辅助弹性网卡 {plan.aux_count})"
        )

        virtual_ip_ports: List[Dict[str, Any]] = []
        if virtual_private_ips(plan):
            all_ports = client.list_ports(pair, timeout=30, retry_attempts=2)
            virtual_ip_ports = self._ensure_virtual_ip_ports(
                client,
                pair,
                plan,
                subnet_network_id,
                security_group_id,
                standard_rows,
                all_ports,
                prefix,
                emit,
                allow_create_missing=True,
            )

        parent_iface_by_port = {row["port_id"]: row["iface"] for row in standard_rows}
        aux_by_ip: Dict[str, Dict[str, Any]] = {}
        try:
            for aux in client.list_sub_network_interfaces(pair, timeout=30, retry_attempts=2):
                aux_subnet_id = str(aux.get("virsubnet_id") or aux.get("subnet_id") or aux.get("neutron_subnet_id") or "")
                if subnet_id and aux_subnet_id and aux_subnet_id != subnet_id:
                    continue
                private_ip = str(aux.get("private_ip_address") or "")
                parent_port_id = str(aux.get("parent_id") or aux.get("parent_port_id") or "")
                parent_iface = parent_iface_by_port.get(parent_port_id, parent_iface_for_index(0))
                if private_ip:
                    if (
                        aux_should_use_primary_parent(plan)
                        and standard_rows
                        and parent_port_id
                        and parent_port_id != standard_rows[0]["port_id"]
                    ):
                        emit(f"辅助弹性网卡 {private_ip} 所属弹性网卡不是 192.168.0.10，按规则不用于绑定/搭建。")
                        continue
                    aux_by_ip[private_ip] = self._annotate_aux_interface(aux, parent_iface, parent_port_id)
        except Exception as exc:
            emit(f"辅助弹性网卡预查询提示: {exc}")
        aux_interfaces = self._ensure_aux_interfaces(
            client,
            pair,
            plan,
            subnet_id,
            security_group_id,
            standard_rows,
            aux_by_ip,
            emit,
            strict_aux_ips=False,
        )
        ip_txt = format_ip_txt(aux_interfaces)
        if not ip_txt.strip():
            raise CloudApiError("辅助网卡创建完成，但未生成有效 ip.txt。")
        emit(f"步骤: 辅助弹性网卡数量={len(aux_interfaces)}")

        self.region_stage[plan.key] = "eip"
        bind_targets = (
            [(row["port_id"], self._standard_port_display_label(row)) for row in standard_rows]
            + [(str(item.get("id") or ""), self._virtual_ip_display_label(item)) for item in virtual_ip_ports]
            + [(str(item.get("id") or ""), self._aux_port_display_label(item)) for item in aux_interfaces]
        )
        if len(bind_targets) != expected_eip_count:
            raise CloudApiError(f"EIP绑定目标数量异常: {len(bind_targets)}/{expected_eip_count}")
        ordered_port_ids = [port_id for port_id, _label in bind_targets]
        port_labels = {str(port_id): label for port_id, label in bind_targets if port_id and label}
        for port_id, label in bind_targets:
            if not port_id:
                raise CloudApiError(f"EIP绑定目标缺少端口ID: {label}")
        public_ips = self._ensure_eips_for_ports(
            client,
            pair,
            plan,
            ordered_port_ids,
            prefix,
            emit,
            static_bgp_enabled=static_bgp_enabled,
            port_labels=port_labels,
        )
        primary_public_ip = public_ips[0] if public_ips else ""
        if not primary_public_ip:
            primary_public_ip = public_ips[0] if public_ips else ""
        if primary_public_ip not in public_ips:
            public_ips.insert(0, primary_public_ip)
        if len(public_ips) != expected_eip_count:
            raise CloudApiError(f"EIP绑定数量异常: {len(public_ips)}/{expected_eip_count}")
        self.call_ui(self.update_region_row, plan, status="EIP已绑定", server_ip=primary_public_ip, server_id=server_id)

        exit_code = -1
        output_text = ""
        do_legacy_build = bool(build_after_purchase)
        if do_legacy_build:
            stabilize_seconds = POST_EIP_STABILIZE_SECONDS_10IP if expected_eip_count >= 10 else POST_EIP_STABILIZE_SECONDS_5IP
            emit(
                f"步骤: EIP/网卡路由稳定等待 {stabilize_seconds}s "
                f"(新购资源立即搭建前等待云端路由收敛)"
            )
            deadline = time.monotonic() + stabilize_seconds
            while time.monotonic() < deadline:
                check_stop()
                time.sleep(min(2, max(0.1, deadline - time.monotonic())))
            refreshed_public_ips = client.public_ips_for_ports(pair, ordered_port_ids)
            if len(refreshed_public_ips) == expected_eip_count:
                public_ips = refreshed_public_ips
                primary_public_ip = public_ips[0]
                emit("步骤: EIP绑定稳定后已重新读取公网IP顺序。")
            else:
                emit(
                    f"警告: EIP稳定后重新读取数量 {len(refreshed_public_ips)}/{expected_eip_count}，"
                    "继续使用首次绑定结果。"
                )
            self.region_stage[plan.key] = "ssh"
            emit("步骤: 等待SSH")
            remote_command, command_label, connection_env = self._remote_command_for_flavor_mode(flavor_mode)
            emit("步骤: 搭建方式=" + command_label)
            ssh_host, used_password, exit_code, output_text = self._ssh_upload_and_run_any_host(
                [primary_public_ip, *public_ips],
                ssh_port,
                ssh_user,
                password,
                ip_txt,
                script_path,
                emit,
                remote_command,
            )
            if used_password != password:
                password = used_password
            emit(f"步骤: SSH搭建入口 {ssh_host}")
            script_public_ips = self._public_ips_from_remote_output(output_text)
            if script_public_ips:
                public_ips = script_public_ips
                if not primary_public_ip or primary_public_ip not in public_ips:
                    primary_public_ip = public_ips[0]
            result_available_public_ips = list(public_ips)
            result_unavailable_public_ips: List[str] = []
            emit("步骤: 已按远程脚本退出结果完成搭建，不再执行最后 SOCKS5 外部检测。")
            status = "购买并搭建完成"
            self.call_ui(self.update_region_row, plan, status=status, server_ip=primary_public_ip, server_id=server_id)
        else:
            self.call_ui(self.update_region_row, plan, status="购买完成", server_ip=primary_public_ip, server_id=server_id)
        self.region_stage[plan.key] = "done"
        emit(f"步骤: 完成地区 {plan.key}")

        result = RegionBuildResult(
            plan=plan,
            server_id=server_id,
            server_ip=primary_public_ip,
            password=password,
            ip_txt=ip_txt,
            public_ips=public_ips,
            root_volume_type=root_volume_type,
            script_exit_code=exit_code,
        )
        if do_legacy_build:
            self._apply_connection_env_to_result(result, connection_env)
            self._apply_script_result_outputs(result, output_text)
            if self._remote_command_requires_result_markers(remote_command):
                setattr(result, "_huawei_require_script_result_output", True)
            result.available_public_ips = result_available_public_ips
            result.unavailable_public_ips = result_unavailable_public_ips
        return result

    def _ssh_connect_with_retry(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
        emit: Callable[[str], None],
        *,
        max_attempts: int = SSH_LOGIN_MAX_ATTEMPTS,
        retry_auth: bool = False,
    ) -> paramiko.SSHClient:
        last_error: Optional[BaseException] = None
        for attempt in range(1, max_attempts + 1):
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            emit(f"SSH连接 {host}:{port} ... ({attempt}/{max_attempts})")
            try:
                ssh.connect(
                    hostname=host,
                    port=port,
                    username=username,
                    password=password,
                    look_for_keys=False,
                    allow_agent=False,
                    timeout=SSH_CONNECT_TIMEOUT,
                    banner_timeout=SSH_BANNER_TIMEOUT,
                    auth_timeout=SSH_AUTH_TIMEOUT,
                )
                return ssh
            except paramiko.AuthenticationException as exc:
                last_error = exc
                ssh.close()
                if retry_auth and attempt < max_attempts:
                    # 中文注释：云服务器重装 Job 成功后，SSH端口可能先于root密码初始化完成。
                    emit(f"SSH认证暂未生效: {exc}；{SSH_RETRY_DELAY_SECONDS}秒后重试")
                    time.sleep(SSH_RETRY_DELAY_SECONDS)
                    continue
                raise RuntimeError(f"SSH认证失败: {exc}") from exc
            except (paramiko.SSHException, OSError, socket.error) as exc:
                last_error = exc
                ssh.close()
                if attempt >= max_attempts:
                    raise RuntimeError(f"SSH登录失败，已重试 {max_attempts} 次: {exc}") from exc
                emit(f"SSH暂未就绪: {exc}；{SSH_RETRY_DELAY_SECONDS}秒后重试")
                time.sleep(SSH_RETRY_DELAY_SECONDS)
        raise RuntimeError(f"SSH登录失败: {last_error}")

    def _ssh_exec_stream(
        self,
        ssh: paramiko.SSHClient,
        command: str,
        emit: Callable[[str], None],
        *,
        capture_output: bool = False,
        get_pty: bool = True,
    ) -> Any:
        _, stdout, stderr = ssh.exec_command(command, get_pty=get_pty)
        channel = stdout.channel
        with self.ssh_channel_lock:
            self.ssh_channel = channel
        pending: List[str] = []
        captured: List[str] = []
        captured_size = 0
        last_flush = time.monotonic()

        def capture(text: str) -> None:
            nonlocal captured_size
            if not capture_output or not text:
                return
            captured.append(text)
            captured_size += len(text)
            # 中文注释：只保留尾部输出用于判断远程脚本是否输出最终结果，避免 apt 输出过长占用内存。
            while captured_size > 300000 and captured:
                removed = captured.pop(0)
                captured_size -= len(removed)

        def flush(force: bool = False) -> None:
            nonlocal last_flush
            if not pending:
                return
            if not force and len(pending) < 8 and time.monotonic() - last_flush < 0.4:
                return
            emit("\n".join(pending))
            pending.clear()
            last_flush = time.monotonic()

        try:
            while True:
                received = False
                if channel.recv_ready():
                    data = channel.recv(4096).decode("utf-8", errors="replace")
                    received = True
                    if data:
                        capture(data)
                        text = data.replace("\r", "\n").rstrip()
                        if len(text) > 3000:
                            text = "...输出过长，已截取末尾...\n" + text[-3000:]
                        pending.append(text)
                        flush()
                if channel.recv_stderr_ready():
                    data = channel.recv_stderr(4096).decode("utf-8", errors="replace")
                    received = True
                    if data:
                        capture(data)
                        text = data.replace("\r", "\n").rstrip()
                        if len(text) > 3000:
                            text = "...输出过长，已截取末尾...\n" + text[-3000:]
                        pending.append(text)
                        flush()
                if channel.exit_status_ready() and not channel.recv_ready() and not channel.recv_stderr_ready():
                    break
                if not received:
                    time.sleep(0.2)
            flush(force=True)
            exit_code = channel.recv_exit_status()
            if capture_output:
                return exit_code, "".join(captured)
            return exit_code
        finally:
            with self.ssh_channel_lock:
                if self.ssh_channel is channel:
                    self.ssh_channel = None

    @staticmethod
    def _ssh_upload_ip_txt(ssh: paramiko.SSHClient, ip_txt: str) -> None:
        sftp = ssh.open_sftp()
        try:
            with sftp.file("/root/ip.txt", "w") as remote_ip:
                remote_ip.write(ip_txt)
        finally:
            sftp.close()

    @staticmethod
    def _ssh_upload_files(ssh: paramiko.SSHClient, ip_txt: str, script_path: str) -> None:
        sftp = ssh.open_sftp()
        try:
            with sftp.file("/root/ip.txt", "w") as remote_ip:
                remote_ip.write(ip_txt)
            with open(script_path, "rb") as local_script:
                with sftp.file("/root/fifteenhuawei.sh", "wb") as remote_script:
                    remote_script.write(local_script.read())
        finally:
            sftp.close()

    @staticmethod
    def _remote_command_requires_result_markers(command: str) -> bool:
        text = str(command or "")
        return "fifteenhuawei.sh" in text or "fifteenhuawei20.sh" in text

    @staticmethod
    def _remote_output_has_result_markers(output: str) -> bool:
        text = str(output or "")
        l2tp_output, socks_output = HuaweiAutoBuyApp._script_result_sections(text)
        if l2tp_output and socks_output:
            return True
        compact = re.sub(r"\s+", "", text).upper()
        return "SOCKS5格式" in compact and ("L2格式" in compact or "L2TP格式" in compact)

    def _ssh_repair_socks(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
        ip_txt: str,
        script_path: str,
        emit: Callable[[str], None],
        *,
        full_rebuild: bool,
        remote_command: Optional[str] = None,
    ) -> None:
        ssh: Optional[paramiko.SSHClient] = None
        errors: List[str] = []
        for password_label, candidate_password in self._ssh_password_candidates(password):
            try:
                emit(f"SOCKS5修复SSH认证尝试: {host}:{port} ({password_label})")
                ssh = self._ssh_connect_with_retry(host, port, username, candidate_password, emit, max_attempts=3)
                if candidate_password != password:
                    emit(f"SOCKS5修复已切换为{password_label}。")
                break
            except Exception as exc:
                errors.append(f"{password_label}: {exc}")
                if self._is_ssh_auth_failure(exc) or self._is_ssh_connect_failure(exc):
                    continue
                raise
        if ssh is None:
            raise RuntimeError("SOCKS5修复SSH连接失败: " + "；".join(errors))
        try:
            ensure_remote_xray(ssh, emit)
            if remote_command:
                self._ssh_upload_ip_txt(ssh, ip_txt)
                emit("已重新上传 /root/ip.txt")
                command = remote_command
                emit("SOCKS5修复: 正在重跑搭建脚本。")
            else:
                self._ssh_upload_files(ssh, ip_txt, script_path)
                emit("已重新上传 /root/ip.txt 和 /root/fifteenhuawei.sh")
                if full_rebuild:
                    command = "cd /root && bash fifteenhuawei.sh"
                    emit("SOCKS5修复: 正在重跑完整搭建脚本。")
                else:
                    command = (
                        "cd /root && "
                        "chmod +x /root/fifteenhuawei.sh /usr/local/bin/vpn_boot_init.sh /usr/local/bin/start_multi_dante.sh /usr/local/bin/start_native_l2tp.sh 2>/dev/null || true; "
                        # 中文注释：vpn_boot_init 会清空 iptables/nat，必须在其后重载 L2TP，否则会出现能拨号但没有网络。
                        "bash /usr/local/bin/vpn_boot_init.sh 2>&1 || true; "
                        "bash /usr/local/bin/start_multi_dante.sh 2>&1 || true; "
                        "bash /usr/local/bin/start_native_l2tp.sh 2>&1 || true; "
                        "sleep 5; "
                        "echo '--- ip.txt ---'; cat /root/ip.txt 2>/dev/null || true; "
                        "echo '--- net_infos ---'; cat /etc/wawacloud/net_infos.db 2>/dev/null || true; "
                        "echo '--- danted ---'; pgrep -a danted || true; "
                        "echo '--- l2tp policy/nat ---'; "
                        "(ip rule show 2>/dev/null | grep '192.168.60' || true); "
                        "(iptables -t nat -S 2>/dev/null | grep '192.168.60' || echo 'l2tp-nat-missing'); "
                        "echo '--- listen 23456 ---'; "
                        "(ss -lntp 2>/dev/null | grep ':23456' || netstat -lntp 2>/dev/null | grep ':23456' || true)"
                    )
                    emit("SOCKS5修复: 重启网络初始化和 danted 服务")
            if remote_command:
                exit_code, output_text = self._ssh_exec_stream(ssh, command, emit, capture_output=True, get_pty=False)
            else:
                exit_code = self._ssh_exec_stream(ssh, command, emit, get_pty=False)
                output_text = ""
            emit(f"SOCKS5修复命令退出码: {exit_code}")
            if (full_rebuild or remote_command) and exit_code != 0:
                raise RuntimeError(f"SOCKS5完整重跑失败，退出码 {exit_code}")
            if (
                remote_command
                and self._remote_command_requires_result_markers(remote_command)
                and not self._remote_output_has_result_markers(output_text)
            ):
                raise RuntimeError("SOCKS5修复远程脚本未输出 Socks5/L2 结果，判定未完整完成。")
        finally:
            ssh.close()

    def _verify_and_repair_socks(
        self,
        host: str,
        ssh_port: int,
        ssh_user: str,
        password: str,
        ip_txt: str,
        script_path: str,
        public_ips: Sequence[str],
        emit: Callable[[str], None],
        *,
        remote_command: Optional[str] = None,
    ) -> Tuple[List[str], List[str]]:
        available, unavailable, reasons = wait_for_socks5_status(
            public_ips,
            SOCKS_PORT,
            emit,
            timeout=SOCKS_INITIAL_VERIFY_TIMEOUT,
            interval=SOCKS_VERIFY_INTERVAL,
        )
        if not unavailable:
            return available, unavailable
        if not should_repair_socks(reasons):
            return available, unavailable

        for attempt in range(1, SOCKS_REPAIR_ATTEMPTS + 1):
            full_rebuild = attempt == SOCKS_REPAIR_ATTEMPTS
            reason_preview = "; ".join(f"{ip}={reasons.get(ip, '')}" for ip in unavailable[:5])
            emit(f"检测到SOCKS5握手异常，自动SSH修复第 {attempt}/{SOCKS_REPAIR_ATTEMPTS} 次；{reason_preview}")
            try:
                self._ssh_repair_socks(
                    host,
                    ssh_port,
                    ssh_user,
                    password,
                    ip_txt,
                    script_path,
                    emit,
                    full_rebuild=full_rebuild,
                    remote_command=remote_command,
                )
            except Exception as exc:
                emit(f"SOCKS5自动修复失败: {exc}")
            available, unavailable, reasons = wait_for_socks5_status(
                public_ips,
                SOCKS_PORT,
                emit,
                timeout=SOCKS_REPAIR_VERIFY_TIMEOUT,
                interval=SOCKS_VERIFY_INTERVAL,
            )
            if not unavailable:
                emit("SOCKS5自动修复完成，全部公网IP已通过认证。")
                return available, unavailable
            if not should_repair_socks(reasons):
                break
        return available, unavailable

    @staticmethod
    def _ssh_password_candidates(primary_password: str) -> List[Tuple[str, str]]:
        candidates: List[Tuple[str, str]] = []
        for label, password in (
            ("界面密码", primary_password),
            ("默认密码", DEFAULT_PASSWORD),
            ("脚本密码", SOCKS_PASSWORD),
        ):
            password = str(password or "")
            if password and all(password != existing for _label, existing in candidates):
                candidates.append((label, password))
        return candidates

    @staticmethod
    def _is_ssh_auth_failure(exc: BaseException) -> bool:
        text = str(exc).lower()
        return "ssh认证失败" in text or "authentication failed" in text

    @staticmethod
    def _is_ssh_connect_failure(exc: BaseException) -> bool:
        text = str(exc).lower()
        return any(
            token in text
            for token in (
                "ssh登录失败",
                "error reading ssh protocol banner",
                "ssh暂未就绪",
                "timed out",
                "timeout",
                "winerror 10060",
                "connection reset",
                "connection refused",
                "connection aborted",
            )
        )

    def _is_new_purchase_ssh_not_ready(self, exc: BaseException) -> bool:
        return self._is_ssh_auth_failure(exc) or self._is_ssh_connect_failure(exc)

    def _ssh_upload_and_run_any_host(
        self,
        hosts: Sequence[str],
        port: int,
        username: str,
        password: str,
        ip_txt: str,
        script_path: str,
        emit: Callable[[str], None],
        remote_command: Optional[str] = None,
    ) -> Tuple[str, str, int, str]:
        unique_hosts = unique_public_hosts(hosts)
        if not unique_hosts:
            raise TimeoutError("没有可用于SSH搭建的公网IP。")
        ready_hosts = wait_for_ssh_hosts(unique_hosts, port, emit, timeout=HUAWEI_SSH_PORT_WAIT_TIMEOUT)
        password_candidates = self._ssh_password_candidates(password)
        errors: List[str] = []
        for host in ready_hosts:
            for password_label, candidate_password in password_candidates:
                try:
                    emit(f"SSH认证尝试: {host}:{port} ({password_label})")
                    exit_code, output_text = self._ssh_upload_and_run(
                        host,
                        port,
                        username,
                        candidate_password,
                        ip_txt,
                        script_path,
                        emit,
                        remote_command,
                        max_attempts=2,
                    )
                    if candidate_password != password:
                        emit(f"SSH已使用{password_label}完成搭建。")
                    return host, candidate_password, exit_code, output_text
                except Exception as exc:
                    error_text = str(exc)
                    errors.append(f"{host}/{password_label}: {error_text}")
                    if self._is_ssh_auth_failure(exc):
                        emit(f"SSH认证失败，切换入口/密码继续: {host} ({password_label})")
                        continue
                    if self._is_ssh_connect_failure(exc):
                        emit(f"SSH入口连接异常，切换入口继续: {host}: {error_text}")
                        break
                    raise
        detail = "；".join(errors[:8])
        if len(errors) > 8:
            detail += f"；其余 {len(errors) - 8} 条已省略"
        raise RuntimeError(f"全部SSH入口认证/连接失败: {detail}")

    def _ssh_upload_and_run(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
        ip_txt: str,
        script_path: str,
        emit: Callable[[str], None],
        remote_command: Optional[str] = None,
        *,
        max_attempts: int = SSH_LOGIN_MAX_ATTEMPTS,
    ) -> Tuple[int, str]:
        ssh = self._ssh_connect_with_retry(host, port, username, password, emit, max_attempts=max_attempts)
        try:
            ensure_remote_xray(ssh, emit)
            if remote_command:
                self._ssh_upload_ip_txt(ssh, ip_txt)
                emit("已上传 /root/ip.txt")
            else:
                self._ssh_upload_files(ssh, ip_txt, script_path)
                emit("已上传 /root/ip.txt 和 /root/fifteenhuawei.sh")

            if remote_command:
                command = remote_command
                emit("开始执行搭建脚本。")
            else:
                command = "cd /root && bash fifteenhuawei.sh"
                emit("开始执行搭建脚本。")
            # 中文注释：保留搭建脚本尾部输出，结果框优先显示脚本实际打印的 L2/Socks5 结果。
            # 不申请PTY可避免 SSH channel 关闭时向伪终端挂靠进程发送挂断信号。
            exit_code, output_text = self._ssh_exec_stream(ssh, command, emit, capture_output=True, get_pty=False)
            emit(f"远程脚本退出码: {exit_code}")
            if remote_command:
                if exit_code != 0:
                    raise RuntimeError(f"远程脚本执行失败，退出码 {exit_code}")
                if self._remote_command_requires_result_markers(remote_command) and not self._remote_output_has_result_markers(output_text):
                    raise RuntimeError("远程脚本未输出 Socks5/L2 结果，判定搭建命令未完整成功。")
                # 中文注释：在线远程脚本已经完成 L2TP/SOCKS5 配置；不要再执行后置重载检测，
                # 避免 vpn_boot_init 等检查命令扰动刚写好的路由/NAT，造成“脚本跑过但客户端无网”。
                emit("远程脚本已执行完成，不再执行后置重载/检测。")
                return exit_code, output_text
            if exit_code != 0:
                raise RuntimeError(f"fifteenhuawei.sh 执行失败，退出码 {exit_code}")
            # 中文注释：本地上传的 fifteenhuawei.sh 与用户手动 bash 执行路径一致；
            # 主脚本成功后直接结束，避免后置 boot_init/服务重载再次改动路由和 NAT。
            emit("fifteenhuawei.sh 已执行完成，不再执行后置重载/检测。")
            return exit_code, output_text
        finally:
            ssh.close()

    def copy_results(self) -> None:
        self.copy_socks_results()

    def _result_account_scope(self) -> str:
        domain_id = self.domain_var.get().strip()
        if not domain_id:
            return ""
        return credential_scope_label(domain_id, "huawei-result")

    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 = "") -> None:
        if not self._result_account_scope():
            return
        self._ensure_result_account_cache()
        write_latest_result_files(self._latest_result_dir(), l2tp=l2tp_output, socks5=socks_output)

    def copy_l2tp_results(self) -> None:
        current = self.l2tp_text.get("1.0", tk.END) if self.l2tp_text is not None else ""
        text = append_mark_to_output(current, self.mark_var.get())
        if not text:
            self.show_info("当前没有 L2TP 输出。")
            return
        self.clipboard_clear()
        self.clipboard_append(text)
        self.show_info("L2TP 输出已复制。")

    def copy_socks_results(self) -> None:
        current = self.socks_text.get("1.0", tk.END) if self.socks_text is not None else ""
        text = append_mark_to_output(current, self.mark_var.get())
        if not text:
            self.show_info("当前没有 Socks5 输出。")
            return
        self.clipboard_clear()
        self.clipboard_append(text)
        self.show_info("Socks5 输出已复制。")

    def copy_plain_socks_results(self) -> None:
        current = self.socks_text.get("1.0", tk.END) if self.socks_text is not None else ""
        text = extract_plain_socks5_output(current).strip()
        if not text:
            self.show_info("当前没有纯 Socks5 输出。")
            return
        self.clipboard_clear()
        self.clipboard_append(text)
        self.show_info("纯 Socks5 输出已复制。")

    def _set_log_context(self, context: str) -> None:
        self.current_log_context = context
        if context == "manage" and self.manage_log_text is not None:
            self.log_text = self.manage_log_text
        elif self.purchase_log_text is not None:
            self.log_text = self.purchase_log_text

    def clear_log(self) -> None:
        if self.log_text is not None:
            self.log_text.delete("1.0", 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._log_section(log_context), self.log_action_names.get(log_context, "界面导出日志"), text))
        return entries

    def export_log_text(self, widget: Optional[tk.Text] = None, context: Optional[str] = None) -> None:
        entries = self._export_log_entries()
        if not entries and widget is not None:
            text = widget.get("1.0", tk.END).strip()
            if text:
                log_context = context or self.current_log_context or "purchase"
                entries.append((self._log_section(log_context), self.log_action_names.get(log_context, "界面导出日志"), text))
        if not entries:
            self.show_info("当前没有实时日志可导出")
            return
        entries.insert(0, ("诊断信息", "版本与网络", self._export_diagnostic_text()))
        path = export_log_to_desktop(
            "HUAWEI",
            "全部日志",
            "导出日志",
            "",
            account_key=self._account_log_scope(),
            current_logs=entries,
            recent_sections=[self._log_section("purchase"), self._log_section("manage")],
            metadata_lines=self._export_diagnostic_text().splitlines(),
        )
        self.show_info(f"日志压缩包已导出到桌面：\n{path}\n\n请把日志压缩包发给维护人员，简要说明情况即可。")

    def copy_log_text(self, widget: Optional[tk.Text] = None) -> None:
        self.export_log_text(widget)

    def clear_results(self) -> None:
        self.l2tp_text.delete("1.0", tk.END)
        self.socks_text.delete("1.0", tk.END)

    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.log_action_names.get(context, "")

    def _is_read_resource_log_context(self, context: str) -> bool:
        action = self.log_action_names.get(context, "")
        return action in {"读取账号资源", "资源一键查询"}

    def _is_manage_resource_action_log_context(self, context: str) -> bool:
        if context != "manage":
            return False
        action = self.log_action_names.get(context, "")
        return action in {
            "更换新IP",
            "更换选择地区IP",
            "退订/删除资源",
            "一键清空账号资源",
            "重装系统",
        }

    def log(self, message: str, context: Optional[str] = None) -> None:
        message = sanitize_log_text(message)
        line = message if message.endswith("\n") else message + "\n"
        safe_line = self._hide_resource_ids_in_log_text(line)
        widget_safe_line = _filter_huawei_realtime_always_hidden_lines(safe_line)
        filtered_line = filter_realtime_log_text(widget_safe_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_widget = False
        for item_context in contexts:
            target = self._log_widget_for_context(item_context)
            if target is None:
                continue
            if (
                self._is_build_log_context(item_context)
                or self._is_read_resource_log_context(item_context)
                or self._is_manage_resource_action_log_context(item_context)
            ):
                display_line = format_realtime_log_text_without_filter(widget_safe_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_log_file(safe_line, item_context)
            wrote_widget = True
        if not wrote_widget 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_widget 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 append_result(self, message: str) -> None:
        self.socks_text.insert(tk.END, message.rstrip() + "\n")
        self.socks_text.see(tk.END)

    @staticmethod
    def _socks5_results_for_display(results: Sequence[RegionBuildResult]) -> List[RegionBuildResult]:
        # 中文注释：ROS镜像搭建只产出L2TP配置，不能把同一批公网IP误显示成SOCKS5结果。
        return [result for result in results if not getattr(result, "_huawei_ros_l2tp_only", False)]

    @staticmethod
    def _l2tp_password_for_display(result: RegionBuildResult) -> str:
        password = str(getattr(result, "_huawei_l2tp_password", "") or "").strip()
        return password or L2TP_PASSWORD

    @staticmethod
    def _l2tp_username_for_display(result: RegionBuildResult) -> str:
        username = str(getattr(result, "_huawei_l2tp_username", "") or "").strip()
        return username or L2TP_USER_PREFIX

    @staticmethod
    def _actual_script_block_for_display(result: RegionBuildResult, attr_name: str) -> str:
        output = str(getattr(result, attr_name, "") or "").strip()
        if not output:
            return ""
        return f"{output_region_label(result.plan)}\n{output}"

    @staticmethod
    def _require_script_result_for_display(result: RegionBuildResult) -> bool:
        return bool(getattr(result, "_huawei_require_script_result_output", False))

    @classmethod
    def _format_l2tp_block_for_display(cls, result: RegionBuildResult) -> str:
        actual = cls._actual_script_block_for_display(result, "_huawei_script_l2tp_output")
        if actual:
            return actual
        if cls._require_script_result_for_display(result):
            return ""
        entry_ip = result.server_ip or (result.public_ips[0] if result.public_ips else "")
        username = cls._l2tp_username_for_display(result)
        password = cls._l2tp_password_for_display(result)
        ros_l2tp_only = bool(getattr(result, "_huawei_ros_l2tp_only", False))
        psk = str(getattr(result, "_huawei_l2tp_psk", "") or "").strip()
        if not psk:
            psk = ROS_L2TP_PSK if ros_l2tp_only else L2TP_PSK
        lines = [
            output_region_label(result.plan),
            L2TP_RESULT_HEADER,
        ]
        for index, ip in enumerate(result.public_ips, start=1):
            lines.append(f"{entry_ip}/{username}{index}/{password}/{psk}/{ip}")
        return "\n".join(lines)

    @classmethod
    def _format_l2tp_output_for_display(cls, results: Sequence[RegionBuildResult], mark: str = "") -> str:
        # 中文注释：普通搭建仍显示默认 L2TP 密码；ROS镜像搭建则显示本次写入 rsc 的 PPP 密码。
        ordered = ordered_results(results)
        blocks = [block for result in ordered if (block := cls._format_l2tp_block_for_display(result))]
        if not blocks:
            return ""
        output = "\n\n".join(blocks)
        return append_result_footer(output, "L2TP", mark)

    @classmethod
    def _format_socks5_block_for_display(cls, result: RegionBuildResult) -> str:
        if getattr(result, "_huawei_ros_l2tp_only", False):
            return ""
        actual = cls._actual_script_block_for_display(result, "_huawei_script_socks_output")
        if actual:
            return actual
        if cls._require_script_result_for_display(result):
            return ""
        return format_socks5_block(result)

    @classmethod
    def _format_socks5_output_for_display(cls, results: Sequence[RegionBuildResult], mark: str = "") -> str:
        ordered = ordered_results(results)
        blocks = [block for result in ordered if (block := cls._format_socks5_block_for_display(result))]
        if not blocks:
            return ""
        output = "\n\n".join(blocks)
        return append_result_footer(output, "Socks5", mark)

    def _clear_results_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
        before_count = len(self.last_results)
        target_region_codes = {
            plan.region
            for plan in REGION_PLANS
            if self._plan_region_tokens_for_display(plan) & region_set
        }

        def keep_result(result: RegionBuildResult) -> bool:
            return not bool(self._result_region_tokens_for_display(result) & region_set)

        self.last_results = [result for result in self.last_results if keep_result(result)]
        cached_l2tp = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_l2tp.txt"),
            kind="l2tp",
        )
        cached_socks = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_socks5.txt"),
            kind="socks",
        )
        cache_changed = False
        for region_code in target_region_codes:
            cache_changed = cached_l2tp.pop(region_code, None) is not None or cache_changed
            cache_changed = cached_socks.pop(region_code, None) is not None or cache_changed
        if cache_changed:
            self._save_latest_result_outputs(
                self._format_huawei_result_blocks(cached_l2tp, kind="l2tp", mark=self.mark_var.get()),
                self._format_huawei_result_blocks(cached_socks, kind="socks", mark=self.mark_var.get()),
            )
        if len(self.last_results) != before_count or cache_changed:
            self.render_results(restore_local=False)
            self.log(reason, context)

    @staticmethod
    def _plan_region_tokens_for_display(plan: RegionPlan) -> set[str]:
        return {
            token
            for token in (
                str(plan.key or "").strip(),
                str(plan.region or "").strip(),
                str(plan.label or "").strip(),
                str(output_region_label(plan) or "").strip(),
            )
            if token
        }

    @classmethod
    def _result_region_tokens_for_display(cls, result: RegionBuildResult) -> set[str]:
        return cls._plan_region_tokens_for_display(result.plan)

    def _resource_result_region_tokens(self) -> set[str]:
        tokens: set[str] = set()
        for item in self.manage_resource_items:
            if bool(item.get("_displayOnly")):
                continue
            if str(item.get("_kind") or "").lower() != "ecs":
                continue
            for key in ("_region", "_region_label", "region", "region_name", "label"):
                value = str(item.get(key) or "").strip()
                if value:
                    tokens.add(value)
            region = str(item.get("_region") or "").strip()
            plan = REGION_BY_CODE.get(region)
            if plan is not None:
                tokens.update(self._plan_region_tokens_for_display(plan))
        return tokens

    def _results_for_current_resource_library(self, results: Sequence[RegionBuildResult]) -> list[RegionBuildResult]:
        allowed = self._resource_result_region_tokens()
        if not allowed:
            return []
        return [
            result
            for result in results
            if self._result_region_tokens_for_display(result) & allowed
        ]

    def render_results(self, *, restore_local: bool = True) -> None:
        self.l2tp_text.delete("1.0", tk.END)
        self.socks_text.delete("1.0", tk.END)
        if not self._result_account_scope():
            return
        self._ensure_result_account_cache()
        if restore_local:
            self._restore_huawei_result_models_from_local_records()

        mark = self.mark_var.get()
        cached_l2tp = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_l2tp.txt"),
            kind="l2tp",
        )
        cached_socks = self._parse_huawei_result_blocks(
            self._read_latest_result_file("last_socks5.txt"),
            kind="socks",
        )
        current_l2tp = self._parse_huawei_result_blocks(
            self._format_l2tp_output_for_display(self.last_results, mark),
            kind="l2tp",
        )
        current_socks = self._parse_huawei_result_blocks(
            self._format_socks5_output_for_display(self._socks5_results_for_display(self.last_results), mark),
            kind="socks",
        )
        complete_regions = {
            region
            for region in set(current_l2tp) & set(current_socks)
            if current_l2tp.get(region) and current_socks.get(region)
        }
        ros_public_ips_by_region: Dict[str, Set[str]] = {}
        for result in self.last_results:
            if not getattr(result, "_huawei_ros_l2tp_only", False):
                continue
            region = result.plan.region
            if not current_l2tp.get(region):
                continue
            ros_public_ips_by_region.setdefault(region, set()).update(
                ip for ip in [result.server_ip, *result.public_ips] if ip
            )
        ros_l2tp_regions = set(ros_public_ips_by_region)
        for region in complete_regions:
            cached_l2tp[region] = list(current_l2tp[region])
            cached_socks[region] = list(current_socks[region])
        # ROS only produces L2TP. Its L2TP result must not depend on a Socks5 block.
        for region in ros_l2tp_regions - complete_regions:
            cached_l2tp[region] = list(current_l2tp[region])
            ros_public_ips = ros_public_ips_by_region[region]
            remaining_socks = [
                row
                for row in cached_socks.get(region, [])
                if row.partition("/")[0].strip() not in ros_public_ips
            ]
            if remaining_socks:
                cached_socks[region] = remaining_socks
            else:
                cached_socks.pop(region, None)

        for region in self.confirmed_no_ecs_regions:
            cached_l2tp.pop(region, None)
            cached_socks.pop(region, None)

        l2tp_output = self._format_huawei_result_blocks(cached_l2tp, kind="l2tp", mark=mark)
        socks_output = self._format_huawei_result_blocks(cached_socks, kind="socks", mark=mark)
        if l2tp_output:
            self.l2tp_text.insert(tk.END, l2tp_output + "\n")
        if socks_output:
            self.socks_text.insert(tk.END, socks_output + "\n")
        self.l2tp_text.see(tk.END)
        self.socks_text.see(tk.END)
        self._save_latest_result_outputs(l2tp_output, socks_output)

    def append_result_blocks(self, result: RegionBuildResult) -> None:
        _ = result
        self.render_results()

    def ask_confirm_cancel(self, message: 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 ""))
        confirmed = self._choice_dialog(message, "确认", "取消")
        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 _apply_dialog_icon(self, dialog: tk.Toplevel) -> None:
        apply_dialog_icon(dialog)

    def _center_dialog(self, dialog: tk.Toplevel, min_width: int = 420, min_height: int = 150) -> 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 _choice_dialog(self, message: str, ok_text: str, cancel_text: str) -> bool:
        return ask_app_confirm_dialog(
            self.winfo_toplevel(),
            APP_TITLE,
            message,
            ok_text=ok_text,
            cancel_text=cancel_text,
        )

    def _message_dialog(self, message: str, *, modal: bool = False) -> None:
        message = friendly_popup_text(message)
        show_app_message_dialog(self.winfo_toplevel(), APP_TITLE, message, modal=modal)

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

    def _task_completion_dialog(self, message: str, *, modal: bool = False) -> None:
        play_task_completion_sound()
        self._message_dialog(message, modal=modal)

    def _is_customer_guard_dialog_message(self, message: str) -> bool:
        text = str(message or "").strip()
        return (
            "当前账号未确认绑定到合作伙伴" in text
            or "客户归属" in text
            or "代理后台" in text
            or "合作伙伴" in text
            or "AK/SK错误" in text
            or "服务器客户归属校验失败" in text
            or "HTTPS安全证书" in text
        )

    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._credential_fingerprint()
        if self.customer_guard_status == "blocked" and self.customer_guard_fingerprint == fingerprint:
            self.after(0, lambda: self._show_customer_guard_dialog(self.customer_guard_message or 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 ask_yes_no(self, message: 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 ""))
        confirmed = self._choice_dialog(message, "确认", "取消")
        if self.delayed_preconfirm_active:
            if confirmed:
                raise DelayedActionPreconfirmAccepted()
            raise DelayedActionPreconfirmCancelled()
        return confirmed

    def show_info(self, message: str) -> None:
        if self._record_delayed_queue_result_popup(message, is_error=False):
            return
        if self._is_task_completion_message(str(message or "")):
            self._task_completion_dialog(message)
            return
        self._message_dialog(message)

    def show_error(self, message: str) -> None:
        if str(message or "").strip() == CUSTOMER_OWNERSHIP_CHECKING_MESSAGE:
            self.status_var.set(CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE)
            self._log_customer_guard_message(CUSTOMER_OWNERSHIP_CHECKING_LOG_MESSAGE)
            return
        if self._is_customer_guard_dialog_message(str(message or "")):
            self._show_customer_guard_dialog(str(message or ""))
            return
        if self._record_delayed_queue_result_popup(message, is_error=True):
            return
        self._message_dialog(message)


