agent_deply/doc_scripts/PAM智能部署 Shell & Bat 脚本实现.md.md
dark f90bccd0ad 1、增加执行间隔
2、添加推送进度流程
2026-05-28 16:01:27 +08:00

31 KiB
Raw Permalink Blame History

PAM智能部署 Shell & Bat 脚本实现

0. 与 Skill 对齐的使用约定

本文是 API脚本 模式的参考实现,不是 MCP 模式说明。Agent 使用本文时,先遵循以下规则:

  1. 用户明确要求 MCP、直接在线执行、不要生成脚本时,不要把本文作为主执行路径。
  2. 用户明确要求“脚本部署”“生成脚本”“离线执行”“输出 config / sh / ps1 / bat”时再读取本文并生成或执行脚本。
  3. 当前目录如果只有本文档而没有真实脚本文件Agent 需要先把对应代码块落地为真实文件,再决定是否执行。
  4. Windows 默认优先 deploy.ps1deploy.bat 仅作为兼容入口或示例入口,不应作为默认正式方案。
  5. 当前文档中的字段名与 Skill 规范字段映射如下:
Skill 字段 脚本字段
HOME_BASE_URL HOME_BASE_URL
client_id CLIENT_ID
client_secret CLIENT_SECRET
airportCode AIRPORT_CODE
applicationName APP_NAME
moduleName MODULE_NAME
versionNumber VERSION_NUMBER
zipFilePath ZIP_FILE_PATH
actionType ACTION_TYPE
timeOut TIMEOUT
logName LOG_NAME
  1. 若用户只要求“生成脚本不执行”,则产物至少应包含:
    • config.txt
    • deploy.shdeploy.ps1
    • 仅在用户明确要求时再提供 deploy.bat
  2. NODE 侧接口路径统一使用 node-proxydownload-cloud/progress 需额外携带 versionNumber,并以异步轮询方式持续展示下载进度。
  3. download-cloud/progress 的完成判定优先读取 msgsteprateOfProgress;当 msg=successstep=DONErateOfProgress=100 时代表下载完成,其中 rateOfProgress 即下载进度值。
  4. upgrade-ip 固定传 timeOut=0,只负责创建推送任务;后续必须通过 upgrade/progress 异步轮询指定 IP 的推送进度。
  5. 正式部署脚本不会自动执行回滚;发现需要回滚时,只输出 PENDING_AGENT_CONFIRMATION(stopFirst=...),由 Agent 先和用户确认,再调用手动回滚入口。
  6. POST /api/mcp/version/upgradePOST /api/mcp/version/upgrade/start-stop 的业务参数都直接放在 URL query 中,不再使用 body 表单;启停接口参数名使用 runStartdownload-cloud 固定传 timeOut=0 创建任务。
  7. 脚本同时提供主流程入口与 action 入口;建议 Agent 优先调用 action 入口,由 Skill 负责主流程编排。
  8. 当前目录中的真实 deploy.sh 已去除 jq 依赖,统一使用 Bash 原生兼容 JSON 解析;若本文中的历史代码块仍出现 jq,以真实脚本文件为准。

0.1 当前实现边界

  1. Linux / Mac 路径以 deploy.sh 为主。
  2. Windows 文档里虽然展示了 deploy.bat,但它更适合作为兼容入口,不适合作为默认生产入口。
  3. 若要在 Windows 正式落地脚本模式,建议整理出独立 deploy.ps1,再由 Agent 优先执行该文件。

1. 配置文件规范 (config.txt)

为了方便管理敏感信息(如 client_secret)和特殊字符,建议将配置信息存储在 config.txt文件中。脚本会自动读取该文件。

config.txt 格式:

HOME_BASE_URL=https://pam.home.com
CLIENT_ID=your_client_id
CLIENT_SECRET=MySecret!Pass123&Special#Chars
AIRPORT_CODE=HET
APP_NAME=PAM
MODULE_NAME=Node
VERSION_NUMBER=2.0.5
ZIP_FILE_PATH=/path/to/pam-2.0.5.zip
; TARGET_IPS 已废弃,脚本将自动从接口获取在线工作站 IP
ACTION_TYPE=FULL
TIMEOUT=120
LOG_NAME=app.log ; 要下载的日志文件名,可根据实际软件调整

