import logging
import sys
import argparse
import shutil
import os
from pathlib import Path
from typing import List, Dict, Any
from ultralytics import YOLO

# --- [!! 关键 !!] 动态添加项目根目录 (来自之前的讨论) ---
script_path = Path(__file__).resolve()
project_root = script_path.parent.parent
sys.path.insert(0, str(project_root))
# ----------------------------------------------------
import yolo_config as config

# --- [新增] 导入 V2 所需的库 ---
import cv2
import numpy as np
import torch
from pytorch_grad_cam import (
    GradCAM, GradCAMPlusPlus, XGradCAM, EigenCAM,
    HiResCAM, LayerCAM, RandomCAM, EigenGradCAM
)
from pytorch_grad_cam.utils.image import show_cam_on_image
from pytorch_grad_cam.base_cam import BaseCAM

# --- 日志设置 ---
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.StreamHandler()]
)

# --- [保留] V2 中的 CAM 方法字典 ---
CAM_METHODS = {
    "GradCAM": GradCAM, # ok # 速度快
    "GradCAMPlusPlus": GradCAMPlusPlus, # ok # 速度快
    # "XGradCAM": XGradCAM, # ok # 速度快
    "EigenCAM": EigenCAM,
    "HiResCAM": HiResCAM, # ok # 速度快
    # "LayerCAM": LayerCAM, # ok # 速度快
    "RandomCAM": RandomCAM, # ok # 速度快
    # "EigenGradCAM": EigenGradCAM, # 这个耗时长
}

# --- [保留] V2 中的 ActivationMaximizationTarget ---
class ActivationMaximizationTarget:
    def __init__(self, channel=0):
        self.channel = channel
    def __call__(self, model_output):
        if model_output.ndim == 4:
            return model_output[:, self.channel, :, :].mean()
        elif model_output.ndim == 3:
            return model_output[self.channel, :, :].mean()
        else:
            raise ValueError(f"Unsupported model_output shape: {model_output.shape}")

