gaoluyang
6 小时以前 f156371f684afd7ab6fee7be2de235220762a4e9
feat: 新增 JHHG.sh —— 九鸿化工前端部署脚本的 macOS 版

由 JHHG.bat 转写,步骤/提示/服务器/账号/远端目录与 .bat 一致:
JHHG -> 42.63.70.90:/home/vue/dist,按 VITE_COMPANY=JHHG 构建。
无 sshpass.exe 依赖,无 sshpass 时退回 macOS 自带的 expect 喂密码;
已按英泽版的两处实机修复固化:scp 远端路径不加引号、远端大小比对只取退出码。

密码含 ! 号:cmd 的 EnableDelayedExpansion 并未吞掉它,服务器接受的就是
字面量 Nxjhhg!@##@!(去掉后半段的 Nxjhhg 实测被拒)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
467 ■■■■■ 文件已修改
JHHG.sh 467 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
JHHG.sh
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,467 @@
#!/usr/bin/env bash
# ============================================================================
#  Vue å‰ç«¯éƒ¨ç½²è„šæœ¬ï¼ˆmacOS / Linux ç‰ˆï¼‰â€”—  é“¶å·ä¹é¸¿åŒ–工(JHHG)
#
#  ç”±åŒç›®å½•çš„ JHHG.bat è½¬å†™è€Œæ¥ï¼šæ­¥éª¤ã€æç¤ºè¯­ã€æœåŠ¡å™¨ã€è´¦å·ã€è¿œç«¯ç›®å½•ã€
#  è¶…时参数与 .bat å®Œå…¨ä¸€è‡´ï¼ŒWindows ä¸Šæ€Žä¹ˆç”¨ï¼Œè¿™é‡Œå°±æ€Žä¹ˆç”¨ã€‚
#
#  ä¸Ž .bat çš„全部差别:
#    1. ä¸ä¾èµ– sshpass.exe。系统里有 sshpass å°±ç”¨ sshpass,没有就退回 macOS
#       è‡ªå¸¦çš„ /usr/bin/expect æ¥å–‚密码 â€”— æ‰€ä»¥è¿™å° Mac ä¸ç”¨è£…任何东西也能跑。
#    2. è„šæœ¬å¼€å¤´ä¼š cd åˆ°è‡ªå·±æ‰€åœ¨ç›®å½•,从任何位置调用都能正确构建和上传。
#    3. ç»“尾的 pause åªåœ¨äº¤äº’式终端里生效,被其它脚本/管道调用时不会卡住。
#    4. ã€Œæ¯”较文件大小」这一步按标题的原意真正做了比对(.bat åªæ‰“印不比对),
#       å¤§å°ä¸ä¸€è‡´ç›´æŽ¥æŠ¥é”™ï¼Œé¿å…æŠŠä¸€ä¸ªä¸Šä¼ æ®‹ç¼ºçš„包解压到线上目录。
#
#  ç”¨æ³•:
#      ./JHHG.sh                 # é¦–次可能需要 chmod +x JHHG.sh
#      bash JHHG.sh
#
#  å¯é€‰çŽ¯å¢ƒå˜é‡ï¼ˆä¸€èˆ¬ç”¨ä¸åˆ°ï¼‰ï¼š
#      JHHG_TRANSPORT=sshpass|expect    å¼ºåˆ¶æŒ‡å®šå–‚密码的方式
# ============================================================================
set -uo pipefail
# =========================
# é…ç½®ï¼ˆä¸Ž JHHG.bat ä¸€å­—不差)
# =========================
readonly COMPANY='JHHG'
readonly REMOTE_USER='root'
readonly REMOTE_HOST='42.63.70.90'
readonly REMOTE_PORT='22'
# å¯†ç å« ! å’Œ #:必须用单引号;不要 source åˆ°äº¤äº’式 shell é‡Œè·‘(那里会做 ! åŽ†å²å±•å¼€ï¼‰ï¼Œ
# ç”¨ ./JHHG.sh æˆ– bash JHHG.sh æ‰§è¡Œã€‚
# 2026-10-12 å®žæµ‹è¿‡ï¼šæœåŠ¡å™¨æŽ¥å—çš„å°±æ˜¯è¿™ä¸ªå®Œæ•´å­—ç¬¦ä¸²ï¼ˆåŽ»æŽ‰ !@##@! çš„ Nxjhhg ä¼šè¢«æ‹’)。
readonly PASSWORD='Nxjhhg!@##@!'
readonly ZIP_FILE='dist.zip'
readonly REMOTE_DIR='/home/vue/dist'
readonly SSH_OPTS=(-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3)
readonly REMOTE_TARGET="${REMOTE_USER}@${REMOTE_HOST}"
readonly REMOTE_TMP="${REMOTE_DIR}/.dist.zip.uploading"
# è„šæœ¬è‡ªèº«æ‰€åœ¨ç›®å½• = é¡¹ç›®æ ¹ç›®å½•
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# =========================
# è¾“出helper(与 .bat çš„æç¤ºæ ¼å¼ä¸€è‡´ï¼‰
# =========================
log()   { printf '%s\n' "$*"; }
ok()    { printf '[OK] %s\n' "$*"; }
info()  { printf '[INFO] %s\n' "$*"; }
warn()  { printf '[WARN] %s\n' "$*"; }
err()   { printf '[ERROR] %s\n' "$*"; }
# ä¸Ž .bat çš„ :END ä¸€æ ·ï¼Œäº¤äº’式终端里停一下再退出
pause_if_interactive() {
    if [[ -t 0 ]]; then
        printf '\n按回车键退出...'
        read -r _ || true
        printf '\n'
    fi
}
fail() {
    log ''
    log '============================================================'
    log '                  DEPLOY FAILED'
    log '============================================================'
    log ''
    log 'Please check the error message above.'
    log ''
    pause_if_interactive
    exit 1
}
# æ²¡è¢«æ˜¾å¼å¤„理就失败的命令,也走到 DEPLOY FAILED,不留半截输出
on_unexpected_error() {
    err "命令失败(退出码 $1,脚本第 $2 è¡Œï¼‰ã€‚"
    fail
}
trap 'on_unexpected_error $? $LINENO' ERR
# =========================
# ä¼ è¾“层:把密码喂给 ssh/scp
# =========================
# ä¼˜å…ˆ sshpass(和 .bat ä¸€æ ·ï¼‰ï¼Œæ²¡æœ‰å°±ç”¨ macOS è‡ªå¸¦çš„ expect
TRANSPORT="${JHHG_TRANSPORT:-}"
if [[ -z "$TRANSPORT" ]]; then
    if command -v sshpass >/dev/null 2>&1; then
        TRANSPORT='sshpass'
    elif command -v expect >/dev/null 2>&1; then
        TRANSPORT='expect'
    fi
