Przeglądaj źródła

feat:为部署脚本添加入口

Jerry Liang 1 miesiąc temu
rodzic
commit
881a56ce81
3 zmienionych plików z 367 dodań i 92 usunięć
  1. 24 6
      deploy/DEPLOYMENT.md
  2. 279 86
      deploy/scripts/setup.sh
  3. 64 0
      deploy/start.sh

+ 24 - 6
deploy/DEPLOYMENT.md

@@ -69,19 +69,36 @@
 
 ## 0. 快速开始:交互式一键部署
 
-如果只想尽快完成部署,直接运行交互式脚本 `deploy/scripts/setup.sh`。它会**分步引导**
-你选择/填写内容,并支持**本地**与**远程(SSH)**两种目标:
+如果只想尽快完成部署,直接运行部署入口 `deploy/start.sh`(它会拉起交互式向导
+`deploy/scripts/setup.sh`)。向导会**分步引导**你选择/填写内容,并支持**本地**与
+**远程(SSH)**两种目标:
 
 ```bash
-cd deploy/scripts
-chmod +x setup.sh
-./setup.sh
+cd deploy
+chmod +x start.sh
+./start.sh
 ```
 
+`start.sh` 会显示一个菜单:既可选「全流程一键部署」,也可只跑某个**单项功能**
+(依赖 / 初始化 DB / MinIO 账号 / pgAdmin4 / Casdoor / Caddy / Kiro CLI / 构建部署前后端)。
+**无论全流程还是单项,都同样支持本地与远程(进入后会先询问 local/remote)。**
+
+单项也可用命令行直达(跳过菜单),本质是给向导传 `--only`:
+
+```bash
+./start.sh --only caddy          # 只部署 Caddy
+./start.sh --only deps,initdb    # 只装依赖 + 初始化数据库
+./start.sh --only deploy         # 只构建部署前后端(再选前端/后端/两者)
+./start.sh --help                # 查看全部可选项
+```
+
+> 可选项:`deps casdoor initdb minioacct pgadmin caddy kiro deploy`。
+> 单项模式仍会询问该步骤所需的参数(如 Caddy 的域名/DNS token)。
+
 向导流程:
 
 1. **选择模式**:本地 (local) 或 远程 (remote,通过 SSH)
-2. **选择步骤**:初始化数据库 / 部署后端 / 部署前端(可自由勾选)
+2. **选择步骤**:初始化数据库 / 部署后端 / 部署前端(可自由勾选;`--only` 时跳过此步)
 3. **(远程)填写 SSH 连接**:主机、端口、用户、私钥,并自动测试连通性
 4. **填写配置**:部署目录、运行用户、PostgreSQL/Mongo/Redis/MinIO/MySQL 等
 5. 自动**生成 config.env** → 本机**构建**前端 dist 与后端二进制 →
@@ -867,6 +884,7 @@ service/cmd/showTodayErrorLog.sh
 | --- | --- |
 | `deploy/DEPLOYMENT.md` | 本部署文档 |
 | `deploy/CHECKLIST.md` | 部署清单(需填写项 + 步骤,可勾选) |
+| `deploy/start.sh` | **部署入口**(从这里开始,转发到交互式向导 setup.sh) |
 | `deploy/scripts/setup.sh` | **交互式一键部署**(本地/远程,分步选择与填写) |
 | `deploy/scripts/install_deps.sh` | 检测并(按需)安装 PostgreSQL/MongoDB/Redis/MinIO/mc/Docker(`DETECT_ONLY=1` 仅检测) |
 | `deploy/scripts/install_casdoor.sh` | 以 Docker 部署 Casdoor + 建其 PostgreSQL 库/角色 |

+ 279 - 86
deploy/scripts/setup.sh

@@ -58,24 +58,163 @@ ask() {
     printf -v "${__var}" '%s' "${__ans}"
 }
 
-# 提示输入密码(不回显):ask_secret VAR "提示" "默认值"
-ask_secret() {
-    local __var="$1" __prompt="$2" __default="${3:-}" __ans
-    read -r -s -p "$(echo -e "${c_bold}${__prompt}${c_reset}${__default:+ [回车用默认]}: ")" __ans
-    echo
-    __ans="${__ans:-${__default}}"
+# 必填输入(非空,否则循环重试):ask_required VAR "提示"
+ask_required() {
+    local __var="$1" __prompt="$2" __ans
+    while :; do
+        read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset}: ")" __ans
+        [[ -n "${__ans}" ]] && break
+        warn "不能为空,请重新输入。"
+    done
     printf -v "${__var}" '%s' "${__ans}"
 }
 
