| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- #!/usr/bin/env bash
- # =============================================================================
- # FranklinWH Toolbox - 交互式一键部署脚本
- # =============================================================================
- # 特性:
- # * 分步交互:逐项选择/填写,回车即用默认值
- # * 支持两种目标:
- # 1) local —— 在当前主机部署
- # 2) remote —— 通过 SSH 部署到远程主机(自动打包并传输构建产物)
- # * 可选步骤:初始化数据库 / 部署后端 / 部署前端
- # * 自动根据填写内容生成 config.env
- #
- # 依赖:
- # 本机需有 bash、ssh、scp、tar;构建前端/后端需 pnpm、go(仅在“构建”步骤需要)
- #
- # 用法:
- # ./setup.sh # 进入交互式向导
- # ./setup.sh --help
- #
- # 说明:
- # 远程部署采用“本地构建 + 远程安装”策略:前端 dist 与后端二进制在本机构建后
- # 打包上传到远程,再由远程执行安装脚本,避免远程主机必须装 Go/Node。
- # 数据库软件的安装请参考 deploy/DEPLOYMENT.md 第 3、4 章,本脚本只做
- # 建库建表建桶与服务部署,不自动安装数据库引擎。
- # =============================================================================
- set -euo pipefail
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" # deploy/
- PROJECT_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" # 仓库根目录
- SERVICE_DIR="${PROJECT_ROOT}/service"
- SQL_FILE="${DEPLOY_DIR}/sql/init_pgsql.sql"
- CONFIG_EXAMPLE="${DEPLOY_DIR}/config.env.example"
- APP_NAME="fwh-toolbox-server"
- MCP_NAME="fwh-mcp-server"
- # ---------------------------------------------------------------------------
- # 输出辅助
- # ---------------------------------------------------------------------------
- c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'; c_bold='\033[1m'
- info() { echo -e "${c_blue}[INFO]${c_reset} $*"; }
- ok() { echo -e "${c_green}[ OK ]${c_reset} $*"; }
- warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
- err() { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; }
- title() { echo -e "\n${c_bold}==== $* ====${c_reset}"; }
- # 提示输入(带默认值):ask VAR "提示" "默认值"
- ask() {
- local __var="$1" __prompt="$2" __default="${3:-}" __ans
- if [[ -n "${__default}" ]]; then
- read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset} [${__default}]: ")" __ans
- __ans="${__ans:-${__default}}"
- else
- read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset}: ")" __ans
- fi
- 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}}"
- printf -v "${__var}" '%s' "${__ans}"
- }
- # 是/否确认:ask_yn "提示" "y|n(默认)" -> 返回 0=yes 1=no
- ask_yn() {
- local __prompt="$1" __default="${2:-y}" __ans
- local __hint="[Y/n]"; [[ "${__default}" == "n" ]] && __hint="[y/N]"
- read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset} ${__hint}: ")" __ans
- __ans="${__ans:-${__default}}"
- [[ "${__ans}" =~ ^[Yy]$ ]]
- }
- require_cmd() {
- command -v "$1" >/dev/null 2>&1 || { err "缺少命令:$1($2)"; return 1; }
- }
- # ---------------------------------------------------------------------------
- # 全局变量(向导填写)
- # ---------------------------------------------------------------------------
- MODE="" # local | remote
- DO_DEPS="y"; DO_CASDOOR="n"; DO_INITDB="y"; DO_BACKEND="y"; DO_FRONTEND="y"; DO_GENCONF="y"; DO_CADDY="n"; DO_KIRO="n"
- # 远程连接
- R_HOST=""; R_PORT="22"; R_USER=""; R_SSH_KEY=""
- # 部署目录
- DEPLOY_ROOT="/var/www/fwh-toolbox"
- RUN_USER=""
- # 数据库/配置项
- # TODO: 以下均为“默认值”,向导会逐项提示你确认或覆盖。以下几项默认口令不安全,
- # 运行向导时务必改为强口令:PG_PASS、MINIO_ROOT_PASSWORD、CASDOOR_PG_PASSWORD;
- # 并按需填写 MINIO_ACCESS_KEY/MINIO_SECRET_KEY、MYSQL_DSN、CASDOOR_ORIGIN。
- APP_ENV="dev"; APP_PORT="8301"; ASYNQMON_PORT="8302"
- PG_HOST="localhost"; PG_PORT="5432"; PG_DB="fwh_toolbox_db"; PG_USER="fwh_toolbox"; PG_PASS="fwh_toolbox" # TODO: [必填] PG_PASS 改强口令
- MONGO_DSN="mongodb://127.0.0.1:27017/FwhToolboxDb"
- REDIS_ADDR="localhost:6379"
- MYSQL_DSN="" # TODO: [必填/可选] 远端只读 MySQL DSN,用到站点查询才需填
- MINIO_ENDPOINT="localhost:9198"; MINIO_ACCESS_KEY=""; MINIO_SECRET_KEY=""; MINIO_BUCKET="fwh-toolbox" # TODO: [必填] MinIO AK/SK
- MINIO_ROOT_USER="minioadmin"; MINIO_ROOT_PASSWORD="minioadmin" # TODO: [必填] MinIO 管理员改强口令
- # Casdoor
- CASDOOR_DIR=""; CASDOOR_PORT="8010"
- CASDOOR_PG_USER="casdoor"; CASDOOR_PG_PASSWORD="casdoor"; CASDOOR_DB_NAME="casdoor" # TODO: [必填] CASDOOR_PG_PASSWORD 改强口令
- CASDOOR_ORIGIN="" # TODO: [必填] Casdoor 对外访问地址,如 https://<域名>/casdoor
- # Caddy 反向代理
- CADDY_DOMAIN="" # TODO: [必填] 对外域名,如 lmt.zenpower.cc
- CADDY_TLS_MODE="internal" # internal | acme | manual
- CADDY_ACME_EMAIL="" # acme 模式邮箱(可空,走默认 ACME)
- CADDY_TLS_CERT=""; CADDY_TLS_KEY="" # manual 模式证书/私钥路径
- CADDY_FRONTEND_PORT="8300"; CADDY_BACKEND_PORT="8301"
- CADDY_INCLUDE_CASDOOR="n"; CADDY_INCLUDE_MINIO="n"
- CADDY_SERVE_STATIC="n" # 由 Caddy 直接 file_server 托管前端
- # Kiro CLI(后端 AI 对话依赖)
- KIRO_INSTALL_METHOD="zip" # zip | curl
- KIRO_LIBC="gnu" # gnu | musl
- KIRO_DO_LOGIN="n" # 安装后是否立即交互式登录
- # 生成的配置文件(本地临时路径)
- GEN_CONFIG=""
- # ---------------------------------------------------------------------------
- # 步骤 0:选择模式与步骤
- # ---------------------------------------------------------------------------
- wizard_intro() {
- title "FranklinWH Toolbox 交互式部署向导"
- echo "本向导将分步引导你完成部署。任何提示都可直接回车使用默认值。"
- echo
- echo "请选择部署目标:"
- echo " 1) 本地部署 (local)"
- echo " 2) 远程部署 (remote, 通过 SSH)"
- local sel; ask sel "输入序号" "1"
- case "${sel}" in
- 1) MODE="local" ;;
- 2) MODE="remote" ;;
- *) err "无效选择"; exit 1 ;;
- esac
- ok "部署模式:${MODE}"
- 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"
- ask_yn "③ 初始化数据库(建库/建表/建桶)?" "y" && DO_INITDB="y" || DO_INITDB="n"
- ask_yn "④ 部署后端(构建二进制 + systemd 服务)?" "y" && DO_BACKEND="y" || DO_BACKEND="n"
- ask_yn "⑤ 部署前端(构建 dist + 同步静态目录)?" "y" && DO_FRONTEND="y" || DO_FRONTEND="n"
- if [[ "${DO_BACKEND}" == "y" || "${DO_INITDB}" == "y" ]]; then
- ask_yn "⑥ 生成 config.env(根据后续填写内容)?" "y" && DO_GENCONF="y" || DO_GENCONF="n"
- else
- DO_GENCONF="n"
- fi
- ask_yn "⑦ 安装并配置 Caddy 反向代理(生成站点 Caddyfile 并 reload)?" "n" && DO_CADDY="y" || DO_CADDY="n"
- ask_yn "⑧ 安装 Kiro CLI(后端 AI 对话依赖,需以运行用户登录 Builder ID)?" "n" && DO_KIRO="y" || DO_KIRO="n"
- }
- # ---------------------------------------------------------------------------
- # 步骤 1:远程连接信息
- # ---------------------------------------------------------------------------
- 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
- err "SSH 连接失败,请检查地址/端口/用户/密钥"
- exit 1
- fi
- }
- # 组装 ssh/scp 通用参数
- ssh_opts() {
- local opts=(-p "${R_PORT}" -o StrictHostKeyChecking=accept-new)
- [[ -n "${R_SSH_KEY}" ]] && opts+=(-i "${R_SSH_KEY}")
- printf '%s\n' "${opts[@]}"
- }
- scp_opts() {
- local opts=(-P "${R_PORT}" -o StrictHostKeyChecking=accept-new)
- [[ -n "${R_SSH_KEY}" ]] && opts+=(-i "${R_SSH_KEY}")
- printf '%s\n' "${opts[@]}"
- }
- # 在远程执行命令
- ssh_run() {
- mapfile -t _o < <(ssh_opts)
- ssh "${_o[@]}" "${R_USER}@${R_HOST}" "$@"
- }
- # 拷贝文件/目录到远程
- scp_to() {
- mapfile -t _o < <(scp_opts)
- scp "${_o[@]}" -r "$1" "${R_USER}@${R_HOST}:$2"
- }
- # 统一执行命令封装:本地直接跑,远程走 ssh
- run() {
- if [[ "${MODE}" == "remote" ]]; then
- ssh_run "$@"
- else
- bash -c "$@"
- fi
- }
- # ---------------------------------------------------------------------------
- # 步骤 2:部署参数与配置项
- # ---------------------------------------------------------------------------
- wizard_params() {
- title "部署目录与运行用户"
- ask DEPLOY_ROOT "部署根目录" "/var/www/fwh-toolbox"
- local def_user="${R_USER:-${USER}}"
- ask RUN_USER "后端 systemd 运行用户" "${def_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"
- fi
- if [[ "${DO_INITDB}" == "y" || "${DO_DEPS}" == "y" ]]; then
- title "MinIO 管理员账户(安装/建桶用)"
- ask MINIO_ROOT_USER "MinIO 管理员用户" "minioadmin"
- ask_secret MINIO_ROOT_PASSWORD "MinIO 管理员密码" "minioadmin"
- 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"
- ask CASDOOR_ORIGIN "Casdoor 对外访问地址(origin)" "http://localhost:${CASDOOR_PORT}"
- fi
- if [[ "${DO_CADDY}" == "y" ]]; then
- title "Caddy 反向代理"
- ask CADDY_DOMAIN "对外域名 (如 lmt.zenpower.cc)"
- [[ -n "${CADDY_DOMAIN}" ]] || { err "Caddy 域名不能为空"; exit 1; }
- echo "TLS 模式:1) internal(自签,测试) 2) acme(自动签发) 3) manual(指定证书)"
- local _tls_sel; ask _tls_sel "输入序号" "1"
- case "${_tls_sel}" in
- 1) CADDY_TLS_MODE="internal" ;;
- 2) CADDY_TLS_MODE="acme"; ask CADDY_ACME_EMAIL "ACME 邮箱(可留空走默认)" "" ;;
- 3) CADDY_TLS_MODE="manual"
- ask CADDY_TLS_CERT "证书文件路径 (.pem/.crt)"
- ask CADDY_TLS_KEY "私钥文件路径 (.key)" ;;
- *) err "无效选择"; exit 1 ;;
- esac
- ask CADDY_FRONTEND_PORT "前端静态端口" "8300"
- ask CADDY_BACKEND_PORT "后端 API 端口" "${APP_PORT}"
- ask_yn "由 Caddy 直接 file_server 托管前端静态(否则反代到前端端口)?" "n" \
- && CADDY_SERVE_STATIC="y" || CADDY_SERVE_STATIC="n"
- ask_yn "加入 /casdoor 反代?" "$( [[ ${DO_CASDOOR} == y ]] && echo y || echo n )" \
- && CADDY_INCLUDE_CASDOOR="y" || CADDY_INCLUDE_CASDOOR="n"
- ask_yn "加入 /fwh-minio (MinIO 控制台) 反代?" "n" \
- && CADDY_INCLUDE_MINIO="y" || CADDY_INCLUDE_MINIO="n"
- fi
- if [[ "${DO_KIRO}" == "y" ]]; then
- title "Kiro CLI(后端 AI 对话依赖)"
- echo "将以运行用户 ${RUN_USER} 身份安装(其 HOME 决定 ~/.kiro 配置位置)。"
- echo "安装方式:1) zip 官方安装包(默认) 2) curl 一键脚本"
- local _km; ask _km "输入序号" "1"
- case "${_km}" in
- 1) KIRO_INSTALL_METHOD="zip"
- echo "C 库变体:1) gnu(默认,主流发行版) 2) musl(Alpine 等)"
- local _kl; ask _kl "输入序号" "1"
- [[ "${_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"
- fi
- }
- # ---------------------------------------------------------------------------
- # 生成 config.env(本地临时文件)
- # ---------------------------------------------------------------------------
- generate_config() {
- [[ "${DO_GENCONF}" == "y" ]] || return 0
- title "生成 config.env"
- GEN_CONFIG="$(mktemp /tmp/fwh_config.env.XXXXXX)"
- # 以模板为基础,替换关键项;未覆盖的项保留模板默认(含 FWH/Plane/MQTT 等)
- cp "${CONFIG_EXAMPLE}" "${GEN_CONFIG}"
- local pg_dsn="host=${PG_HOST} user=${PG_USER} password=${PG_PASS} dbname=${PG_DB} port=${PG_PORT} sslmode=disable"
- # 使用 python 做安全替换(避免特殊字符干扰 sed)
- if command -v python3 >/dev/null 2>&1; then
- APP_ENV="${APP_ENV}" APP_PORT="${APP_PORT}" ASYNQMON_PORT="${ASYNQMON_PORT}" \
- PG_DSN="${pg_dsn}" MONGO_DSN="${MONGO_DSN}" REDIS_ADDR="${REDIS_ADDR}" MYSQL_DSN="${MYSQL_DSN}" \
- MINIO_ENDPOINT="${MINIO_ENDPOINT}" MINIO_ACCESS_KEY="${MINIO_ACCESS_KEY}" \
- MINIO_SECRET_KEY="${MINIO_SECRET_KEY}" MINIO_BUCKET="${MINIO_BUCKET}" \
- RUN_USER="${RUN_USER}" \
- python3 - "${GEN_CONFIG}" <<'PY'
- import os, re, sys
- path = sys.argv[1]
- repl = {
- "APP_ENV": os.environ["APP_ENV"],
- "APP_PORT": os.environ["APP_PORT"],
- "ASYNQMON_PORT": os.environ["ASYNQMON_PORT"],
- "POSTGRES_DSN": os.environ["PG_DSN"],
- "MONGO_DSN": os.environ["MONGO_DSN"],
- "REDIS_ADDR": os.environ["REDIS_ADDR"],
- "MINIO_ENDPOINT": os.environ["MINIO_ENDPOINT"],
- "MINIO_ACCESS_KEY": os.environ["MINIO_ACCESS_KEY"],
- "MINIO_SECRET_KEY": os.environ["MINIO_SECRET_KEY"],
- "MINIO_BUCKET_NAME": os.environ["MINIO_BUCKET"],
- "KIRO_RUN_AS_USER": os.environ["RUN_USER"],
- }
- mysql = os.environ.get("MYSQL_DSN", "")
- if mysql:
- repl["MYSQL_DSN"] = mysql
- lines = open(path, encoding="utf-8").read().splitlines()
- out = []
- for line in lines:
- m = re.match(r'^(\s*)([A-Z0-9_]+)\s*=', line)
- if m and m.group(2) in repl:
- key = m.group(2)
- val = repl[key]
- # KIRO_RUN_AS_USER 模板里是无引号写法,其余用双引号包裹
- if key == "KIRO_RUN_AS_USER":
- out.append(f'{key}={val}')
- else:
- out.append(f'{key}="{val}"')
- else:
- out.append(line)
- open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
- print("config.env 已生成")
- PY
- else
- warn "未找到 python3,改用 sed 简易替换(含特殊字符时请手工核对)"
- _sed_kv() { sed -i -E "s#^(${1})=.*#${1}=\"${2//#/\\#}\"#" "${GEN_CONFIG}"; }
- _sed_kv APP_ENV "${APP_ENV}"
- _sed_kv APP_PORT "${APP_PORT}"
- _sed_kv ASYNQMON_PORT "${ASYNQMON_PORT}"
- _sed_kv POSTGRES_DSN "${pg_dsn}"
- _sed_kv MONGO_DSN "${MONGO_DSN}"
- _sed_kv REDIS_ADDR "${REDIS_ADDR}"
- [[ -n "${MYSQL_DSN}" ]] && _sed_kv MYSQL_DSN "${MYSQL_DSN}"
- _sed_kv MINIO_ENDPOINT "${MINIO_ENDPOINT}"
- _sed_kv MINIO_ACCESS_KEY "${MINIO_ACCESS_KEY}"
- _sed_kv MINIO_SECRET_KEY "${MINIO_SECRET_KEY}"
- _sed_kv MINIO_BUCKET_NAME "${MINIO_BUCKET}"
- sed -i -E "s#^KIRO_RUN_AS_USER=.*#KIRO_RUN_AS_USER=${RUN_USER}#" "${GEN_CONFIG}"
- fi
- ok "已生成配置:${GEN_CONFIG}"
- }
- # ---------------------------------------------------------------------------
- # 构建产物(前端 dist、后端二进制)——始终在本机构建
- # ---------------------------------------------------------------------------
- build_frontend() {
- [[ "${DO_FRONTEND}" == "y" ]] || return 0
- title "构建前端"
- require_cmd pnpm "安装 Node 22 + pnpm,见 DEPLOYMENT.md 3.3"
- ( cd "${PROJECT_ROOT}" && pnpm install --frozen-lockfile && pnpm build )
- ok "前端构建完成:${PROJECT_ROOT}/dist"
- }
- build_backend() {
- [[ "${DO_BACKEND}" == "y" ]] || return 0
- title "构建后端"
- require_cmd go "安装 Go 1.26,见 DEPLOYMENT.md 3.2"
- ( cd "${SERVICE_DIR}" \
- && GOOS=linux GOARCH=amd64 go build -o "${APP_NAME}" ./ \
- && GOOS=linux GOARCH=amd64 go build -o "${MCP_NAME}" ./cmd/mcp/ )
- ok "后端构建完成"
- }
- # ---------------------------------------------------------------------------
- # 打包上传(远程模式)
- # ---------------------------------------------------------------------------
- REMOTE_STAGE="/tmp/fwh-deploy-stage"
- prepare_remote_stage() {
- [[ "${MODE}" == "remote" ]] || return 0
- title "上传部署产物到远程"
- ssh_run "rm -rf ${REMOTE_STAGE} && mkdir -p ${REMOTE_STAGE}/sql ${REMOTE_STAGE}/cert ${REMOTE_STAGE}/scripts"
- # 辅助脚本(依赖安装/ Casdoor)
- scp_to "${SCRIPT_DIR}/install_deps.sh" "${REMOTE_STAGE}/scripts/"
- scp_to "${SCRIPT_DIR}/install_casdoor.sh" "${REMOTE_STAGE}/scripts/"
- scp_to "${SCRIPT_DIR}/install_caddy.sh" "${REMOTE_STAGE}/scripts/"
- scp_to "${SCRIPT_DIR}/install_kiro_cli.sh" "${REMOTE_STAGE}/scripts/"
- ssh_run "chmod +x ${REMOTE_STAGE}/scripts/*.sh"
- # SQL 与二进制、前端、配置、证书
- scp_to "${SQL_FILE}" "${REMOTE_STAGE}/sql/"
- if [[ "${DO_BACKEND}" == "y" ]]; then
- scp_to "${SERVICE_DIR}/${APP_NAME}" "${REMOTE_STAGE}/"
- scp_to "${SERVICE_DIR}/${MCP_NAME}" "${REMOTE_STAGE}/"
- # 证书目录(若存在)
- [[ -d "${SERVICE_DIR}/cert" ]] && scp_to "${SERVICE_DIR}/cert" "${REMOTE_STAGE}/"
- fi
- if [[ "${DO_FRONTEND}" == "y" ]]; then
- ( cd "${PROJECT_ROOT}" && tar czf /tmp/fwh-dist.tgz -C dist . )
- scp_to "/tmp/fwh-dist.tgz" "${REMOTE_STAGE}/"
- rm -f /tmp/fwh-dist.tgz
- fi
- [[ -n "${GEN_CONFIG}" ]] && scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.env"
- [[ -n "${GEN_CONFIG}" ]] && scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.mcp.env"
- ok "产物已上传到远程 ${REMOTE_STAGE}"
- }
- # ---------------------------------------------------------------------------
- # 检测/安装依赖(数据库引擎等)
- # ---------------------------------------------------------------------------
- # 返回辅助脚本在“执行侧”的路径(本地=仓库,远程=暂存目录)
- _scripts_base() {
- if [[ "${MODE}" == "remote" ]]; then echo "${REMOTE_STAGE}/scripts"; else echo "${SCRIPT_DIR}"; fi
- }
- step_deps() {
- [[ "${DO_DEPS}" == "y" ]] || return 0
- title "检测/安装依赖"
- local base; base="$(_scripts_base)"
- local api_port="${MINIO_ENDPOINT##*:}"
- # 需要的组件:若要装 Casdoor 则一并确保 docker
- local comps="postgres mongo redis minio mc"
- [[ "${DO_CASDOOR}" == "y" ]] && comps="${comps} docker"
- run "MINIO_API_PORT='${api_port}' MINIO_ROOT_USER='${MINIO_ROOT_USER}' MINIO_ROOT_PASSWORD='${MINIO_ROOT_PASSWORD}' bash '${base}/install_deps.sh' ${comps}"
- ok "依赖检测/安装完成"
- }
- # ---------------------------------------------------------------------------
- # 安装 Casdoor
- # ---------------------------------------------------------------------------
- step_casdoor() {
- [[ "${DO_CASDOOR}" == "y" ]] || return 0
- title "安装 Casdoor"
- local base; base="$(_scripts_base)"
- run "CASDOOR_DIR='${CASDOOR_DIR}' CASDOOR_PORT='${CASDOOR_PORT}' \
- CASDOOR_PG_USER='${CASDOOR_PG_USER}' CASDOOR_PG_PASSWORD='${CASDOOR_PG_PASSWORD}' \
- CASDOOR_DB_NAME='${CASDOOR_DB_NAME}' CASDOOR_ORIGIN='${CASDOOR_ORIGIN}' \
- bash '${base}/install_casdoor.sh'"
- ok "Casdoor 安装步骤结束(后续配置见 DEPLOYMENT.md)"
- }
- # ---------------------------------------------------------------------------
- # 安装/配置 Caddy 反向代理
- # ---------------------------------------------------------------------------
- step_caddy() {
- [[ "${DO_CADDY}" == "y" ]] || return 0
- title "安装/配置 Caddy 反向代理"
- local base; base="$(_scripts_base)"
- run "CADDY_DOMAIN='${CADDY_DOMAIN}' CADDY_TLS_MODE='${CADDY_TLS_MODE}' \
- CADDY_ACME_EMAIL='${CADDY_ACME_EMAIL}' \
- CADDY_TLS_CERT='${CADDY_TLS_CERT}' CADDY_TLS_KEY='${CADDY_TLS_KEY}' \
- CADDY_FRONTEND_PORT='${CADDY_FRONTEND_PORT}' CADDY_BACKEND_PORT='${CADDY_BACKEND_PORT}' \
- CADDY_INCLUDE_CASDOOR='${CADDY_INCLUDE_CASDOOR}' CADDY_CASDOOR_PORT='${CASDOOR_PORT}' \
- CADDY_INCLUDE_MINIO='${CADDY_INCLUDE_MINIO}' CADDY_MINIO_CONSOLE_PORT='9199' \
- CADDY_SERVE_STATIC='${CADDY_SERVE_STATIC}' CADDY_STATIC_ROOT='${DEPLOY_ROOT}/fwh-toolbox' \
- bash '${base}/install_caddy.sh'"
- ok "Caddy 反向代理配置结束"
- }
- # ---------------------------------------------------------------------------
- # 安装 Kiro CLI(后端 AI 对话依赖)
- # ---------------------------------------------------------------------------
- step_kiro() {
- [[ "${DO_KIRO}" == "y" ]] || return 0
- title "安装 Kiro CLI"
- local base; base="$(_scripts_base)"
- # 以后端运行用户身份安装;HOME 用 config.env 约定的 /home/<user>(KIRO_HOME 默认取家目录)
- run "KIRO_RUN_AS_USER='${RUN_USER}' KIRO_HOME='/home/${RUN_USER}' \
- KIRO_INSTALL_METHOD='${KIRO_INSTALL_METHOD}' KIRO_LIBC='${KIRO_LIBC}' \
- KIRO_DO_LOGIN='${KIRO_DO_LOGIN}' \
- bash '${base}/install_kiro_cli.sh'"
- ok "Kiro CLI 安装步骤结束"
- }
- # ---------------------------------------------------------------------------
- # 初始化数据库
- # ---------------------------------------------------------------------------
- step_initdb() {
- [[ "${DO_INITDB}" == "y" ]] || return 0
- title "初始化数据库"
- local sql_path minio_ep
- minio_ep="http://${MINIO_ENDPOINT}"
- if [[ "${MODE}" == "remote" ]]; then
- sql_path="${REMOTE_STAGE}/sql/init_pgsql.sql"
- else
- sql_path="${SQL_FILE}"
- fi
- # 1) PostgreSQL
- info "PostgreSQL 建库/建表 ..."
- run "sudo -u postgres psql -v ON_ERROR_STOP=1 -v fwh_pwd='$(printf '%s' "${PG_PASS}")' -f '${sql_path}'" \
- && ok "PostgreSQL 完成" || { err "PostgreSQL 初始化失败"; return 1; }
- # 2) Mongo / Redis 连通性
- run "command -v mongosh >/dev/null 2>&1 && mongosh --quiet '${MONGO_DSN%%/*}//${MONGO_DSN#*//}' --eval 'db.runCommand({ping:1}).ok' >/dev/null 2>&1 && echo mongo-ok || true"
- run "command -v redis-cli >/dev/null 2>&1 && [ \"\$(redis-cli -h ${REDIS_ADDR%%:*} -p ${REDIS_ADDR##*:} ping 2>/dev/null)\" = PONG ] && echo redis-ok || true"
- # 3) MinIO bucket
- info "MinIO 建桶(若已存在则跳过)..."
- run "if command -v mc >/dev/null 2>&1; then \
- mc alias set fwhlocal '${minio_ep}' '${MINIO_ROOT_USER}' '${MINIO_ROOT_PASSWORD}' >/dev/null 2>&1 || true; \
- mc ls fwhlocal/'${MINIO_BUCKET}' >/dev/null 2>&1 || mc mb fwhlocal/'${MINIO_BUCKET}'; \
- echo minio-ok; \
- else echo 'no-mc(请手动在 MinIO 控制台建桶)'; fi"
- ok "数据库初始化步骤结束"
- }
- # ---------------------------------------------------------------------------
- # 部署后端
- # ---------------------------------------------------------------------------
- step_backend() {
- [[ "${DO_BACKEND}" == "y" ]] || return 0
- title "部署后端"
- local server_dir="${DEPLOY_ROOT}/server"
- local svc="/etc/systemd/system/${APP_NAME}.service"
- if [[ "${MODE}" == "remote" ]]; then
- run "sudo mkdir -p '${server_dir}' && \
- sudo mv '${REMOTE_STAGE}/${APP_NAME}' '${server_dir}/' && \
- sudo mv '${REMOTE_STAGE}/${MCP_NAME}' '${server_dir}/' && \
- sudo chmod +x '${server_dir}/${APP_NAME}' '${server_dir}/${MCP_NAME}' && \
- { [ -d '${REMOTE_STAGE}/cert' ] && sudo cp -r '${REMOTE_STAGE}/cert' '${server_dir}/' || true; } && \
- { [ -f '${REMOTE_STAGE}/config.env' ] && sudo cp '${REMOTE_STAGE}/config.env' '${server_dir}/config.env' || true; } && \
- { [ -f '${REMOTE_STAGE}/config.mcp.env' ] && sudo cp '${REMOTE_STAGE}/config.mcp.env' '${server_dir}/config.mcp.env' || true; }"
- else
- sudo mkdir -p "${server_dir}"
- sudo mv "${SERVICE_DIR}/${APP_NAME}" "${server_dir}/"
- sudo mv "${SERVICE_DIR}/${MCP_NAME}" "${server_dir}/"
- sudo chmod +x "${server_dir}/${APP_NAME}" "${server_dir}/${MCP_NAME}"
- [[ -d "${SERVICE_DIR}/cert" ]] && sudo cp -r "${SERVICE_DIR}/cert" "${server_dir}/" || true
- [[ -n "${GEN_CONFIG}" ]] && sudo cp "${GEN_CONFIG}" "${server_dir}/config.env" || true
- [[ -n "${GEN_CONFIG}" ]] && sudo cp "${GEN_CONFIG}" "${server_dir}/config.mcp.env" || true
- fi
- # 写入 systemd 服务
- local unit_content
- unit_content="[Unit]
- Description=${APP_NAME} service
- After=network.target postgresql.service mongod.service redis-server.service minio.service
- [Service]
- ExecStart=${server_dir}/${APP_NAME}
- Restart=always
- User=${RUN_USER}
- WorkingDirectory=${server_dir}
- CPUQuota=200%
- MemoryLimit=2G
- [Install]
- WantedBy=multi-user.target"
- run "echo '$(printf '%s' "${unit_content}" | sed "s/'/'\\\\''/g")' | sudo tee '${svc}' >/dev/null && \
- sudo systemctl daemon-reload && sudo systemctl enable '${APP_NAME}' && \
- ( sudo systemctl restart '${APP_NAME}' || sudo systemctl start '${APP_NAME}' ) && \
- sudo systemctl --no-pager status '${APP_NAME}' | head -n 6"
- ok "后端部署完成"
- }
- # ---------------------------------------------------------------------------
- # 部署前端
- # ---------------------------------------------------------------------------
- step_frontend() {
- [[ "${DO_FRONTEND}" == "y" ]] || return 0
- title "部署前端"
- local dist_dir="${DEPLOY_ROOT}/fwh-toolbox"
- if [[ "${MODE}" == "remote" ]]; then
- run "sudo mkdir -p '${dist_dir}' && sudo rm -rf '${dist_dir}'/* && \
- sudo tar xzf '${REMOTE_STAGE}/fwh-dist.tgz' -C '${dist_dir}'"
- else
- sudo mkdir -p "${dist_dir}"
- sudo rm -rf "${dist_dir:?}/"*
- sudo cp -rf "${PROJECT_ROOT}/dist/"* "${dist_dir}/"
- fi
- ok "前端部署完成:${dist_dir}"
- }
- # ---------------------------------------------------------------------------
- # 收尾
- # ---------------------------------------------------------------------------
- cleanup() {
- [[ -n "${GEN_CONFIG}" && -f "${GEN_CONFIG}" ]] && rm -f "${GEN_CONFIG}"
- if [[ "${MODE}" == "remote" ]]; then
- ssh_run "rm -rf ${REMOTE_STAGE}" >/dev/null 2>&1 || true
- fi
- }
- summary() {
- title "部署完成"
- echo "模式 : ${MODE}"
- [[ "${MODE}" == "remote" ]] && echo "远程主机 : ${R_USER}@${R_HOST}:${R_PORT}"
- echo "部署根目录 : ${DEPLOY_ROOT}"
- echo "已执行步骤 : $( [[ ${DO_DEPS} == y ]] && echo -n '依赖 ' )$( [[ ${DO_CASDOOR} == y ]] && echo -n 'Casdoor ' )$( [[ ${DO_INITDB} == y ]] && echo -n '初始化DB ' )$( [[ ${DO_BACKEND} == y ]] && echo -n '后端 ' )$( [[ ${DO_FRONTEND} == y ]] && echo -n '前端 ' )$( [[ ${DO_CADDY} == y ]] && echo -n 'Caddy ' )$( [[ ${DO_KIRO} == y ]] && echo -n 'KiroCLI ' )"
- echo
- echo "后续:"
- if [[ "${DO_CADDY}" == "y" ]]; then
- echo " * Caddy 已配置:https://${CADDY_DOMAIN}/fwh-toolbox/ (站点文件 /etc/caddy/sites/fwh-toolbox.caddy)"
- [[ "${CADDY_TLS_MODE}" == "internal" ]] && echo " 注意:TLS=internal 为自签证书,浏览器会告警,生产请改用 acme/manual。"
- else
- echo " * 配置并重载反向代理 Caddy(见 deploy/DEPLOYMENT.md 第 9 章)"
- fi
- if [[ "${DO_CASDOOR}" == "y" ]]; then
- echo " * 配置 Casdoor:登录 ${CASDOOR_ORIGIN} (admin/123),创建组织/应用,"
- echo " 取得 clientId/clientSecret/证书,并反代 /casdoor(见 DEPLOYMENT.md《Casdoor 配置》)"
- fi
- if [[ "${DO_KIRO}" == "y" ]]; then
- echo " * Kiro CLI 已安装(用户 ${RUN_USER})。"
- if [[ "${KIRO_DO_LOGIN}" != "y" ]]; then
- if [[ "${MODE}" == "remote" ]]; then
- echo " 登录:ssh ${R_USER}@${R_HOST} 'sudo -u ${RUN_USER} -H kiro-cli login'(Builder ID,交互式)"
- else
- echo " 登录:sudo -u ${RUN_USER} -H kiro-cli login(Builder ID,交互式)"
- fi
- fi
- echo " 确认 config.env 的 KIRO_RUN_AS_USER=${RUN_USER}、KIRO_HOME=/home/${RUN_USER} 一致,后端方能拉起 AI 对话。"
- fi
- echo " * 验证:curl http://127.0.0.1:${APP_PORT}/swagger/index.html"
- if [[ "${MODE}" == "remote" ]]; then
- echo " * 查看后端日志:ssh ${R_USER}@${R_HOST} 'journalctl -u ${APP_NAME} -f'"
- else
- echo " * 查看后端日志:journalctl -u ${APP_NAME} -f"
- fi
- }
- usage() {
- cat <<EOF
- FranklinWH Toolbox 交互式一键部署
- 用法:
- $0 进入交互式向导(选择 local/remote,分步选择与填写)
- $0 --help 显示本帮助
- 向导步骤:
- 0. 选择模式(本地/远程)与要执行的步骤
- 1. (远程)填写 SSH 连接信息并测试
- 2. 填写部署目录、运行用户与各项配置
- 3. 生成 config.env
- 4. 本地构建前端 dist 与后端二进制
- 5. (远程)打包上传产物与辅助脚本
- 6. 检测/安装依赖 → 安装 Casdoor → 初始化数据库 → 部署后端 → 部署前端 → 配置 Caddy → 安装 Kiro CLI
- 可选步骤(向导中勾选):
- * 检测/安装数据库引擎与依赖(PostgreSQL/MongoDB/Redis/MinIO/Docker)
- * 安装 Casdoor(Docker,认证服务)
- * 安装并配置 Caddy 反向代理(生成站点 Caddyfile 并 reload)
- * 安装 Kiro CLI(后端 AI 对话依赖,以运行用户安装并登录 Builder ID)
- EOF
- }
- main() {
- if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage; exit 0; fi
- trap cleanup EXIT
- wizard_intro
- wizard_remote
- wizard_params
- title "确认"
- echo "模式=${MODE} 目录=${DEPLOY_ROOT} 运行用户=${RUN_USER}"
- echo "步骤:deps=${DO_DEPS} casdoor=${DO_CASDOOR} initdb=${DO_INITDB} backend=${DO_BACKEND} frontend=${DO_FRONTEND} genconf=${DO_GENCONF} caddy=${DO_CADDY} kiro=${DO_KIRO}"
- ask_yn "确认开始执行?" "y" || { warn "已取消"; exit 0; }
- generate_config
- build_frontend
- build_backend
- prepare_remote_stage
- step_deps
- step_casdoor
- step_initdb
- step_backend
- step_frontend
- step_caddy
- step_kiro
- summary
- }
- main "$@"
|