fi
# ç”¨å½“前 TRANSPORT æ‰§è¡Œä¸€æ¡ ssh/scp å‘½ä»¤ï¼Œå‚数原样透传
# å¯†ç é€šè¿‡çŽ¯å¢ƒå˜é‡ä¼ é€’ï¼Œä¸å‡ºçŽ°åœ¨å‘½ä»¤è¡Œå‚æ•°é‡Œï¼ˆä¸ä¼šè¿› ps è¾“出)
run_remote() {
    if [[ "$TRANSPORT" == 'sshpass' ]]; then
        SSHPASS="$PASSWORD" sshpass -e "$@"
        return $?
    fi
    # expect åˆ†æ”¯ï¼šç¨‹åºå¿…须写成文件再用 -f è°ƒç”¨ï¼Œå› ä¸º `expect -c è„šæœ¬ å‚æ•°...`
    # ä¸ä¼šæŠŠå‚数放进 $argv(会把它当脚本文件名)。
    local exp_file rc
    exp_file="$(mktemp "${TMPDIR:-/tmp}/jhhg-deploy.XXXXXX")" || {
        err 'temp file creation failed.'
        return 1
    }
    cat >"$exp_file" <<'EXPECT_PROGRAM'
set timeout 300
log_user 1
if {[catch {spawn {*}$argv} spawn_err]} {
    puts stderr "spawn failed: $spawn_err"
    exit 127
}
expect {
    -re "(?i)are you sure you want to continue connecting" { send "yes\r"; exp_continue }
    -re "(?i)password:" { send "$env(JHHG_PW)\r"; exp_continue }
    eof
}
# æŠŠå­è¿›ç¨‹çš„退出码原样带出去(密码错时 ssh è¿”回 255,必须让上层感知到)
set wait_result {}
catch wait wait_result
set rc [lindex $wait_result 3]
if {![string is integer -strict $rc]} { set rc 1 }
exit $rc
EXPECT_PROGRAM
    JHHG_PW="$PASSWORD" expect -f "$exp_file" "$@"
    rc=$?
    rm -f "$exp_file"
    return $rc
}
ssh_remote() {                       # ssh_remote "远端命令"
    run_remote ssh -p "$REMOTE_PORT" "${SSH_OPTS[@]}" "$REMOTE_TARGET" "$1"
}
scp_up() {                           # scp_up æœ¬åœ°æ–‡ä»¶ è¿œç«¯è·¯å¾„
    run_remote scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$1" "${REMOTE_TARGET}:$2"
}
# =========================
# å¼€å§‹
# =========================
log ''
log '============================================================'
log '             Vue Dist Auto Deploy'
log '============================================================'
log ''
log "Company    : $COMPANY"
log "Server     : $REMOTE_USER@$REMOTE_HOST"
log "Port       : $REMOTE_PORT"
log "Remote Dir : $REMOTE_DIR"
log "Package    : $ZIP_FILE"
log ''
# =========================
# [1/7] å¯†ç å·¥å…·
# =========================
log '[1/7] Checking password tool...'
if [[ -z "$TRANSPORT" ]]; then
    err 'sshpass å’Œ expect éƒ½æ²¡æœ‰ï¼Œæ— æ³•自动输入密码。'
    info 'macOS ä¸€èˆ¬è‡ªå¸¦ /usr/bin/expect,正常不会走到这里;'
    info '如果确实没有,装 sshpass å³å¯ï¼ˆå¯èƒ½éœ€å…ˆåŠ ç¬¬ä¸‰æ–¹ tap):brew install sshpass'
    info 'Linux: apt install sshpass  /  yum install sshpass'
    fail
