setup.sh 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. #!/usr/bin/env bash
  2. # =============================================================================
  3. # FranklinWH Toolbox - 交互式一键部署脚本
  4. # =============================================================================
  5. # 特性:
  6. # * 分步交互:逐项选择/填写,回车即用默认值
  7. # * 支持两种目标:
  8. # 1) local —— 在当前主机部署
  9. # 2) remote —— 通过 SSH 部署到远程主机(自动打包并传输构建产物)
  10. # * 可选步骤:初始化数据库 / 部署后端 / 部署前端
  11. # * 自动根据填写内容生成 config.env
  12. #
  13. # 依赖:
  14. # 本机需有 bash、ssh、scp、tar;构建前端/后端需 pnpm、go(仅在“构建”步骤需要)
  15. #
  16. # 用法:
  17. # ./setup.sh # 进入交互式向导
  18. # ./setup.sh --help
  19. #
  20. # 说明:
  21. # 远程部署采用“本地构建 + 远程安装”策略:前端 dist 与后端二进制在本机构建后
  22. # 打包上传到远程,再由远程执行安装脚本,避免远程主机必须装 Go/Node。
  23. # 数据库软件的安装请参考 deploy/DEPLOYMENT.md 第 3、4 章,本脚本只做
  24. # 建库建表建桶与服务部署,不自动安装数据库引擎。
  25. # =============================================================================
  26. set -euo pipefail
  27. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  28. DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" # deploy/
  29. PROJECT_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" # 仓库根目录
  30. SERVICE_DIR="${PROJECT_ROOT}/service"
  31. SQL_FILE="${DEPLOY_DIR}/sql/init_pgsql.sql"
  32. CONFIG_EXAMPLE="${DEPLOY_DIR}/config.env.example"
  33. MCP_CONFIG_EXAMPLE="${DEPLOY_DIR}/config.mcp.env.example"
  34. APP_NAME="fwh-toolbox-server"
  35. MCP_NAME="fwh-mcp-server"
  36. # ---------------------------------------------------------------------------
  37. # 输出辅助
  38. # ---------------------------------------------------------------------------
  39. 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'
  40. info() { echo -e "${c_blue}[INFO]${c_reset} $*"; }
  41. ok() { echo -e "${c_green}[ OK ]${c_reset} $*"; }
  42. warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
  43. err() { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; }
  44. title() { echo -e "\n${c_bold}==== $* ====${c_reset}"; }
  45. # 提示输入(带默认值):ask VAR "提示" "默认值"
  46. ask() {
  47. local __var="$1" __prompt="$2" __default="${3:-}" __ans
  48. if [[ -n "${__default}" ]]; then
  49. read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset} [${__default}]: ")" __ans
  50. __ans="${__ans:-${__default}}"
  51. else
  52. read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset}: ")" __ans
  53. fi
  54. printf -v "${__var}" '%s' "${__ans}"
  55. }
  56. # 必填输入(非空,否则循环重试):ask_required VAR "提示"
  57. ask_required() {
  58. local __var="$1" __prompt="$2" __ans
  59. while :; do
  60. read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset}: ")" __ans
  61. [[ -n "${__ans}" ]] && break
  62. warn "不能为空,请重新输入。"
  63. done
  64. printf -v "${__var}" '%s' "${__ans}"
  65. }
  66. # 序号选择(限定合法值,否则循环重试):ask_choice VAR "提示" "默认" "1 2 3"
  67. ask_choice() {
  68. local __var="$1" __prompt="$2" __default="$3" __valid="$4" __ans __v
  69. while :; do
  70. read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset} [${__default}]: ")" __ans
  71. __ans="${__ans:-${__default}}"
  72. for __v in ${__valid}; do
  73. [[ "${__ans}" == "${__v}" ]] && { printf -v "${__var}" '%s' "${__ans}"; return 0; }
  74. done
  75. warn "无效选择:${__ans},请输入 [${__valid// /\/}] 之一。"
  76. done
  77. }
  78. # ---------------------------------------------------------------------------
  79. # 模块小结 + 回改驱动器
  80. # 用法:edit_module "模块标题" "VAR|提示|类型|默认" ...
  81. # 类型:plain(普通,带默认) | required(必填非空) | optional(可空)
  82. # secret(密码,带默认,回车用默认) | secret_required(必填密码)
  83. # 交互:先逐项询问 → 显示小结 → 输编号改单项 / r 全部重来 / 回车确认
  84. # ---------------------------------------------------------------------------
  85. # 单字段询问:按类型调用相应输入函数
  86. _ask_field() {
  87. local var="$1" prompt="$2" type="$3" default="$4"
  88. case "${type}" in
  89. plain) ask "${var}" "${prompt}" "${default}" ;;
  90. required) ask_required "${var}" "${prompt}" ;;
  91. optional) ask "${var}" "${prompt}" "${default}" ;;
  92. secret) ask_secret "${var}" "${prompt}" "${default}" ;;
  93. secret_required) ask_secret_required "${var}" "${prompt}" ;;
  94. *) ask "${var}" "${prompt}" "${default}" ;;
  95. esac
  96. }
  97. # 小结里字段值的展示(密码做掩码;secret 用默认时显默认明文)
  98. _display_field() {
  99. local type="$3" default="$4" val="${!1}"
  100. case "${type}" in
  101. secret)
  102. if [[ -n "${val}" && "${val}" == "${default}" ]]; then
  103. printf '%s(默认)' "${val}" # 用默认 → 明文显示
  104. elif [[ -n "${val}" ]]; then
  105. printf '******' # 手动输入 → 掩码
  106. else
  107. printf '(空)'
  108. fi
  109. ;;
  110. secret_required)
  111. [[ -n "${val}" ]] && printf '******' || printf '(未设置)'
  112. ;;
  113. *)
  114. [[ -n "${val}" ]] && printf '%s' "${val}" || printf '(空)'
  115. ;;
  116. esac
  117. }
  118. edit_module() {
  119. local mtitle="$1"; shift
  120. local specs=("$@")
  121. local n="${#specs[@]}"
  122. local i spec var prompt type default sel
  123. # 首轮逐项询问
  124. title "${mtitle}"
  125. for spec in "${specs[@]}"; do
  126. IFS='|' read -r var prompt type default <<<"${spec}"
  127. _ask_field "${var}" "${prompt}" "${type}" "${default}"
  128. done
  129. # 小结 + 回改循环
  130. while :; do
  131. echo
  132. echo -e "${c_bold}${mtitle} — 配置小结:${c_reset}"
  133. for ((i=0; i<n; i++)); do
  134. IFS='|' read -r var prompt type default <<<"${specs[i]}"
  135. printf " %d) %-28s = %s\n" "$((i+1))" "${prompt}" "$(_display_field "${var}" "${prompt}" "${type}" "${default}")"
  136. done
  137. read -r -p "$(echo -e "操作:输编号改单项 | ${c_bold}r${c_reset} 全部重来 | ${c_bold}回车${c_reset} 确认: ")" sel
  138. if [[ -z "${sel}" ]]; then
  139. break
  140. elif [[ "${sel}" == "r" || "${sel}" == "R" ]]; then
  141. for spec in "${specs[@]}"; do
  142. IFS='|' read -r var prompt type default <<<"${spec}"
  143. _ask_field "${var}" "${prompt}" "${type}" "${default}"
  144. done
  145. elif [[ "${sel}" =~ ^[0-9]+$ && "${sel}" -ge 1 && "${sel}" -le "${n}" ]]; then
  146. IFS='|' read -r var prompt type default <<<"${specs[$((sel-1))]}"
  147. _ask_field "${var}" "${prompt}" "${type}" "${default}"
  148. else
  149. warn "无效操作:${sel}"
  150. fi
  151. done
  152. }
  153. # 底层:逐字符读取密码,用 * 回显长度,支持退格,回车结束。
  154. # 用法:_read_secret_stars VAR "提示文案"
  155. _read_secret_stars() {
  156. local __var="$1" __prompt="$2" __ch __acc=""
  157. # 提示(不换行)
  158. printf '%b' "${c_bold}${__prompt}${c_reset}: " > /dev/tty
  159. # 逐字符读取(从 /dev/tty,避免受重定向影响)
  160. while IFS= read -rsn1 __ch < /dev/tty; do
  161. # 回车/换行结束
  162. [[ -z "${__ch}" ]] && break
  163. # 退格(DEL 0x7f 或 BS 0x08)
  164. if [[ "${__ch}" == $'\x7f' || "${__ch}" == $'\x08' ]]; then
  165. if [[ -n "${__acc}" ]]; then
  166. __acc="${__acc%?}"
  167. printf '\b \b' > /dev/tty
  168. fi
  169. continue
  170. fi
  171. __acc+="${__ch}"
  172. printf '*' > /dev/tty
  173. done
  174. printf '\n' > /dev/tty
  175. printf -v "${__var}" '%s' "${__acc}"
  176. }
  177. # 提示输入密码(* 回显 + 两次验证):ask_secret VAR "提示" "默认值"
  178. # 直接回车(空输入)则采用默认值(提示中明文显示),跳过二次确认;
  179. # 输入了内容则要求两次一致。
  180. ask_secret() {
  181. local __var="$1" __prompt="$2" __default="${3:-}" __a __b
  182. while :; do
  183. _read_secret_stars __a "${__prompt}${__default:+ [回车用默认: ${__default}]}"
  184. if [[ -z "${__a}" ]]; then
  185. __a="${__default}"
  186. break
  187. fi
  188. _read_secret_stars __b "${__prompt}(再输一次确认)"
  189. [[ "${__a}" == "${__b}" ]] && break
  190. warn "两次输入不一致,请重新输入。"
  191. done
  192. printf -v "${__var}" '%s' "${__a}"
  193. }
  194. # 强制输入密码(* 回显、无默认、必须非空且两次一致):ask_secret_required VAR "提示"
  195. ask_secret_required() {
  196. local __var="$1" __prompt="$2" __a __b
  197. while :; do
  198. _read_secret_stars __a "${__prompt}"
  199. if [[ -z "${__a}" ]]; then
  200. warn "不能为空,请重新输入。"; continue
  201. fi
  202. _read_secret_stars __b "${__prompt}(再输一次确认)"
  203. if [[ "${__a}" != "${__b}" ]]; then
  204. warn "两次输入不一致,请重新输入。"; continue
  205. fi
  206. break
  207. done
  208. printf -v "${__var}" '%s' "${__a}"
  209. }
  210. # 是/否确认:ask_yn "提示" "y|n(默认)" -> 返回 0=yes 1=no
  211. ask_yn() {
  212. local __prompt="$1" __default="${2:-y}" __ans
  213. local __hint="[Y/n]"; [[ "${__default}" == "n" ]] && __hint="[y/N]"
  214. read -r -p "$(echo -e "${c_bold}${__prompt}${c_reset} ${__hint}: ")" __ans
  215. __ans="${__ans:-${__default}}"
  216. [[ "${__ans}" =~ ^[Yy]$ ]]
  217. }
  218. require_cmd() {
  219. command -v "$1" >/dev/null 2>&1 || { err "缺少命令:$1($2)"; return 1; }
  220. }
  221. # ---------------------------------------------------------------------------
  222. # 全局变量(向导填写)
  223. # ---------------------------------------------------------------------------
  224. MODE="" # local | remote
  225. 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"
  226. # 远程连接
  227. R_HOST=""; R_PORT="22"; R_USER=""; R_SSH_KEY=""
  228. # 部署目录
  229. DEPLOY_ROOT="/var/www/fwh-toolbox"
  230. RUN_USER=""
  231. # 数据库/配置项
  232. # TODO: 以下均为“默认值”,向导会逐项提示你确认或覆盖。以下几项默认口令不安全,
  233. # 运行向导时务必改为强口令:PG_PASS、MINIO_ROOT_PASSWORD、CASDOOR_PG_PASSWORD;
  234. # 并按需填写 MINIO_ACCESS_KEY/MINIO_SECRET_KEY、MYSQL_DSN、CASDOOR_ORIGIN。
  235. APP_ENV="dev"; APP_PORT="8301"; ASYNQMON_PORT="8302"
  236. PG_HOST="localhost"; PG_PORT="5432"; PG_DB="fwh_toolbox_db"; PG_USER="fwh_toolbox"; PG_PASS="fwh_toolbox" # TODO: [必填] PG_PASS 改强口令
  237. MONGO_DSN="mongodb://127.0.0.1:27017/FwhToolboxDb"
  238. REDIS_ADDR="localhost:6379"
  239. MYSQL_DSN="" # TODO: [必填/可选] 远端只读 MySQL DSN,用到站点查询才需填
  240. MINIO_ENDPOINT="localhost:9198"; MINIO_ACCESS_KEY="fwh-toolbox"; MINIO_SECRET_KEY="fwh-toolbox"; MINIO_BUCKET="fwh-toolbox" # TODO: [确认] MinIO 业务 AK/SK(默认 fwh-toolbox)
  241. 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 创建)
  242. MINIO_ROOT_USER="jerry"; MINIO_ROOT_PASSWORD="" # TODO: [必填] MinIO 管理员密码向导内强制输入
  243. # pgAdmin4(数据库 Web 管理,可选)
  244. PGADMIN_PORT="5480"; PGADMIN_BIND="127.0.0.1"; PGADMIN_EMAIL="liang_jlyx@163.com"
  245. PGADMIN_PASSWORD="" # TODO: [必填] 向导内强制输入
  246. POSTGRES_ADMIN_PASSWORD="" # TODO: [必填] 向导内强制输入
  247. # Casdoor
  248. CASDOOR_DIR=""; CASDOOR_PORT="8010"
  249. CASDOOR_PG_USER="casdoor"; CASDOOR_PG_PASSWORD="casdoor"; CASDOOR_DB_NAME="casdoor" # TODO: [必填] CASDOOR_PG_PASSWORD 改强口令
  250. CASDOOR_ORIGIN="" # TODO: [必填] Casdoor 对外访问地址,如 https://<域名>/casdoor
  251. # Caddy 反向代理
  252. CADDY_DOMAIN="" # TODO: [必填] 对外域名,如 lmt.zenpower.cc
  253. LIBDNS_SPACESHIP_APIKEY=""; LIBDNS_SPACESHIP_APISECRET="" # TODO: [必填] spaceship DNS API 凭据(向导内输入,写入 systemd 服务)
  254. CADDY_FRONTEND_PORT="8300"; CADDY_BACKEND_PORT="8301"
  255. CADDY_INCLUDE_CASDOOR="n"; CADDY_INCLUDE_MINIO="n"
  256. CADDY_SERVE_STATIC="n" # 由 Caddy 直接 file_server 托管前端
  257. # Kiro CLI(后端 AI 对话依赖)
  258. KIRO_INSTALL_METHOD="zip" # zip | curl
  259. KIRO_LIBC="gnu" # gnu | musl
  260. KIRO_DO_LOGIN="n" # 安装后是否立即交互式登录
  261. # 生成的配置文件(本地临时路径)
  262. GEN_CONFIG=""
  263. GEN_MCP_CONFIG=""
  264. # 单项模式(--only):非空表示仅执行指定步骤,跳过步骤勾选交互
  265. ONLY_MODE=""
  266. ONLY_LABEL=""
  267. # ---------------------------------------------------------------------------
  268. # 步骤 0:选择模式与步骤
  269. # ---------------------------------------------------------------------------
  270. wizard_intro() {
  271. title "FranklinWH Toolbox 交互式部署向导"
  272. echo "本向导将分步引导你完成部署。任何提示都可直接回车使用默认值。"
  273. echo
  274. echo "请选择部署目标:"
  275. echo " 1) 本地部署 (local)"
  276. echo " 2) 远程部署 (remote, 通过 SSH)"
  277. local sel; ask_choice sel "输入序号" "1" "1 2"
  278. case "${sel}" in
  279. 1) MODE="local" ;;
  280. 2) MODE="remote" ;;
  281. esac
  282. ok "部署模式:${MODE}"
  283. # --only 模式:步骤已由命令行参数设定,跳过交互勾选
  284. if [[ -n "${ONLY_MODE}" ]]; then
  285. echo "(单项模式:仅执行 ${ONLY_LABEL})"
  286. return 0
  287. fi
  288. title "选择要执行的步骤"
  289. ask_yn "① 检测/安装数据库引擎与依赖(PostgreSQL/Mongo/Redis/MinIO/Docker)?" "y" && DO_DEPS="y" || DO_DEPS="n"
  290. ask_yn "② 安装 Casdoor(Docker,认证服务)?" "n" && DO_CASDOOR="y" || DO_CASDOOR="n"
  291. ask_yn "③ 初始化数据库(建库/建表/建桶)?" "y" && DO_INITDB="y" || DO_INITDB="n"
  292. ask_yn "④ 部署后端(构建二进制 + systemd 服务)?" "y" && DO_BACKEND="y" || DO_BACKEND="n"
  293. ask_yn "⑤ 部署前端(构建 dist + 同步静态目录)?" "y" && DO_FRONTEND="y" || DO_FRONTEND="n"
  294. if [[ "${DO_BACKEND}" == "y" || "${DO_INITDB}" == "y" ]]; then
  295. ask_yn "⑥ 生成 config.env(根据后续填写内容)?" "y" && DO_GENCONF="y" || DO_GENCONF="n"
  296. else
  297. DO_GENCONF="n"
  298. fi
  299. ask_yn "⑦ 安装并配置 Caddy 反向代理(生成站点 Caddyfile 并 reload)?" "n" && DO_CADDY="y" || DO_CADDY="n"
  300. ask_yn "⑧ 安装 Kiro CLI(后端 AI 对话依赖,需以运行用户登录 Builder ID)?" "n" && DO_KIRO="y" || DO_KIRO="n"
  301. ask_yn "⑨ 部署 pgAdmin4(Docker,数据库 Web 管理,并给 postgres 设密码)?" "n" && DO_PGADMIN="y" || DO_PGADMIN="n"
  302. ask_yn "⑩ 创建 MinIO 业务账号与策略(fwh-toolbox / fwh-toolbox-ai)?" "y" && DO_MINIOACCT="y" || DO_MINIOACCT="n"
  303. }
  304. # ---------------------------------------------------------------------------
  305. # 步骤 1:远程连接信息
  306. # ---------------------------------------------------------------------------
  307. wizard_remote() {
  308. [[ "${MODE}" == "remote" ]] || return 0
  309. title "远程主机连接信息"
  310. while :; do
  311. ask_required R_HOST "远程主机地址 (IP 或域名)"
  312. ask R_PORT "SSH 端口" "22"
  313. ask R_USER "SSH 登录用户" "${USER}"
  314. ask R_SSH_KEY "SSH 私钥路径(留空则用默认/密码)" ""
  315. info "测试 SSH 连接 ..."
  316. if ssh_run "echo connected" >/dev/null 2>&1; then
  317. ok "SSH 连接成功"
  318. break
  319. fi
  320. err "SSH 连接失败,请检查地址/端口/用户/密钥"
  321. ask_yn "重新填写连接信息并重试?" "y" || { warn "已取消"; exit 0; }
  322. done
  323. }
  324. # 组装 ssh/scp 通用参数
  325. ssh_opts() {
  326. local opts=(-p "${R_PORT}" -o StrictHostKeyChecking=accept-new)
  327. [[ -n "${R_SSH_KEY}" ]] && opts+=(-i "${R_SSH_KEY}")
  328. printf '%s\n' "${opts[@]}"
  329. }
  330. scp_opts() {
  331. local opts=(-P "${R_PORT}" -o StrictHostKeyChecking=accept-new)
  332. [[ -n "${R_SSH_KEY}" ]] && opts+=(-i "${R_SSH_KEY}")
  333. printf '%s\n' "${opts[@]}"
  334. }
  335. # 在远程执行命令
  336. ssh_run() {
  337. mapfile -t _o < <(ssh_opts)
  338. ssh "${_o[@]}" "${R_USER}@${R_HOST}" "$@"
  339. }
  340. # 拷贝文件/目录到远程
  341. scp_to() {
  342. mapfile -t _o < <(scp_opts)
  343. scp "${_o[@]}" -r "$1" "${R_USER}@${R_HOST}:$2"
  344. }
  345. # 统一执行命令封装:本地直接跑,远程走 ssh
  346. run() {
  347. if [[ "${MODE}" == "remote" ]]; then
  348. ssh_run "$@"
  349. else
  350. bash -c "$@"
  351. fi
  352. }
  353. # ---------------------------------------------------------------------------
  354. # 步骤 2:部署参数与配置项
  355. # ---------------------------------------------------------------------------
  356. wizard_params() {
  357. edit_module "部署目录与运行用户" \
  358. "DEPLOY_ROOT|部署根目录|plain|/var/www/fwh-toolbox" \
  359. "RUN_USER|后端 systemd 运行用户|plain|${R_USER:-${USER}}"
  360. if [[ "${DO_GENCONF}" == "y" ]]; then
  361. edit_module "应用配置" \
  362. "APP_ENV|运行环境 (pro/dev)|plain|dev" \
  363. "APP_PORT|后端监听端口|plain|8301" \
  364. "ASYNQMON_PORT|Asynqmon 端口|plain|8302"
  365. edit_module "PostgreSQL(本地)" \
  366. "PG_HOST|PG 主机|plain|localhost" \
  367. "PG_PORT|PG 端口|plain|5432" \
  368. "PG_DB|PG 数据库名|plain|fwh_toolbox_db" \
  369. "PG_USER|PG 业务角色|plain|fwh_toolbox" \
  370. "PG_PASS|PG 业务角色密码|secret|fwh_toolbox"
  371. edit_module "MongoDB / Redis" \
  372. "MONGO_DSN|Mongo 连接串|plain|mongodb://127.0.0.1:27017/FwhToolboxDb" \
  373. "REDIS_ADDR|Redis 地址|plain|localhost:6379"
  374. edit_module "远端只读 MySQL(可留空跳过)" \
  375. "MYSQL_DSN|MySQL DSN|optional|"
  376. edit_module "MinIO(本地对象存储)" \
  377. "MINIO_ENDPOINT|MinIO S3 API 地址|plain|localhost:9198" \
  378. "MINIO_ACCESS_KEY|MinIO Access Key|plain|fwh-toolbox" \
  379. "MINIO_SECRET_KEY|MinIO Secret Key|secret|fwh-toolbox" \
  380. "MINIO_BUCKET|MinIO Bucket 名|plain|fwh-toolbox"
  381. fi
  382. if [[ "${DO_INITDB}" == "y" || "${DO_DEPS}" == "y" || "${DO_MINIOACCT}" == "y" ]]; then
  383. edit_module "MinIO 管理员账户(安装/建桶/建账号用)" \
  384. "MINIO_ROOT_USER|MinIO 管理员用户|plain|jerry" \
  385. "MINIO_ROOT_PASSWORD|MinIO 管理员密码|secret_required|"
  386. fi
  387. if [[ "${DO_CASDOOR}" == "y" ]]; then
  388. edit_module "Casdoor(Docker 认证服务)" \
  389. "CASDOOR_DIR|Casdoor 部署目录|plain|/home/${RUN_USER}/docker/casdoor" \
  390. "CASDOOR_PORT|Casdoor 对外端口 (->容器8000)|plain|8010" \
  391. "CASDOOR_PG_USER|Casdoor 使用的 PG 角色|plain|casdoor" \
  392. "CASDOOR_PG_PASSWORD|Casdoor PG 角色密码|secret|casdoor" \
  393. "CASDOOR_DB_NAME|Casdoor 数据库名|plain|casdoor"
  394. # origin 默认值需引用上面刚填的 CASDOOR_PORT,故在 edit_module 之后单独询问
  395. ask CASDOOR_ORIGIN "Casdoor 对外访问地址(origin)" "http://localhost:${CASDOOR_PORT}"
  396. fi
  397. if [[ "${DO_PGADMIN}" == "y" ]]; then
  398. edit_module "pgAdmin4(数据库 Web 管理)" \
  399. "PGADMIN_PORT|pgAdmin4 对外端口|plain|5480" \
  400. "PGADMIN_BIND|pgAdmin4 绑定地址(默认仅本机,防公网暴露)|plain|127.0.0.1" \
  401. "PGADMIN_EMAIL|pgAdmin4 登录邮箱|plain|liang_jlyx@163.com" \
  402. "PGADMIN_PASSWORD|pgAdmin4 登录密码|secret_required|" \
  403. "POSTGRES_ADMIN_PASSWORD|postgres 超级用户新密码(供 pgAdmin 连接)|secret_required|"
  404. fi
  405. if [[ "${DO_MINIOACCT}" == "y" ]]; then
  406. edit_module "MinIO 业务账号(后端 fwh-toolbox / MCP fwh-toolbox-ai)" \
  407. "MINIO_ACCESS_KEY|后端业务 Access Key|plain|fwh-toolbox" \
  408. "MINIO_SECRET_KEY|后端业务 Secret Key|secret|fwh-toolbox" \
  409. "MINIO_AI_ACCESS_KEY|AI Access Key(MCP 用)|plain|fwh-toolbox-ai" \
  410. "MINIO_AI_SECRET_KEY|AI Secret Key(MCP 用)|secret|fwh-toolbox-ai" \
  411. "MINIO_AI_BUCKET|AI 文件 bucket 名|plain|fwh-toolbox-ai-file"
  412. fi
  413. if [[ "${DO_CADDY}" == "y" ]]; then
  414. echo "TLS 走 spaceship DNS-01(自编译 caddy 内置该插件),需填 spaceship API 凭据。"
  415. edit_module "Caddy 反向代理" \
  416. "CADDY_DOMAIN|对外域名 (如 lmt.zenpower.cc)|required|" \
  417. "LIBDNS_SPACESHIP_APIKEY|spaceship API Key|secret_required|" \
  418. "LIBDNS_SPACESHIP_APISECRET|spaceship API Secret|secret_required|" \
  419. "CADDY_FRONTEND_PORT|前端静态端口|plain|8300" \
  420. "CADDY_BACKEND_PORT|后端 API 端口|plain|${APP_PORT}"
  421. # 以下为 y/n 分支选项(即时询问,不纳入小结回改)
  422. ask_yn "由 Caddy 直接 file_server 托管前端静态(否则反代到前端端口)?" "n" \
  423. && CADDY_SERVE_STATIC="y" || CADDY_SERVE_STATIC="n"
  424. ask_yn "加入 /casdoor 反代?" "$( [[ ${DO_CASDOOR} == y ]] && echo y || echo n )" \
  425. && CADDY_INCLUDE_CASDOOR="y" || CADDY_INCLUDE_CASDOOR="n"
  426. ask_yn "加入 /fwh-minio (MinIO 控制台) 反代?" "n" \
  427. && CADDY_INCLUDE_MINIO="y" || CADDY_INCLUDE_MINIO="n"
  428. fi
  429. if [[ "${DO_KIRO}" == "y" ]]; then
  430. title "Kiro CLI(后端 AI 对话依赖)"
  431. echo "将以运行用户 ${RUN_USER} 身份安装(其 HOME 决定 ~/.kiro 配置位置)。"
  432. echo "安装方式:1) zip 官方安装包(默认) 2) curl 一键脚本"
  433. local _km; ask_choice _km "输入序号" "1" "1 2"
  434. case "${_km}" in
  435. 1) KIRO_INSTALL_METHOD="zip"
  436. echo "C 库变体:1) gnu(默认,主流发行版) 2) musl(Alpine 等)"
  437. local _kl; ask_choice _kl "输入序号" "1" "1 2"
  438. [[ "${_kl}" == "2" ]] && KIRO_LIBC="musl" || KIRO_LIBC="gnu" ;;
  439. 2) KIRO_INSTALL_METHOD="curl" ;;
  440. esac
  441. ask_yn "安装后立即以 ${RUN_USER} 身份登录 Builder ID(交互式,需浏览器)?" "n" \
  442. && KIRO_DO_LOGIN="y" || KIRO_DO_LOGIN="n"
  443. fi
  444. }
  445. # ---------------------------------------------------------------------------
  446. # 生成 config.env(本地临时文件)
  447. # ---------------------------------------------------------------------------
  448. generate_config() {
  449. [[ "${DO_GENCONF}" == "y" ]] || return 0
  450. title "生成 config.env"
  451. GEN_CONFIG="$(mktemp /tmp/fwh_config.env.XXXXXX)"
  452. # 以模板为基础,替换关键项;未覆盖的项保留模板默认(含 FWH/Plane/MQTT 等)
  453. cp "${CONFIG_EXAMPLE}" "${GEN_CONFIG}"
  454. local pg_dsn="host=${PG_HOST} user=${PG_USER} password=${PG_PASS} dbname=${PG_DB} port=${PG_PORT} sslmode=disable"
  455. # 使用 python 做安全替换(避免特殊字符干扰 sed)
  456. if command -v python3 >/dev/null 2>&1; then
  457. APP_ENV="${APP_ENV}" APP_PORT="${APP_PORT}" ASYNQMON_PORT="${ASYNQMON_PORT}" \
  458. PG_DSN="${pg_dsn}" MONGO_DSN="${MONGO_DSN}" REDIS_ADDR="${REDIS_ADDR}" MYSQL_DSN="${MYSQL_DSN}" \
  459. MINIO_ENDPOINT="${MINIO_ENDPOINT}" MINIO_ACCESS_KEY="${MINIO_ACCESS_KEY}" \
  460. MINIO_SECRET_KEY="${MINIO_SECRET_KEY}" MINIO_BUCKET="${MINIO_BUCKET}" \
  461. RUN_USER="${RUN_USER}" \
  462. python3 - "${GEN_CONFIG}" <<'PY'
  463. import os, re, sys
  464. path = sys.argv[1]
  465. repl = {
  466. "APP_ENV": os.environ["APP_ENV"],
  467. "APP_PORT": os.environ["APP_PORT"],
  468. "ASYNQMON_PORT": os.environ["ASYNQMON_PORT"],
  469. "POSTGRES_DSN": os.environ["PG_DSN"],
  470. "MONGO_DSN": os.environ["MONGO_DSN"],
  471. "REDIS_ADDR": os.environ["REDIS_ADDR"],
  472. "MINIO_ENDPOINT": os.environ["MINIO_ENDPOINT"],
  473. "MINIO_ACCESS_KEY": os.environ["MINIO_ACCESS_KEY"],
  474. "MINIO_SECRET_KEY": os.environ["MINIO_SECRET_KEY"],
  475. "MINIO_BUCKET_NAME": os.environ["MINIO_BUCKET"],
  476. "KIRO_RUN_AS_USER": os.environ["RUN_USER"],
  477. }
  478. mysql = os.environ.get("MYSQL_DSN", "")
  479. if mysql:
  480. repl["MYSQL_DSN"] = mysql
  481. lines = open(path, encoding="utf-8").read().splitlines()
  482. out = []
  483. for line in lines:
  484. m = re.match(r'^(\s*)([A-Z0-9_]+)\s*=', line)
  485. if m and m.group(2) in repl:
  486. key = m.group(2)
  487. val = repl[key]
  488. # KIRO_RUN_AS_USER 模板里是无引号写法,其余用双引号包裹
  489. if key == "KIRO_RUN_AS_USER":
  490. out.append(f'{key}={val}')
  491. else:
  492. out.append(f'{key}="{val}"')
  493. else:
  494. out.append(line)
  495. open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
  496. print("config.env 已生成")
  497. PY
  498. else
  499. warn "未找到 python3,改用 sed 简易替换(含特殊字符时请手工核对)"
  500. _sed_kv() { sed -i -E "s#^(${1})=.*#${1}=\"${2//#/\\#}\"#" "${GEN_CONFIG}"; }
  501. _sed_kv APP_ENV "${APP_ENV}"
  502. _sed_kv APP_PORT "${APP_PORT}"
  503. _sed_kv ASYNQMON_PORT "${ASYNQMON_PORT}"
  504. _sed_kv POSTGRES_DSN "${pg_dsn}"
  505. _sed_kv MONGO_DSN "${MONGO_DSN}"
  506. _sed_kv REDIS_ADDR "${REDIS_ADDR}"
  507. [[ -n "${MYSQL_DSN}" ]] && _sed_kv MYSQL_DSN "${MYSQL_DSN}"
  508. _sed_kv MINIO_ENDPOINT "${MINIO_ENDPOINT}"
  509. _sed_kv MINIO_ACCESS_KEY "${MINIO_ACCESS_KEY}"
  510. _sed_kv MINIO_SECRET_KEY "${MINIO_SECRET_KEY}"
  511. _sed_kv MINIO_BUCKET_NAME "${MINIO_BUCKET}"
  512. sed -i -E "s#^KIRO_RUN_AS_USER=.*#KIRO_RUN_AS_USER=${RUN_USER}#" "${GEN_CONFIG}"
  513. fi
  514. ok "已生成配置:${GEN_CONFIG}"
  515. # -----------------------------------------------------------------------
  516. # 单独生成 config.mcp.env(基于 config.mcp.env.example)
  517. # 与后端共享库/缓存/MinIO endpoint/MySQL 等,但 MinIO 使用 AI 专用账号
  518. # fwh-toolbox-ai(对 fwh-toolbox-ai* 桶可读写、其余桶只读;MCP 工具仅只读)。
  519. # -----------------------------------------------------------------------
  520. if [[ -f "${MCP_CONFIG_EXAMPLE}" ]]; then
  521. GEN_MCP_CONFIG="$(mktemp /tmp/fwh_config.mcp.env.XXXXXX)"
  522. cp "${MCP_CONFIG_EXAMPLE}" "${GEN_MCP_CONFIG}"
  523. # MCP 侧 MinIO 使用 AI 账号;默认账号=密码=fwh-toolbox-ai(可用环境变量覆盖)
  524. local mcp_ak="${MINIO_AI_ACCESS_KEY:-fwh-toolbox-ai}"
  525. local mcp_sk="${MINIO_AI_SECRET_KEY:-fwh-toolbox-ai}"
  526. if command -v python3 >/dev/null 2>&1; then
  527. APP_ENV="${APP_ENV}" PG_DSN="${pg_dsn}" MONGO_DSN="${MONGO_DSN}" \
  528. REDIS_ADDR="${REDIS_ADDR}" MYSQL_DSN="${MYSQL_DSN}" \
  529. MINIO_ENDPOINT="${MINIO_ENDPOINT}" MINIO_ACCESS_KEY="${mcp_ak}" \
  530. MINIO_SECRET_KEY="${mcp_sk}" MINIO_BUCKET="${MINIO_BUCKET}" \
  531. python3 - "${GEN_MCP_CONFIG}" <<'PY'
  532. import os, re, sys
  533. path = sys.argv[1]
  534. repl = {
  535. "APP_ENV": os.environ["APP_ENV"],
  536. "POSTGRES_DSN": os.environ["PG_DSN"],
  537. "MONGO_DSN": os.environ["MONGO_DSN"],
  538. "REDIS_ADDR": os.environ["REDIS_ADDR"],
  539. "MINIO_ENDPOINT": os.environ["MINIO_ENDPOINT"],
  540. "MINIO_ACCESS_KEY": os.environ["MINIO_ACCESS_KEY"],
  541. "MINIO_SECRET_KEY": os.environ["MINIO_SECRET_KEY"],
  542. "MINIO_BUCKET_NAME": os.environ["MINIO_BUCKET"],
  543. }
  544. mysql = os.environ.get("MYSQL_DSN", "")
  545. if mysql:
  546. repl["MYSQL_DSN"] = mysql
  547. lines = open(path, encoding="utf-8").read().splitlines()
  548. out = []
  549. for line in lines:
  550. m = re.match(r'^(\s*)([A-Z0-9_]+)\s*=', line)
  551. if m and m.group(2) in repl:
  552. out.append(f'{m.group(2)}="{repl[m.group(2)]}"')
  553. else:
  554. out.append(line)
  555. open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
  556. print("config.mcp.env 已生成")
  557. PY
  558. else
  559. warn "未找到 python3,改用 sed 简易替换(含特殊字符时请手工核对)"
  560. _sed_mcp() { sed -i -E "s#^(${1})=.*#${1}=\"${2//#/\\#}\"#" "${GEN_MCP_CONFIG}"; }
  561. _sed_mcp APP_ENV "${APP_ENV}"
  562. _sed_mcp POSTGRES_DSN "${pg_dsn}"
  563. _sed_mcp MONGO_DSN "${MONGO_DSN}"
  564. _sed_mcp REDIS_ADDR "${REDIS_ADDR}"
  565. [[ -n "${MYSQL_DSN}" ]] && _sed_mcp MYSQL_DSN "${MYSQL_DSN}"
  566. _sed_mcp MINIO_ENDPOINT "${MINIO_ENDPOINT}"
  567. _sed_mcp MINIO_ACCESS_KEY "${mcp_ak}"
  568. _sed_mcp MINIO_SECRET_KEY "${mcp_sk}"
  569. _sed_mcp MINIO_BUCKET_NAME "${MINIO_BUCKET}"
  570. fi
  571. ok "已生成配置:${GEN_MCP_CONFIG}"
  572. else
  573. warn "未找到 ${MCP_CONFIG_EXAMPLE},跳过 config.mcp.env 生成"
  574. fi
  575. }
  576. # ---------------------------------------------------------------------------
  577. # 构建产物(前端 dist、后端二进制)——始终在本机构建
  578. # ---------------------------------------------------------------------------
  579. build_frontend() {
  580. [[ "${DO_FRONTEND}" == "y" ]] || return 0
  581. title "构建前端"
  582. require_cmd pnpm "安装 Node 22 + pnpm,见 DEPLOYMENT.md 3.3"
  583. ( cd "${PROJECT_ROOT}" && pnpm install --frozen-lockfile && pnpm build )
  584. ok "前端构建完成:${PROJECT_ROOT}/dist"
  585. }
  586. build_backend() {
  587. [[ "${DO_BACKEND}" == "y" ]] || return 0
  588. title "构建后端"
  589. require_cmd go "安装 Go 1.26,见 DEPLOYMENT.md 3.2"
  590. ( cd "${SERVICE_DIR}" \
  591. && GOOS=linux GOARCH=amd64 go build -o "${APP_NAME}" ./ \
  592. && GOOS=linux GOARCH=amd64 go build -o "${MCP_NAME}" ./cmd/mcp/ )
  593. ok "后端构建完成"
  594. }
  595. # ---------------------------------------------------------------------------
  596. # 打包上传(远程模式)
  597. # ---------------------------------------------------------------------------
  598. REMOTE_STAGE="/tmp/fwh-deploy-stage"
  599. prepare_remote_stage() {
  600. [[ "${MODE}" == "remote" ]] || return 0
  601. title "上传部署产物到远程"
  602. ssh_run "rm -rf ${REMOTE_STAGE} && mkdir -p ${REMOTE_STAGE}/sql ${REMOTE_STAGE}/cert ${REMOTE_STAGE}/scripts ${REMOTE_STAGE}/bin"
  603. # 辅助脚本(依赖安装/ Casdoor)
  604. scp_to "${SCRIPT_DIR}/install_deps.sh" "${REMOTE_STAGE}/scripts/"
  605. scp_to "${SCRIPT_DIR}/install_casdoor.sh" "${REMOTE_STAGE}/scripts/"
  606. scp_to "${SCRIPT_DIR}/install_caddy.sh" "${REMOTE_STAGE}/scripts/"
  607. # 自编译 caddy 二进制(仅在需要装 Caddy 时上传,约 45MB)
  608. if [[ "${DO_CADDY}" == "y" && -f "${SCRIPT_DIR}/../bin/caddy" ]]; then
  609. scp_to "${SCRIPT_DIR}/../bin/caddy" "${REMOTE_STAGE}/bin/"
  610. fi
  611. scp_to "${SCRIPT_DIR}/install_pgadmin.sh" "${REMOTE_STAGE}/scripts/"
  612. scp_to "${SCRIPT_DIR}/provision_minio.sh" "${REMOTE_STAGE}/scripts/"
  613. scp_to "${SCRIPT_DIR}/install_kiro_cli.sh" "${REMOTE_STAGE}/scripts/"
  614. ssh_run "chmod +x ${REMOTE_STAGE}/scripts/*.sh"
  615. # SQL 与二进制、前端、配置、证书
  616. scp_to "${SQL_FILE}" "${REMOTE_STAGE}/sql/"
  617. if [[ "${DO_BACKEND}" == "y" ]]; then
  618. scp_to "${SERVICE_DIR}/${APP_NAME}" "${REMOTE_STAGE}/"
  619. scp_to "${SERVICE_DIR}/${MCP_NAME}" "${REMOTE_STAGE}/"
  620. # 证书目录(若存在)
  621. [[ -d "${SERVICE_DIR}/cert" ]] && scp_to "${SERVICE_DIR}/cert" "${REMOTE_STAGE}/"
  622. fi
  623. if [[ "${DO_FRONTEND}" == "y" ]]; then
  624. ( cd "${PROJECT_ROOT}" && tar czf /tmp/fwh-dist.tgz -C dist . )
  625. scp_to "/tmp/fwh-dist.tgz" "${REMOTE_STAGE}/"
  626. rm -f /tmp/fwh-dist.tgz
  627. fi
  628. [[ -n "${GEN_CONFIG}" ]] && scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.env"
  629. if [[ -n "${GEN_MCP_CONFIG}" ]]; then
  630. scp_to "${GEN_MCP_CONFIG}" "${REMOTE_STAGE}/config.mcp.env"
  631. elif [[ -n "${GEN_CONFIG}" ]]; then
  632. # 回退:未单独生成 MCP 配置时沿用后端配置
  633. scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.mcp.env"
  634. fi
  635. ok "产物已上传到远程 ${REMOTE_STAGE}"
  636. }
  637. # ---------------------------------------------------------------------------
  638. # 检测/安装依赖(数据库引擎等)
  639. # ---------------------------------------------------------------------------
  640. # 返回辅助脚本在“执行侧”的路径(本地=仓库,远程=暂存目录)
  641. _scripts_base() {
  642. if [[ "${MODE}" == "remote" ]]; then echo "${REMOTE_STAGE}/scripts"; else echo "${SCRIPT_DIR}"; fi
  643. }
  644. step_deps() {
  645. [[ "${DO_DEPS}" == "y" ]] || return 0
  646. title "检测/安装依赖"
  647. local base; base="$(_scripts_base)"
  648. local api_port="${MINIO_ENDPOINT##*:}"
  649. # 需要的组件:若要装 Casdoor 则一并确保 docker
  650. # uv 作为 MCP/AI 对话运行时依赖(uvx 拉起 MCP server),默认一并检测安装
  651. local comps="postgres mongo redis minio mc uv"
  652. [[ "${DO_CASDOOR}" == "y" || "${DO_PGADMIN}" == "y" ]] && comps="${comps} docker"
  653. 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}"
  654. ok "依赖检测/安装完成"
  655. }
  656. # ---------------------------------------------------------------------------
  657. # 安装 Casdoor
  658. # ---------------------------------------------------------------------------
  659. step_casdoor() {
  660. [[ "${DO_CASDOOR}" == "y" ]] || return 0
  661. title "安装 Casdoor"
  662. local base; base="$(_scripts_base)"
  663. run "CASDOOR_DIR='${CASDOOR_DIR}' CASDOOR_PORT='${CASDOOR_PORT}' \
  664. CASDOOR_PG_USER='${CASDOOR_PG_USER}' CASDOOR_PG_PASSWORD='${CASDOOR_PG_PASSWORD}' \
  665. CASDOOR_DB_NAME='${CASDOOR_DB_NAME}' CASDOOR_ORIGIN='${CASDOOR_ORIGIN}' \
  666. bash '${base}/install_casdoor.sh'"
  667. ok "Casdoor 安装步骤结束(后续配置见 DEPLOYMENT.md)"
  668. }
  669. # ---------------------------------------------------------------------------
  670. # 部署 pgAdmin4(Docker)+ 给 postgres 设密码
  671. # ---------------------------------------------------------------------------
  672. step_pgadmin() {
  673. [[ "${DO_PGADMIN}" == "y" ]] || return 0
  674. title "部署 pgAdmin4(Web)"
  675. local base; base="$(_scripts_base)"
  676. run "POSTGRES_PASSWORD='$(printf '%s' "${POSTGRES_ADMIN_PASSWORD}")' \
  677. PGADMIN_EMAIL='${PGADMIN_EMAIL}' \
  678. PGADMIN_PASSWORD='$(printf '%s' "${PGADMIN_PASSWORD}")' \
  679. PGADMIN_PORT='${PGADMIN_PORT}' PGADMIN_BIND='${PGADMIN_BIND}' \
  680. PG_PORT='${PG_PORT}' \
  681. bash '${base}/install_pgadmin.sh'"
  682. ok "pgAdmin4 部署步骤结束(访问 http://${PGADMIN_BIND}:${PGADMIN_PORT})"
  683. }
  684. # ---------------------------------------------------------------------------
  685. # 创建 MinIO 业务账号与策略(fwh-toolbox / fwh-toolbox-ai)
  686. # ---------------------------------------------------------------------------
  687. step_minioacct() {
  688. [[ "${DO_MINIOACCT}" == "y" ]] || return 0
  689. title "创建 MinIO 业务账号与策略"
  690. local base; base="$(_scripts_base)"
  691. local minio_ep="http://${MINIO_ENDPOINT}"
  692. run "MINIO_ENDPOINT='${minio_ep}' \
  693. MINIO_ROOT_USER='${MINIO_ROOT_USER}' MINIO_ROOT_PASSWORD='$(printf '%s' "${MINIO_ROOT_PASSWORD}")' \
  694. MINIO_ACCESS_KEY='${MINIO_ACCESS_KEY:-fwh-toolbox}' MINIO_SECRET_KEY='$(printf '%s' "${MINIO_SECRET_KEY:-fwh-toolbox}")' \
  695. MINIO_AI_ACCESS_KEY='${MINIO_AI_ACCESS_KEY}' MINIO_AI_SECRET_KEY='$(printf '%s' "${MINIO_AI_SECRET_KEY}")' \
  696. MINIO_BUCKET='${MINIO_BUCKET}' MINIO_AI_BUCKET='${MINIO_AI_BUCKET}' \
  697. bash '${base}/provision_minio.sh'"
  698. ok "MinIO 业务账号步骤结束(后端 ${MINIO_ACCESS_KEY:-fwh-toolbox} / MCP ${MINIO_AI_ACCESS_KEY})"
  699. }
  700. # ---------------------------------------------------------------------------
  701. # 安装/配置 Caddy 反向代理
  702. # ---------------------------------------------------------------------------
  703. step_caddy() {
  704. [[ "${DO_CADDY}" == "y" ]] || return 0
  705. title "安装/配置 Caddy 反向代理"
  706. local base; base="$(_scripts_base)"
  707. run "CADDY_DOMAIN='${CADDY_DOMAIN}' \
  708. LIBDNS_SPACESHIP_APIKEY='$(printf '%s' "${LIBDNS_SPACESHIP_APIKEY}")' \
  709. LIBDNS_SPACESHIP_APISECRET='$(printf '%s' "${LIBDNS_SPACESHIP_APISECRET}")' \
  710. CADDY_FRONTEND_PORT='${CADDY_FRONTEND_PORT}' CADDY_BACKEND_PORT='${CADDY_BACKEND_PORT}' \
  711. CADDY_INCLUDE_CASDOOR='${CADDY_INCLUDE_CASDOOR}' CADDY_CASDOOR_PORT='${CASDOOR_PORT}' \
  712. CADDY_INCLUDE_MINIO='${CADDY_INCLUDE_MINIO}' CADDY_MINIO_CONSOLE_PORT='9199' \
  713. CADDY_SERVE_STATIC='${CADDY_SERVE_STATIC}' CADDY_STATIC_ROOT='${DEPLOY_ROOT}/fwh-toolbox' \
  714. bash '${base}/install_caddy.sh'"
  715. ok "Caddy 反向代理配置结束"
  716. }
  717. # ---------------------------------------------------------------------------
  718. # 安装 Kiro CLI(后端 AI 对话依赖)
  719. # ---------------------------------------------------------------------------
  720. step_kiro() {
  721. [[ "${DO_KIRO}" == "y" ]] || return 0
  722. title "安装 Kiro CLI"
  723. local base; base="$(_scripts_base)"
  724. # 以后端运行用户身份安装;HOME 用 config.env 约定的 /home/<user>(KIRO_HOME 默认取家目录)
  725. run "KIRO_RUN_AS_USER='${RUN_USER}' KIRO_HOME='/home/${RUN_USER}' \
  726. KIRO_INSTALL_METHOD='${KIRO_INSTALL_METHOD}' KIRO_LIBC='${KIRO_LIBC}' \
  727. KIRO_DO_LOGIN='${KIRO_DO_LOGIN}' \
  728. bash '${base}/install_kiro_cli.sh'"
  729. ok "Kiro CLI 安装步骤结束"
  730. }
  731. # ---------------------------------------------------------------------------
  732. # 初始化数据库
  733. # ---------------------------------------------------------------------------
  734. step_initdb() {
  735. [[ "${DO_INITDB}" == "y" ]] || return 0
  736. title "初始化数据库"
  737. local sql_path
  738. if [[ "${MODE}" == "remote" ]]; then
  739. sql_path="${REMOTE_STAGE}/sql/init_pgsql.sql"
  740. else
  741. sql_path="${SQL_FILE}"
  742. fi
  743. # 1) PostgreSQL
  744. info "PostgreSQL 建库/建表 ..."
  745. run "sudo -u postgres psql -v ON_ERROR_STOP=1 -v fwh_pwd='$(printf '%s' "${PG_PASS}")' -f '${sql_path}'" \
  746. && ok "PostgreSQL 完成" || { err "PostgreSQL 初始化失败"; return 1; }
  747. # 2) Mongo / Redis 连通性
  748. 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"
  749. 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"
  750. # 注:MinIO 建桶 + 业务账号 + 策略统一由 step_minioacct(provision_minio.sh)负责,此处不再建桶。
  751. ok "数据库初始化步骤结束"
  752. }
  753. # ---------------------------------------------------------------------------
  754. # 部署后端
  755. # ---------------------------------------------------------------------------
  756. step_backend() {
  757. [[ "${DO_BACKEND}" == "y" ]] || return 0
  758. title "部署后端"
  759. local server_dir="${DEPLOY_ROOT}/server"
  760. local svc="/etc/systemd/system/${APP_NAME}.service"
  761. if [[ "${MODE}" == "remote" ]]; then
  762. run "sudo mkdir -p '${server_dir}' && \
  763. sudo mv '${REMOTE_STAGE}/${APP_NAME}' '${server_dir}/' && \
  764. sudo mv '${REMOTE_STAGE}/${MCP_NAME}' '${server_dir}/' && \
  765. sudo chmod +x '${server_dir}/${APP_NAME}' '${server_dir}/${MCP_NAME}' && \
  766. { [ -d '${REMOTE_STAGE}/cert' ] && sudo cp -r '${REMOTE_STAGE}/cert' '${server_dir}/' || true; } && \
  767. { [ -f '${REMOTE_STAGE}/config.env' ] && sudo cp '${REMOTE_STAGE}/config.env' '${server_dir}/config.env' || true; } && \
  768. { [ -f '${REMOTE_STAGE}/config.mcp.env' ] && sudo cp '${REMOTE_STAGE}/config.mcp.env' '${server_dir}/config.mcp.env' || true; }"
  769. else
  770. sudo mkdir -p "${server_dir}"
  771. sudo mv "${SERVICE_DIR}/${APP_NAME}" "${server_dir}/"
  772. sudo mv "${SERVICE_DIR}/${MCP_NAME}" "${server_dir}/"
  773. sudo chmod +x "${server_dir}/${APP_NAME}" "${server_dir}/${MCP_NAME}"
  774. [[ -d "${SERVICE_DIR}/cert" ]] && sudo cp -r "${SERVICE_DIR}/cert" "${server_dir}/" || true
  775. [[ -n "${GEN_CONFIG}" ]] && sudo cp "${GEN_CONFIG}" "${server_dir}/config.env" || true
  776. if [[ -n "${GEN_MCP_CONFIG}" ]]; then
  777. sudo cp "${GEN_MCP_CONFIG}" "${server_dir}/config.mcp.env" || true
  778. elif [[ -n "${GEN_CONFIG}" ]]; then
  779. sudo cp "${GEN_CONFIG}" "${server_dir}/config.mcp.env" || true
  780. fi
  781. fi
  782. # 写入 systemd 服务
  783. local unit_content
  784. unit_content="[Unit]
  785. Description=${APP_NAME} service
  786. After=network.target postgresql.service mongod.service redis-server.service minio.service
  787. [Service]
  788. ExecStart=${server_dir}/${APP_NAME}
  789. Restart=always
  790. User=${RUN_USER}
  791. WorkingDirectory=${server_dir}
  792. CPUQuota=200%
  793. MemoryLimit=2G
  794. [Install]
  795. WantedBy=multi-user.target"
  796. run "echo '$(printf '%s' "${unit_content}" | sed "s/'/'\\\\''/g")' | sudo tee '${svc}' >/dev/null && \
  797. sudo systemctl daemon-reload && sudo systemctl enable '${APP_NAME}' && \
  798. ( sudo systemctl restart '${APP_NAME}' || sudo systemctl start '${APP_NAME}' ) && \
  799. sudo systemctl --no-pager status '${APP_NAME}' | head -n 6"
  800. ok "后端部署完成"
  801. }
  802. # ---------------------------------------------------------------------------
  803. # 部署前端
  804. # ---------------------------------------------------------------------------
  805. step_frontend() {
  806. [[ "${DO_FRONTEND}" == "y" ]] || return 0
  807. title "部署前端"
  808. local dist_dir="${DEPLOY_ROOT}/fwh-toolbox"
  809. if [[ "${MODE}" == "remote" ]]; then
  810. run "sudo mkdir -p '${dist_dir}' && sudo rm -rf '${dist_dir}'/* && \
  811. sudo tar xzf '${REMOTE_STAGE}/fwh-dist.tgz' -C '${dist_dir}'"
  812. else
  813. sudo mkdir -p "${dist_dir}"
  814. sudo rm -rf "${dist_dir:?}/"*
  815. sudo cp -rf "${PROJECT_ROOT}/dist/"* "${dist_dir}/"
  816. fi
  817. ok "前端部署完成:${dist_dir}"
  818. }
  819. # ---------------------------------------------------------------------------
  820. # 收尾
  821. # ---------------------------------------------------------------------------
  822. cleanup() {
  823. [[ -n "${GEN_CONFIG}" && -f "${GEN_CONFIG}" ]] && rm -f "${GEN_CONFIG}"
  824. [[ -n "${GEN_MCP_CONFIG}" && -f "${GEN_MCP_CONFIG}" ]] && rm -f "${GEN_MCP_CONFIG}"
  825. if [[ "${MODE}" == "remote" ]]; then
  826. ssh_run "rm -rf ${REMOTE_STAGE}" >/dev/null 2>&1 || true
  827. fi
  828. }
  829. summary() {
  830. title "部署完成"
  831. echo "模式 : ${MODE}"
  832. [[ "${MODE}" == "remote" ]] && echo "远程主机 : ${R_USER}@${R_HOST}:${R_PORT}"
  833. echo "部署根目录 : ${DEPLOY_ROOT}"
  834. 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账号 ' )"
  835. echo
  836. echo "后续:"
  837. if [[ "${DO_CADDY}" == "y" ]]; then
  838. echo " * Caddy 已配置:https://${CADDY_DOMAIN}/fwh-toolbox/ (站点文件 /etc/caddy/sites/fwh-toolbox.caddy)"
  839. echo " TLS 走 spaceship DNS-01;token 已写入 systemd 服务(/lib/systemd/system/caddy.service,权限 600)。"
  840. else
  841. echo " * 配置并重载反向代理 Caddy(见 deploy/DEPLOYMENT.md 第 9 章)"
  842. fi
  843. if [[ "${DO_CASDOOR}" == "y" ]]; then
  844. echo " * 配置 Casdoor:登录 ${CASDOOR_ORIGIN} (admin/123),创建组织/应用,"
  845. echo " 取得 clientId/clientSecret/证书,并反代 /casdoor(见 DEPLOYMENT.md《Casdoor 配置》)"
  846. fi
  847. if [[ "${DO_KIRO}" == "y" ]]; then
  848. echo " * Kiro CLI 已安装(用户 ${RUN_USER})。"
  849. if [[ "${KIRO_DO_LOGIN}" != "y" ]]; then
  850. if [[ "${MODE}" == "remote" ]]; then
  851. echo " 登录:ssh ${R_USER}@${R_HOST} 'sudo -u ${RUN_USER} -H kiro-cli login'(Builder ID,交互式)"
  852. else
  853. echo " 登录:sudo -u ${RUN_USER} -H kiro-cli login(Builder ID,交互式)"
  854. fi
  855. fi
  856. echo " 确认 config.env 的 KIRO_RUN_AS_USER=${RUN_USER}、KIRO_HOME=/home/${RUN_USER} 一致,后端方能拉起 AI 对话。"
  857. fi
  858. if [[ "${DO_PGADMIN}" == "y" ]]; then
  859. echo " * pgAdmin4 已部署:http://${PGADMIN_BIND}:${PGADMIN_PORT}(登录 ${PGADMIN_EMAIL})"
  860. echo " 新建 Server 连本机 PG:主机 host.docker.internal、端口 ${PG_PORT}、用户 postgres。"
  861. echo " 若连接报认证失败,多为 pg_hba.conf 未启用 scram-sha-256,见脚本提示。"
  862. echo " 安全:勿把 ${PGADMIN_PORT} 暴露公网,建议置于反向代理 + 访问控制之后。"
  863. fi
  864. if [[ "${DO_MINIOACCT}" == "y" ]]; then
  865. echo " * MinIO 业务账号已创建:后端 ${MINIO_ACCESS_KEY:-fwh-toolbox}(写 fwh-toolbox* 桶)、"
  866. echo " MCP ${MINIO_AI_ACCESS_KEY}(写 fwh-toolbox-ai* 桶,其余只读)。"
  867. echo " 确认 config.env 用后端账号、config.mcp.env 用 AI 账号;核对:mc admin user list fwhlocal"
  868. fi
  869. echo " * 验证:curl http://127.0.0.1:${APP_PORT}/swagger/index.html"
  870. if [[ "${MODE}" == "remote" ]]; then
  871. echo " * 查看后端日志:ssh ${R_USER}@${R_HOST} 'journalctl -u ${APP_NAME} -f'"
  872. else
  873. echo " * 查看后端日志:journalctl -u ${APP_NAME} -f"
  874. fi
  875. }
  876. usage() {
  877. cat <<EOF
  878. FranklinWH Toolbox 交互式一键部署
  879. 用法:
  880. $0 进入交互式向导(选择 local/remote,分步选择与填写)
  881. $0 --only <项[,项...]> 只执行指定步骤(仍会先问 local/remote 与该步骤参数)
  882. 可选项:deps casdoor initdb minioacct pgadmin caddy kiro deploy
  883. 例:$0 --only caddy 或 $0 --only deps,initdb
  884. $0 --help 显示本帮助
  885. 向导步骤:
  886. 0. 选择模式(本地/远程)与要执行的步骤
  887. 1. (远程)填写 SSH 连接信息并测试
  888. 2. 填写部署目录、运行用户与各项配置
  889. 3. 生成 config.env
  890. 4. 本地构建前端 dist 与后端二进制
  891. 5. (远程)打包上传产物与辅助脚本
  892. 6. 检测/安装依赖 → 安装 Casdoor → 初始化数据库 → 部署后端 → 部署前端 → 配置 Caddy → 安装 Kiro CLI
  893. 可选步骤(向导中勾选):
  894. * 检测/安装数据库引擎与依赖(PostgreSQL/MongoDB/Redis/MinIO/Docker)
  895. * 安装 Casdoor(Docker,认证服务)
  896. * 安装并配置 Caddy 反向代理(生成站点 Caddyfile 并 reload)
  897. * 安装 Kiro CLI(后端 AI 对话依赖,以运行用户安装并登录 Builder ID)
  898. EOF
  899. }
  900. # --only 解析:把所有步骤关掉,再按传入项打开对应步骤
  901. # 支持项:deps casdoor initdb minioacct pgadmin caddy kiro deploy
  902. # 多项用逗号分隔,如 --only deps,initdb
  903. parse_only() {
  904. local items="$1"
  905. [[ -n "${items}" ]] || { err "--only 需要指定步骤,如:--only caddy(可选 deps,casdoor,initdb,minioacct,pgadmin,caddy,kiro,deploy)"; exit 1; }
  906. # 全部关闭
  907. DO_DEPS="n"; DO_CASDOOR="n"; DO_INITDB="n"; DO_BACKEND="n"; DO_FRONTEND="n"
  908. DO_GENCONF="n"; DO_CADDY="n"; DO_KIRO="n"; DO_PGADMIN="n"; DO_MINIOACCT="n"
  909. ONLY_MODE=1
  910. ONLY_LABEL="${items}"
  911. local IFS=','; local it
  912. for it in ${items}; do
  913. case "${it}" in
  914. deps) DO_DEPS="y" ;;
  915. casdoor) DO_CASDOOR="y" ;;
  916. initdb) DO_INITDB="y"; DO_GENCONF="y" ;; # 初始化通常配合生成 config
  917. minioacct) DO_MINIOACCT="y" ;;
  918. pgadmin) DO_PGADMIN="y" ;;
  919. caddy) DO_CADDY="y" ;;
  920. kiro) DO_KIRO="y" ;;
  921. deploy)
  922. # 部署前后端:子交互选择范围
  923. echo "部署范围:1) 前端+后端(all) 2) 仅后端 3) 仅前端"
  924. local _sel; ask_choice _sel "输入序号" "1" "1 2 3"
  925. case "${_sel}" in
  926. 1) DO_BACKEND="y"; DO_FRONTEND="y" ;;
  927. 2) DO_BACKEND="y"; DO_FRONTEND="n" ;;
  928. 3) DO_BACKEND="n"; DO_FRONTEND="y" ;;
  929. esac
  930. DO_GENCONF="y" # 部署后端需要 config.env
  931. ;;
  932. *) err "未知 --only 项:${it}"; exit 1 ;;
  933. esac
  934. done
  935. }
  936. main() {
  937. if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage; exit 0; fi
  938. # --only <项[,项...]>:单项/多项模式,仅执行指定步骤
  939. if [[ "${1:-}" == "--only" ]]; then
  940. parse_only "${2:-}"
  941. fi
  942. trap cleanup EXIT
  943. wizard_intro
  944. wizard_remote
  945. wizard_params
  946. title "确认"
  947. echo "模式=${MODE} 目录=${DEPLOY_ROOT} 运行用户=${RUN_USER}"
  948. 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} pgadmin=${DO_PGADMIN} minio-acct=${DO_MINIOACCT}"
  949. ask_yn "确认开始执行?" "y" || { warn "已取消"; exit 0; }
  950. generate_config
  951. build_frontend
  952. build_backend
  953. prepare_remote_stage
  954. step_deps
  955. step_casdoor
  956. step_initdb
  957. step_minioacct
  958. step_pgadmin
  959. step_backend
  960. step_frontend
  961. step_caddy
  962. step_kiro
  963. summary
  964. }
  965. main "$@"