-# 强制输入密码(不回显、无默认,必须非空且两次一致):ask_secret_required VAR "提示"
+# 序号选择(限定合法值,否则循环重试):ask_choice VAR "提示" "默认" "1 2 3"
+ask_choice() {
+    local __var="$1" __prompt="$2" __default="$3" __valid="$4" __ans __v
+    while :; do
+        read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset} [${__default}]: ")" __ans
+        __ans="${__ans:-${__default}}"
+        for __v in ${__valid}; do
+            [[ "${__ans}" == "${__v}" ]] && { printf -v "${__var}" '%s' "${__ans}"; return 0; }
+        done
+        warn "无效选择:${__ans},请输入 [${__valid// /\/}] 之一。"
+    done
+}
+
+# ---------------------------------------------------------------------------
+# 模块小结 + 回改驱动器
+#   用法:edit_module "模块标题" "VAR|提示|类型|默认" ...
+#   类型:plain(普通,带默认) | required(必填非空) | optional(可空)
+#         secret(密码,带默认,回车用默认) | secret_required(必填密码)
+#   交互:先逐项询问 → 显示小结 → 输编号改单项 / r 全部重来 / 回车确认
+# ---------------------------------------------------------------------------
+# 单字段询问:按类型调用相应输入函数
+_ask_field() {
+    local var="$1" prompt="$2" type="$3" default="$4"
+    case "${type}" in
+        plain)           ask "${var}" "${prompt}" "${default}" ;;
+        required)        ask_required "${var}" "${prompt}" ;;
+        optional)        ask "${var}" "${prompt}" "${default}" ;;
+        secret)          ask_secret "${var}" "${prompt}" "${default}" ;;
+        secret_required) ask_secret_required "${var}" "${prompt}" ;;
+        *)               ask "${var}" "${prompt}" "${default}" ;;
+    esac
+}
+
+# 小结里字段值的展示(密码做掩码;secret 用默认时显默认明文)
+_display_field() {
+    local type="$3" default="$4" val="${!1}"
+    case "${type}" in
+        secret)
+            if [[ -n "${val}" && "${val}" == "${default}" ]]; then
+                printf '%s(默认)' "${val}"          # 用默认 → 明文显示
+            elif [[ -n "${val}" ]]; then
+                printf '******'                        # 手动输入 → 掩码
+            else
+                printf '(空)'
+            fi
+            ;;
+        secret_required)
+            [[ -n "${val}" ]] && printf '******' || printf '(未设置)'
+            ;;
+        *)
+            [[ -n "${val}" ]] && printf '%s' "${val}" || printf '(空)'
+            ;;
+    esac
+}
+
+edit_module() {
+    local mtitle="$1"; shift
+    local specs=("$@")
+    local n="${#specs[@]}"
+    local i spec var prompt type default sel
+
+    # 首轮逐项询问
+    title "${mtitle}"
+    for spec in "${specs[@]}"; do
+        IFS='|' read -r var prompt type default <<<"${spec}"
+        _ask_field "${var}" "${prompt}" "${type}" "${default}"
+    done
+
+    # 小结 + 回改循环
+    while :; do
+        echo
+        echo -e "${c_bold}${mtitle} — 配置小结:${c_reset}"
+        for ((i=0; i<n; i++)); do
+            IFS='|' read -r var prompt type default <<<"${specs[i]}"
+            printf "  %d) %-28s = %s\n" "$((i+1))" "${prompt}" "$(_display_field "${var}" "${prompt}" "${type}" "${default}")"
+        done
+        read -r -p "$(echo -e "操作:输编号改单项 | ${c_bold}r${c_reset} 全部重来 | ${c_bold}回车${c_reset} 确认: ")" sel
+
+        if [[ -z "${sel}" ]]; then
+            break
+        elif [[ "${sel}" == "r" || "${sel}" == "R" ]]; then
+            for spec in "${specs[@]}"; do
+                IFS='|' read -r var prompt type default <<<"${spec}"
+                _ask_field "${var}" "${prompt}" "${type}" "${default}"
+            done
+        elif [[ "${sel}" =~ ^[0-9]+$ && "${sel}" -ge 1 && "${sel}" -le "${n}" ]]; then
+            IFS='|' read -r var prompt type default <<<"${specs[$((sel-1))]}"
+            _ask_field "${var}" "${prompt}" "${type}" "${default}"
+        else
+            warn "无效操作:${sel}"
+        fi
+    done
+}
+
+# 底层:逐字符读取密码,用 * 回显长度,支持退格,回车结束。
+#   用法:_read_secret_stars VAR "提示文案"
+_read_secret_stars() {
+    local __var="$1" __prompt="$2" __ch __acc=""
+    # 提示(不换行)
+    printf '%b' "${c_bold}${__prompt}${c_reset}: " > /dev/tty
+    # 逐字符读取(从 /dev/tty,避免受重定向影响)
+    while IFS= read -rsn1 __ch < /dev/tty; do
+        # 回车/换行结束
+        [[ -z "${__ch}" ]] && break
+        # 退格(DEL 0x7f 或 BS 0x08)
+        if [[ "${__ch}" == $'\x7f' || "${__ch}" == $'\x08' ]]; then
+            if [[ -n "${__acc}" ]]; then
+                __acc="${__acc%?}"
+                printf '\b \b' > /dev/tty
+            fi
+            continue
+        fi
+        __acc+="${__ch}"
+        printf '*' > /dev/tty
+    done
+    printf '\n' > /dev/tty
+    printf -v "${__var}" '%s' "${__acc}"
+}
+
+# 提示输入密码(* 回显 + 两次验证):ask_secret VAR "提示" "默认值"
+#   直接回车(空输入)则采用默认值(提示中明文显示),跳过二次确认;
+#   输入了内容则要求两次一致。
+ask_secret() {
+    local __var="$1" __prompt="$2" __default="${3:-}" __a __b
+    while :; do
+        _read_secret_stars __a "${__prompt}${__default:+ [回车用默认: ${__default}]}"
+        if [[ -z "${__a}" ]]; then
+            __a="${__default}"
+            break
+        fi
+        _read_secret_stars __b "${__prompt}(再输一次确认)"
+        [[ "${__a}" == "${__b}" ]] && break
+        warn "两次输入不一致,请重新输入。"
+    done
+    printf -v "${__var}" '%s' "${__a}"
+}
+
+# 强制输入密码(* 回显、无默认、必须非空且两次一致):ask_secret_required VAR "提示"
 ask_secret_required() {
     local __var="$1" __prompt="$2" __a __b
     while :; do
-        read -r -s -p "$(echo -e "${c_bold}${__prompt}${c_reset}: ")" __a; echo
+        _read_secret_stars __a "${__prompt}"
         if [[ -z "${__a}" ]]; then
             warn "不能为空,请重新输入。"; continue
         fi
-        read -r -s -p "$(echo -e "${c_bold}${__prompt}(再输一次确认)${c_reset}: ")" __b; echo
+        _read_secret_stars __b "${__prompt}(再输一次确认)"
         if [[ "${__a}" != "${__b}" ]]; then
             warn "两次输入不一致,请重新输入。"; continue
         fi
@@ -149,6 +288,10 @@ KIRO_DO_LOGIN="n"                                        # 安装后是否立即
 GEN_CONFIG=""
 GEN_MCP_CONFIG=""
 
+# 单项模式(--only):非空表示仅执行指定步骤,跳过步骤勾选交互
+ONLY_MODE=""
+ONLY_LABEL=""
+
 # ---------------------------------------------------------------------------
 # 步骤 0:选择模式与步骤
 # ---------------------------------------------------------------------------
@@ -159,14 +302,19 @@ wizard_intro() {
     echo "请选择部署目标:"
     echo "  1) 本地部署 (local)"
     echo "  2) 远程部署 (remote, 通过 SSH)"
-    local sel; ask sel "输入序号" "1"
+    local sel; ask_choice sel "输入序号" "1" "1 2"
     case "${sel}" in
         1) MODE="local" ;;
         2) MODE="remote" ;;