fi
if [[ "$TRANSPORT" == 'sshpass' ]]; then
    ok "sshpass found: $(command -v sshpass)"
else
    ok "expect found: $(command -v expect)(macOS è‡ªå¸¦ï¼Œæ— éœ€å®‰è£… sshpass)"
fi
# =========================
# [2/7] pnpm
# =========================
log ''
log '[2/7] Checking pnpm...'
if ! command -v pnpm >/dev/null 2>&1; then
    err 'pnpm not found.'
    info 'HINT: npm install -g pnpm'
    fail
fi
if ! pnpm --version; then
    err 'pnpm execution failed.'
    fail
fi
# =========================
# [3/7] node
# =========================
log ''
log '[3/7] Checking Node.js...'
if ! command -v node >/dev/null 2>&1; then
    err 'Node.js not found.'
    fail
fi
if ! node --version; then
    err 'Node.js execution failed.'
    fail
fi
# =========================
# node_modules
# =========================
if [[ ! -d node_modules ]]; then
    log ''
    info 'node_modules not found.'
    info 'Running pnpm install...'
    if ! pnpm install; then
        err 'pnpm install failed.'
        fail
    fi
    ok 'pnpm install completed.'
else
    ok 'node_modules exists.'
fi
# =========================
# [4/7] æž„建
# =========================
log ''
log '[4/7] Building project...'
SKIP_BUILD=''
if [[ -f "$ZIP_FILE" ]]; then
    log ''
    warn "$ZIP_FILE already exists."
    printf 'Rebuild? Y/N (default Y): '
    read -r REBUILD_ANSWER || true
    case "$(printf '%s' "${REBUILD_ANSWER:-}" | tr '[:lower:]' '[:upper:]')" in
        N) info 'Skip build.'; SKIP_BUILD='1' ;;
    esac
fi
if [[ -z "$SKIP_BUILD" ]]; then
    info "Company: $COMPANY"
    info 'Command: pnpm run build'
    log ''
    export VITE_COMPANY="$COMPANY"
    if ! pnpm run build; then
        log ''
        err 'Build failed.'
        fail
    fi
    log ''
    ok 'Build completed.'
fi
# =========================
# [5/7] æ£€æŸ¥åŒ…
# =========================
log ''
log '[5/7] Checking package...'
if [[ ! -f "$ZIP_FILE" ]]; then
    err "$ZIP_FILE not found."
    info 'HINT: Please check whether your build generates dist.zip.'
    fail
fi
LOCAL_ZIP_BYTES="$(wc -c < "$ZIP_FILE" | tr -d ' ')"
LOCAL_ZIP_MB="$(awk -v b="$LOCAL_ZIP_BYTES" 'BEGIN { printf "%.2f", b / 1048576 }')"
ok 'Package found.'
info "File size: $LOCAL_ZIP_BYTES bytes"
info "File size: $LOCAL_ZIP_MB MB"
# =========================
# [6/7] æµ‹è¯• SSH
# =========================
log ''
log '[6/7] Testing SSH connection...'
if ! ssh_remote 'echo SSH_OK'; then
    err 'SSH connection failed.'
    fail
fi
ok 'SSH connection successful.'
# =========================
# å»ºè¿œç«¯ç›®å½•
# =========================
log ''
info 'Creating remote directory...'
if ! ssh_remote "mkdir -p '$REMOTE_DIR'"; then
    err 'Failed to create remote directory.'
    fail