注意:

  1. = 左侧为变量名,右侧为值。

  2. 不要在值两侧加引号(除非是 Windows Batch 中无法避免的情况,建议尽量不加)。

  3. 特殊字符如 !%& 等在配置文件中是安全的,脚本会原样读取。

  4. Windows 和 Linux 脚本都优先读取同目录下的 config.txt。如果不存在,则回退到脚本内定义的默认值。


2. Linux/Mac Shell 脚本 (deploy.sh)

(此部分与之前版本一致Shell 脚本对特殊字符的处理比 Batch 友好得多,无需修改)

#!/bin/bash

# ==============================================================================
# PAM 智能部署脚本 (Linux/Mac)
# ==============================================================================

# --- 读取配置文件 ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/config.txt"

if [ -f "$CONFIG_FILE" ]; then
    echo "[INFO] 正在读取配置文件: ${CONFIG_FILE}"
    while IFS='=' read -r key value; do
        # 忽略注释和空行
        [[ "$key" =~ ^#.*$ ]] && continue
        [[ -z "$key" ]] && continue
        
        # 去除两端空格
        key=$(echo "$key" | xargs)
        value=$(echo "$value" | xargs)
        
        # 动态设置变量
        declare "$key=$value"
    done < "$CONFIG_FILE"
else
    echo "[WARN] 未找到配置文件 ${CONFIG_FILE},使用默认配置"
fi

# --- 默认配置 (如果 config.txt 缺失或未定义) ---
HOME_BASE_URL="${HOME_BASE_URL:-https://pam.home.com}"
CLIENT_ID="${CLIENT_ID:-your_client_id}"
CLIENT_SECRET="${CLIENT_SECRET:-your_client_secret}"
AIRPORT_CODE="${AIRPORT_CODE:-HET}"
APP_NAME="${APP_NAME:-PAM}"
MODULE_NAME="${MODULE_NAME:-Node}"
VERSION_NUMBER="${VERSION_NUMBER:-2.0.5}"
ZIP_FILE_PATH="${ZIP_FILE_PATH:-/path/to/pam-2.0.5.zip}"
ACTION_TYPE="${ACTION_TYPE:-FULL}"
TIMEOUT="${TIMEOUT:-120}"
LOG_NAME="${LOG_NAME:-app.log}"

# --- 全局变量 ---
TOKEN=""
HASH_CODE=""
NODE_URL=""
SUCCESS_COUNT=0
FAIL_COUNT=0
TOTAL_COUNT=0
RESULTS_JSON="[]" # 用于存储结果,便于最终报告生成

# --- 颜色输出 ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }

# --- 工具函数 ---

# 获取 Token
get_token() {
    log_info "正在获取 Token..."
    # 注意:移除了 -s 参数,以便 Agent 能看到 curl 的详细错误信息
    local response
    response=$(curl -X POST "${HOME_BASE_URL}/oauth/token" \
        -d "grant_type=client_credentials" \
        -d "client_id=${CLIENT_ID}" \
        -d "client_secret=${CLIENT_SECRET}" 2>&1)
    
    TOKEN=$(echo $response | jq -r '.access_token' 2>/dev/null)
    
    if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
        log_error "获取 Token 失败: $response"
        exit 1
    fi
    log_info "Token 获取成功"
}

# 通用 HTTP 请求函数
http_request() {
    local method=$1
    local url=$2
    local data=$3
    local headers=$4
    
    local curl_cmd="curl -X ${method} '${url}'"
    
    # 添加认证头
    curl_cmd="${curl_cmd} -H 'Authorization: Bearer ${TOKEN}'"
    
    # 添加额外 headers
    if [ -n "$headers" ]; then
        IFS=',' read -ra HEADER_ARRAY <<< "$headers"
        for header in "${HEADER_ARRAY[@]}"; do
            curl_cmd="${curl_cmd} -H '${header}'"
        done
    fi
    
    # 添加数据
    if [ -n "$data" ]; then
        curl_cmd="${curl_cmd} -d '${data}'"
    fi
    
    # 执行请求,重定向 stderr 到 stdout 以便捕获网络错误
    eval $curl_cmd 2>&1
}

# 上传文件函数
upload_file() {
    local url=$1
    local file_path=$2
    local form_data=$3
    
    local curl_cmd="curl -X POST '${url}' -H 'Authorization: Bearer ${TOKEN}'"
    
    # 添加 form 字段
    IFS='&' read -ra FORM_ARRAY <<< "$form_data"
    for field in "${FORM_ARRAY[@]}"; do
        key=$(echo $field | cut -d'=' -f1)
        value=$(echo $field | cut -d'=' -f2)
        curl_cmd="${curl_cmd} -F '${key}=${value}'"
    done
    
    curl_cmd="${curl_cmd} -F 'file=@${file_path}'"
    
    # 执行请求,捕获错误信息
    eval $curl_cmd 2>&1
}

# 轮询进度函数
poll_progress() {
    local url=$1
    local max_retries=${2:-60}
    local interval=${3:-2}
    local retry=0
    
    log_info "开始异步轮询下载进度..."
    
    while [ $retry -lt $max_retries ]; do
        local response
        response=$(http_request "GET" "$url")
        
        local step
        local msg
        local progress
        local status
        local success_flag
        step=$(echo $response | jq -r '.step // ""' 2>/dev/null)
        msg=$(echo $response | jq -r '.msg // .message // ""' 2>/dev/null)
        progress=$(echo $response | jq -r '.rateOfProgress // .progress // .percent // ""' 2>/dev/null)
        status=$(echo $response | jq -r '.status // ""' 2>/dev/null)
        success_flag=$(echo $response | jq -r '.success // ""' 2>/dev/null)

        if [ -n "$msg" ] || [ -n "$step" ] || [ -n "$progress" ] || [ -n "$status" ] || [ -n "$success_flag" ]; then
            log_info "异步下载进度: msg=${msg} step=${step} rateOfProgress=${progress} status=${status} success=${success_flag}"
        fi

        if [ "$step" == "DONE" ] || [ "$status" == "completed" ] || [ "$success_flag" == "true" ] || { [ "$msg" == "success" ] && [ "$progress" == "100" ]; }; then
            log_info "操作完成"
            return 0
        fi
        
        local error_msg
        error_msg="$msg"
        
        # 如果 jq 解析失败,说明返回的可能不是 JSON而是 HTTP 错误页或 curl 错误
        if [ -z "$error_msg" ] && ! echo "$response" | grep -q "success"; then
            log_error "请求异常,原始响应: $response"
            return 1
        fi
        
        if echo "$error_msg" | grep -qi "fail\|error"; then
            log_error "操作失败: $error_msg"
            return 1
        fi
        
        retry=$((retry + 1))
        log_info "等待中... ($retry/$max_retries)"
        sleep $interval
    done
    
    log_error "操作超时"
    return 1
}

# 下载日志并保存
download_log() {
    local ip=$1
    local log_name=$2
    local log_dir="./logs"
    
    mkdir -p "$log_dir"
    local log_file="${log_dir}/deploy_${ip}.log"
    
    log_info "正在下载 ${ip} 的日志: ${log_name}..."
    
    # 使用 -o 保存文件,同时保留 stderr 信息以便调试
    http_request "GET" "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/log-download?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}&logName=${log_name}" \
        "" \
        "Target-Node:${NODE_URL}" > "$log_file" 2>>"${log_dir}/error_${ip}.log"
        
    if [ $? -eq 0 ]; then
        # 检查文件大小如果为0可能是空响应但HTTP成功
        if [ ! -s "$log_file" ]; then
             log_warn "日志文件为空 (HTTP可能返回204或无内容): ${ip}"
             echo "Log content empty or no data" > "${log_file}.summary"
        else
             tail -n 5 "$log_file" > "${log_file}.summary" 2>/dev/null || true
             log_info "日志已保存至: ${log_file}"
        fi
    else
        log_error "日志下载命令执行失败 (请查看 error_${ip}.log)"
        echo "Command execution failed, see error_${ip}.log" > "$log_file"
        echo "Execution failed" > "${log_file}.summary"
    fi
}

# 添加结果到报告数据
add_result() {
    local ip=$1
    local status=$2
    local message=$3
    
    RESULTS_JSON=$(echo $RESULTS_JSON | jq --arg ip "$ip" --arg status "$status" --arg msg "$message" '. + [{"ip": $ip, "status": $status, "message": $msg}]')
    
    if [ "$status" == "SUCCESS" ]; then
        SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
    else
        FAIL_COUNT=$((FAIL_COUNT + 1))
    fi
}

# --- 主流程 ---

main() {
    log_info "=========================================="
    log_info "PAM 智能部署开始"
    log_info "机场: ${AIRPORT_CODE}, 版本: ${VERSION_NUMBER}"
    log_info "模块: ${APP_NAME}/${MODULE_NAME}"
    log_info "日志文件名: ${LOG_NAME}"
    log_info "=========================================="

    # Step 1: 获取 Token
    get_token

    # Step 2.1: 新建版本
    log_info "Step 2.1: 新建版本..."
    local create_version_url="${HOME_BASE_URL}/api/version/upgrade"
    local create_version_data="versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&description=Auto Deploy"
    http_request "POST" "$create_version_url" "$create_version_data" > /dev/null
    log_info "版本创建完成"

    # Step 2.2: 上传软件包
    log_info "Step 2.2: 上传软件包..."
    local upload_url="${HOME_BASE_URL}/api/version/upgrade/upload"
    local upload_form="applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&versionNumber=${VERSION_NUMBER}"
    HASH_CODE=$(upload_file "$upload_url" "$ZIP_FILE_PATH" "$upload_form")
    log_info "软件包上传完成Hash: ${HASH_CODE}"

    # Step 2.3: 发布版本
    log_info "Step 2.3: 发布版本..."
    local publish_url="${HOME_BASE_URL}/api/version/upgrade/profile?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}"
    local publish_data="{\"airportCodesWhite\":[\"${AIRPORT_CODE}\"],\"hashCode\":\"${HASH_CODE}\",\"state\":\"RELEASE\"}"
    http_request "PUT" "$publish_url" "$publish_data" > /dev/null
    log_info "版本发布完成"

    # Step 3.1: 获取 Node URL
    log_info "Step 3.1: 获取 Node 地址..."
    local node_info_url="${HOME_BASE_URL}/api/mcp/airport/target-node?airportCode=${AIRPORT_CODE}"
    local node_response
    node_response=$(http_request "GET" "$node_info_url")
    NODE_URL=$(echo $node_response | jq -r 'keys[0]')
    
    if [ -z "$NODE_URL" ] || [ "$NODE_URL" == "null" ]; then
        log_error "无法获取 Node 地址. Response: $node_response"
        exit 1
    fi
    log_info "Node URL: ${NODE_URL}"

    # Step 3.2: 获取在线工作站 IP (动态获取)
    log_info "Step 3.2: 获取在线工作站列表..."
    local ips_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/ips?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}"
    
    local ips_response
    ips_response=$(http_request "GET" "$ips_url" "" "Target-Node:${NODE_URL}")
    
    # 解析 IP 数组
    local ip_array
    ip_array=($(echo $ips_response | jq -r '.[]' 2>/dev/null))
    
    TOTAL_COUNT=${#ip_array[@]}
    
    if [ $TOTAL_COUNT -eq 0 ]; then
        log_warn "未找到任何在线工作站!部署终止。"
        log_warn "原始响应: $ips_response"
        exit 0
    fi
    
    log_info "找到 ${TOTAL_COUNT} 个在线工作站: ${ip_array[*]}"

    # Step 3.3: 下载软件包到 Node
    log_info "Step 3.3: 下载软件包到 Node..."
    local download_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud"
    local download_params="?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=0"
    
    http_request "GET" "${download_url}${download_params}" \
        "" \
        "airport-code:${AIRPORT_CODE},Target-Node:${NODE_URL}" > /dev/null
    
    # 轮询下载进度
    local progress_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud/progress?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&versionNumber=${VERSION_NUMBER}"
    poll_progress "$progress_url" 60 2
    if [ $? -ne 0 ]; then
        log_error "软件包下载失败"
        exit 1
    fi

    # Step 4: 升级推送至每个工作站 (动态循环)
    for ip in "${ip_array[@]}"; do
        log_info "------------------------------------------"
        log_info "处理工作站: ${ip}"
        log_info "------------------------------------------"
        
        local ip_status="SUCCESS"
        local ip_message=""

        # 4.1: 执行升级
        log_info "Step 4.1: 执行升级..."
        local upgrade_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade?airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&versionNumber=${VERSION_NUMBER}&action=${ACTION_TYPE}&autoStart=false&timeOut=${TIMEOUT}"
        
        local upgrade_response
        upgrade_response=$(http_request "POST" "$upgrade_url" "" "Target-Node:${NODE_URL}")
        
        local upgrade_success
        upgrade_success=$(echo $upgrade_response | jq -r '.success' 2>/dev/null)
        
        if [ "$upgrade_success" != "true" ]; then
            ip_status="FAILURE"
            ip_message="Upgrade failed"
            log_error "升级失败: $upgrade_response"
            log_warn "需要回滚,但当前脚本不会自动执行。请由 Agent 与用户确认后,再调用手动回滚入口。"
            rollback_result="PENDING_AGENT_CONFIRMATION(stopFirst=false)"
        else
            # 4.2: 启动应用
            log_info "Step 4.2: 启动应用..."
            local start_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop?airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runStart=true"
            http_request "POST" "$start_url" "" "Target-Node:${NODE_URL}" > /dev/null
            
            # 4.3: 健康检测
            log_info "Step 4.3: 健康检测..."
            local verify_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/verify?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}"
            local verify_response
            verify_response=$(http_request "GET" "$verify_url" "" "Target-Node:${NODE_URL}")
            
            local verify_success
            verify_success=$(echo $verify_response | jq -r '.success' 2>/dev/null)
            
            if [ "$verify_success" != "true" ]; then
                ip_status="FAILURE"
                ip_message=$(echo $verify_response | jq -r '.message // "Unknown error"' 2>/dev/null)
                log_error "健康检测失败: ${ip_message} (原始响应: $verify_response)"
                
                # 健康检测失败时只标记待确认回滚,不自动执行
                log_warn "需要回滚,但当前脚本不会自动执行。请由 Agent 与用户确认后,再调用手动回滚入口。"
                rollback_result="PENDING_AGENT_CONFIRMATION(stopFirst=true)"
            else
                log_info "健康检测通过"
            fi
        fi
        
        # 4.4: 下载日志 (无论成功与否)
        download_log "$ip" "${LOG_NAME}"
        
        # 记录结果
        add_result "$ip" "$ip_status" "$ip_message"
    done

    # Step 5: 生成报告
    log_info "=========================================="
    log_info "PAM 智能部署结束"
    log_info "=========================================="
    
    echo ""
    echo "====================== 部署报告 ======================"
    echo -e "机场: ${AIRPORT_CODE} | 版本: ${VERSION_NUMBER}"
    echo -e "总数: ${TOTAL_COUNT} | ${GREEN}成功: ${SUCCESS_COUNT}${NC} | ${RED}失败: ${FAIL_COUNT}${NC}"
    echo "-----------------------------------------------------"
    
    # 打印每个 IP 的状态
    for ip in "${ip_array[@]}"; do
        local status
        status=$(echo $RESULTS_JSON | jq -r --arg ip "$ip" '.[] | select(.ip==$ip) | .status')
        
        if [ "$status" == "SUCCESS" ]; then
            echo -e "[${GREEN}SUCCESS${NC}] ${ip}"
        else
            local msg
            msg=$(echo $RESULTS_JSON | jq -r --arg ip "$ip" '.[] | select(.ip==$ip) | .message')
            echo -e "[${RED}FAILED${NC}] ${ip} (Reason: ${msg:-N/A})"
        fi
    done
    echo "======================================================"
    log_info "详细日志文件保存在 ./logs/ 目录下"
    log_info "网络错误日志保存在 ./logs/error_*.log 文件中"
}

# 执行主函数
main "$@"

3. Windows Batch 脚本 (deploy.bat) - 兼容入口,不建议作为默认正式方案

本节主要用于兼容旧入口,或说明 Batch 在特殊字符场景下的处理方式。即使引入了 PowerShell 辅助读取配置并安全传递变量,它仍不适合作为默认正式方案。

@echo off
chcp 65001 >nul
setlocal EnableDelayedExpansion

:: ==============================================================================
:: PAM 智能部署脚本 (Windows) - 特殊字符安全版
:: ==============================================================================

:: --- 读取配置文件 (使用 PowerShell 避免 Batch 解析问题) ---
set "SCRIPT_DIR=%~dp0"
set "CONFIG_FILE=%SCRIPT_DIR%config.txt"

if exist "%CONFIG_FILE%" (
    echo [INFO] 正在读取配置文件: %CONFIG_FILE%
    :: 使用 PowerShell 读取 key=value 文件,并输出为 set 命令序列
    powershell -Command "$content = Get-Content '%CONFIG_FILE%'; foreach($line in $content) { if($line -match '^(.*?)=(.*)$') { Write-Output ('set "%%A=%%B"') } }" > config_set.tmp
    
    :: 执行生成的 set 命令
    for /f "usebackq delims=" %%L in ("config_set.tmp") do (
        %%L
    )
    
    :: 清理临时文件
    del /q config_set.tmp >nul 2>nul
) else (
    echo [WARN] 未找到配置文件 %CONFIG_FILE%,使用默认配置
)

:: --- 默认配置 (如果 config.txt 缺失或未定义) ---
set "HOME_BASE_URL=%HOME_BASE_URL: =%"
if "%HOME_BASE_URL%"=="" set "HOME_BASE_URL=https://pam.home.com"
set "CLIENT_ID=%CLIENT_ID: =%"
if "%CLIENT_ID%"=="" set "CLIENT_ID=your_client_id"
set "CLIENT_SECRET=%CLIENT_SECRET: =%"
if "%CLIENT_SECRET%"=="" set "CLIENT_SECRET=your_client_secret"
set "AIRPORT_CODE=%AIRPORT_CODE: =%"
if "%AIRPORT_CODE%"=="" set "AIRPORT_CODE=HET"
set "APP_NAME=%APP_NAME: =%"
if "%APP_NAME%"=="" set "APP_NAME=PAM"
set "MODULE_NAME=%MODULE_NAME: =%"
if "%MODULE_NAME%"=="" set "MODULE_NAME=Node"
set "VERSION_NUMBER=%VERSION_NUMBER: =%"
if "%VERSION_NUMBER%"=="" set "VERSION_NUMBER=2.0.5"
set "ZIP_FILE_PATH=%ZIP_FILE_PATH: =%"
if "%ZIP_FILE_PATH%"=="" set "ZIP_FILE_PATH=C:\path\to\pam-2.0.5.zip"
set "ACTION_TYPE=%ACTION_TYPE: =%"
if "%ACTION_TYPE%"=="" set "ACTION_TYPE=FULL"
set "TIMEOUT=%TIMEOUT: =%"
if "%TIMEOUT%"=="" set "TIMEOUT=120"
set "LOG_NAME=%LOG_NAME: =%"
if "%LOG_NAME%"=="" set "LOG_NAME=app.log"

:: --- 全局变量 ---
set "TOKEN="
set "HASH_CODE="
set "NODE_URL="
set "SUCCESS_COUNT=0"
set "FAIL_COUNT=0"
set "TOTAL_COUNT=0"

:: 临时文件用于存储结果
set "RESULTS_FILE=%TEMP%\deploy_results.json"
echo [] > "%RESULTS_FILE%"

:: --- 工具函数 ---

:: 获取 Token (使用 PowerShell 确保密码安全)
:GET_TOKEN
echo [INFO] 正在获取 Token...

:: 通过 PowerShell 构造 POST 请求,避免 Batch 转义问题
powershell -Command "$id = '%CLIENT_ID%'; $sec = '%CLIENT_SECRET%'; $url = '%HOME_BASE_URL%/oauth/token'; $body = 'grant_type=client_credentials&client_id=' + $id + '&client_secret=' + $sec; Invoke-RestMethod -Uri $url -Method POST -Body $body -ContentType 'application/x-www-form-urlencoded' | ConvertTo-Json" > token_response.json

:: 检查 PowerShell 是否成功
if not exist token_response.json (
    echo [ERROR] PowerShell 执行失败,请检查网络或配置。
    exit /b 1
)

for /f "delims=" %%i in ('powershell -Command "(Get-Content token_response.json | ConvertFrom-Json).access_token"') do (
    set "TOKEN=%%i"
)

if "%TOKEN%"=="" (
    echo [ERROR] 获取 Token 失败。请检查 client_secret 是否正确,或查看 token_response.json 内容。
    type token_response.json
    exit /b 1
)
echo [INFO] Token 获取成功
goto :eof

:: 通用 HTTP 请求函数 (使用 PowerShell 辅助,确保参数安全)
:HTTP_REQUEST
set "METHOD=%1"
set "URL=%2"
set "DATA=%3"
set "EXTRA_HEADERS=%4"
set "OUTPUT_FILE=%5"
set "ERROR_FILE=%6"

:: 构建 PowerShell 命令
set "PS_CMD=powershell -Command """
set "PS_CMD=!PS_CMD!$url = '%URL%'; $method = '%METHOD%'; "

:: 处理 Headers
if defined EXTRA_HEADERS (
    set "PS_CMD=!PS_CMD!$headers = @{'Authorization'='Basic %TOKEN%'}; "
    for %%H in (%EXTRA_HEADERS%) do (
        set "key=%%~H"
        set "val=!key:*:=!"
        set "key=!key:=%VAL%=!"
        :: 这里简化处理,复杂 header 可能需要更精细的解析,通常 Target-Node 和 airport-code 是简单的 key-value
    )
    :: 更简单的方式:直接拼接到命令中
)

:: 对于 HTTP 请求,如果数据中包含特殊字符,直接使用 PowerShell 的 Invoke-RestMethod 或 Invoke-WebRequest 更可靠
:: 这里为了保持与 Batch 逻辑的一致性,我们使用 curl 但通过 PowerShell 调用 curl 以保留 stderr
if defined OUTPUT_FILE (
    powershell -Command "curl.exe -X '%METHOD%' '%URL%' -H 'Authorization: Bearer %TOKEN%'" > "%OUTPUT_FILE%" 2>"%ERROR_FILE%"
    :: 注意:如果 DATA 非空,需要重新构造命令。为了简化,我们假设大部分 POST 数据是简单的 form-urlencoded且不含极端特殊字符或者使用 PowerShell 内部变量传递。
    :: **更安全的做法**:对于包含复杂 Data 的请求,直接使用 PowerShell。
) else (
    curl -X %METHOD% '%URL%' -H 'Authorization: Bearer %TOKEN%' 2>&1
)

:: *修正*:为了确保所有请求(特别是带 DATA 的)都能正确处理特殊字符,我们将核心请求逻辑交给 PowerShell 执行,或者确保 curl 命令被正确转义。
:: 鉴于 Batch 的局限性,以下是一个折中方案:使用 PowerShell 执行 curl。

goto :eof

:: 由于 Batch 处理复杂 HTTP 请求困难,建议将核心 API 调用脚本提取为独立的 .ps1 文件,或接受此版本的限制。
:: 为保证可用性和特殊字符安全,下面提供基于 PowerShell 封装的通用请求函数

:PS_HTTP_REQUEST
:: $1=Method, $2=URL, $3=Data(JSON), $4=Headers(Key=Value;Key=Value)
set "METHOD=%~1"
set "URL=%~2"
set "DATA=%~3"
set "HEADERS=%~4"
set "OUT_FILE=%~5"

set "PS_SCRIPT=powershell -Command """
set "PS_SCRIPT=!PS_SCRIPT!try {"

if "%METHOD%"=="GET" (
    set "PS_CMD=!PS_CMD!IWR -Uri '%URL%' -Headers @{'Authorization'='Basic %TOKEN%'} !HEADERS!"
) else if "%METHOD%"=="POST" (
    if defined DATA (
        :: 如果 Data 是 JSON直接传递如果是 Form需要转换
        set "PS_CMD=!PS_CMD!IWR -Uri '%URL%' -Method POST -Headers @{'Authorization'='Basic %TOKEN%'} !HEADERS! -Body '@'"
        :: 注意Body 的处理较为复杂,这里简化处理
    ) else (
        set "PS_CMD=!PS_CMD!IWR -Uri '%URL%' -Method POST -Headers @{'Authorization'='Basic %TOKEN%'} !HEADERS!"
    )
)

:: ... (由于 Batch 动态构造 PowerShell 命令极易出错,生产环境强烈建议将 API 调用逻辑完全迁移至 PowerShell)
:: 此处为了演示,我们保留 curl 但强调特殊字符风险。对于包含特殊字符的请求,请单独使用 PowerShell 调用。

echo [WARN] 复杂的 HTTP 请求且含特殊字符时Batch 脚本可能无法完美处理。建议将此类请求移至 .ps1 脚本中。
goto :eof

:: --- 主流程简化版 (仅展示 Token 获取的安全处理) ---

:MAIN
echo ==========================================
echo PAM 智能部署开始 (注意:特殊字符支持有限,建议配置文件中避免使用极端特殊字符或改用 PowerShell 入口)
echo ==========================================

call :GET_TOKEN

:: ... 后续步骤由于 Batch 的局限性,对于含特殊字符的 Data/URL 仍可能存在风险。
:: **最佳实践**:如果 CLIENT_SECRET 包含 !, %, &, # 等,请考虑将 `deploy.bat` 改为 `deploy.ps1`。
echo [INFO] 脚本执行完成 (测试模式)

endlocal
pause

⚠️ 重要说明:关于 Windows Batch 的特殊字符限制

经过测试,Windows Batch 原生无法完美支持在变量中存储并安全传递包含 !, %, &, <, >, & 等字符的值用于后续命令参数。即使使用 EnableDelayedExpansion,在构建 curl -d "client_secret=!CLIENT_SECRET!" 时,!...! 的语法冲突也无法避免。

最终建议方案:
如果你的 client_secret 确实包含这些特殊字符,请不要使用 Batch 脚本作为入口。请创建一个 deploy.ps1 (PowerShell 脚本),它可以完美处理所有特殊字符,并且能直接调用 .NET API 或封装 curl.exe,实现与 Shell 脚本同等甚至更强的功能。

如果你必须使用 Batch请尝试以下 最小化修改 来确保 Token 获取成功:

  1. config.txt 中,确保 CLIENT_SECRET 没有引号。

  2. deploy.bat 中,使用我提供的 powershell -Command "... Invoke-RestMethod ..." 方式获取 Token这是目前 Batch 环境下最安全的做法。


4. Linux/Mac 测试脚本 (test_deploy.sh)

(同前)

5. Windows Batch 测试脚本 (test_deploy.bat)

(同前,注意测试脚本也需更新以使用 PowerShell 获取 Token)

@echo off
chcp 65001 >nul
setlocal EnableDelayedExpansion

:: ... (配置读取逻辑同上) ...

:TEST_GET_TOKEN
echo ==========================================
echo 测试 1: 获取 Token
echo ==========================================

:: 使用 PowerShell 获取,确保特殊字符安全
powershell -Command "$id = '%CLIENT_ID%'; $sec = '%CLIENT_SECRET%'; $url = '%HOME_BASE_URL%/oauth/token'; $body = 'grant_type=client_credentials&client_id=' + $id + '&client_secret=' + $sec; Invoke-RestMethod -Uri $url -Method POST -Body $body -ContentType 'application/x-www-form-urlencoded' | ConvertTo-Json" > token_test.json

if not exist token_test.json (
    echo %RED% Token 获取失败 (PowerShell 错误)
    set /a FAIL_COUNT+=1
) else (
    for /f "delims=" %%i in ('powershell -Command "(Get-Content token_test.json | ConvertFrom-Json).access_token"') do (
        set "TOKEN=%%i"
    )

    if "%TOKEN%"=="" (
        echo %RED% Token 获取失败
        type token_test.json
        set /a FAIL_COUNT+=1
    ) else (
        echo %GREEN% Token 获取成功
        set /a PASS_COUNT+=1
    )
)
goto :eof

:: ... (其他测试步骤类似,对于涉及 POST Data 的测试,建议也使用 PowerShell) ...

6. 使用说明

Agent action 调用示例

bash ./deploy.sh --config ./config.txt --action get-online-ips
bash ./deploy.sh --config ./config.txt --action create-download-task
bash ./deploy.sh --config ./config.txt --action poll-download-progress
bash ./deploy.sh --config ./config.txt --action upgrade-ip --ip 192.168.1.10
bash ./deploy.sh --config ./config.txt --action poll-upgrade-progress --ip 192.168.1.10
powershell -File .\deploy.ps1 -ConfigPath .\config.txt -Action GetOnlineIps
powershell -File .\deploy.ps1 -ConfigPath .\config.txt -Action CreateDownloadTask
powershell -File .\deploy.ps1 -ConfigPath .\config.txt -Action PollDownloadProgress
powershell -File .\deploy.ps1 -ConfigPath .\config.txt -Action UpgradeIp -Ip 192.168.1.10

前置依赖

  1. Linux/Mac:

    • curl: 通常预装
  2. Windows:

    • PowerShell: 必须使用 PowerShell 5.0+ (Windows 10 默认)

    • curl.exe: Windows 10+ 自带

关键配置建议

  • Linux/Mac: config.txt 中的特殊字符可以安全使用。

  • Windows:

    • Token 获取已通过 PowerShell 包装,支持特殊字符。

    • 其他涉及 POST Body 包含复杂特殊字符的请求Batch 脚本可能存在局限。如果遇到问题,请将对应步骤移至 PowerShell 脚本中执行。