-        *) err "无效选择"; exit 1 ;;
     esac
     ok "部署模式:${MODE}"
 
+    # --only 模式:步骤已由命令行参数设定,跳过交互勾选
+    if [[ -n "${ONLY_MODE}" ]]; then
+        echo "(单项模式:仅执行 ${ONLY_LABEL})"
+        return 0
+    fi
+
     title "选择要执行的步骤"
     ask_yn "① 检测/安装数据库引擎与依赖(PostgreSQL/Mongo/Redis/MinIO/Docker)?" "y" && DO_DEPS="y" || DO_DEPS="n"
     ask_yn "② 安装 Casdoor(Docker,认证服务)?" "n" && DO_CASDOOR="y" || DO_CASDOOR="n"
@@ -190,19 +338,20 @@ wizard_intro() {
 wizard_remote() {
     [[ "${MODE}" == "remote" ]] || return 0
     title "远程主机连接信息"
-    ask R_HOST "远程主机地址 (IP 或域名)"
-    [[ -n "${R_HOST}" ]] || { err "远程地址不能为空"; exit 1; }
-    ask R_PORT "SSH 端口" "22"
-    ask R_USER "SSH 登录用户" "${USER}"
-    ask R_SSH_KEY "SSH 私钥路径(留空则用默认/密码)" ""
-
-    info "测试 SSH 连接 ..."
-    if ssh_run "echo connected" >/dev/null 2>&1; then
-        ok "SSH 连接成功"
-    else
+    while :; do
+        ask_required R_HOST "远程主机地址 (IP 或域名)"
+        ask R_PORT "SSH 端口" "22"
+        ask R_USER "SSH 登录用户" "${USER}"
+        ask R_SSH_KEY "SSH 私钥路径(留空则用默认/密码)" ""
+
+        info "测试 SSH 连接 ..."
+        if ssh_run "echo connected" >/dev/null 2>&1; then
+            ok "SSH 连接成功"
+            break
+        fi
         err "SSH 连接失败,请检查地址/端口/用户/密钥"
-        exit 1
-    fi
+        ask_yn "重新填写连接信息并重试?" "y" || { warn "已取消"; exit 0; }
+    done
 }
 
 # 组装 ssh/scp 通用参数
@@ -240,84 +389,81 @@ run() {
 # 步骤 2:部署参数与配置项
 # ---------------------------------------------------------------------------
 wizard_params() {
-    title "部署目录与运行用户"
-    ask DEPLOY_ROOT "部署根目录" "/var/www/fwh-toolbox"
-    local def_user="${R_USER:-${USER}}"
-    ask RUN_USER "后端 systemd 运行用户" "${def_user}"
+    edit_module "部署目录与运行用户" \
+        "DEPLOY_ROOT|部署根目录|plain|/var/www/fwh-toolbox" \
+        "RUN_USER|后端 systemd 运行用户|plain|${R_USER:-${USER}}"
 
     if [[ "${DO_GENCONF}" == "y" ]]; then
-        title "应用配置"
-        ask APP_ENV "运行环境 (pro/dev)" "dev"
-        ask APP_PORT "后端监听端口" "8301"
-        ask ASYNQMON_PORT "Asynqmon 端口" "8302"
-
-        title "PostgreSQL(本地)"
-        ask PG_HOST "PG 主机" "localhost"
-        ask PG_PORT "PG 端口" "5432"
-        ask PG_DB   "PG 数据库名" "fwh_toolbox_db"
-        ask PG_USER "PG 业务角色" "fwh_toolbox"
-        ask_secret PG_PASS "PG 业务角色密码" "fwh_toolbox"
-
-        title "MongoDB / Redis"
-        ask MONGO_DSN "Mongo 连接串" "mongodb://127.0.0.1:27017/FwhToolboxDb"
-        ask REDIS_ADDR "Redis 地址" "localhost:6379"
-
-        title "远端只读 MySQL(FWH 生产库,可留空跳过)"
-        ask MYSQL_DSN "MySQL DSN" ""
-
-        title "MinIO(本地对象存储)"
-        ask MINIO_ENDPOINT "MinIO S3 API 地址" "localhost:9198"
-        ask MINIO_ACCESS_KEY "MinIO Access Key" "fwh-toolbox"
-        ask_secret MINIO_SECRET_KEY "MinIO Secret Key" "fwh-toolbox"
-        ask MINIO_BUCKET "MinIO Bucket 名" "fwh-toolbox"
+        edit_module "应用配置" \
+            "APP_ENV|运行环境 (pro/dev)|plain|dev" \
+            "APP_PORT|后端监听端口|plain|8301" \
+            "ASYNQMON_PORT|Asynqmon 端口|plain|8302"
+
+        edit_module "PostgreSQL(本地)" \
+            "PG_HOST|PG 主机|plain|localhost" \
+            "PG_PORT|PG 端口|plain|5432" \
+            "PG_DB|PG 数据库名|plain|fwh_toolbox_db" \
+            "PG_USER|PG 业务角色|plain|fwh_toolbox" \
+            "PG_PASS|PG 业务角色密码|secret|fwh_toolbox"
+
+        edit_module "MongoDB / Redis" \
+            "MONGO_DSN|Mongo 连接串|plain|mongodb://127.0.0.1:27017/FwhToolboxDb" \
+            "REDIS_ADDR|Redis 地址|plain|localhost:6379"
+
+        edit_module "远端只读 MySQL(可留空跳过)" \
+            "MYSQL_DSN|MySQL DSN|optional|"
+
+        edit_module "MinIO(本地对象存储)" \
+            "MINIO_ENDPOINT|MinIO S3 API 地址|plain|localhost:9198" \
+            "MINIO_ACCESS_KEY|MinIO Access Key|plain|fwh-toolbox" \
+            "MINIO_SECRET_KEY|MinIO Secret Key|secret|fwh-toolbox" \
+            "MINIO_BUCKET|MinIO Bucket 名|plain|fwh-toolbox"
     fi
 
     if [[ "${DO_INITDB}" == "y" || "${DO_DEPS}" == "y" || "${DO_MINIOACCT}" == "y" ]]; then
-        title "MinIO 管理员账户(安装/建桶/建账号用)"
-        ask MINIO_ROOT_USER "MinIO 管理员用户" "jerry"
-        ask_secret_required MINIO_ROOT_PASSWORD "MinIO 管理员密码(必填)"
+        edit_module "MinIO 管理员账户(安装/建桶/建账号用)" \
+            "MINIO_ROOT_USER|MinIO 管理员用户|plain|jerry" \
+            "MINIO_ROOT_PASSWORD|MinIO 管理员密码|secret_required|"
     fi
 
     if [[ "${DO_CASDOOR}" == "y" ]]; then
-        title "Casdoor(Docker 认证服务)"
-        local def_cdir="/home/${RUN_USER}/docker/casdoor"
-        ask CASDOOR_DIR "Casdoor 部署目录" "${def_cdir}"
-        ask CASDOOR_PORT "Casdoor 对外端口 (->容器8000)" "8010"
-        ask CASDOOR_PG_USER "Casdoor 使用的 PG 角色" "casdoor"
-        ask_secret CASDOOR_PG_PASSWORD "Casdoor PG 角色密码" "casdoor"
-        ask CASDOOR_DB_NAME "Casdoor 数据库名" "casdoor"
+        edit_module "Casdoor(Docker 认证服务)" \
+            "CASDOOR_DIR|Casdoor 部署目录|plain|/home/${RUN_USER}/docker/casdoor" \
+            "CASDOOR_PORT|Casdoor 对外端口 (->容器8000)|plain|8010" \
+            "CASDOOR_PG_USER|Casdoor 使用的 PG 角色|plain|casdoor" \
+            "CASDOOR_PG_PASSWORD|Casdoor PG 角色密码|secret|casdoor" \
+            "CASDOOR_DB_NAME|Casdoor 数据库名|plain|casdoor"
+        # origin 默认值需引用上面刚填的 CASDOOR_PORT,故在 edit_module 之后单独询问
         ask CASDOOR_ORIGIN "Casdoor 对外访问地址(origin)" "http://localhost:${CASDOOR_PORT}"
     fi
 
     if [[ "${DO_PGADMIN}" == "y" ]]; then
-        title "pgAdmin4(数据库 Web 管理)"
-        ask PGADMIN_PORT "pgAdmin4 对外端口" "5480"
-        ask PGADMIN_BIND "pgAdmin4 绑定地址(默认仅本机,防公网暴露)" "127.0.0.1"
-        ask PGADMIN_EMAIL "pgAdmin4 登录邮箱" "liang_jlyx@163.com"
-        ask_secret_required PGADMIN_PASSWORD "pgAdmin4 登录密码(必填)"
-        ask_secret_required POSTGRES_ADMIN_PASSWORD "postgres 超级用户新密码(供 pgAdmin 连接,必填)"
+        edit_module "pgAdmin4(数据库 Web 管理)" \
+            "PGADMIN_PORT|pgAdmin4 对外端口|plain|5480" \
+            "PGADMIN_BIND|pgAdmin4 绑定地址(默认仅本机,防公网暴露)|plain|127.0.0.1" \
+            "PGADMIN_EMAIL|pgAdmin4 登录邮箱|plain|liang_jlyx@163.com" \
+            "PGADMIN_PASSWORD|pgAdmin4 登录密码|secret_required|" \
+            "POSTGRES_ADMIN_PASSWORD|postgres 超级用户新密码(供 pgAdmin 连接)|secret_required|"
     fi
 
     if [[ "${DO_MINIOACCT}" == "y" ]]; then
-        title "MinIO 业务账号(后端 fwh-toolbox / MCP fwh-toolbox-ai)"
-        # 业务账号:默认 fwh-toolbox;若已在生成 config 时填过则复用,此处不重复询问
-        [[ -n "${MINIO_ACCESS_KEY}" ]] || ask MINIO_ACCESS_KEY "后端业务 Access Key" "fwh-toolbox"
-        [[ -n "${MINIO_SECRET_KEY}" ]] || ask_secret MINIO_SECRET_KEY "后端业务 Secret Key" "fwh-toolbox"
-        # AI 账号(供 MCP 使用)
-        ask MINIO_AI_ACCESS_KEY "AI Access Key(MCP 用)" "fwh-toolbox-ai"
-        ask_secret MINIO_AI_SECRET_KEY "AI Secret Key(MCP 用,默认与账号一致)" "fwh-toolbox-ai"
-        ask MINIO_AI_BUCKET "AI 文件 bucket 名" "fwh-toolbox-ai-file"
+        edit_module "MinIO 业务账号(后端 fwh-toolbox / MCP fwh-toolbox-ai)" \
+            "MINIO_ACCESS_KEY|后端业务 Access Key|plain|fwh-toolbox" \
+            "MINIO_SECRET_KEY|后端业务 Secret Key|secret|fwh-toolbox" \
+            "MINIO_AI_ACCESS_KEY|AI Access Key(MCP 用)|plain|fwh-toolbox-ai" \
+            "MINIO_AI_SECRET_KEY|AI Secret Key(MCP 用)|secret|fwh-toolbox-ai" \
+            "MINIO_AI_BUCKET|AI 文件 bucket 名|plain|fwh-toolbox-ai-file"
     fi
 
     if [[ "${DO_CADDY}" == "y" ]]; then
-        title "Caddy 反向代理"
-        ask CADDY_DOMAIN "对外域名 (如 lmt.zenpower.cc)"
-        [[ -n "${CADDY_DOMAIN}" ]] || { err "Caddy 域名不能为空"; exit 1; }
-        echo "TLS 走 spaceship DNS-01(自编译 caddy 内置该插件),请输入 spaceship API 凭据:"
-        ask_secret_required LIBDNS_SPACESHIP_APIKEY "spaceship API Key(必填)"
-        ask_secret_required LIBDNS_SPACESHIP_APISECRET "spaceship API Secret(必填)"
-        ask CADDY_FRONTEND_PORT "前端静态端口" "8300"
-        ask CADDY_BACKEND_PORT "后端 API 端口" "${APP_PORT}"
+        echo "TLS 走 spaceship DNS-01(自编译 caddy 内置该插件),需填 spaceship API 凭据。"
+        edit_module "Caddy 反向代理" \
+            "CADDY_DOMAIN|对外域名 (如 lmt.zenpower.cc)|required|" \
+            "LIBDNS_SPACESHIP_APIKEY|spaceship API Key|secret_required|" \
+            "LIBDNS_SPACESHIP_APISECRET|spaceship API Secret|secret_required|" \
+            "CADDY_FRONTEND_PORT|前端静态端口|plain|8300" \
+            "CADDY_BACKEND_PORT|后端 API 端口|plain|${APP_PORT}"
+        # 以下为 y/n 分支选项(即时询问,不纳入小结回改)
         ask_yn "由 Caddy 直接 file_server 托管前端静态(否则反代到前端端口)?" "n" \
             && CADDY_SERVE_STATIC="y" || CADDY_SERVE_STATIC="n"
         ask_yn "加入 /casdoor 反代?" "$( [[ ${DO_CASDOOR} == y ]] && echo y || echo n )" \
@@ -330,14 +476,13 @@ wizard_params() {
         title "Kiro CLI(后端 AI 对话依赖)"
         echo "将以运行用户 ${RUN_USER} 身份安装(其 HOME 决定 ~/.kiro 配置位置)。"
         echo "安装方式:1) zip 官方安装包(默认)  2) curl 一键脚本"
-        local _km; ask _km "输入序号" "1"
+        local _km; ask_choice _km "输入序号" "1" "1 2"
         case "${_km}" in
             1) KIRO_INSTALL_METHOD="zip"
                echo "C 库变体:1) gnu(默认,主流发行版)  2) musl(Alpine 等)"
-               local _kl; ask _kl "输入序号" "1"
+               local _kl; ask_choice _kl "输入序号" "1" "1 2"
                [[ "${_kl}" == "2" ]] && KIRO_LIBC="musl" || KIRO_LIBC="gnu" ;;
             2) KIRO_INSTALL_METHOD="curl" ;;
-            *) err "无效选择"; exit 1 ;;
         esac
         ask_yn "安装后立即以 ${RUN_USER} 身份登录 Builder ID(交互式,需浏览器)?" "n" \
             && KIRO_DO_LOGIN="y" || KIRO_DO_LOGIN="n"
@@ -812,6 +957,9 @@ FranklinWH Toolbox 交互式一键部署
 
 用法:
   $0            进入交互式向导(选择 local/remote,分步选择与填写)
+  $0 --only <项[,项...]>  只执行指定步骤(仍会先问 local/remote 与该步骤参数)
+                可选项:deps casdoor initdb minioacct pgadmin caddy kiro deploy
+                例:$0 --only caddy   或   $0 --only deps,initdb
   $0 --help     显示本帮助
 
 向导步骤:
@@ -831,9 +979,54 @@ FranklinWH Toolbox 交互式一键部署
 EOF
 }
 
