29 KiB
PAM智能部署 Shell & Bat 脚本实现
0. 与 Skill 对齐的使用约定
本文是 API脚本 模式的参考实现,不是 MCP 模式说明。Agent 使用本文时,先遵循以下规则:
- 用户明确要求
MCP、直接在线执行、不要生成脚本时,不要把本文作为主执行路径。 - 用户明确要求“脚本部署”“生成脚本”“离线执行”“输出 config / sh / ps1 / bat”时,再读取本文并生成或执行脚本。
- 当前目录如果只有本文档而没有真实脚本文件,Agent 需要先把对应代码块落地为真实文件,再决定是否执行。
- Windows 默认优先
deploy.ps1;deploy.bat仅作为兼容入口或示例入口,不应作为默认正式方案。 - 当前文档中的字段名与 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 |
- 若用户只要求“生成脚本不执行”,则产物至少应包含:
config.txtdeploy.sh或deploy.ps1- 仅在用户明确要求时再提供
deploy.bat
0.1 当前实现边界
- Linux / Mac 路径以
deploy.sh为主。 - Windows 文档里虽然展示了
deploy.bat,但它更适合作为兼容入口,不适合作为默认生产入口。 - 若要在 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 ; 要下载的日志文件名,可根据实际软件调整
注意:
-
=左侧为变量名,右侧为值。 -
不要在值两侧加引号(除非是 Windows Batch 中无法避免的情况,建议尽量不加)。
-
特殊字符如
!、%、&等在配置文件中是安全的,脚本会原样读取。 -
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: 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 辅助读取配置并安全传递变量,它仍不适合作为默认正式方案。
@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 获取成功:
-
在
config.txt中,确保CLIENT_SECRET没有引号。 -
在
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. 使用说明
前置依赖
-
Linux/Mac:
-
curl: 通常预装 -
jq: JSON 解析工具
-
-
Windows:
-
PowerShell: 必须使用 PowerShell 5.0+ (Windows 10 默认) -
curl.exe: Windows 10+ 自带
-
关键配置建议
-
Linux/Mac:
config.txt中的特殊字符可以安全使用。 -
Windows:
-
Token 获取已通过 PowerShell 包装,支持特殊字符。
-
其他涉及 POST Body 包含复杂特殊字符的请求,Batch 脚本可能存在局限。如果遇到问题,请将对应步骤移至 PowerShell 脚本中执行。
-