#!/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" MCP_CONFIG_EXAMPLE="${DEPLOY_DIR}/config.mcp.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_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 if [[ -z "${__a}" ]]; then warn "不能为空,请重新输入。"; continue fi read -r -s -p "$(echo -e "${c_bold}${__prompt}(再输一次确认)${c_reset}: ")" __b; echo if [[ "${__a}" != "${__b}" ]]; then warn "两次输入不一致,请重新输入。"; continue fi break done printf -v "${__var}" '%s' "${__a}" } # 是/否确认: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"; DO_PGADMIN="n"; DO_MINIOACCT="y" # 远程连接 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="fwh-toolbox"; MINIO_SECRET_KEY="fwh-toolbox"; MINIO_BUCKET="fwh-toolbox" # TODO: [确认] MinIO 业务 AK/SK(默认 fwh-toolbox) MINIO_AI_ACCESS_KEY="fwh-toolbox-ai"; MINIO_AI_SECRET_KEY="fwh-toolbox-ai"; MINIO_AI_BUCKET="fwh-toolbox-ai-file" # MCP 用 AI 账号(provision_minio.sh 创建) MINIO_ROOT_USER="jerry"; MINIO_ROOT_PASSWORD="" # TODO: [必填] MinIO 管理员密码向导内强制输入 # pgAdmin4(数据库 Web 管理,可选) PGADMIN_PORT="5480"; PGADMIN_BIND="127.0.0.1"; PGADMIN_EMAIL="liang_jlyx@163.com" PGADMIN_PASSWORD="" # TODO: [必填] 向导内强制输入 POSTGRES_ADMIN_PASSWORD="" # TODO: [必填] 向导内强制输入 # 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 LIBDNS_SPACESHIP_APIKEY=""; LIBDNS_SPACESHIP_APISECRET="" # TODO: [必填] spaceship DNS API 凭据(向导内输入,写入 systemd 服务) 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="" GEN_MCP_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" ask_yn "⑨ 部署 pgAdmin4(Docker,数据库 Web 管理,并给 postgres 设密码)?" "n" && DO_PGADMIN="y" || DO_PGADMIN="n" ask_yn "⑩ 创建 MinIO 业务账号与策略(fwh-toolbox / fwh-toolbox-ai)?" "y" && DO_MINIOACCT="y" || DO_MINIOACCT="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" || "${DO_MINIOACCT}" == "y" ]]; then title "MinIO 管理员账户(安装/建桶/建账号用)" ask MINIO_ROOT_USER "MinIO 管理员用户" "jerry" ask_secret_required MINIO_ROOT_PASSWORD "MinIO 管理员密码(必填)" 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_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 连接,必填)" 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" 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}" 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}" # ----------------------------------------------------------------------- # 单独生成 config.mcp.env(基于 config.mcp.env.example) # 与后端共享库/缓存/MinIO endpoint/MySQL 等,但 MinIO 使用 AI 专用账号 # fwh-toolbox-ai(对 fwh-toolbox-ai* 桶可读写、其余桶只读;MCP 工具仅只读)。 # ----------------------------------------------------------------------- if [[ -f "${MCP_CONFIG_EXAMPLE}" ]]; then GEN_MCP_CONFIG="$(mktemp /tmp/fwh_config.mcp.env.XXXXXX)" cp "${MCP_CONFIG_EXAMPLE}" "${GEN_MCP_CONFIG}" # MCP 侧 MinIO 使用 AI 账号;默认账号=密码=fwh-toolbox-ai(可用环境变量覆盖) local mcp_ak="${MINIO_AI_ACCESS_KEY:-fwh-toolbox-ai}" local mcp_sk="${MINIO_AI_SECRET_KEY:-fwh-toolbox-ai}" if command -v python3 >/dev/null 2>&1; then APP_ENV="${APP_ENV}" PG_DSN="${pg_dsn}" MONGO_DSN="${MONGO_DSN}" \ REDIS_ADDR="${REDIS_ADDR}" MYSQL_DSN="${MYSQL_DSN}" \ MINIO_ENDPOINT="${MINIO_ENDPOINT}" MINIO_ACCESS_KEY="${mcp_ak}" \ MINIO_SECRET_KEY="${mcp_sk}" MINIO_BUCKET="${MINIO_BUCKET}" \ python3 - "${GEN_MCP_CONFIG}" <<'PY' import os, re, sys path = sys.argv[1] repl = { "APP_ENV": os.environ["APP_ENV"], "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"], } 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: out.append(f'{m.group(2)}="{repl[m.group(2)]}"') else: out.append(line) open(path, "w", encoding="utf-8").write("\n".join(out) + "\n") print("config.mcp.env 已生成") PY else warn "未找到 python3,改用 sed 简易替换(含特殊字符时请手工核对)" _sed_mcp() { sed -i -E "s#^(${1})=.*#${1}=\"${2//#/\\#}\"#" "${GEN_MCP_CONFIG}"; } _sed_mcp APP_ENV "${APP_ENV}" _sed_mcp POSTGRES_DSN "${pg_dsn}" _sed_mcp MONGO_DSN "${MONGO_DSN}" _sed_mcp REDIS_ADDR "${REDIS_ADDR}" [[ -n "${MYSQL_DSN}" ]] && _sed_mcp MYSQL_DSN "${MYSQL_DSN}" _sed_mcp MINIO_ENDPOINT "${MINIO_ENDPOINT}" _sed_mcp MINIO_ACCESS_KEY "${mcp_ak}" _sed_mcp MINIO_SECRET_KEY "${mcp_sk}" _sed_mcp MINIO_BUCKET_NAME "${MINIO_BUCKET}" fi ok "已生成配置:${GEN_MCP_CONFIG}" else warn "未找到 ${MCP_CONFIG_EXAMPLE},跳过 config.mcp.env 生成" fi } # --------------------------------------------------------------------------- # 构建产物(前端 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 ${REMOTE_STAGE}/bin" # 辅助脚本(依赖安装/ 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/" # 自编译 caddy 二进制(仅在需要装 Caddy 时上传,约 45MB) if [[ "${DO_CADDY}" == "y" && -f "${SCRIPT_DIR}/../bin/caddy" ]]; then scp_to "${SCRIPT_DIR}/../bin/caddy" "${REMOTE_STAGE}/bin/" fi scp_to "${SCRIPT_DIR}/install_pgadmin.sh" "${REMOTE_STAGE}/scripts/" scp_to "${SCRIPT_DIR}/provision_minio.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" if [[ -n "${GEN_MCP_CONFIG}" ]]; then scp_to "${GEN_MCP_CONFIG}" "${REMOTE_STAGE}/config.mcp.env" elif [[ -n "${GEN_CONFIG}" ]]; then # 回退:未单独生成 MCP 配置时沿用后端配置 scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.mcp.env" fi 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 # uv 作为 MCP/AI 对话运行时依赖(uvx 拉起 MCP server),默认一并检测安装 local comps="postgres mongo redis minio mc uv" [[ "${DO_CASDOOR}" == "y" || "${DO_PGADMIN}" == "y" ]] && comps="${comps} docker" run "MINIO_API_PORT='${api_port}' MINIO_ROOT_USER='${MINIO_ROOT_USER}' MINIO_ROOT_PASSWORD='$(printf '%s' "${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)" } # --------------------------------------------------------------------------- # 部署 pgAdmin4(Docker)+ 给 postgres 设密码 # --------------------------------------------------------------------------- step_pgadmin() { [[ "${DO_PGADMIN}" == "y" ]] || return 0 title "部署 pgAdmin4(Web)" local base; base="$(_scripts_base)" run "POSTGRES_PASSWORD='$(printf '%s' "${POSTGRES_ADMIN_PASSWORD}")' \ PGADMIN_EMAIL='${PGADMIN_EMAIL}' \ PGADMIN_PASSWORD='$(printf '%s' "${PGADMIN_PASSWORD}")' \ PGADMIN_PORT='${PGADMIN_PORT}' PGADMIN_BIND='${PGADMIN_BIND}' \ PG_PORT='${PG_PORT}' \ bash '${base}/install_pgadmin.sh'" ok "pgAdmin4 部署步骤结束(访问 http://${PGADMIN_BIND}:${PGADMIN_PORT})" } # --------------------------------------------------------------------------- # 创建 MinIO 业务账号与策略(fwh-toolbox / fwh-toolbox-ai) # --------------------------------------------------------------------------- step_minioacct() { [[ "${DO_MINIOACCT}" == "y" ]] || return 0 title "创建 MinIO 业务账号与策略" local base; base="$(_scripts_base)" local minio_ep="http://${MINIO_ENDPOINT}" run "MINIO_ENDPOINT='${minio_ep}' \ MINIO_ROOT_USER='${MINIO_ROOT_USER}' MINIO_ROOT_PASSWORD='$(printf '%s' "${MINIO_ROOT_PASSWORD}")' \ MINIO_ACCESS_KEY='${MINIO_ACCESS_KEY:-fwh-toolbox}' MINIO_SECRET_KEY='$(printf '%s' "${MINIO_SECRET_KEY:-fwh-toolbox}")' \ MINIO_AI_ACCESS_KEY='${MINIO_AI_ACCESS_KEY}' MINIO_AI_SECRET_KEY='$(printf '%s' "${MINIO_AI_SECRET_KEY}")' \ MINIO_BUCKET='${MINIO_BUCKET}' MINIO_AI_BUCKET='${MINIO_AI_BUCKET}' \ bash '${base}/provision_minio.sh'" ok "MinIO 业务账号步骤结束(后端 ${MINIO_ACCESS_KEY:-fwh-toolbox} / MCP ${MINIO_AI_ACCESS_KEY})" } # --------------------------------------------------------------------------- # 安装/配置 Caddy 反向代理 # --------------------------------------------------------------------------- step_caddy() { [[ "${DO_CADDY}" == "y" ]] || return 0 title "安装/配置 Caddy 反向代理" local base; base="$(_scripts_base)" run "CADDY_DOMAIN='${CADDY_DOMAIN}' \ LIBDNS_SPACESHIP_APIKEY='$(printf '%s' "${LIBDNS_SPACESHIP_APIKEY}")' \ LIBDNS_SPACESHIP_APISECRET='$(printf '%s' "${LIBDNS_SPACESHIP_APISECRET}")' \ 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/(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 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" # 注:MinIO 建桶 + 业务账号 + 策略统一由 step_minioacct(provision_minio.sh)负责,此处不再建桶。 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 if [[ -n "${GEN_MCP_CONFIG}" ]]; then sudo cp "${GEN_MCP_CONFIG}" "${server_dir}/config.mcp.env" || true elif [[ -n "${GEN_CONFIG}" ]]; then sudo cp "${GEN_CONFIG}" "${server_dir}/config.mcp.env" || true fi 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}" [[ -n "${GEN_MCP_CONFIG}" && -f "${GEN_MCP_CONFIG}" ]] && rm -f "${GEN_MCP_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 ' )$( [[ ${DO_PGADMIN} == y ]] && echo -n 'pgAdmin4 ' )$( [[ ${DO_MINIOACCT} == y ]] && echo -n 'MinIO账号 ' )" echo echo "后续:" if [[ "${DO_CADDY}" == "y" ]]; then echo " * Caddy 已配置:https://${CADDY_DOMAIN}/fwh-toolbox/ (站点文件 /etc/caddy/sites/fwh-toolbox.caddy)" echo " TLS 走 spaceship DNS-01;token 已写入 systemd 服务(/lib/systemd/system/caddy.service,权限 600)。" 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 if [[ "${DO_PGADMIN}" == "y" ]]; then echo " * pgAdmin4 已部署:http://${PGADMIN_BIND}:${PGADMIN_PORT}(登录 ${PGADMIN_EMAIL})" echo " 新建 Server 连本机 PG:主机 host.docker.internal、端口 ${PG_PORT}、用户 postgres。" echo " 若连接报认证失败,多为 pg_hba.conf 未启用 scram-sha-256,见脚本提示。" echo " 安全:勿把 ${PGADMIN_PORT} 暴露公网,建议置于反向代理 + 访问控制之后。" fi if [[ "${DO_MINIOACCT}" == "y" ]]; then echo " * MinIO 业务账号已创建:后端 ${MINIO_ACCESS_KEY:-fwh-toolbox}(写 fwh-toolbox* 桶)、" echo " MCP ${MINIO_AI_ACCESS_KEY}(写 fwh-toolbox-ai* 桶,其余只读)。" echo " 确认 config.env 用后端账号、config.mcp.env 用 AI 账号;核对:mc admin user list fwhlocal" 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 <