agent_deply/doc_scripts/PAM智能部署 Shell & Bat 脚本实现.md.md
2026-05-20 14:41:25 +08:00

766 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
7. NODE 侧接口路径统一使用 `node-proxy``download-cloud/progress` 需额外携带 `versionNumber`,并以异步轮询方式持续展示下载进度。
8. `download-cloud/progress` 的完成判定优先读取 `msg``step``rateOfProgress`;当 `msg=success``step=DONE``rateOfProgress=100` 时代表下载完成,其中 `rateOfProgress` 即下载进度值。
9. 正式部署脚本不会自动执行回滚;发现需要回滚时,只输出 `PENDING_AGENT_CONFIRMATION(stopFirst=...)`,由 Agent 先和用户确认,再调用手动回滚入口。
10. `POST /api/mcp/version/upgrade``POST /api/mcp/version/upgrade/start-stop` 的业务参数都直接放在 URL query 中,不再使用 body 表单;启停接口参数名使用 `runStart``download-cloud` 固定传 `timeOut=0` 创建任务。
## 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: 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 辅助读取配置并安全传递变量**,它仍不适合作为默认正式方案。
```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: 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)_
```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 脚本中执行。