+# --only 解析:把所有步骤关掉,再按传入项打开对应步骤
+#   支持项:deps casdoor initdb minioacct pgadmin caddy kiro deploy
+#   多项用逗号分隔,如 --only deps,initdb
+parse_only() {
+    local items="$1"
+    [[ -n "${items}" ]] || { err "--only 需要指定步骤,如:--only caddy(可选 deps,casdoor,initdb,minioacct,pgadmin,caddy,kiro,deploy)"; exit 1; }
+
+    # 全部关闭
+    DO_DEPS="n"; DO_CASDOOR="n"; DO_INITDB="n"; DO_BACKEND="n"; DO_FRONTEND="n"
+    DO_GENCONF="n"; DO_CADDY="n"; DO_KIRO="n"; DO_PGADMIN="n"; DO_MINIOACCT="n"
+
+    ONLY_MODE=1
+    ONLY_LABEL="${items}"
+
+    local IFS=','; local it
+    for it in ${items}; do
+        case "${it}" in
+            deps)      DO_DEPS="y" ;;
+            casdoor)   DO_CASDOOR="y" ;;
+            initdb)    DO_INITDB="y"; DO_GENCONF="y" ;;   # 初始化通常配合生成 config
+            minioacct) DO_MINIOACCT="y" ;;
+            pgadmin)   DO_PGADMIN="y" ;;
+            caddy)     DO_CADDY="y" ;;
+            kiro)      DO_KIRO="y" ;;
+            deploy)
+                # 部署前后端:子交互选择范围
+                echo "部署范围:1) 前端+后端(all)  2) 仅后端  3) 仅前端"
+                local _sel; ask_choice _sel "输入序号" "1" "1 2 3"
+                case "${_sel}" in
+                    1) DO_BACKEND="y"; DO_FRONTEND="y" ;;
+                    2) DO_BACKEND="y"; DO_FRONTEND="n" ;;
+                    3) DO_BACKEND="n"; DO_FRONTEND="y" ;;
+                esac
+                DO_GENCONF="y"   # 部署后端需要 config.env
+                ;;
+            *) err "未知 --only 项:${it}"; exit 1 ;;
+        esac
+    done
+}
+
 main() {
     if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage; exit 0; fi
 
+    # --only <项[,项...]>:单项/多项模式,仅执行指定步骤
+    if [[ "${1:-}" == "--only" ]]; then
+        parse_only "${2:-}"
+    fi
+
     trap cleanup EXIT
 
     wizard_intro

+ 64 - 0
deploy/start.sh

@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - 部署入口(从这里开始)
+# =============================================================================
+# 顶层菜单:选择"全流程一键部署"或"单项功能"。所有选项最终都转发到
+# deploy/scripts/setup.sh —— 因此无论全流程还是单项,都同样支持
+# 本地(local)与远程(remote, SSH) 两种目标(进入后由 setup.sh 询问)。
+#
+# 用法:
+#   ./start.sh            显示菜单
+#   ./start.sh --help     显示 setup.sh 帮助
+#   ./start.sh --only <项> 直接透传给 setup.sh(跳过菜单)
+# =============================================================================
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SETUP="${SCRIPT_DIR}/scripts/setup.sh"
+
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_bold='\033[1m'; c_red='\033[1;31m'
+
+if [[ ! -f "${SETUP}" ]]; then
+    echo -e "${c_red}[FAIL]${c_reset} 未找到部署向导:${SETUP}" >&2
+    echo "       请确认在项目的 deploy/ 目录下运行本脚本。" >&2
+    exit 1
+fi
+
+# 透传 --help / --only 等参数,跳过菜单
+if [[ $# -gt 0 ]]; then
+    exec bash "${SETUP}" "$@"
+fi
+
+echo -e "${c_blue}== FranklinWH Toolbox 部署入口 ==${c_reset}"
+echo "请选择要执行的操作(每项均支持本地/远程,进入后会询问):"
+echo
+echo -e "  ${c_bold}1)${c_reset} 交互式一键部署(全流程向导,推荐)"
+echo -e "  ${c_bold}2)${c_reset} 仅检测/安装依赖引擎(PostgreSQL/Mongo/Redis/MinIO/Docker)"
+echo -e "  ${c_bold}3)${c_reset} 仅初始化数据库(PG 建库建表 + Mongo/Redis 检查)"
+echo -e "  ${c_bold}4)${c_reset} 仅创建 MinIO 账号与策略(fwh-toolbox / fwh-toolbox-ai)"
+echo -e "  ${c_bold}5)${c_reset} 仅部署 pgAdmin4(+ 给 postgres 设密码)"
+echo -e "  ${c_bold}6)${c_reset} 仅安装 Casdoor(Docker 认证服务)"
+echo -e "  ${c_bold}7)${c_reset} 仅部署 Caddy 反向代理(自编译 + spaceship DNS)"
+echo -e "  ${c_bold}8)${c_reset} 仅安装 Kiro CLI"
+echo -e "  ${c_bold}9)${c_reset} 仅构建部署前后端(可选前端/后端/两者)"
+echo -e "  ${c_bold}0)${c_reset} 退出"
+echo
+
+while :; do
+    read -r -p "$(echo -e "${c_bold}输入序号${c_reset} [1]: ")" sel
+    sel="${sel:-1}"
+
+    case "${sel}" in
+        1) exec bash "${SETUP}" ;;
+        2) exec bash "${SETUP}" --only deps ;;
+        3) exec bash "${SETUP}" --only initdb ;;
+        4) exec bash "${SETUP}" --only minioacct ;;
+        5) exec bash "${SETUP}" --only pgadmin ;;
+        6) exec bash "${SETUP}" --only casdoor ;;
+        7) exec bash "${SETUP}" --only caddy ;;
+        8) exec bash "${SETUP}" --only kiro ;;
+        9) exec bash "${SETUP}" --only deploy ;;
+        0) echo -e "${c_green}已退出${c_reset}"; exit 0 ;;
+        *) echo -e "${c_red}无效选择:${sel},请重新输入(0 退出)。${c_reset}" >&2 ;;
+    esac
+done