fi
ok 'Remote directory ready.'
# =========================
# ä¸Šä¼ ä¸´æ—¶æ–‡ä»¶
# =========================
log ''
info 'Uploading package...'
ssh_remote "rm -f '$REMOTE_TMP'" || true
# è¿œç«¯è·¯å¾„不能加引号:ssh é‚£æ¡çš„单引号是给远端 shell å‰¥çš„,而 scp çš„参数不经 shell,
# å•引号会变成文件名的真实字符(远端会去开 "'/home/vue/dist/..." è¿™ä¸ªç›¸å¯¹è·¯å¾„)
if ! scp_up "$ZIP_FILE" "$REMOTE_TMP"; then
    err 'Upload failed.'
    fail
fi
ok 'Upload completed.'
# =========================
# æ ¡éªŒè¿œç«¯æ–‡ä»¶
# =========================
log ''
info 'Checking remote package...'
if ! ssh_remote "test -f '$REMOTE_TMP'"; then
    err 'Remote package does not exist.'
    fail
fi
ok 'Remote package exists.'
# =========================
# æ¯”较文件大小
# =========================
log ''
info 'Checking package size...'
log ''
# å¤§å°æ¯”对放在远端做、只取退出码。
# ä¸è¦ç”¨ $(run_remote ...) åŽ»æˆªå–è¿œç«¯è¾“å‡ºï¼šexpect ä¼šæŠŠæ•´æ®µäº¤äº’回显(spawn å‘½ä»¤è¡Œã€
# å¯†ç æç¤ºï¼‰ä¸€èµ·å†™è¿› stdout,截出来的不是命令结果,比对必然失败。
if ! ssh_remote "test \"\$(stat -c %s '$REMOTE_TMP')\" -eq $LOCAL_ZIP_BYTES"; then
    # å˜é‡ä¸€å¾‹åŠ  ${}:bash 3.2 ä¼šæŠŠç´§è·Ÿå…¶åŽçš„全角字符(如「(」)当成变量名的一部分,
    # äºŽæ˜¯ $ZIP_FILE( ä¼šè¢«è§£æžæˆåä¸º "ZIP_FILE(" çš„变量,配 set -u ç›´æŽ¥æŠ¥ unbound variable
    err "包大小不一致:远端 ${REMOTE_TMP} ä¸Žæœ¬åœ° ${ZIP_FILE}(${LOCAL_ZIP_BYTES} å­—节)不符,上传可能被截断。"
    info "远端临时文件已保留。可在服务器上执行 stat -c %s '${REMOTE_TMP}' æ ¸å¯¹ï¼Œç¡®è®¤åŽåˆ é™¤ã€‚"
    fail
fi
ok "包大小一致:${LOCAL_ZIP_BYTES} å­—节。"
# =========================
# æ”¹åç”Ÿæ•ˆ
# =========================
log ''
info 'Activating package...'
if ! ssh_remote "mv -f '$REMOTE_TMP' '$REMOTE_DIR/$ZIP_FILE'"; then
    err 'Failed to activate package.'
    fail
fi
ok 'Package activated.'
# =========================
# æ£€æŸ¥ unzip
# =========================
log ''
info 'Checking unzip...'
if ! ssh_remote 'command -v unzip'; then
    err 'unzip is not installed on server.'
    log ''
    log 'CentOS/RHEL:'
    log 'yum install -y unzip'
    log ''
    log 'Ubuntu/Debian:'
    log 'apt install -y unzip'
    fail
fi
ok 'unzip found.'
# =========================
# æµ‹è¯• ZIP
# =========================
log ''
info 'Testing remote ZIP...'
if ! ssh_remote "cd '$REMOTE_DIR' && unzip -t '$ZIP_FILE'"; then
    log ''
    err 'ZIP test failed.'
    info 'Remote ZIP has been kept.'
    fail
fi
ok 'ZIP test passed.'
# =========================
# è§£åŽ‹
# =========================
log ''
info 'Extracting package...'
if ! ssh_remote "cd '$REMOTE_DIR' && unzip -o '$ZIP_FILE'"; then
    log ''
    err 'Remote unzip failed.'
    info 'Remote ZIP has been kept.'
    fail
fi
log ''
ok 'Extract completed.'
# =========================
# åˆ é™¤è¿œç«¯ ZIP
# =========================
log ''
info 'Removing remote ZIP...'
if ssh_remote "rm -f '$REMOTE_DIR/$ZIP_FILE'"; then
    ok 'Remote ZIP removed.'
else
    warn 'Failed to remove remote ZIP.'
fi
# =========================
# [7/7] å®Œæˆ
# =========================
log ''
log '============================================================'
log '                 DEPLOY SUCCESS'
log '============================================================'
log ''
log "Company    : $COMPANY"
log "Server     : $REMOTE_HOST:$REMOTE_PORT"
log "Remote Dir : $REMOTE_DIR"
log "Package    : $ZIP_FILE"
log "Size       : $LOCAL_ZIP_MB MB"
log ''
log 'Deployment completed successfully.'
log ''
pause_if_interactive