# --- [保留] 图像预处理函数 ---
def preprocess_image(bgr_img: np.ndarray, imgsz: int, device: str) -> (torch.Tensor, Dict[str, Any]):
    # (函数内容与原版相同，为节省篇幅已折叠)
    orig_h, orig_w = bgr_img.shape[:2]
    rgb_img = bgr_img[:, :, ::-1]
    scale = min(imgsz / orig_w, imgsz / orig_h)
    new_w, new_h = int(orig_w * scale), int(orig_h * scale)
    resized_image = cv2.resize(rgb_img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
    pad_w, pad_h = imgsz - new_w, imgsz - new_h
    pad_left, pad_right = pad_w // 2, pad_w - pad_w // 2
    pad_top, pad_bottom = pad_h // 2, pad_h - pad_h // 2
    padded_image = cv2.copyMakeBorder(resized_image, pad_top, pad_bottom, pad_left, pad_right,
                                      cv2.BORDER_CONSTANT, value=(114, 114, 114))
    img_float = np.float32(padded_image) / 255.0
    input_tensor = torch.from_numpy(img_float).permute(2, 0, 1).unsqueeze(0).to(device)
    padding_info = {
        "pad_top": pad_top, "new_h": new_h, "pad_left": pad_left,
        "new_w": new_w, "orig_h": orig_h, "orig_w": orig_w
    }
    return input_tensor, padding_info


# --- [保留] V1 中的关键修复函数 ---
def reshape_transform(outputs):
    if isinstance(outputs, tuple):
        return outputs[0] 
    return outputs

# --- [保留] V1 中的模型查找函数 ---
def find_trained_models(outputs_dir: Path, model_key: str, pt_name: str) -> List[str]:
    # (函数内容与原版相同，为节省篇幅已折叠)
    trained_models = []
    if not outputs_dir.is_dir():
        logging.warning(f"输出目录不存在: {outputs_dir}")
        return []
    for project_folder in outputs_dir.iterdir():
        if project_folder.is_dir() and project_folder.name.startswith(model_key + '_'):
            if (project_folder / 'weights' / pt_name).exists():
                trained_models.append(project_folder.name)
    trained_models.sort()
    return trained_models


# --- [!! 核心重构 !!] 融合 V1 和 V2 的可视化函数 ---
def visualize_nn_comprehensive(
    model_path: str, 
    source_dir: str, 
    base_save_dir: Path, 
    pt_name: str,
    cam_method_name: str,
    target_layer_str: str,
    model_key: str  # [!! 新增 !!] 接收模型 Key (e.g., "YOLO11m-seg")
):
    """
    使用 V2 的多方法和 V1 的框架进行 CAM 可视化。
    """
    
    # --- 1. 加载模型 (V1 逻辑) ---
    try:
        logging.info(f"正在加载模型: {model_path}")
        device = 'cuda' if torch.cuda.is_available() else 'cpu'
        model = YOLO(model_path).to(device)
        model.model.eval()
        imgsz = model.model.args['imgsz'] 
        logging.info(f"模型加载成功。设备: {device}, 图像尺寸: {imgsz}")
    except Exception as e:
        logging.error(f"无法加载模型 {model_path}: {e}", exc_info=True)
        return

    # --- 2. [!! 关键修改 !!] 解析目标层 ---
    target_layers_to_process = [] # 存储 (name, layer_module) 元组
    
    if target_layer_str.strip().lower() == "default":
        logging.info("检测到 'default' 目标层，正在解析...")
                
        # 对 YOLOv8 系列模型的 'default' 逻辑 # TODO # 对模型进行修改
        if model_key.lower().startswith("yolov8"):
            logging.info(f"检测到 YOLOv8 系列模型 ({model_key})。将使用精选的8个默认层。")
            default_yolo8_layers = [
                (1, "model.model.model[1]"),  # Layer_01_Conv
                (4, "model.model.model[4]"),  # Layer_04_C2f
                (9, "model.model.model[9]"),  # Layer_09_SPPF
                (11, "model.model.model[11]"), # Layer_11_Concat
                (14, "model.model.model[14]"), # Layer_14_Concat
                (19, "model.model.model[19]"), # Layer_19_Conv
                (20, "model.model.model[20]"), # Layer_20_Concat
                (21, "model.model.model[21]")  # Layer_21_C2f
            ]

            # 如果使用 EigenCAM 或 EigenGradCAM，则只分析最后两个层
            if cam_method_name in ["EigenCAM", "EigenGradCAM"]:
                logging.info(f"  -> 使用 {cam_method_name}。将自动裁剪为最后2个默认层。")
                default_yolo8_layers = default_yolo8_layers[-3:] # (只保留最后两个)

            for idx, path_str in default_yolo8_layers:
                try:
                    layer = model.model.model[idx]
                    target_layers_to_process.append((path_str, layer))
                    logging.info(f"  -> 已添加 YOLOv8 默认层: {path_str} (索引 {idx})")
                except Exception as e:
                    logging.warning(f"  -> [!] 无法添加 YOLOv8 默认层 {path_str}: {e}")

        # 对 YOLOv9 系列模型的 'default' 逻辑 # TODO # 对模型进行修改
        elif model_key.lower().startswith("yolov9"):
            logging.info(f"检测到 YOLOv9 系列模型 ({model_key})。将使用精选的8个默认层。")
            # (这是您新指定的列表)
            default_yolo9_layers = [
                (2, "model.model.model[2]"),   # Layer_02_Conv
                (7, "model.model.model[7]"),   # Layer_07_RepNCSPELAN4
                (29, "model.model.model[29]"), # Layer_29_SPPELAN
                (31, "model.model.model[31]"), # Layer_31_Concat
                (34, "model.model.model[34]"), # Layer_34_Concat
                (37, "model.model.model[37]"), # Layer_37_Concat
                (40, "model.model.model[40]"), # Layer_40_Concat
                (41, "model.model.model[41]")  # Layer_41_RepNCSPELAN4
            ]

            # 如果使用 EigenCAM 或 EigenGradCAM，则只分析最后两个层
            if cam_method_name in ["EigenCAM", "EigenGradCAM"]:
                logging.info(f"  -> 使用 {cam_method_name}。将自动裁剪为最后2个默认层。")
                default_yolo9_layers = default_yolo9_layers[-3:] # (只保留最后两个)

            for idx, path_str in default_yolo9_layers:
                try:
                    layer = model.model.model[idx]
                    target_layers_to_process.append((path_str, layer))
                    logging.info(f"  -> 已添加 YOLOv9 默认层: {path_str} (索引 {idx})")
                except Exception as e:
                    logging.warning(f"  -> [!] 无法添加 YOLOv9 默认层 {path_str}: {e}")

        # 对 YOLO11 系列模型的 'default' 逻辑 # TODO # 对模型进行修改
        elif model_key.lower().startswith("yolo11"):
            logging.info(f"检测到 YOLO11 系列模型 ({model_key})。将使用精选的8个默认层。")
            # (索引, 路径字符串)
            default_yolo11_layers = [
                (0, "model.model.model[0]"),  # Layer_00_Conv
                (4, "model.model.model[4]"),  # Layer_04_C3k2
                (9, "model.model.model[9]"),  # Layer_09_SPPF
                (12, "model.model.model[12]"), # Layer_12_Concat
                (15, "model.model.model[15]"), # Layer_15_Concat
                (20, "model.model.model[20]"), # Layer_20_Conv
                (21, "model.model.model[21]"), # Layer_21_Concat
                (22, "model.model.model[22]")  # Layer_22_C3k2
            ]
            
            # 如果使用 EigenCAM 或 EigenGradCAM，则只分析最后两个层
            if cam_method_name in ["EigenCAM", "EigenGradCAM"]:
                logging.info(f"  -> 使用 {cam_method_name}。将自动裁剪为最后2个默认层。")
                default_yolo11_layers = default_yolo11_layers[-3:] # (只保留最后两个)

            for idx, path_str in default_yolo11_layers:
                try:
                    # 假设 'model.model.model' 是一个列表
                    layer = model.model.model[idx]
                    target_layers_to_process.append((path_str, layer))
                    logging.info(f"  -> 已添加 YOLO11 默认层: {path_str} (索引 {idx})")
                except Exception as e:
                    logging.warning(f"  -> [!] 无法添加 YOLO11 默认层 {path_str}: {e}")
        
        # 对 YOLO12 系列模型的 'default' 逻辑 # TODO # 对模型进行修改
        elif model_key.lower().startswith("yolo12"):
            logging.info(f"检测到 YOLOv12 系列模型 ({model_key})。将使用您精选的8个默认层。")
            # (这是您新指定的列表)
            default_yolo12_layers = [
                (2, "model.model.model[2]"),   # Layer_02_C3k2
                (4, "model.model.model[4]"),   # Layer_04_C3k2
                (8, "model.model.model[8]"),   # Layer_08_A2C2f
                (10, "model.model.model[10]"), # Layer_10_Concat
                (13, "model.model.model[13]"), # Layer_13_Concat
                (17, "model.model.model[17]"), # Layer_17_A2C2f
                (19, "model.model.model[19]"), # Layer_19_Concat
                (20, "model.model.model[20]")  # Layer_20_C3k2
            ]

            # 如果使用 EigenCAM 或 EigenGradCAM，则只分析最后两个层
            if cam_method_name in ["EigenCAM", "EigenGradCAM"]:
                logging.info(f"  -> 使用 {cam_method_name}。将自动裁剪为最后2个默认层。")
                default_yolo12_layers = default_yolo12_layers[-3:] # (只保留最后两个)

            for idx, path_str in default_yolo12_layers:
                try:
                    layer = model.model.model[idx]
                    target_layers_to_process.append((path_str, layer))
                    logging.info(f"  -> 已添加 YOLOv12 默认层: {path_str} (索引 {idx})")
                except Exception as e:
                    logging.warning(f"  -> [!] 无法添加 YOLOv12 默认层 {path_str}: {e}")

        # [!! 保留 !!] 非 YOLOv8、YOLOv9、YOLO11、YOLO12 模型的 'default' 逻辑
        else:
            logging.info(f"非 YOLO11 模型 ({model_key})。将使用标准 'default' 逻辑。")
            try:
                layer = model.model.model[8].proto.cv3 # (来自您 V2 文件的 `[8]`)
                name = "model.model.model[8].proto.cv3"
                target_layers_to_process.append((name, layer))
                logging.info(f"  -> 已添加默认层: {name}")
            except Exception as e:
                logging.warning(f"  -> 无法定位 'model[8].proto.cv3'。回退到 'model[15]'。错误: {e}")
                try:
                    layer = model.model.model[15]
                    name = "model.model.model[15]"
                    target_layers_to_process.append((name, layer))
                    logging.info(f"  -> 已添加回退层: {name}")
                except Exception as e_fallback:
                    logging.error(f"  -> [!] 无法定位 'model[15]'。跳过 'default'。错误: {e_fallback}")
    
    # [!! 保留 !!] 处理非 'default' 的自定义层路径
    else:
        logging.info(f"正在解析自定义目标层: {target_layer_str}")
        layer_paths = target_layer_str.split(',')
        for path in layer_paths:
            path_cleaned = path.strip()
            if not path_cleaned:
                continue
            try:
                layer = eval(path_cleaned, {"model": model})
                if isinstance(layer, torch.nn.Module):
                    target_layers_to_process.append((path_cleaned, layer))
                    logging.info(f"  -> 已添加指定层: {path_cleaned}")
                else:
                    logging.warning(f"  -> 路径 '{path_cleaned}' 不是一个 nn.Module，已跳过。")
            except Exception as e:
                logging.error(f"  -> [!] 无法解析目标层路径 '{path_cleaned}': {e}。已跳过。")
                
    if not target_layers_to_process:
        logging.error("未找到有效的目标层进行可视化。程序退出。")
        return

    # --- 3. 查找源图片 (V1 逻辑) ---
    source_path = Path(source_dir)
    image_paths = []
    if source_path.is_file():
        image_paths = [source_path]
    elif source_path.is_dir():
        image_paths = sorted(
            list(source_path.glob("*.jpg")) +
            list(source_path.glob("*.jpeg")) +
            list(source_path.glob("*.png"))
        )
    if not image_paths:
        logging.error(f"在源路径中未找到任何图片: {source_dir}")
        return

    # --- 4. 获取 CAM 方法 (V2 逻辑) ---
    cam_class = CAM_METHODS.get(cam_method_name)
    if not cam_class:
        logging.error(f"无效的 CAM 方法: {cam_method_name}。")
        return
    logging.info(f"--- 将使用 CAM 方法: {cam_method_name} ---")

    # --- 5. [!! 循环顺序保留 !!] ---
    
    targets = None
    if cam_method_name != "EigenCAM":
        targets = [ActivationMaximizationTarget(channel=0)]
        logging.info("已为非 EigenCAM 方法设置 ActivationMaximizationTarget(channel=0)。")

    pt_name_raw = pt_name.replace('.pt', '') # 移到循环外

    # 外循环：遍历图片
    for img_path in image_paths:
        logging.info(f"\n--- 正在处理图片: {img_path.name} ---")
        
        try:
            # --- a. 加载和预处理 ---
            orig_img_bgr = cv2.imread(str(img_path))
            if orig_img_bgr is None:
                logging.warning(f"  -> 无法读取图片: {img_path.name}，已跳过。")
                continue
            
            orig_img_rgb_float = np.float32(orig_img_bgr[:, :, ::-1]) / 255
            input_tensor, pad_info = preprocess_image(orig_img_bgr, imgsz, device)
            input_tensor.requires_grad_()

            # --- b. [!! 关键修改 !!] 创建新的三层输出目录 ---
            image_stem = img_path.stem
            
            # 1. 创建算法根目录 (e.g., .../train6/HeartMap_Visual/GradCAM/)
            cam_root = base_save_dir / "HeartMap_Visual" / f"{cam_method_name}_{pt_name_raw}"
            
            # 2. 创建该图片专用的子目录 (e.g., .../HeartMap_Visual/GradCAM/image01_GradCAM/)
            nn_visual_root = cam_root / f"{image_stem}_{cam_method_name}"
            nn_visual_root.mkdir(parents=True, exist_ok=True)
            
            # 3. 创建该图片专用的 _ORI 子目录 (e.g., .../HeartMap_Visual/GradCAM/image01_GradCAM_ORI/)
            nn_visual_ori_root = cam_root / f"{image_stem}_{cam_method_name}_ORI"
            nn_visual_ori_root.mkdir(parents=True, exist_ok=True)

            # [!! 关键修改 !!] 更新日志打印路径，使其相对于 base_save_dir
            logging.info(f"  -> 覆盖图将保存到: {nn_visual_root.relative_to(base_save_dir)}")
            logging.info(f"  -> 纯热度图将保存到: {nn_visual_ori_root.relative_to(base_save_dir)}")


            # --- c. 内循环：遍历层 ---
            for layer_name, layer_module in target_layers_to_process:
                safe_layer_name = layer_name.replace('.', '_').replace('[', '').replace(']', '')
                logging.info(f"    -> 正在处理层: {layer_name}")
                
                try:
                    # --- d. 初始化 CAM ---
                    with cam_class(model=model.model, 
                                   target_layers=[layer_module], 
                                   reshape_transform=reshape_transform) as cam:

                        # --- e. 运行 CAM ---
                        cam_output = cam(input_tensor=input_tensor, targets=targets)

                        # --- f. CAM 输出后处理 ---
                        # (此部分逻辑与 V2 相同)
                        grayscale_cam = None
                        if isinstance(cam_output, (list, tuple)):
                            grayscale_cam = cam_output[0]
                        else:
                            grayscale_cam = cam_output
                        if isinstance(grayscale_cam, torch.Tensor):
                            grayscale_cam = grayscale_cam.detach().cpu().numpy()
                        if grayscale_cam.ndim == 4:
                            grayscale_cam = grayscale_cam[0].mean(axis=0) 
                        elif grayscale_cam.ndim == 3:
                            grayscale_cam = grayscale_cam[0, :]
                        elif grayscale_cam.ndim == 2:
                            pass # OK
                        else:
                            logging.warning(f"      -> [!] 层 {layer_name} 的 CAM 结果维度异常: {grayscale_cam.shape}，已跳过。")
                            continue
                        
                        # --- g. 裁剪 Padding 并 Resize ---
                        cam_cropped = grayscale_cam[pad_info['pad_top'] : pad_info['pad_top'] + pad_info['new_h'],
                                                    pad_info['pad_left'] : pad_info['pad_left'] + pad_info['new_w']]
                        cam_upscaled = cv2.resize(cam_cropped, 
                                                  (pad_info['orig_w'], pad_info['orig_h']), 
                                                  interpolation=cv2.INTER_LINEAR)
                        cam_upscaled = (cam_upscaled - cam_upscaled.min()) / (cam_upscaled.max() + 1e-8)

                        # --- h. 定义文件名和路径 ---
                        save_filename_overlay = f"Layer_{safe_layer_name}_overlay.jpg"
                        save_filename_ori = f"Layer_{safe_layer_name}_heatmap.jpg"
                        
                        save_path_overlay = nn_visual_root / save_filename_overlay
                        save_path_ori = nn_visual_ori_root / save_filename_ori

                        # --- i. 保存覆盖图 ---
                        visualization = show_cam_on_image(orig_img_rgb_float, 
                                                          cam_upscaled, 
                                                          use_rgb=True, 
                                                          image_weight=0.5)
                        viz_bgr = visualization[:, :, ::-1]
                        cv2.imwrite(str(save_path_overlay), viz_bgr)

                        # --- j. 保存纯热度图 ---
                        heatmap_uint8 = np.uint8(255 * cam_upscaled)
                        heatmap_color = cv2.applyColorMap(heatmap_uint8, cv2.COLORMAP_JET)
                        cv2.imwrite(str(save_path_ori), heatmap_color)
                        
                        logging.info(f"      -> (√) {layer_name} 可视化结果已保存")

                except Exception as e_layer:
                    logging.error(f"      -> [!] 处理层 {layer_name} 时发生错误: {e_layer}", exc_info=False)

        except Exception as e_img:
            logging.error(f"  -> [!] 处理图片 {img_path.name} 时发生严重错误: {e_img}", exc_info=True)

    logging.info("\n--- 所有图片已处理完毕 ---")


if __name__ == "__main__":
    config.show_config_summary()
    
    parser = argparse.ArgumentParser(description="使用 Grad-CAM (EigenCAM) 生成 YOLO 神经网络可视化。")
    
    parser.add_argument(
        "--model",
        type=str,
        required=True,
        choices=list(config.MODEL_CONFIGS.keys()),
        help="选择一个基础模型类型来筛选其训练历史。"
    )
    parser.add_argument(
        "--source",
        type=str,
        default=str(config.TEST_IMAGE_DIR),
        help="图片或图片文件夹的路径。"
    )
    parser.add_argument(
        "--pt_name",
        type=str,
        default="best.pt",
        help="要使用的权重文件名 (例如 'best.pt' 或 'epoch100.pt')。"
    )
    all_cam_choices = list(CAM_METHODS.keys()) + ["All"]
    parser.add_argument(
        "--cam_method",
        type=str,
        default="All",
        choices=all_cam_choices, 
        help=f"选择要使用的 CAM 可视化方法。默认为: All。"
    )
    parser.add_argument(
        "--target_layers",
        type=str,
        default="default",
        help="指定要可视化的目标层。 "
             "'default': (YOLOv8/YOLO11系列使用精选8层，其他使用标准默认层); "
             "'model.model.model[15]': (指定单个层); "
             "'model.model.model[10],model.model.model[15]': (指定多个层)"
    )
    args = parser.parse_args()
    
    # --- [!! 关键修改 !!] 移除路径校准 ---
    # 假设 yolo_config.py 已被修复，config.PREDICT_BEST_MODEL_DIR 是绝对路径
    logging.info(f"正在搜索模型目录: {config.PREDICT_BEST_MODEL_DIR}")
    available_runs = find_trained_models(config.PREDICT_BEST_MODEL_DIR, args.model, args.pt_name)
    
    run_to_use = None

    if not available_runs:
        logging.error(f"错误：在 {config.PREDICT_BEST_MODEL_DIR} 中未找到模型 '{args.model}' (使用 '{args.pt_name}') 的有效训练记录。")
        sys.exit(1)
    
    elif len(available_runs) == 1:
        run_to_use = available_runs[0]
        logging.info(f"只找到一个训练版本，已自动选择: {run_to_use}")
    
    else:
        # (V1 的模型选择菜单)
        print(f"\n为模型 '{args.model}' (使用 '{args.pt_name}') 找到多个训练版本:")
        for i, run_name in enumerate(available_runs, 1):
            print(f"  [{i}] {run_name}")
        while True:
            try:
                choice = input(f"请输入您想使用的版本序号 (1-{len(available_runs)}): ")
                choice_index = int(choice)
                if 1 <= choice_index <= len(available_runs):
                    run_to_use = available_runs[choice_index - 1]
                    break
                else:
                    print("错误：输入无效，请输入列表中的序号。")
            except ValueError:
                print("错误：请输入一个数字。")
            except (KeyboardInterrupt, EOFError):
                print("\n操作已取消。")
                sys.exit(0)

    # --- 4. 调用 [!! 新的 !!] 可视化函数 ---
    if run_to_use:
        model_to_use = config.PREDICT_BEST_MODEL_DIR / run_to_use / 'weights' / args.pt_name
        project_to_save_in = config.PREDICT_BEST_MODEL_DIR / run_to_use

        if not model_to_use.exists():
            logging.error(f"严重错误：找不到权重文件 {model_to_use}。")
        else:
            methods_to_run = []
            if args.cam_method == "All":
                methods_to_run = list(CAM_METHODS.keys()) #
                logging.info(f"--- [!!] 检测到 'All' 选项，将运行全部 {len(methods_to_run)} 种 CAM 方法 ---")
            else:
                methods_to_run = [args.cam_method] # 列表中只有一项

            for method_name in methods_to_run:
                # (可选) 检查 Eigen* 方法是否与 'default' 一起使用
                is_slow_method = method_name in ["EigenCAM", "EigenGradCAM"] #
                is_default_layers = args.target_layers == "default" #

                if is_slow_method and not is_default_layers:
                    logging.warning(f"--- [警告] 您正在对自定义层运行 {method_name}。")
                    logging.warning(f"--- 如果目标层是浅层，可能会非常缓慢或卡住。---")

                logging.info(f"\n--- 正在启动 CAM 方法: [ {method_name} ] ---")

                # 将原始调用放入循环中
                visualize_nn_comprehensive(
                    model_path=str(model_to_use), 
                    source_dir=args.source, 
                    base_save_dir=project_to_save_in,
                    pt_name=args.pt_name,
                    cam_method_name=method_name, # [!! 修改 !!] 使用循环中的变量
                    target_layer_str=args.target_layers,
                    model_key=args.model 
                )

            logging.info("\n--- [!!] 所有指定的 CAM 方法均已执行完毕 ---")