# PAM智能部署 Shell & Bat 脚本实现 ## 0. 与 Skill 对齐的使用约定 本文是 `API脚本` 模式的参考实现,不是 `MCP` 模式说明。Agent 使用本文时,先遵循以下规则: 1. 用户明确要求 `MCP`、直接在线执行、不要生成脚本时,不要把本文作为主执行路径。 2. 用户明确要求“脚本部署”“生成脚本”“离线执行”“输出 config / sh / ps1 / bat”时,再读取本文并生成或执行脚本。 3. 当前目录如果只有本文档而没有真实脚本文件,Agent 需要先把对应代码块落地为真实文件,再决定是否执行。 4. Windows 默认优先 `deploy.ps1`;`deploy.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` | 6. 若用户只要求“生成脚本不执行”,则产物至少应包含: - `config.txt` - `deploy.sh` 或 `deploy.ps1` - 仅在用户明确要求时再提供 `deploy.bat` ## 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 格式:** ```ini 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 友好得多,无需修改)_ ```bash #!/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: Basic ${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: Basic ${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 status # 尝试解析 JSON,如果失败则可能是网络错误 status=$(echo $response | jq -r '.status // .success // ""' 2>/dev/null) if [ "$status" == "completed" ] || [ "$status" == "true" ]; then log_info "操作完成" return 0 fi local error_msg error_msg=$(echo $response | jq -r '.message // ""' 2>/dev/null) # 如果 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=${TIMEOUT}" 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}" 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" local upgrade_data="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" "$upgrade_data" "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 "尝试回滚..." # 回滚逻辑 local rollback_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/rollback" local rollback_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}" http_request "POST" "$rollback_url" "$rollback_data" "Target-Node:${NODE_URL}" > /dev/null log_warn "已触发回滚" else # 4.2: 启动应用 log_info "Step 4.2: 启动应用..." local start_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop" local start_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runstart=true" http_request "POST" "$start_url" "$start_data" "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 "尝试回滚..." local rollback_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/rollback" local rollback_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}" http_request "POST" "$rollback_url" "$rollback_data" "Target-Node:${NODE_URL}" > /dev/null 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 辅助读取配置并安全传递变量**,它仍不适合作为默认正式方案。 ```bat @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: Basic %TOKEN%'" > "%OUTPUT_FILE%" 2>"%ERROR_FILE%" :: 注意:如果 DATA 非空,需要重新构造命令。为了简化,我们假设大部分 POST 数据是简单的 form-urlencoded,且不含极端特殊字符,或者使用 PowerShell 内部变量传递。 :: **更安全的做法**:对于包含复杂 Data 的请求,直接使用 PowerShell。 ) else ( curl -X %METHOD% '%URL%' -H 'Authorization: Basic %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)_ ```bat @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. 使用说明 ### 前置依赖 1. **Linux/Mac**: * `curl`: 通常预装 * `jq`: JSON 解析工具 2. **Windows**: * `PowerShell`: 必须使用 PowerShell 5.0+ (Windows 10 默认) * `curl.exe`: Windows 10+ 自带 ### 关键配置建议 * **Linux/Mac**: `config.txt` 中的特殊字符可以安全使用。 * **Windows**: * Token 获取已通过 PowerShell 包装,支持特殊字符。 * 其他涉及 POST Body 包含复杂特殊字符的请求,Batch 脚本可能存在局限。如果遇到问题,请将对应步骤移至 PowerShell 脚本中执行。