#!/usr/bin/env bash # ============================================================ # OpenClaw 工具箱 — 本地开发工具集 # # 交互式菜单,选择要执行的功能。 # 也支持命令行直接调用:./main.sh <命令> [参数...] # # 命令列表: # bootstrap 交互式部署 OpenClaw 到 HF Space # rebuild-space 推送代码到 Space 并触发重建 # restart-space 重启 Space # pause-space 暂停 Space # factory-rebuild Factory 重建 Space(删除重建) # cleanup 批量清理 Dataset 中的旧备份 # find-backup 查找最佳备份(可选清理旧备份) # rm-hf 删除 HF 仓库/文件/存储 # hf-backup HF Dataset 本地↔远程交互备份(快照/验证/清理/恢复) # hf-account HF 多账号管理(添加/切换/删除账号) # storage HF 存储工具(上传/下载/同步) # ============================================================ set -euo pipefail # ---- Fix terminal for interactive input ---- if [[ -t 0 ]]; then stty sane 2>/dev/null || true fi SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" # 引入 HF 用户 helper # shellcheck source=/dev/null source "$SCRIPT_DIR/_hf_user.sh" 2>/dev/null || true # 获取默认 HF 用户名(用于示例提示) get_default_hf_user() { if declare -f get_hf_username >/dev/null 2>&1; then local u u="$(get_hf_username 2>/dev/null)" || true if [[ -n "$u" ]]; then printf '%s' "$u" return 0 fi fi return 1 } # 获取默认 Space 或 Dataset 名(基于当前目录名) get_default_repo_name() { basename "$(pwd)" 2>/dev/null || true } # ---- HF 账号切换提示 ---- prompt_yes_no() { local prompt="$1" local default="${2:-}" local choice="" local answer="" while [[ -z "$answer" ]]; do if [[ -t 0 ]]; then if [[ -n "$default" ]]; then read -r -p "$prompt (Y/n) [$default]: " choice || true else read -r -p "$prompt (y/N) [$default]: " choice || true fi else choice="$default" fi choice="$(printf '%s' "$choice" | tr -d '\r\n' | tr '[:upper:]' '[:lower:]')" if [[ -z "$choice" ]]; then choice="$default" fi case "$choice" in y|yes) answer="yes" ;; n|no) answer="no" ;; *) printf 'Please answer y or n.\n' >&2 ;; esac done printf '%s' "$answer" } prompt_secret() { local prompt="$1" local value="" local char="" if [[ -t 0 && -t 1 ]]; then printf '%s: ' "$prompt" >&2 while IFS= read -r -s -n 1 char; do if [[ -z "$char" ]]; then break fi case "$char" in $'\e') break ;; '') break ;; *) value+="$char"; printf '*' >&2 ;; esac done printf '\n' >&2 fi printf '%s' "$value" } prompt_hf_account_switch() { local current_username="" local hf_whoami_output hf_whoami_output="$(hf auth whoami 2>&1 || true)" current_username="$(printf '%s\n' "$hf_whoami_output" | sed -nE 's/^[[:space:]]*user:[[:space:]]*([^[:space:]]+).*/\1/p' | head -n 1)" if [[ -z "$current_username" ]]; then current_username="$(printf '%s\n' "$hf_whoami_output" | sed -nE 's/.*[Ll]ogged in as[[:space:]]+([^[:space:]]+).*/\1/p' | head -n 1)" fi if [[ -n "$current_username" ]] && [[ -t 0 ]]; then local use_current use_current="$(prompt_yes_no "HF CLI is logged in as '$current_username'. Use this user?" "y")" if [[ "$use_current" == "yes" ]]; then return 0 fi fi while true; do if [[ -t 0 ]]; then local token token="$(prompt_secret "Enter HF_TOKEN to switch account")" if [[ -z "$token" ]]; then printf '[WARN] Empty token, please try again or press Ctrl+C to cancel\n' >&2 continue fi local login_output login_output="$(hf auth login --token "$token" 2>&1)" if [[ $? -eq 0 ]]; then local verify_output verify_output="$(hf auth whoami 2>&1 || true)" local new_username new_username="$(printf '%s\n' "$verify_output" | sed -nE 's/^[[:space:]]*user:[[:space:]]*([^[:space:]]+).*/\1/p' | head -n 1)" if [[ -z "$new_username" ]]; then new_username="$(printf '%s\n' "$verify_output" | sed -nE 's/.*[Ll]ogged in as[[:space:]]+([^[:space:]]+).*/\1/p' | head -n 1)" fi if [[ -n "$current_username" ]] && [[ -n "$new_username" ]] && [[ "$current_username" != "$new_username" ]]; then printf '[INFO] HF account switched: %s -> %s\n' "$current_username" "$new_username" elif [[ -n "$new_username" ]]; then printf '[INFO] HF login successful as: %s\n' "$new_username" else printf '[INFO] HF login successful\n' fi return 0 else printf '[ERROR] HF login failed: %s\n' "$login_output" >&2 continue fi else return 1 fi done } # ---- 检测 uv 并设置 Python 命令 ---- if command -v uv &>/dev/null && [[ -f "$REPO_ROOT/pyproject.toml" ]]; then PYTHON="uv run python3" else PYTHON="python3" fi RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' info() { printf "${GREEN}%s${NC}\n" "$*"; } warn() { printf "${YELLOW}%s${NC}\n" "$*" >&2; } error() { printf "${RED}%s${NC}\n" "$*" >&2; } title() { printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; } # ---- List datasets owned by the current HF user ---- # Echoes dataset IDs (one per line) on stdout. Empty stdout = no datasets # (or hf CLI not logged in). list_user_datasets() { local user user="$(get_default_hf_user 2>/dev/null)" || return 1 hf datasets list --author "$user" 2>/dev/null \ | grep -E '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+[[:space:]]' \ | awk '{print $1}' \ | sort } # ---- Generic numbered picker ---- # Args: # $1 = prompt text (shown above the list) # $2 = cancel label (e.g. "取消") # $3 = "yes" / "no" — whether to add a manual-input option # $@ = items to choose from (each becomes one row) # Behavior: # - All UI text goes to stderr; the chosen item is on stdout. # - Re-prompts on invalid input; non-numeric / out-of-range → warn. # - Returns 0 + item on stdout on success, 1 on cancel, 2 on non-TTY. pick_from_list() { local prompt="$1" local cancel_label="$2" local allow_manual="$3" shift 3 local -a items=("$@") if [[ ! -t 0 ]]; then error "Picker requires a TTY" return 2 fi if [[ ${#items[@]} -eq 0 ]]; then warn "没有可选项" return 1 fi echo "$prompt:" >&2 local i for i in "${!items[@]}"; do printf " %d) %s\n" "$((i+1))" "${items[$i]}" >&2 done printf " c) %s\n" "$cancel_label" >&2 if [[ "$allow_manual" == "yes" ]]; then printf " m) 手动输入 ID\n" >&2 fi while true; do local max="${#items[@]}" local prompt_suffix="1-$max, c" [[ "$allow_manual" == "yes" ]] && prompt_suffix="$prompt_suffix, m" read -r -p "请选择 [$prompt_suffix]: " choice case "$choice" in c|"") return 1 ;; m) if [[ "$allow_manual" == "yes" ]]; then read -r -p "请输入 ID: " manual if [[ -n "$manual" ]]; then printf '%s' "$manual" return 0 fi warn "输入不能为空" else warn "无效选择" fi ;; *) if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= max )); then printf '%s' "${items[$((choice-1))]}" return 0 fi warn "无效选择" ;; esac done } # ---- Pick a dataset (current user's datasets, with manual fallback) ---- # Falls back to manual entry if the user has no datasets (or hf CLI # isn't logged in). pick_dataset() { local prompt="${1:-选择 dataset}" local -a list mapfile -t list < <(list_user_datasets) if [[ ${#list[@]} -eq 0 || -z "${list[0]:-}" ]]; then warn "账号下没有 dataset(或 hf CLI 未登录),请手动输入" read -r -p "Dataset ID (user/name): " manual if [[ -n "$manual" ]]; then printf '%s' "$manual" return 0 fi return 1 fi pick_from_list "$prompt" "取消" "yes" "${list[@]}" } # ============================================================ # 命令实现 # ============================================================ cmd_bootstrap() { info "▶ 启动交互式部署引导..." exec "$SCRIPT_DIR/bootstrap-hf.sh" } cmd_rebuild() { local default_user default_repo default_user="$(get_default_hf_user)" || default_user="" default_repo="$(get_default_repo_name)" || default_repo="" if [ $# -gt 0 ]; then exec "$SCRIPT_DIR/rebuild-space.sh" "$@" fi read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo read -r -p "HF_TOKEN (留空从文件读取): " token exec "$SCRIPT_DIR/rebuild-space.sh" "${repo}" ${token:+"$token"} } cmd_cleanup() { local default_user default_repo default_user="$(get_default_hf_user)" || default_user="" default_repo="$(get_default_repo_name)" || default_repo="" if [ $# -gt 1 ]; then exec "$SCRIPT_DIR/delete-backups.sh" "$@" fi local repo repo=$(pick_dataset "选择 dataset (提示示例: ${default_user}/${default_repo}-backup)") || return 1 read -r -p "删除此日期及更早的备份 (YYYYMMDD): " date read -r -p "HF_TOKEN (留空从文件读取): " token exec "$SCRIPT_DIR/delete-backups.sh" "${repo}" "${date}" ${token:+"$token"} } cmd_find_backup() { local default_user default_repo default_user="$(get_default_hf_user)" || default_user="" default_repo="$(get_default_repo_name)" || default_repo="" if [ $# -gt 0 ]; then exec $PYTHON "$SCRIPT_DIR/find-largest-backup.py" "$@" fi local repo repo=$(pick_dataset "选择 dataset (提示示例: ${default_user}/${default_repo}-backup)") || return 1 shift_args=() read -r -p "详细模式? (y/n, 默认 n): " verbose [[ "$verbose" == "y" ]] && shift_args+=("--verbose") read -r -p "找到最佳备份后清理旧备份? (y/n, 默认 n): " del [[ "$del" == "y" ]] && shift_args+=("--delete-before") if [[ "${del}" == "y" ]]; then read -r -p "清理后跳过超级压缩(真正释放存储空间)? (y/n, 默认 n): " nosquash [[ "$nosquash" == "y" ]] && shift_args+=("--no-super-squash") fi exec $PYTHON "$SCRIPT_DIR/find-largest-backup.py" "$repo" "${shift_args[@]}" } cmd_delete_hf() { while true; do echo "删除类型:" echo " 1) 删除整个仓库 (Space/Dataset/Model)" echo " 2) 按通配符批量删除文件" echo " 3) 删除 Space 持久存储" echo " 9) 返回主菜单" read -r -p "请选择 [1-3, 9]: " t echo "" case "$t" in 1) read -r -p "Repo ID: " rid read -r -p "类型 (space/dataset/model): " rtype "$SCRIPT_DIR/delete-hf.py" repo "$rid" --type "$rtype" echo ""; echo "按回车继续..."; read -r ;; 2) read -r -p "Repo ID: " rid read -r -p "通配符模式: " pat "$SCRIPT_DIR/delete-hf.py" files "$rid" --pattern "$pat" echo ""; echo "按回车继续..."; read -r ;; 3) read -r -p "Space ID: " sid "$SCRIPT_DIR/delete-hf.py" storage "$sid" echo ""; echo "按回车继续..."; read -r ;; 9) break ;; *) error "无效选择" ;; esac done } cmd_hf_backup() { local default_user default_user="$(get_default_hf_user)" || default_user="" while true; do echo "" echo "HF Dataset 备份工具" echo "====================" echo " 1) backup 创建增量备份" echo " 2) restore 从本地恢复到 HF" echo " 3) verify 验证备份完整性" echo " 4) prune 清理旧快照(释放空间)" echo " 5) snapshots 查看快照列表" echo " 6) info 查看 HF Dataset 信息" echo " 9) 返回主菜单" read -r -p "请选择 [1-6, 9]: " op echo "" case "$op" in 1) rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/-backup)") || continue "$SCRIPT_DIR/hf-dataset-mgr.sh" backup "$rid" echo ""; echo "按回车继续..."; read -r ;; 2) rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/-backup)") || continue read -r -p "指定快照名? (留空使用最新): " snap if [[ -n "$snap" ]]; then "$SCRIPT_DIR/hf-dataset-mgr.sh" restore "$rid" --snap "$snap" else "$SCRIPT_DIR/hf-dataset-mgr.sh" restore "$rid" fi echo ""; echo "按回车继续..."; read -r ;; 3) rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/-backup)") || continue "$SCRIPT_DIR/hf-dataset-mgr.sh" verify "$rid" echo ""; echo "按回车继续..."; read -r ;; 4) rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/-backup)") || continue read -r -p "保留快照数 (默认 5): " keep read -r -p "清理后执行 Super-Squash? (y/n, 默认 y): " squash [[ "$squash" != "n" ]] && SQUASH="--squash" || SQUASH="" "$SCRIPT_DIR/hf-dataset-mgr.sh" prune "$rid" --keep "${keep:-5}" $SQUASH echo ""; echo "按回车继续..."; read -r ;; 5) rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/-backup)") || continue "$SCRIPT_DIR/hf-dataset-mgr.sh" snapshots "$rid" echo ""; echo "按回车继续..."; read -r ;; 6) rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/-backup)") || continue "$SCRIPT_DIR/hf-dataset-mgr.sh" info "$rid" echo ""; echo "按回车继续..."; read -r ;; 9) break ;; *) error "无效选择" ;; esac done } cmd_hf_account() { while true; do echo "" echo "HF 账号管理工具" echo "================" echo " 1) add 添加/更新账号" echo " 2) list 列出所有账号" echo " 3) use 切换默认账号" echo " 4) current 显示当前账号" echo " 5) remove 删除账号" echo " 6) get-token 获取账号 token" echo " 9) 返回主菜单" read -r -p "请选择 [1-6, 9]: " op echo "" case "$op" in 1) read -r -p "用户名: " name [[ -z "$name" ]] && { error "用户名不能为空"; continue; } read -r -p "Token (留空交互式输入): " token if [[ -z "$token" ]]; then "$SCRIPT_DIR/hf-account.sh" add "$name" else "$SCRIPT_DIR/hf-account.sh" add "$name" "$token" fi echo ""; echo "按回车继续..."; read -r ;; 2) "$SCRIPT_DIR/hf-account.sh" list; echo ""; echo "按回车继续..."; read -r ;; 3) "$SCRIPT_DIR/hf-account.sh" use echo ""; echo "按回车继续..."; read -r ;; 4) "$SCRIPT_DIR/hf-account.sh" current; echo ""; echo "按回车继续..."; read -r ;; 5) read -r -p "用户名: " name [[ -z "$name" ]] && { error "用户名不能为空"; continue; } "$SCRIPT_DIR/hf-account.sh" remove "$name" echo ""; echo "按回车继续..."; read -r ;; 6) read -r -p "用户名 (留空当前账号): " name if [[ -z "$name" ]]; then "$SCRIPT_DIR/hf-account.sh" get-token else "$SCRIPT_DIR/hf-account.sh" get-token "$name" fi echo ""; echo "按回车继续..."; read -r ;; 9) break ;; *) error "无效选择" ;; esac done } cmd_storage() { while true; do echo "" echo "存储操作" echo "=========" echo " 1) 上传文件" echo " 2) 上传目录" echo " 3) 下载文件" echo " 4) 列出文件" echo " 5) 同步目录" echo " 9) 返回主菜单" read -r -p "请选择 [1-5, 9]: " op echo "" case "$op" in 1) read -r -p "文件路径: " f read -r -p "目标路径 (留空同上): " d "$SCRIPT_DIR/hf-storage.sh" upload "$f" ${d:+"$d"} echo ""; echo "按回车继续..."; read -r ;; 2) read -r -p "目录路径: " d read -r -p "前缀 (留空无前缀): " p "$SCRIPT_DIR/hf-storage.sh" upload-dir "$d" ${p:+"$p"} echo ""; echo "按回车继续..."; read -r ;; 3) read -r -p "远程文件: " f read -r -p "本地路径 (留空当前目录): " l "$SCRIPT_DIR/hf-storage.sh" download "$f" ${l:+"$l"} echo ""; echo "按回车继续..."; read -r ;; 4) "$SCRIPT_DIR/hf-storage.sh" list; echo ""; echo "按回车继续..."; read -r ;; 5) read -r -p "本地目录: " l read -r -p "远程前缀 (留空无): " r "$SCRIPT_DIR/hf-storage.sh" sync "$l" ${r:+"$r"} echo ""; echo "按回车继续..."; read -r ;; 9) break ;; *) error "无效选择" ;; esac done } cmd_restart_space() { local default_user default_repo default_user="$(get_default_hf_user)" || default_user="" default_repo="$(get_default_repo_name)" || default_repo="" local repo="$1" if [ -z "$repo" ]; then read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo fi info "▶ 重启 Space: $repo" if hf spaces restart "$repo" 2>&1; then info "✓ 重启请求已发送" else error "✗ 重启失败" return 1 fi } cmd_pause_space() { local default_user default_repo default_user="$(get_default_hf_user)" || default_user="" default_repo="$(get_default_repo_name)" || default_repo="" local repo="$1" if [ -z "$repo" ]; then read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo fi warn "▶ 暂停 Space: $repo" warn "警告: Space 暂停后无法通过网络访问,需手动重启恢复" read -r -p "确认暂停? (y/n): " confirm [[ "$confirm" != "y" ]] && info "已取消" && return 0 if hf spaces pause "$repo" 2>&1; then info "✓ Space 已暂停" else error "✗ 暂停失败" return 1 fi } cmd_factory_rebuild() { local default_user default_repo default_user="$(get_default_hf_user)" || default_user="" default_repo="$(get_default_repo_name)" || default_repo="" local repo="$1" if [ -z "$repo" ]; then read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo fi warn "▶ Factory 重建 Space: $repo" warn "警告: 这将删除 Space 后重新创建!所有持久存储数据将被清除!" read -r -p "确认重建? (输入 Space 名称确认): " confirm [[ "$confirm" != "${repo#*/}" ]] && error "名称不匹配,已取消" && return 1 info "1/3 删除 Space..." $PYTHON -c " from huggingface_hub import HfApi import os api = HfApi() try: api.delete_repo('${repo}', repo_type='space') print('✓ 已删除') except Exception as e: print(f'删除失败(可能不存在): {e}') " sleep 3 info "2/3 重新创建 Space..." hf repos create "$repo" --repo-type space --space-sdk docker --private info "3/3 上传代码..." exec "$SCRIPT_DIR/rebuild-space.sh" "$repo" } show_menu() { clear 2>/dev/null || true title "╔══════════════════════════════════════╗" title "║ OpenClaw 工具箱 ║" title "║ 本地开发工具集 ║" title "╚══════════════════════════════════════╝" echo "" info " 1) 🚀 部署到 HF Space" echo " 交互式全流程部署 OpenClaw 到 Hugging Face Space" echo "" info " 2) 📤 推送代码到 Space" echo " 将本地最新代码强制推送到 Space 并触发重建" echo "" info " 3) 🗑️ 清理备份" echo " 删除 Dataset 中指定日期及更早的所有备份文件" echo "" info " 4) 🔍 查找最佳备份" echo " 综合评分找出最佳备份,可选清理旧备份" echo "" info " 5) 💾 HF Dataset 备份" echo " 本地↔远程交互备份,支持快照/验证/清理/恢复" echo "" info " 6) 👤 HF 账号管理" echo " 多账号切换,添加/删除/切换 HF 用户" echo "" info " 7) ❌ 删除 HF 资源" echo " 删除仓库、批量删除文件、或删除 Space 持久存储" echo "" info " 8) 📦 存储管理" echo " HuggingFace 存储工具:上传/下载/同步文件" echo "" info " ── Space 运维 ──" echo "" info " 9) 🔄 重启 Space" echo " 发送重启请求到 HuggingFace Space" echo "" info " 10) ⏸️ 暂停 Space" echo " 暂停 Space,暂停后无法网络访问" echo "" info " 11) 🏭 Factory 重建" echo " 删除 Space 后重新创建并上传代码(清除所有数据和持久存储)" echo "" info " 0) ❎ 退出" echo "" } # ============================================================ # 交互菜单 # ============================================================ interactive_mode() { while true; do show_menu read -r -p "请选择 [0-11]: " choice echo "" case "$choice" in 1) cmd_bootstrap ;; 2) cmd_rebuild ;; 3) cmd_cleanup ;; 4) cmd_find_backup ;; 5) cmd_hf_backup ;; 6) cmd_hf_account ;; 7) cmd_delete_hf ;; 8) cmd_storage ;; 9) cmd_restart_space ;; 10) cmd_pause_space ;; 11) cmd_factory_rebuild ;; 0) info "再见!"; exit 0 ;; *) warn "无效选择,请重新输入"; sleep 1 ;; esac done } show_usage() { title "OpenClaw 工具箱 — 本地开发工具集" echo "" echo "用法: $0 [命令] [参数...]" echo "" echo "不带参数时启动交互式菜单。" echo "" info "命令模式:" echo " $0 bootstrap 交互式部署到 HF Space" echo " $0 rebuild-space [TOKEN] 推送代码到 Space" echo " $0 restart-space 重启 Space" echo " $0 pause-space 暂停 Space" echo " $0 factory-rebuild Factory 重建 Space(删除重建)" echo " $0 cleanup <日期> [TOKEN] 清理旧备份" echo " $0 find-backup [选项] 查找最佳备份" echo " $0 hf-backup <命令> [选项] HF Dataset 本地↔远程交互备份" echo " $0 hf-account <命令> [选项] HF 多账号管理" echo " $0 rm-hf <子命令> [参数] 删除 HF 资源" echo " $0 storage <子命令> [参数] 存储管理" echo "" info "hf-backup 子命令:" echo " $0 hf-backup backup 创建增量备份" echo " $0 hf-backup restore 从本地恢复到 HF" echo " $0 hf-backup verify 验证备份完整性" echo " $0 hf-backup prune 清理旧快照" echo " $0 hf-backup snapshots 查看快照列表" echo " $0 hf-backup info 查看 Dataset 信息" echo "" info "hf-account 子命令:" echo " $0 hf-account add [token] 添加/更新账号" echo " $0 hf-account list 列出所有账号" echo " $0 hf-account use [user] 切换默认账号(无参:交互式选择器)" echo " $0 hf-account current 显示当前账号" echo " $0 hf-account remove 删除账号" echo " $0 hf-account get-token [user] 获取 token" echo "" info "示例:" echo " $0 # 交互菜单" echo " $0 rebuild-space / # 推送代码" echo " $0 hf-backup backup /-backup # 备份 dataset" echo " $0 hf-account add # 添加 HF 账号" } # ============================================================ # 主入口 # ============================================================ main() { # 检查并获取 HF token(支持账号切换) if [[ -t 0 ]]; then prompt_hf_account_switch || true fi if [ $# -eq 0 ]; then interactive_mode exit 0 fi local cmd="$1" shift case "$cmd" in -h|--help|help) show_usage ;; bootstrap) cmd_bootstrap "$@" ;; rebuild-space|rebuild) cmd_rebuild "$@" ;; restart-space) cmd_restart_space "$@" ;; pause-space) cmd_pause_space "$@" ;; factory-rebuild) cmd_factory_rebuild "$@" ;; cleanup) cmd_cleanup "$@" ;; find-backup|find) cmd_find_backup "$@" ;; hf-backup|hfb) cmd_hf_backup "$@" ;; hf-account|hfa) cmd_hf_account "$@" ;; rm-hf|delete-hf) cmd_delete_hf "$@" ;; storage) cmd_storage "$@" ;; *) error "未知命令: $cmd" echo "使用 '$0 --help' 查看可用命令" exit 1 ;; esac } main "$@"