Bläddra i källkod

feat:增加一键部署功能(未测试)

Jerry Liang 2 månader sedan
förälder
incheckning
a3afdb7b35

+ 139 - 0
deploy/CHECKLIST.md

@@ -0,0 +1,139 @@
+# FranklinWH Toolbox 部署清单(CHECKLIST)
+
+本清单汇总从零部署所需**填写/确认的内容**与**执行步骤**,可逐项打勾。
+详细说明见 [DEPLOYMENT.md](./DEPLOYMENT.md)。脚本中对应位置均有 `# TODO:` 注释,
+可用以下命令快速定位:
+
+```bash
+grep -rn 'TODO' deploy/scripts deploy/config.env.example
+```
+
+图例:**[必填]** 不填无法运行/有安全风险 | **[确认]** 有默认值需核对 | **[可选]** 用到才填
+
+---
+
+## A. 需要你填写/确认的内容
+
+### A1. 密码与密钥(必填,安全相关)
+
+| # | 项目 | 位置 | 说明 |
+|---|------|------|------|
+| ☐ | **PostgreSQL 业务口令** | `init_databases.sh:PG_FWH_PASSWORD` + `config.env:POSTGRES_DSN` | `fwh_toolbox` 角色密码,两处必须**完全一致** |
+| ☐ | **MinIO 管理员账户** | `install_deps.sh` / `init_databases.sh` 的 `MINIO_ROOT_USER/PASSWORD` | 勿用默认 `minioadmin/minioadmin` |
+| ☐ | **MinIO 业务密钥** | `config.env:MINIO_ACCESS_KEY / MINIO_SECRET_KEY` | 后端访问对象存储用 |
+| ☐ | **FWH 生产账号** | `config.env:FWH_USER / FWH_PASSWORD` | 密码填明文,后端自动 MD5 |
+| ☐ | **Casdoor 数据库口令** | `install_casdoor.sh:CASDOOR_PG_PASSWORD` | 勿用默认 `casdoor` |
+
+### A2. 证书文件(必填)
+
+| # | 项目 | 位置 | 说明 |
+|---|------|------|------|
+| ☐ | **FWH 生产 MQTT 证书** | `server/cert/pro/`(CA/crt/key 三件) | 路径与 `config.env:FWH_MQTT_CA/CERT/KEY` 对应 |
+| ☐ | **Casdoor 应用证书** | Casdoor 后台获取的 X.509 PEM | 后端用于校验 JWT 签名(见 A4) |
+
+### A3. 域名与网络(必填)
+
+| # | 项目 | 位置 | 说明 |
+|---|------|------|------|
+| ☐ | **对外域名** | Caddyfile 的 `你的域名` | 反向代理入口 |
+| ☐ | **Casdoor origin/回调** | `install_casdoor.sh:CASDOOR_ORIGIN` + Casdoor 应用 Redirect URLs | 如 `https://<域名>/casdoor`;回调 `https://<域名>/fwh-toolbox/casdoor/callback` |
+| ☐ | **远端只读 MySQL** | `config.env:MYSQL_DSN` | 用到站点信息查询功能时**必填**;替换 USER/PASSWORD/HOST/PORT |
+
+### A4. Casdoor 应用信息(必填)
+
+| # | 项目 | 说明 |
+|---|------|------|
+| ☐ | 创建组织 `organization_franklinwh_toolbox` | 名称必须与前后端硬编码一致 |
+| ☐ | 创建应用 `app-franklinwh-toolbox` | 记录 Client ID / Client Secret / 证书 |
+| ☐ | 同步到源码(仅新建应用时) | `service/utils/casdoor/casdoor.go`(endpoint/clientId/clientSecret/certificate)与 `src/utils/casdoor/casdoor.ts`(ClientId),改后需**重新构建**前后端 |
+
+> ⚠️ 沿用现有 Casdoor 应用可跳过 A4 的源码修改;新建应用则必须改源码后重编译。
+
+### A5. 需确认的默认值
+
+| # | 项目 | 默认 | 位置 |
+|---|------|------|------|
+| ☐ | 部署根目录 | `/var/www/fwh-toolbox` | `setup.sh` / `deploy.sh:DEPLOY_ROOT` |
+| ☐ | 后端运行用户 | 当前用户 | `RUN_USER` / `config.env:KIRO_RUN_AS_USER` |
+| ☐ | 后端端口 | `8301` | `config.env:APP_PORT` |
+| ☐ | Asynqmon 端口 | `8302` | `config.env:ASYNQMON_PORT` |
+| ☐ | MinIO 端口 | `9198`/`9199` | `install_deps.sh` |
+| ☐ | Casdoor 端口 | `8010` | `install_casdoor.sh:CASDOOR_PORT` |
+| ☐ | 运行环境 | `dev` | `config.env:APP_ENV`(生产改 `pro`) |
+| ☐ | Mongo 库名 | `FwhToolboxDb` | `config.env:MONGO_DSN`(开发 `FwhToolboxDbDev`) |
+| ☐ | MinIO bucket | `fwh-toolbox` | `config.env:MINIO_BUCKET_NAME`(与建桶一致) |
+| ☐ | Kiro 用户/HOME | `jerry-remote` | `config.env:KIRO_RUN_AS_USER/KIRO_HOME` |
+
+### A6. 可选功能
+
+| # | 项目 | 位置 | 说明 |
+|---|------|------|------|
+| ☐ | FWH 测试环境 | `config.env:FWH_TEST_*` + `server/cert/test/` | 连测试服才填 |
+| ☐ | Plane 集成 | `config.env:PLANE_*` | 启用反馈/任务同步才填 |
+
+---
+
+## B. 部署步骤清单
+
+> 推荐直接用交互式向导 `deploy/scripts/setup.sh`,它会逐项提示 A 节内容。
+> 以下为等价的分步操作,可据此逐项打勾。
+
+### B1. 前置环境
+
+- ☐ 目标机为 Ubuntu(参考 22.04),当前用户具备 `sudo` 权限
+- ☐ (远程部署)本机可 SSH 到目标机,已准备好私钥或密码
+- ☐ 已克隆代码到目标机(或本机构建后上传)
+
+### B2. 安装依赖(数据库引擎 + Docker)
+
+- ☐ 运行 `DETECT_ONLY=1 deploy/scripts/install_deps.sh` 检测现状
+- ☐ 安装缺失项:PostgreSQL 14 / MongoDB 8.0 / Redis / MinIO + mc / Docker
+- ☐ 确认服务已运行:`systemctl status postgresql mongod redis-server minio docker`
+
+### B3. 安装并配置 Casdoor
+
+- ☐ `CASDOOR_PG_PASSWORD='强口令' deploy/scripts/install_casdoor.sh`
+- ☐ 配置宿主 PostgreSQL 允许容器连接(`listen_addresses` + `pg_hba.conf`,见 DEPLOYMENT.md 10.2)
+- ☐ 登录 Casdoor(默认 admin/123,**立即改密**),完成 A4 的组织/应用配置
+- ☐ 配置反向代理 `/casdoor`(见 B7)
+
+### B4. 初始化数据库
+
+- ☐ `PG_FWH_PASSWORD='强口令' MINIO_ROOT_USER=... MINIO_ROOT_PASSWORD=... deploy/scripts/init_databases.sh`
+- ☐ 验证:`psql -d fwh_toolbox_db -c "\dt alarm_snapshot.*"`、`mc ls fwhlocal/`
+
+### B5. 配置后端
+
+- ☐ `cp deploy/config.env.example service/config.env`
+- ☐ 按 A 节逐项填写 `config.env`(搜索文件内 `TODO`)
+- ☐ 放置 MQTT 证书到 `service/cert/pro/`(及 `test/`)
+
+### B6. 构建并部署前后端
+
+- ☐ `deploy/scripts/deploy.sh backend`(构建二进制 + systemd 服务)
+- ☐ `deploy/scripts/deploy.sh frontend`(构建 dist + 同步静态目录)
+- ☐ 或一次性 `deploy/scripts/deploy.sh all`
+
+### B7. 反向代理(Caddy)
+
+- ☐ 安装 Caddy 2
+- ☐ 配置站点:`/fwh-toolbox/*`→静态、`/fwh-toolbox/api/*`→`localhost:8301`、`/casdoor/*`→`localhost:8010`
+- ☐ 把 `你的域名` 替换为实际域名,`systemctl reload caddy`
+
+### B8. 验证
+
+- ☐ 后端:`curl http://127.0.0.1:8301/swagger/index.html`
+- ☐ 任务面板:`curl http://127.0.0.1:8302/asynqmon/`
+- ☐ Casdoor:`curl http://127.0.0.1:8010/`
+- ☐ 前端(经反代):`curl -k https://<域名>/fwh-toolbox/`
+- ☐ 登录流程:访问前端 → 跳转 Casdoor 登录 → 回调成功
+- ☐ 日志无异常:`journalctl -u fwh-toolbox-server -f`
+
+---
+
+## C. 安全收尾
+
+- ☐ 所有默认口令(minioadmin / casdoor / admin@123 等)均已改为强口令
+- ☐ `config.env` 权限收紧,且**未**提交到 Git(含密钥/证书)
+- ☐ 数据库/MinIO/Redis 仅监听本地或受信网段
+- ☐ 备份策略:PostgreSQL(`pg_dump`)、MongoDB(`mongodump`)、MinIO 数据目录

+ 817 - 0
deploy/DEPLOYMENT.md

@@ -0,0 +1,817 @@
+# FranklinWH Toolbox 全量部署文档
+
+本文档描述如何在一台**全新的 Ubuntu 服务器**上,从零部署 FranklinWH Toolbox
+(Vue 3 前端 + Go 后端)的完整流程,包括所有依赖数据库的安装、配置、建库建表、
+反向代理与 systemd 服务。安装方式与版本参照当前生产服务器的实际部署。
+
+> 参考环境(当前服务器实测):Ubuntu 22.04.5 LTS,Go 1.26,Node 22.15,pnpm 8.6,
+> PostgreSQL 14,MongoDB 8.0,Redis 7.4,MinIO(RELEASE 版),Caddy 2.11。
+
+---
+
+## ⚑ 部署前需要你确认/填写的清单(TODO)
+
+下面列出**必须由你确认或填写**的项。脚本与 `config.env.example` 中对应位置均已用
+`# TODO:` 注释标出,可搜索 `TODO` 快速定位。
+
+> 📋 完整的可勾选清单(含填写项与部署步骤)见 [CHECKLIST.md](./CHECKLIST.md)。
+
+**必填(不改则无法正常运行 / 有安全风险)**
+
+- [ ] **PostgreSQL 业务口令** `fwh_toolbox` 角色密码:`init_databases.sh` 的 `PG_FWH_PASSWORD`
+      与 `config.env` 的 `POSTGRES_DSN` 中 `password=` 必须一致。
+- [ ] **MinIO 管理员口令** `MINIO_ROOT_USER/MINIO_ROOT_PASSWORD`(勿用默认 `minioadmin`)。
+- [ ] **MinIO 业务密钥** `config.env` 的 `MINIO_ACCESS_KEY/MINIO_SECRET_KEY`。
+- [ ] **FWH 生产账号** `config.env` 的 `FWH_USER/FWH_PASSWORD`。
+- [ ] **FWH MQTT 证书** 放入 `server/cert/pro/`,并核对 `config.env` 中路径。
+- [ ] **Casdoor 数据库口令** `CASDOOR_PG_PASSWORD`(勿用默认 `casdoor`)。
+- [ ] **Casdoor 应用信息** 在 Casdoor 建 `organization_franklinwh_toolbox` /
+      `app-franklinwh-toolbox`,若新建应用需把 clientId/secret/证书同步到源码
+      (`service/utils/casdoor/casdoor.go`、`src/utils/casdoor/casdoor.ts`)后重新构建。
+- [ ] **域名** 反向代理 Caddyfile 中的 `你的域名`、Casdoor `origin/回调 URL`。
+- [ ] **远端 MySQL** `config.env` 的 `MYSQL_DSN`(用到站点信息查询功能时必填)。
+
+**需确认(有默认值,请核对是否符合你的环境)**
+
+- [ ] 部署根目录 `DEPLOY_ROOT`(默认 `/var/www/fwh-toolbox`)与运行用户 `RUN_USER`。
+- [ ] 端口:后端 `APP_PORT=8301`、Asynqmon `8302`、MinIO `9198/9199`、Casdoor `8010`。
+- [ ] `APP_ENV`(pro/dev)、`MONGO_DSN` 库名(生产 `FwhToolboxDb`)、`MINIO_BUCKET_NAME`。
+- [ ] `KIRO_RUN_AS_USER`/`KIRO_HOME` 改成目标机实际用户。
+
+**可选(用到对应功能才填)**
+
+- [ ] FWH 测试环境账号与 MQTT(`FWH_TEST_*`)。
+- [ ] Plane 集成(`PLANE_*`)。
+
+> 交互式脚本 `deploy/scripts/setup.sh` 会逐项提示你填写以上内容,推荐优先使用。
+
+---
+
+## 目录
+
+0. [快速开始:交互式一键部署](#0-快速开始交互式一键部署)
+1. [架构总览](#1-架构总览)
+2. [端口与目录约定](#2-端口与目录约定)
+3. [基础环境与依赖安装](#3-基础环境与依赖安装)
+4. [数据库安装](#4-数据库安装)
+5. [数据库初始化与数据表导入](#5-数据库初始化与数据表导入)
+6. [后端配置](#6-后端配置)
+7. [构建与部署后端](#7-构建与部署后端)
+8. [构建与部署前端](#8-构建与部署前端)
+9. [反向代理(Caddy)](#9-反向代理caddy)
+10. [Casdoor 认证服务安装与配置](#10-casdoor-认证服务安装与配置)
+11. [启动、验证与排错](#11-启动验证与排错)
+12. [附录:一键脚本清单](#12-附录一键脚本清单)
+
+---
+
+## 0. 快速开始:交互式一键部署
+
+如果只想尽快完成部署,直接运行交互式脚本 `deploy/scripts/setup.sh`。它会**分步引导**
+你选择/填写内容,并支持**本地**与**远程(SSH)**两种目标:
+
+```bash
+cd deploy/scripts
+chmod +x setup.sh
+./setup.sh
+```
+
+向导流程:
+
+1. **选择模式**:本地 (local) 或 远程 (remote,通过 SSH)
+2. **选择步骤**:初始化数据库 / 部署后端 / 部署前端(可自由勾选)
+3. **(远程)填写 SSH 连接**:主机、端口、用户、私钥,并自动测试连通性
+4. **填写配置**:部署目录、运行用户、PostgreSQL/Mongo/Redis/MinIO/MySQL 等
+5. 自动**生成 config.env** → 本机**构建**前端 dist 与后端二进制 →
+   (远程时)**打包上传** → 执行所选步骤
+
+设计要点:
+
+- **本地构建 + 远程安装**:前端/后端在运行脚本的机器上构建,再上传到远程,
+  因此**远程主机无需安装 Go/Node**。
+- 脚本**只做建库建表建桶与服务部署**,不自动安装数据库引擎;数据库软件的
+  安装仍请参考下文第 3、4 章(可在目标机预先装好 PostgreSQL/MongoDB/Redis/MinIO)。
+- 反向代理(Caddy)需按第 9 章手动配置。
+
+> 想了解每一步的底层动作、或需要单独执行某一环节,请继续阅读后续章节;
+> `setup.sh` 内部即是对第 5~8 章操作的编排。
+
+---
+
+## 1. 架构总览
+
+```
+                        ┌─────────────────────────────────────────┐
+   浏览器 ── HTTPS ────▶ │  Caddy (反向代理, :80/:443)              │
+                        │   /fwh-toolbox/*      → 前端静态 (:8300) │
+                        │   /fwh-toolbox/api/*  → 后端 API (:8301) │
+                        └───────────────┬─────────────────────────┘
+                                        │
+        ┌───────────────────────────────┼──────────────────────────────┐
+        ▼                               ▼                              ▼
+┌───────────────┐            ┌────────────────────┐          ┌──────────────────┐
+│ 前端静态文件   │            │ Go 后端            │          │ fwh-mcp-server   │
+│ Apache :8300  │            │ fwh-toolbox-server │          │ (MCP,可选)       │
+│ /var/www/...  │            │ localhost:8301     │          └──────────────────┘
+└───────────────┘            │ Asynqmon :8302     │
+                             └─────────┬──────────┘
+                                       │
+    ┌──────────────┬───────────────────┼───────────────┬────────────────┐
+    ▼              ▼                   ▼               ▼                ▼
+┌─────────┐  ┌──────────┐        ┌──────────┐   ┌──────────┐    ┌─────────────────┐
+│PostgreSQL│  │ MongoDB  │        │  Redis   │   │  MinIO   │    │ 远端 MySQL(只读) │
+│  :5432   │  │ :27017   │        │  :6379   │   │  :9198   │    │ FWH 生产库       │
+│ 本地     │  │ 本地     │        │ 本地     │   │ 本地     │    │ 远程 34.x:3308   │
+└─────────┘  └──────────┘        └──────────┘   └──────────┘    └─────────────────┘
+```
+
+数据库职责说明:
+
+| 数据库 | 位置 | 用途 |
+| --- | --- | --- |
+| **PostgreSQL** | 本地 | 保存站点原始数据(`original` schema,运行时按雪花ID动态建表)与告警快照(`alarm_snapshot` schema,两张固定表) |
+| **MongoDB** | 本地 | 保存业务文档数据,集合首次写入时自动创建(生产库 `FwhToolboxDb`) |
+| **Redis** | 本地 | 缓存 + Asynq 异步任务队列(任务使用 DB 12) |
+| **MinIO** | 本地 | 对象存储(快照/日志/故障录波等文件),bucket `fwh-toolbox` |
+| **MySQL** | 远程只读 | FranklinWH 生产库 `home_energy_storage_server`,仅查询站点信息(`iot_gateway` 表)。**新服务器无需安装 MySQL**,只需网络可达并在 `config.env` 配置只读账号 |
+
+> 注:后端代码中 MySQL 初始化默认被注释(见 `service/database/database.go`),
+> 是否启用取决于所用功能;PostgreSQL 与 MongoDB 为必需。
+
+---
+
+## 2. 端口与目录约定
+
+| 组件 | 端口 | 绑定 | 备注 |
+| --- | --- | --- | --- |
+| 后端 API | 8301 | 127.0.0.1 | `APP_PORT`,仅本机,经反代对外 |
+| Asynqmon | 8302 | 0.0.0.0 | `ASYNQMON_PORT`,任务监控面板 `/asynqmon` |
+| 前端静态 | 8300 | 由 Web 服务器(Apache/Nginx)托管 dist 目录 |
+| PostgreSQL | 5432 | 本地 | |
+| MongoDB | 27017 | 127.0.0.1 | |
+| Redis | 6379 | 127.0.0.1 | |
+| MinIO API | 9198 | 本地 | S3 API |
+| MinIO 控制台 | 9199 | 本地 | Web 控制台 |
+| Caddy | 80 / 443 | 对外 | 反向代理入口 |
+
+部署目录(与线上一致):
+
+```
+/var/www/fwh-toolbox/
+├── fwh-toolbox/          # 前端构建产物(dist/* 拷贝到此)
+└── server/               # 后端运行目录
+    ├── fwh-toolbox-server    # 后端主程序二进制
+    ├── fwh-mcp-server        # MCP 服务二进制(可选)
+    ├── config.env            # 后端配置(由 config.env.example 复制修改)
+    └── cert/                 # FWH MQTT 证书 (pro/ 与 test/)
+```
+
+---
+
+## 3. 基础环境与依赖安装
+
+以下命令在全新 Ubuntu 22.04 上以具备 `sudo` 权限的用户执行。
+
+### 3.1 系统更新与基础工具
+
+```bash
+sudo apt update && sudo apt upgrade -y
+sudo apt install -y curl wget git build-essential ca-certificates gnupg lsb-release
+```
+
+### 3.2 Go 1.26
+
+参照线上使用官方二进制安装(不使用 apt 版本,避免过旧):
+
+```bash
+GO_VERSION=1.26.4
+wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
+sudo rm -rf /usr/local/go
+sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
+echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh
+source /etc/profile.d/go.sh
+go version    # 期望输出 go version go1.26.x linux/amd64
+```
+
+### 3.3 Node.js 22 + pnpm
+
+```bash
+# Node 22 (NodeSource)
+curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
+sudo apt install -y nodejs
+node -v        # v22.x
+
+# pnpm(项目使用 pnpm,lock 文件为 pnpm-lock.yaml)
+sudo npm install -g pnpm@8
+pnpm -v        # 8.x
+```
+
+---
+
+## 4. 数据库安装
+
+> **自动检测/安装**:可直接用 `deploy/scripts/install_deps.sh` 一次性完成本章
+> 各数据库引擎的检测与(缺失时)安装(幂等,可重复运行):
+>
+> ```bash
+> cd deploy/scripts && chmod +x install_deps.sh
+> DETECT_ONLY=1 ./install_deps.sh                 # 仅检测各组件是否已安装
+> MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' \
+>   ./install_deps.sh postgres mongo redis minio mc docker   # 检测并安装缺失项
+> ```
+>
+> 下面各小节是等价的手动步骤与版本参考(与线上一致)。
+
+### 4.1 PostgreSQL 14
+
+```bash
+sudo apt install -y postgresql postgresql-contrib
+sudo systemctl enable --now postgresql
+psql --version     # PostgreSQL 14.x
+```
+
+### 4.2 MongoDB 8.0
+
+Ubuntu 官方仓库没有 MongoDB,使用官方源:
+
+```bash
+curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
+  sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
+echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | \
+  sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
+sudo apt update
+sudo apt install -y mongodb-org
+sudo systemctl enable --now mongod
+mongosh --eval 'db.runCommand({ping:1})'
+```
+
+> 线上 MongoDB 绑定 `127.0.0.1:27017`。如需限制,编辑 `/etc/mongod.conf` 的
+> `net.bindIp: 127.0.0.1`。
+
+### 4.3 Redis 7.x
+
+```bash
+sudo apt install -y redis-server
+sudo systemctl enable --now redis-server
+redis-cli ping     # PONG
+```
+
+> 后端将 Asynq 任务队列放在 Redis **DB 12**,无需额外配置。
+
+### 4.4 MinIO(对象存储)
+
+参照线上使用官方二进制 + 独立 `minio-user` + systemd:
+
+```bash
+# 1. 下载二进制
+sudo wget -O /usr/local/bin/minio https://dl.min.io/server/minio/release/linux-amd64/minio
+sudo chmod +x /usr/local/bin/minio
+
+# 2. 创建运行用户与数据目录
+sudo useradd -r minio-user -s /sbin/nologin || true
+sudo mkdir -p /var/lib/minio/data
+sudo chown -R minio-user:minio-user /var/lib/minio
+
+# 3. 环境变量文件
+sudo tee /etc/default/minio > /dev/null <<'EOF'
+MINIO_VOLUMES="/var/lib/minio/data"
+# API :9198,控制台 :9199(与线上一致)
+MINIO_OPTS="--address :9198 --console-address :9199"
+MINIO_ROOT_USER="CHANGE_ME_ADMIN"
+MINIO_ROOT_PASSWORD="CHANGE_ME_STRONG_PASSWORD"
+EOF
+
+# 4. systemd 服务
+sudo tee /etc/systemd/system/minio.service > /dev/null <<'EOF'
+[Unit]
+Description=MinIO
+Documentation=https://docs.min.io
+Wants=network-online.target
+After=network-online.target
+AssertFileIsExecutable=/usr/local/bin/minio
+
+[Service]
+Type=notify
+WorkingDirectory=/usr/local
+User=minio-user
+Group=minio-user
+EnvironmentFile=-/etc/default/minio
+ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
+Restart=always
+LimitNOFILE=1048576
+TasksMax=infinity
+OOMScoreAdjust=-1000
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+sudo systemctl daemon-reload
+sudo systemctl enable --now minio
+```
+
+安装 `mc`(MinIO Client,用于建 bucket):
+
+```bash
+sudo wget -O /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc
+sudo chmod +x /usr/local/bin/mc
+```
+
+### 4.5 远端 MySQL
+
+**新服务器不需要安装 MySQL**。它连接的是 FranklinWH 的远程生产库
+(示例 `34.211.183.235:3308` 的 `home_energy_storage_server`),只需:
+
+- 保证服务器网络能访问该地址与端口;
+- 在 `config.env` 的 `MYSQL_DSN` 中填入只读账号。
+
+仓库中 `service/database/sql/home_energy_storage_server_iot_gateway_2025-08-11_152722.sql`
+是该远程库 `iot_gateway` 表的结构快照,仅供参考/本地测试导入使用。
+
+---
+
+## 5. 数据库初始化与数据表导入
+
+### 5.1 PostgreSQL:建角色 / 建库 / 建 schema / 建表
+
+后端的 `alarm_snapshot` 两张表**不会**自动迁移,需要预先创建;`original` schema
+下的原始数据表在运行时按雪花ID动态创建,因此只需建好 schema。仓库提供了幂等 SQL:
+
+`deploy/sql/init_pgsql.sql`
+
+一键执行(推荐使用脚本,见下节);或手动:
+
+```bash
+# 使用自定义密码执行;不传则默认 fwh_toolbox(仅限本地开发)
+# 密码直接传入、无需加引号(脚本内部用 %L 安全转义)
+sudo -u postgres psql -v ON_ERROR_STOP=1 \
+  -v fwh_pwd=你的强密码 \
+  -f deploy/sql/init_pgsql.sql
+```
+
+执行后将得到:
+
+- 角色 `fwh_toolbox`(可登录)
+- 数据库 `fwh_toolbox_db`(owner=`fwh_toolbox`)
+- schema:`original`、`alarm_snapshot`、`public`
+- 表:`alarm_snapshot.site_alarm_snapshot_data`、
+  `alarm_snapshot.site_alarm_snapshot_file_record`
+
+验证:
+
+```bash
+sudo -u postgres psql -d fwh_toolbox_db -c "\dn"
+sudo -u postgres psql -d fwh_toolbox_db -c "\dt alarm_snapshot.*"
+```
+
+### 5.2 一键初始化所有本地数据库
+
+仓库提供 `deploy/scripts/init_databases.sh`,会完成 PostgreSQL 初始化、
+MongoDB/Redis 连通性检查、MinIO bucket 创建:
+
+```bash
+cd deploy/scripts
+chmod +x init_databases.sh
+
+PG_FWH_PASSWORD='你的强密码' \
+MINIO_ROOT_USER='你的MinIO管理员' \
+MINIO_ROOT_PASSWORD='你的MinIO密码' \
+MINIO_BUCKET='fwh-toolbox' \
+./init_databases.sh
+```
+
+### 5.3 MongoDB
+
+无需手动建集合——集合会在后端首次写入时自动创建。生产环境使用库名
+`FwhToolboxDb`(在 `MONGO_DSN` 末尾指定)。若要从旧库迁移数据:
+
+```bash
+# 导出(旧服务器)
+mongodump --uri="mongodb://127.0.0.1:27017/FwhToolboxDb" --out=/tmp/fwhdump
+# 导入(新服务器)
+mongorestore --uri="mongodb://127.0.0.1:27017" /tmp/fwhdump
+```
+
+### 5.4 迁移已有 PostgreSQL 业务数据(可选)
+
+若需把旧服务器的完整业务数据搬到新服务器:
+
+```bash
+# 旧服务器导出(含 original 动态表 + alarm_snapshot 表)
+sudo -u postgres pg_dump -Fc -d fwh_toolbox_db -f /tmp/fwh_toolbox_db.dump
+# 新服务器恢复(先用 init_pgsql.sql 建好角色/库,再恢复)
+sudo -u postgres pg_restore -d fwh_toolbox_db --no-owner --role=fwh_toolbox /tmp/fwh_toolbox_db.dump
+```
+
+---
+
+## 6. 后端配置
+
+后端使用 Viper 读取配置,优先级:`./.env` → `./config.env`,环境变量可覆盖。
+仓库提供模板 `deploy/config.env.example`。
+
+```bash
+# 复制模板到 service 目录(本地运行)或直接到部署目录 /var/www/fwh-toolbox/server/
+cp deploy/config.env.example service/config.env
+# 编辑 config.env,至少修改:
+#   POSTGRES_DSN 中的 password
+#   MONGO_DSN(生产用 FwhToolboxDb)
+#   MINIO_ACCESS_KEY / MINIO_SECRET_KEY
+#   MYSQL_DSN 只读账号
+#   FWH_PASSWORD / FWH_TEST_PASSWORD 等密钥
+```
+
+关键项对照(详见模板注释):
+
+| 变量 | 示例/默认 | 说明 |
+| --- | --- | --- |
+| `APP_PORT` | `8301` | 后端监听端口(localhost) |
+| `ASYNQMON_PORT` | `8302` | 任务监控面板端口 |
+| `POSTGRES_DSN` | `host=localhost user=fwh_toolbox password=... dbname=fwh_toolbox_db port=5432 sslmode=disable` | 本地 PG |
+| `MONGO_DSN` | `mongodb://127.0.0.1:27017/FwhToolboxDb` | 本地 Mongo |
+| `REDIS_ADDR` | `localhost:6379` | 本地 Redis |
+| `MINIO_ENDPOINT` | `localhost:9198` | MinIO S3 API |
+| `MINIO_BUCKET_NAME` | `fwh-toolbox` | 业务 bucket |
+| `MYSQL_DSN` | 远程只读账号 | FWH 生产库 |
+
+> **MQTT 证书**:把 `cert/pro/` 与 `cert/test/` 下的 CA/证书/私钥放到
+> `server/cert/` 对应目录(路径与 `config.env` 中 `FWH_MQTT_*` 保持相对一致)。
+> 证书文件属敏感凭据,请勿提交到仓库。
+
+---
+
+## 7. 构建与部署后端
+
+### 7.1 手动方式
+
+```bash
+cd service
+GOOS=linux GOARCH=amd64 go build -o fwh-toolbox-server ./
+GOOS=linux GOARCH=amd64 go build -o fwh-mcp-server ./cmd/mcp/
+
+sudo mkdir -p /var/www/fwh-toolbox/server
+sudo mv fwh-toolbox-server fwh-mcp-server /var/www/fwh-toolbox/server/
+sudo cp config.env /var/www/fwh-toolbox/server/config.env
+# 拷贝证书目录
+sudo cp -r cert /var/www/fwh-toolbox/server/
+```
+
+### 7.2 systemd 服务
+
+创建 `/etc/systemd/system/fwh-toolbox-server.service`:
+
+```ini
+[Unit]
+Description=fwh-toolbox-server service
+After=network.target postgresql.service mongod.service redis-server.service minio.service
+
+[Service]
+ExecStart=/var/www/fwh-toolbox/server/fwh-toolbox-server
+Restart=always
+User=jerry-remote
+WorkingDirectory=/var/www/fwh-toolbox/server
+CPUQuota=200%
+MemoryLimit=2G
+
+[Install]
+WantedBy=multi-user.target
+```
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable --now fwh-toolbox-server
+sudo systemctl status fwh-toolbox-server --no-pager
+```
+
+> `User` 应与配置的 `KIRO_RUN_AS_USER` 一致,并保证该用户对
+> `WorkingDirectory` 及 `cert/`、`function/` 等目录有读写权限。
+
+### 7.3 一键脚本
+
+```bash
+cd deploy/scripts
+chmod +x deploy.sh
+./deploy.sh backend      # 仅后端
+```
+
+---
+
+## 8. 构建与部署前端
+
+前端使用 Vite(rolldown-vite),生产环境变量见 `.env.production`:
+
+- `VITE_PUBLIC_PATH = /fwh-toolbox/`(与反代路径一致)
+- `VITE_API_URL = /fwh-toolbox/api`
+- `VITE_ROUTER_MODE = hash`
+
+### 8.1 手动方式
+
+```bash
+# 仓库根目录
+pnpm install --frozen-lockfile
+pnpm build       # 产物在 ./dist
+
+sudo mkdir -p /var/www/fwh-toolbox/fwh-toolbox
+sudo rm -rf /var/www/fwh-toolbox/fwh-toolbox/*
+sudo cp -rf ./dist/* /var/www/fwh-toolbox/fwh-toolbox/
+```
+
+> `package.json` 中的 `install` 脚本即等价于上面的拷贝操作。
+
+### 8.2 前端静态托管
+
+线上由本地 Web 服务器(Apache,监听 8300)托管该静态目录,再由 Caddy 反代。
+用 Apache 为例,站点根指向 `/var/www/fwh-toolbox/fwh-toolbox`,监听 8300。
+也可直接用 Caddy 的 `file_server` 托管,省去 Apache。
+
+### 8.3 一键脚本
+
+```bash
+cd deploy/scripts
+./deploy.sh frontend     # 仅前端
+# 或 ./deploy.sh all      # 前端 + 后端
+```
+
+---
+
+## 9. 反向代理(Caddy)
+
+安装 Caddy 2:
+
+```bash
+sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
+curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
+  sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
+curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
+  sudo tee /etc/apt/sources.list.d/caddy-stable.list
+sudo apt update && sudo apt install -y caddy
+```
+
+在站点 Caddyfile 中加入以下片段(`你的域名` 替换为实际域名)。前端静态走 8300,
+API 走 8301(与 `VITE_API_URL=/fwh-toolbox/api` 对应):
+
+```caddyfile
+你的域名 {
+    tls internal        # 生产用真实证书时替换为 ACME/DNS 配置
+    encode zstd gzip
+
+    # fwh-toolbox 前端 + API
+    redir /fwh-toolbox /fwh-toolbox/ 301
+    handle /fwh-toolbox/* {
+        # API:/fwh-toolbox/api/* -> 后端 8301(去掉前缀)
+        handle_path /fwh-toolbox/api/* {
+            reverse_proxy localhost:8301
+        }
+        # 其余:前端静态 8300
+        reverse_proxy localhost:8300
+    }
+
+    # MinIO 控制台(可选)
+    redir /fwh-minio /fwh-minio/ 301
+    handle_path /fwh-minio/* {
+        reverse_proxy localhost:9199
+    }
+}
+```
+
+> 线上实际由 Apache 在 8300 托管静态、Caddy 统一入口反代;也可让 Caddy 直接
+> `file_server` 托管 `/var/www/fwh-toolbox/fwh-toolbox`,二选一即可。
+
+重载:
+
+```bash
+sudo systemctl reload caddy
+# 或 caddy reload --config /etc/caddy/Caddyfile
+```
+
+---
+
+## 10. Casdoor 认证服务安装与配置
+
+前后端的登录鉴权由 [Casdoor](https://casdoor.org/) 提供(OAuth/OIDC + JWT)。
+线上以 **Docker** 方式运行 `casbin/casdoor`,容器端口 `8000` 映射到宿主 `8010`,
+后端 PostgreSQL 中有独立的 `casdoor` 数据库存放其数据,Caddy 将 `/casdoor/*`
+反代到 `localhost:8010`。
+
+### 10.1 一键安装(推荐)
+
+```bash
+cd deploy/scripts
+chmod +x install_casdoor.sh
+CASDOOR_PG_PASSWORD='强密码' ./install_casdoor.sh
+```
+
+脚本会:
+
+1. 在本地 PostgreSQL 创建角色 `casdoor` 与数据库 `casdoor`(幂等);
+2. 在 `~/docker/casdoor/` 生成 `docker-compose.yml` 与 `conf/app.conf`;
+3. 首次以 `--createDatabase=true` 建表并写入初始数据;
+4. `docker compose up -d` 启动容器(默认端口 `8010`)。
+
+> 也可在 `setup.sh` 向导中勾选「安装 Casdoor」步骤,本地/远程均支持
+> (远程会自动确保 Docker 已安装)。
+
+可用环境变量:`CASDOOR_DIR`、`CASDOOR_PORT`、`CASDOOR_PG_USER`、
+`CASDOOR_PG_PASSWORD`、`CASDOOR_DB_NAME`、`CASDOOR_ORIGIN`。
+
+### 10.2 PostgreSQL 连接说明(容器 → 宿主机)
+
+Casdoor 在容器内,需通过 `host.docker.internal`(compose 里已配 `host-gateway`)
+访问宿主机 PostgreSQL。确保宿主 PostgreSQL 允许该连接:
+
+```bash
+# postgresql.conf
+listen_addresses = '*'          # 或至少包含 docker 网关地址
+
+# pg_hba.conf 增加一行(Docker 默认网段)
+host  casdoor  casdoor  172.16.0.0/12  scram-sha-256
+
+sudo systemctl reload postgresql
+```
+
+`app.conf` 中的数据源形如:
+
+```ini
+driverName = postgres
+dataSourceName = user=casdoor password=*** host=host.docker.internal port=5432 sslmode=disable dbname=
+dbName = casdoor
+```
+
+### 10.3 手动安装(等价步骤)
+
+```bash
+mkdir -p ~/docker/casdoor/conf && cd ~/docker/casdoor
+# docker-compose.yml
+cat > docker-compose.yml <<'EOF'
+services:
+  casdoor:
+    restart: always
+    image: casbin/casdoor:latest
+    entrypoint: /bin/sh -c './server'
+    ports:
+      - "8010:8000"
+    environment:
+      RUNNING_IN_DOCKER: "true"
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    volumes:
+      - ./conf:/conf/
+EOF
+# conf/app.conf 见 install_casdoor.sh 生成的模板
+docker compose up -d
+```
+
+### 10.4 在 Casdoor 中配置组织与应用
+
+首次访问 `http://<服务器>:8010`(或经反代 `/casdoor`),默认管理员 **admin / 123**
+(登录后请立即改密)。然后:
+
+1. **创建组织**:名称必须为 `organization_franklinwh_toolbox`
+   (与前后端硬编码一致)。
+2. **创建应用**:名称 `app-franklinwh-toolbox`,归属上面的组织。
+   - 记录 **Client ID** 与 **Client Secret**;
+   - 在 **Redirect URLs** 添加前端回调地址:
+     - 生产:`https://<域名>/fwh-toolbox/casdoor/callback`
+     - 测试:`https://<域名>/fwh-toolbox-test/casdoor/callback`
+     (前端 `redirectPath = ${VITE_PUBLIC_PATH}/casdoor/callback`)
+   - 勾选授权模式(Authorization Code)与需要的登录方式。
+3. **获取应用证书**:在应用的 Cert 中查看/下载 X.509 证书(PEM),
+   后端用它验证 JWT 签名。
+
+### 10.5 前后端如何消费 Casdoor
+
+- **前端**(`src/utils/casdoor/casdoor.ts`)硬编码:
+  `serverUrl = /casdoor`、`clientId`、`appName = app-franklinwh-toolbox`、
+  `organizationName = organization_franklinwh_toolbox`、
+  `redirectPath = ${VITE_PUBLIC_PATH}/casdoor/callback`。
+  如更换 Casdoor 应用,需同步修改此文件里的 `ClientId` 并重新构建前端。
+- **后端**(`service/utils/casdoor/casdoor.go`)硬编码:
+  `endpoint`、`clientId`、`clientSecret`、`certificate`、`organizationName`、
+  `applicationName`。更换应用/证书后需修改这些常量并重新构建后端。
+
+> ⚠️ 这些值当前写死在源码中而非 `config.env`。在新环境部署时,
+> 若沿用同一套 Casdoor 应用与证书则无需改动;若新建 Casdoor 应用,
+> 必须更新上述前后端源码常量并重新构建。
+
+### 10.6 反向代理
+
+Caddyfile 中加入(`/casdoor/*` → 容器映射端口 8010):
+
+```caddyfile
+你的域名 {
+    # ... 其他 handle ...
+
+    redir /casdoor /casdoor/ 301
+    handle_path /casdoor/* {
+        reverse_proxy localhost:8010
+    }
+}
+```
+
+`handle_path` 会去掉 `/casdoor` 前缀,与 Casdoor 容器根路径匹配。
+
+### 10.7 验证
+
+```bash
+docker compose -f ~/docker/casdoor/docker-compose.yml ps
+curl -sS http://127.0.0.1:8010/ | head        # 返回 Casdoor 首页
+sudo -u postgres psql -d casdoor -c "\dt" | head   # 应有 casdoor 的数据表
+# 经反代
+curl -skS https://<域名>/casdoor/ | head
+```
+
+---
+
+## 11. 启动、验证与排错
+
+### 11.1 启动顺序
+
+```bash
+sudo systemctl status postgresql mongod redis-server minio --no-pager
+sudo systemctl restart fwh-toolbox-server
+sudo systemctl reload caddy
+```
+
+### 11.2 验证
+
+```bash
+# 后端存活(本机)
+curl -sS http://127.0.0.1:8301/swagger/index.html | head
+# 任务面板
+curl -sS http://127.0.0.1:8302/asynqmon/ | head
+# 前端(经反代)
+curl -skS https://你的域名/fwh-toolbox/ | head
+# PostgreSQL
+sudo -u postgres psql -d fwh_toolbox_db -c "\dt alarm_snapshot.*"
+# MinIO bucket
+mc ls fwhlocal/
+```
+
+### 11.3 日志
+
+```bash
+sudo journalctl -u fwh-toolbox-server -f
+# 项目自带脚本
+service/cmd/showTodayLog.sh
+service/cmd/showTodayErrorLog.sh
+```
+
+### 11.4 常见问题
+
+- **后端启动 panic:database init failed** —— PostgreSQL 未就绪或 `POSTGRES_DSN`
+  错误。确认库/角色已由 `init_pgsql.sql` 创建、密码一致、`sslmode=disable`。
+- **Mongo ping error** —— `mongod` 未运行或 `MONGO_DSN` 主机端口错误。
+- **前端 404 / 资源路径错乱** —— `VITE_PUBLIC_PATH` 必须为 `/fwh-toolbox/`,
+  且反代路径一致。
+- **API 请求 404** —— 检查 Caddy 的 `handle_path /fwh-toolbox/api/*` 是否正确
+  去前缀并指向 8301。
+- **MinIO 上传失败** —— bucket 不存在或 AK/SK 不匹配,用 `mc` 检查。
+
+---
+
+## 12. 附录:一键脚本清单
+
+| 文件 | 作用 |
+| --- | --- |
+| `deploy/DEPLOYMENT.md` | 本部署文档 |
+| `deploy/CHECKLIST.md` | 部署清单(需填写项 + 步骤,可勾选) |
+| `deploy/scripts/setup.sh` | **交互式一键部署**(本地/远程,分步选择与填写) |
+| `deploy/scripts/install_deps.sh` | 检测并(按需)安装 PostgreSQL/MongoDB/Redis/MinIO/mc/Docker(`DETECT_ONLY=1` 仅检测) |
+| `deploy/scripts/install_casdoor.sh` | 以 Docker 部署 Casdoor + 建其 PostgreSQL 库/角色 |
+| `deploy/sql/init_pgsql.sql` | PostgreSQL 从零建角色/库/schema/表(幂等) |
+| `deploy/scripts/init_databases.sh` | 初始化本地数据库(PG 建表 + Mongo/Redis 检查 + MinIO 建桶) |
+| `deploy/scripts/deploy.sh` | 构建并部署前端/后端(`all`/`frontend`/`backend`) |
+| `deploy/config.env.example` | 后端配置模板 |
+| `service/cmd/deploy_toolbox.sh` | 仅构建部署后端主程序(历史脚本) |
+| `service/cmd/deploy_mcp.sh` | 仅构建部署 MCP(历史脚本) |
+
+### 全新服务器最短部署路径
+
+最省事:直接跑交互式向导 `./deploy/scripts/setup.sh`(勾选“安装依赖”“安装 Casdoor”
+“初始化数据库”“部署后端/前端”即可)。以下为等价的分步命令:
+
+```bash
+# 0) 克隆代码
+git clone <repo> FranklinWH-ToolBox && cd FranklinWH-ToolBox
+
+# 1) 检测/安装依赖(数据库引擎 + Docker)
+cd deploy/scripts && chmod +x *.sh
+MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' \
+  ./install_deps.sh postgres mongo redis minio mc docker
+
+# 2) 安装 Casdoor(认证服务,Docker)
+CASDOOR_PG_PASSWORD='强密码' ./install_casdoor.sh
+
+# 3) 初始化数据库(建库/建表/建桶)
+PG_FWH_PASSWORD='强密码' MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' ./init_databases.sh
+
+# 4) 配置后端(回到仓库根目录)
+cd ../..
+cp deploy/config.env.example service/config.env && vim service/config.env   # 填入真实密钥
+mkdir -p service/cert/pro service/cert/test                                 # 放入 MQTT 证书
+
+# 5) 部署前后端
+./deploy/scripts/deploy.sh all
+
+# 6) 配置并重载 Caddy(第 9 章)+ 在 Casdoor 建组织/应用(第 10 章),完成
+```

+ 93 - 0
deploy/config.env.example

@@ -0,0 +1,93 @@
+# =============================================================================
+# FranklinWH Toolbox 后端配置示例
+# 使用方式:复制为 config.env(或 .env)后按需修改,切勿把真实密钥提交到仓库。
+# 读取优先级:./.env  ->  ./config.env ;环境变量可覆盖文件内配置。
+#
+# 【填写说明】带 “# TODO:” 的行是需要你确认或填写的项。
+#   - [必填] 必须改成真实值,否则服务无法正常工作
+#   - [确认] 有默认值,请确认是否符合你的环境
+#   - [可选] 用到对应功能才需要填(Plane / MQTT / 测试环境等)
+# =============================================================================
+
+# ---------- APP ----------
+# TODO: [确认] 运行环境:pro=生产 / dev=开发
+APP_ENV="dev"
+# TODO: [确认] 后端监听端口(仅 localhost)。反向代理 /fwh-toolbox/api 指向此端口。
+APP_PORT="8301"
+# TODO: [确认] Asynqmon 任务监控面板监听端口
+ASYNQMON_PORT="8302"
+
+# ---------- 本地数据库 ----------
+# TODO: [必填] PostgreSQL 连接串——把 password=CHANGE_ME 改为 fwh_toolbox 角色的真实密码
+#             (需与初始化数据库时设置的 PG_FWH_PASSWORD 一致)
+POSTGRES_DSN="host=localhost user=fwh_toolbox password=CHANGE_ME dbname=fwh_toolbox_db port=5432 sslmode=disable"
+# TODO: [确认] MongoDB 连接串——生产用 FwhToolboxDb,开发用 FwhToolboxDbDev
+MONGO_DSN="mongodb://127.0.0.1:27017/FwhToolboxDb"
+# TODO: [确认] Redis 地址(缓存 + Asynq 任务队列)
+REDIS_ADDR="localhost:6379"
+
+# TODO: [必填/可选] 远端只读 MySQL(FWH 生产库 home_energy_storage_server)。
+#       用到站点信息查询功能时必填:替换 USER/PASSWORD/HOST/PORT 为只读账号;不用可留空。
+MYSQL_DSN="USER:PASSWORD@tcp(HOST:PORT)/home_energy_storage_server?charset=utf8mb4&parseTime=True&loc=Local"
+
+# ---------- MinIO(本地对象存储)----------
+# TODO: [确认] MinIO S3 API 地址
+MINIO_ENDPOINT="localhost:9198"
+# TODO: [必填] MinIO Access Key(须与 MinIO 服务实际配置一致)
+MINIO_ACCESS_KEY="CHANGE_ME"
+# TODO: [必填] MinIO Secret Key
+MINIO_SECRET_KEY="CHANGE_ME"
+# TODO: [确认] 业务 bucket 名(需与 init_databases.sh 创建的 bucket 一致)
+MINIO_BUCKET_NAME="fwh-toolbox"
+
+# ---------- FWH API(生产)----------
+# TODO: [确认] FWH 生产 API 域名
+FWH_ENERGY="energy.franklinwh.com"
+FWH_OSS="oss.franklinwh.com"
+# TODO: [必填] FWH 生产账号
+FWH_USER="FMU006"
+# TODO: [必填] FWH 生产账号密码(后端会自动做 MD5,此处填明文)
+FWH_PASSWORD="CHANGE_ME"
+
+# ---------- FWH API(测试,可选)----------
+# TODO: [可选] 测试环境 FWH 账号,仅在需要连测试服时填写
+FWH_TEST_ENERGY="test.franklinwh.com"
+FWH_TEST_OSS="test.franklinwh.com"
+FWH_TEST_USER="SuperAdmin"
+FWH_TEST_PASSWORD="CHANGE_ME"
+
+# ---------- FWH MQTT(生产)----------
+# TODO: [确认] MQTT Broker 地址
+FWH_MQTT_BROKER="tls://a6f63crce3ufk-ats.iot.us-east-2.amazonaws.com:8883"
+# TODO: [必填] 把生产 MQTT 证书放到 server/cert/pro/ 下,并确认下面路径正确
+FWH_MQTT_CA="./cert/pro/AmazonRootCA1.pem"
+FWH_MQTT_CERT="./cert/pro/deviceCert.crt"
+FWH_MQTT_KEY="./cert/pro/deviceCert.key"
+
+# ---------- FWH MQTT(测试,可选)----------
+# TODO: [可选] 测试环境 MQTT,用到测试服才需要;证书放 server/cert/test/
+FWH_TEST_MQTT_BROKER="tls://a35sger20yyec4-ats.iot.us-west-2.amazonaws.com:8883"
+FWH_TEST_MQTT_CA="./cert/test/AmazonRootCA1.pem"
+FWH_TEST_MQTT_CERT="./cert/test/01.certificate.pem.crt"
+FWH_TEST_MQTT_KEY="./cert/test/01.private.pem.key"
+
+# ---------- Plane(项目管理,可选)----------
+# TODO: [可选] 仅在启用 Plane 反馈/任务同步功能时填写以下所有 Plane 相关项
+PLANE_HOST="http://localhost:8600"
+PLANE_API_KEY="CHANGE_ME"
+PLANE_WORKSPACE_SLUG="fwh"
+PLANE_PROJECT_ID="CHANGE_ME"
+
+PLANE_WEB_HOST="http://localhost:8600"
+PLANE_WEB_EMAIL="CHANGE_ME"
+PLANE_WEB_PASSWORD="CHANGE_ME"
+PLANE_WEB_WORKSPACE_SLUG="fwh"
+PLANE_WEB_PROJECT_ID="CHANGE_ME"
+PLANE_WEB_LABEL_BUG="CHANGE_ME"
+PLANE_WEB_LABEL_FEATURE="CHANGE_ME"
+PLANE_WEB_LABEL_FEEDBACK="CHANGE_ME"
+
+# ---------- Kiro(AI Chat 子进程降权,可选)----------
+# TODO: [确认] 拉起 kiro-cli 子进程时降权到的系统用户及其 HOME(改成目标机的实际用户)
+KIRO_RUN_AS_USER=jerry-remote
+KIRO_HOME=/home/jerry-remote

+ 131 - 0
deploy/scripts/deploy.sh

@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - 一键构建 & 部署脚本(前端 + 后端)
+# =============================================================================
+# 作用:
+#   1. 构建前端(pnpm build)并同步到静态目录
+#   2. 构建后端主程序 fwh-toolbox-server 与 fwh-mcp-server,安装 systemd 服务
+#
+# 目录约定(与线上服务器保持一致,可用环境变量覆盖):
+#   DEPLOY_ROOT   /var/www/fwh-toolbox
+#     ├── fwh-toolbox/   前端静态文件(构建产物 dist/*)
+#     └── server/        后端二进制 + config.env + cert/
+#
+# 用法:
+#   ./deploy.sh all        # 前端 + 后端(默认)
+#   ./deploy.sh frontend   # 仅前端
+#   ./deploy.sh backend    # 仅后端
+#
+# 前置条件:
+#   - 已安装 Go、Node、pnpm(见 docs/DEPLOYMENT.md)
+#   - service/config.env 已配置好(首次部署请从 config.env.example 复制)
+# =============================================================================
+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"                  # Go 后端目录
+CONFIG_EXAMPLE="${DEPLOY_DIR}/config.env.example"      # 配置模板
+
+DEPLOY_ROOT="${DEPLOY_ROOT:-/var/www/fwh-toolbox}"
+FRONTEND_DIST_DIR="${DEPLOY_ROOT}/fwh-toolbox"
+SERVER_DIR="${DEPLOY_ROOT}/server"
+
+APP_NAME="fwh-toolbox-server"
+MCP_NAME="fwh-mcp-server"
+SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
+RUN_USER="${RUN_USER:-jerry-remote}"
+
+info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
+ok()   { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
+
+# ---------------------------------------------------------------------------
+# 前端
+# ---------------------------------------------------------------------------
+deploy_frontend() {
+    info "构建前端 ..."
+    cd "${PROJECT_ROOT}"
+    pnpm install --frozen-lockfile
+    pnpm build
+    sudo mkdir -p "${FRONTEND_DIST_DIR}"
+    info "同步静态文件到 ${FRONTEND_DIST_DIR} ..."
+    sudo rm -rf "${FRONTEND_DIST_DIR:?}/"*
+    sudo cp -rf ./dist/* "${FRONTEND_DIST_DIR}/"
+    ok "前端部署完成"
+}
+
+# ---------------------------------------------------------------------------
+# 后端
+# ---------------------------------------------------------------------------
+build_backend() {
+    info "构建后端二进制 ..."
+    cd "${SERVICE_DIR}"
+    GOOS=linux GOARCH=amd64 go build -o "${APP_NAME}" ./
+    GOOS=linux GOARCH=amd64 go build -o "${MCP_NAME}" ./cmd/mcp/
+    ok "后端构建完成"
+}
+
+install_service() {
+    if [[ ! -f "${SERVICE_FILE}" ]]; then
+        info "创建 systemd 服务:${SERVICE_FILE}"
+        sudo tee "${SERVICE_FILE}" > /dev/null <<EOF
+[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
+EOF
+        sudo systemctl daemon-reload
+        sudo systemctl enable "${APP_NAME}"
+        ok "systemd 服务已创建并设为开机自启"
+    fi
+}
+
+deploy_backend() {
+    build_backend
+    sudo mkdir -p "${SERVER_DIR}"
+    info "安装二进制到 ${SERVER_DIR} ..."
+    sudo mv "${SERVICE_DIR}/${APP_NAME}" "${SERVER_DIR}/${APP_NAME}"
+    sudo mv "${SERVICE_DIR}/${MCP_NAME}" "${SERVER_DIR}/${MCP_NAME}"
+    sudo chmod +x "${SERVER_DIR}/${APP_NAME}" "${SERVER_DIR}/${MCP_NAME}"
+
+    if [[ ! -f "${SERVER_DIR}/config.env" ]]; then
+        info "未发现 ${SERVER_DIR}/config.env,从示例模板复制(请务必修改其中的密钥)"
+        sudo cp "${CONFIG_EXAMPLE}" "${SERVER_DIR}/config.env"
+    fi
+
+    install_service
+
+    if systemctl is-active --quiet "${APP_NAME}"; then
+        info "重启 ${APP_NAME} ..."
+        sudo systemctl restart "${APP_NAME}"
+    else
+        info "启动 ${APP_NAME} ..."
+        sudo systemctl start "${APP_NAME}"
+    fi
+    sudo systemctl status "${APP_NAME}" --no-pager || true
+    ok "后端部署完成"
+}
+
+main() {
+    local target="${1:-all}"
+    case "${target}" in
+        frontend) deploy_frontend ;;
+        backend)  deploy_backend ;;
+        all)      deploy_frontend; deploy_backend ;;
+        *) echo "用法: $0 [all|frontend|backend]"; exit 1 ;;
+    esac
+    echo "===== 部署结束 ====="
+}
+
+main "$@"

+ 129 - 0
deploy/scripts/init_databases.sh

@@ -0,0 +1,129 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - 数据库从零初始化脚本
+# =============================================================================
+# 作用:在一台全新的 Ubuntu 服务器上,完成本项目依赖的本地数据库初始化:
+#   1. PostgreSQL:创建角色 / 数据库 / schema / alarm_snapshot 表
+#   2. MongoDB   :无需建表(集合首次写入自动创建),此处仅做连通性检查
+#   3. Redis     :仅做连通性检查
+#   4. MinIO     :创建业务 bucket(需要 mc 客户端)
+#
+# 前置条件:相关服务已安装并运行(见 docs/DEPLOYMENT.md 第 3 章)。
+# 本脚本只做“建库建表建桶”,不负责安装数据库软件。
+#
+# 用法:
+#   ./init_databases.sh                     # 使用默认值
+#   PG_FWH_PASSWORD='强密码' ./init_databases.sh
+#
+# 可用环境变量(均有默认值):
+#   PG_FWH_PASSWORD   PostgreSQL 业务角色 fwh_toolbox 的密码 (默认 fwh_toolbox)
+#   MINIO_ALIAS       mc 的 alias 名                        (默认 fwhlocal)
+#   MINIO_ENDPOINT    MinIO S3 API 地址                     (默认 http://localhost:9198)
+#   MINIO_ROOT_USER   MinIO 管理员用户                       (默认 minioadmin)
+#   MINIO_ROOT_PASSWORD MinIO 管理员密码                     (默认 minioadmin)
+#   MINIO_BUCKET      业务 bucket 名                         (默认 fwh-toolbox)
+#   MONGO_URI         Mongo 连接串                           (默认 mongodb://127.0.0.1:27017)
+#   REDIS_HOST/PORT   Redis 地址                             (默认 127.0.0.1:6379)
+# =============================================================================
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SQL_FILE="${SCRIPT_DIR}/../sql/init_pgsql.sql"
+
+# TODO: [必填] PostgreSQL 业务角色 fwh_toolbox 的密码——请传入强口令,勿用默认 fwh_toolbox
+#             (必须与 config.env 里 POSTGRES_DSN 的 password 一致)
+PG_FWH_PASSWORD="${PG_FWH_PASSWORD:-fwh_toolbox}"
+MINIO_ALIAS="${MINIO_ALIAS:-fwhlocal}"
+# TODO: [确认] MinIO S3 API 地址
+MINIO_ENDPOINT="${MINIO_ENDPOINT:-http://localhost:9198}"
+# TODO: [必填] MinIO 管理员账户(建桶用)——传入真实强口令,勿用默认 minioadmin
+MINIO_ROOT_USER="${MINIO_ROOT_USER:-minioadmin}"
+MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-minioadmin}"
+# TODO: [确认] 业务 bucket 名(须与 config.env 的 MINIO_BUCKET_NAME 一致)
+MINIO_BUCKET="${MINIO_BUCKET:-fwh-toolbox}"
+MONGO_URI="${MONGO_URI:-mongodb://127.0.0.1:27017}"
+REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+REDIS_PORT="${REDIS_PORT:-6379}"
+
+info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
+ok()   { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
+warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
+err()  { echo -e "\033[1;31m[FAIL]\033[0m $*" >&2; }
+
+# ---------------------------------------------------------------------------
+# 1. PostgreSQL
+# ---------------------------------------------------------------------------
+init_postgres() {
+    info "初始化 PostgreSQL ..."
+    if ! command -v psql >/dev/null 2>&1; then
+        err "未找到 psql,请先安装 PostgreSQL。"
+        return 1
+    fi
+    if [[ ! -f "${SQL_FILE}" ]]; then
+        err "找不到 SQL 文件:${SQL_FILE}"
+        return 1
+    fi
+    # 以 postgres 超级用户执行;密码作为 psql 变量传入(脚本内部用 %L 转义)
+    sudo -u postgres psql -v ON_ERROR_STOP=1 \
+        -v fwh_pwd="${PG_FWH_PASSWORD}" \
+        -f "${SQL_FILE}"
+    ok "PostgreSQL 初始化完成"
+}
+
+# ---------------------------------------------------------------------------
+# 2. MongoDB(连通性检查)
+# ---------------------------------------------------------------------------
+check_mongo() {
+    info "检查 MongoDB 连通性 ..."
+    if command -v mongosh >/dev/null 2>&1; then
+        mongosh --quiet "${MONGO_URI}" --eval 'db.runCommand({ ping: 1 }).ok' >/dev/null \
+            && ok "MongoDB 可连接(集合会在首次写入时自动创建)" \
+            || warn "MongoDB 连接失败,请检查 mongod 是否运行"
+    else
+        warn "未找到 mongosh,跳过 MongoDB 连通性检查"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 3. Redis(连通性检查)
+# ---------------------------------------------------------------------------
+check_redis() {
+    info "检查 Redis 连通性 ..."
+    if command -v redis-cli >/dev/null 2>&1; then
+        [[ "$(redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" ping 2>/dev/null)" == "PONG" ]] \
+            && ok "Redis 可连接" \
+            || warn "Redis 连接失败,请检查 redis-server 是否运行"
+    else
+        warn "未找到 redis-cli,跳过 Redis 连通性检查"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 4. MinIO bucket
+# ---------------------------------------------------------------------------
+init_minio() {
+    info "初始化 MinIO bucket ..."
+    if ! command -v mc >/dev/null 2>&1; then
+        warn "未找到 mc(MinIO Client),跳过 bucket 创建。"
+        warn "可在 MinIO 控制台(默认 :9199)手动创建 bucket:${MINIO_BUCKET}"
+        return 0
+    fi
+    mc alias set "${MINIO_ALIAS}" "${MINIO_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" >/dev/null
+    if mc ls "${MINIO_ALIAS}/${MINIO_BUCKET}" >/dev/null 2>&1; then
+        ok "MinIO bucket 已存在:${MINIO_BUCKET}"
+    else
+        mc mb "${MINIO_ALIAS}/${MINIO_BUCKET}"
+        ok "MinIO bucket 已创建:${MINIO_BUCKET}"
+    fi
+}
+
+main() {
+    echo "===== FranklinWH Toolbox 数据库初始化 ====="
+    init_postgres
+    check_mongo
+    check_redis
+    init_minio
+    echo "===== 全部完成 ====="
+}
+
+main "$@"

+ 265 - 0
deploy/scripts/install_caddy.sh

@@ -0,0 +1,265 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - Caddy 安装与站点配置脚本
+# =============================================================================
+# 作用:安装 Caddy 2(缺失才装),并为本项目生成/部署一个站点 Caddyfile:
+#   1. 前端静态:/fwh-toolbox/*        -> localhost:${CADDY_FRONTEND_PORT}(8300)
+#   2. 后端 API :/fwh-toolbox/api/*   -> localhost:${CADDY_BACKEND_PORT}(8301)
+#   3. (可选)Casdoor:/casdoor/*      -> localhost:${CADDY_CASDOOR_PORT}(8010)
+#   4. (可选)MinIO 控制台:/fwh-minio/* -> localhost:${CADDY_MINIO_CONSOLE_PORT}(9199)
+# 生成的片段风格参考线上 /etc/caddy/sites/002-lmt.zenpower.cc.caddy。
+#
+# 站点文件默认写入 /etc/caddy/sites/,并确保主 /etc/caddy/Caddyfile 里存在
+# `import sites/*.caddy`(若缺失则自动追加),最后 validate + reload。
+#
+# 前置条件:
+#   - Ubuntu/Debian(apt)。其他发行版请参考 DEPLOYMENT.md 第 9 章手动安装。
+#
+# 环境变量:
+#   CADDY_DOMAIN               对外域名(必填,如 lmt.zenpower.cc)
+#   CADDY_TLS_MODE             TLS 模式:internal | acme | manual   (默认 internal)
+#   CADDY_ACME_EMAIL           acme 模式下的邮箱                     (默认空)
+#   CADDY_TLS_CERT             manual 模式下证书路径                 (默认空)
+#   CADDY_TLS_KEY              manual 模式下私钥路径                 (默认空)
+#   CADDY_SITES_DIR            站点片段目录                          (默认 /etc/caddy/sites)
+#   CADDY_MAIN_FILE            主配置文件                            (默认 /etc/caddy/Caddyfile)
+#   CADDY_FRONTEND_PORT        前端静态端口                          (默认 8300)
+#   CADDY_BACKEND_PORT         后端 API 端口                        (默认 8301)
+#   CADDY_INCLUDE_CASDOOR      是否加入 /casdoor 反代 (y/n)          (默认 n)
+#   CADDY_CASDOOR_PORT         Casdoor 端口                         (默认 8010)
+#   CADDY_INCLUDE_MINIO        是否加入 /fwh-minio 反代 (y/n)        (默认 n)
+#   CADDY_MINIO_CONSOLE_PORT   MinIO 控制台端口                      (默认 9199)
+#   CADDY_SERVE_STATIC         由 Caddy 直接 file_server 托管前端 (y/n) (默认 n)
+#   CADDY_STATIC_ROOT          file_server 的静态根目录              (默认 /var/www/fwh-toolbox/fwh-toolbox)
+# =============================================================================
+set -euo pipefail
+
+CADDY_DOMAIN="${CADDY_DOMAIN:-}"
+CADDY_TLS_MODE="${CADDY_TLS_MODE:-internal}"
+CADDY_ACME_EMAIL="${CADDY_ACME_EMAIL:-}"
+CADDY_TLS_CERT="${CADDY_TLS_CERT:-}"
+CADDY_TLS_KEY="${CADDY_TLS_KEY:-}"
+CADDY_SITES_DIR="${CADDY_SITES_DIR:-/etc/caddy/sites}"
+CADDY_MAIN_FILE="${CADDY_MAIN_FILE:-/etc/caddy/Caddyfile}"
+CADDY_FRONTEND_PORT="${CADDY_FRONTEND_PORT:-8300}"
+CADDY_BACKEND_PORT="${CADDY_BACKEND_PORT:-8301}"
+CADDY_INCLUDE_CASDOOR="${CADDY_INCLUDE_CASDOOR:-n}"
+CADDY_CASDOOR_PORT="${CADDY_CASDOOR_PORT:-8010}"
+CADDY_INCLUDE_MINIO="${CADDY_INCLUDE_MINIO:-n}"
+CADDY_MINIO_CONSOLE_PORT="${CADDY_MINIO_CONSOLE_PORT:-9199}"
+CADDY_SERVE_STATIC="${CADDY_SERVE_STATIC:-n}"
+CADDY_STATIC_ROOT="${CADDY_STATIC_ROOT:-/var/www/fwh-toolbox/fwh-toolbox}"
+
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
+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; }
+
+has() { command -v "$1" >/dev/null 2>&1; }
+
+# ---------------------------------------------------------------------------
+# 1. 安装 Caddy(缺失才装)
+# ---------------------------------------------------------------------------
+ensure_caddy() {
+    if has caddy; then
+        ok "Caddy 已安装:$(caddy version 2>/dev/null | head -n1)"
+        return 0
+    fi
+    info "安装 Caddy 2 ..."
+    if ! has apt-get; then
+        err "当前系统未提供 apt-get,请参考 DEPLOYMENT.md 第 9 章手动安装 Caddy。"
+        return 1
+    fi
+    sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg
+    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
+        | sudo gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
+    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
+        | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
+    sudo apt-get update -y
+    sudo apt-get install -y caddy
+    sudo systemctl enable caddy >/dev/null 2>&1 || true
+    ok "Caddy 安装完成:$(caddy version 2>/dev/null | head -n1)"
+}
+
+# ---------------------------------------------------------------------------
+# 2. 组装 tls 指令块
+# ---------------------------------------------------------------------------
+_tls_block() {
+    case "${CADDY_TLS_MODE}" in
+        internal)
+            printf '%s' "tls internal"
+            ;;
+        acme)
+            if [[ -n "${CADDY_ACME_EMAIL}" ]]; then
+                printf '%s' "tls ${CADDY_ACME_EMAIL}"
+            else
+                # 走全局/默认 ACME,无需站点级 tls 指令
+                printf ''
+            fi
+            ;;
+        manual)
+            if [[ -z "${CADDY_TLS_CERT}" || -z "${CADDY_TLS_KEY}" ]]; then
+                err "TLS 模式=manual 需要同时提供 CADDY_TLS_CERT 与 CADDY_TLS_KEY"
+                return 1
+            fi
+            printf '%s' "tls ${CADDY_TLS_CERT} ${CADDY_TLS_KEY}"
+            ;;
+        *)
+            err "未知 CADDY_TLS_MODE:${CADDY_TLS_MODE}(可选 internal|acme|manual)"
+            return 1
+            ;;
+    esac
+}
+
+# ---------------------------------------------------------------------------
+# 3. 生成站点 Caddyfile
+# ---------------------------------------------------------------------------
+write_site() {
+    [[ -n "${CADDY_DOMAIN}" ]] || { err "CADDY_DOMAIN 不能为空"; return 1; }
+
+    sudo mkdir -p "${CADDY_SITES_DIR}"
+    local site_file="${CADDY_SITES_DIR}/fwh-toolbox.caddy"
+    local tls_directive; tls_directive="$(_tls_block)" || return 1
+    # 组装 tls 行(为空则不输出该行)
+    local tls_line=""
+    [[ -n "${tls_directive}" ]] && tls_line=$'\t'"${tls_directive}"$'\n'
+
+    info "生成站点配置:${site_file}"
+
+    # 前端处理块:file_server 直托管 或 反代到静态端口
+    local frontend_block
+    if [[ "${CADDY_SERVE_STATIC}" == "y" ]]; then
+        frontend_block=$(cat <<EOF
+        # 前端静态:由 Caddy 直接托管
+        reverse_proxy /fwh-toolbox/api/* localhost:${CADDY_BACKEND_PORT}
+        handle_path /fwh-toolbox/* {
+                root * ${CADDY_STATIC_ROOT}
+                try_files {path} /index.html
+                file_server
+        }
+EOF
+)
+    else
+        frontend_block=$(cat <<EOF
+        # fwh-toolbox 前端 + API
+        handle /fwh-toolbox/* {
+                # API:/fwh-toolbox/api/* -> 后端 ${CADDY_BACKEND_PORT}(去掉前缀)
+                handle_path /fwh-toolbox/api/* {
+                        reverse_proxy localhost:${CADDY_BACKEND_PORT}
+                }
+                # 其余:前端静态 ${CADDY_FRONTEND_PORT}
+                reverse_proxy localhost:${CADDY_FRONTEND_PORT}
+        }
+EOF
+)
+    fi
+
+    # 可选:Casdoor
+    local casdoor_block=""
+    if [[ "${CADDY_INCLUDE_CASDOOR}" == "y" ]]; then
+        casdoor_block=$(cat <<EOF
+
+        # casdoor login
+        redir /casdoor /casdoor/ 301
+        handle_path /casdoor/* {
+                reverse_proxy localhost:${CADDY_CASDOOR_PORT}
+        }
+EOF
+)
+    fi
+
+    # 可选:MinIO 控制台
+    local minio_block=""
+    if [[ "${CADDY_INCLUDE_MINIO}" == "y" ]]; then
+        minio_block=$(cat <<EOF
+
+        # minio 控制台
+        redir /fwh-minio /fwh-minio/ 301
+        handle_path /fwh-minio/* {
+                reverse_proxy localhost:${CADDY_MINIO_CONSOLE_PORT}
+        }
+EOF
+)
+    fi
+
+    # 写入站点文件(用 sudo tee,内容经 heredoc 组装)
+    sudo tee "${site_file}" >/dev/null <<EOF
+${CADDY_DOMAIN} {
+${tls_line}	encode zstd gzip
+
+        # 前端 redirect(补斜杠)
+        redir /fwh-toolbox /fwh-toolbox/ 301
+${frontend_block}
+${casdoor_block}
+${minio_block}
+}
+EOF
+    ok "已生成站点配置:${site_file}"
+}
+
+# ---------------------------------------------------------------------------
+# 4. 确保主 Caddyfile 引入 sites/*.caddy
+# ---------------------------------------------------------------------------
+ensure_import() {
+    local import_line="import sites/*.caddy"
+    if [[ ! -f "${CADDY_MAIN_FILE}" ]]; then
+        info "主配置 ${CADDY_MAIN_FILE} 不存在,创建并写入 import 指令"
+        sudo mkdir -p "$(dirname "${CADDY_MAIN_FILE}")"
+        echo "${import_line}" | sudo tee "${CADDY_MAIN_FILE}" >/dev/null
+        return 0
+    fi
+    if sudo grep -qE '^\s*import\s+sites/\*\.caddy' "${CADDY_MAIN_FILE}"; then
+        ok "主配置已包含 import sites/*.caddy"
+    else
+        info "在主配置追加:${import_line}"
+        echo "" | sudo tee -a "${CADDY_MAIN_FILE}" >/dev/null
+        echo "${import_line}" | sudo tee -a "${CADDY_MAIN_FILE}" >/dev/null
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 5. 校验并重载
+# ---------------------------------------------------------------------------
+validate_reload() {
+    info "校验 Caddy 配置 ..."
+    if sudo caddy validate --config "${CADDY_MAIN_FILE}" >/dev/null 2>&1; then
+        ok "配置校验通过"
+    else
+        warn "配置校验未通过,输出详细信息:"
+        sudo caddy validate --config "${CADDY_MAIN_FILE}" || true
+        err "请检查后手动 reload:sudo systemctl reload caddy"
+        return 1
+    fi
+
+    info "重载 Caddy ..."
+    if sudo systemctl reload caddy >/dev/null 2>&1; then
+        ok "Caddy 已重载"
+    else
+        warn "reload 失败,尝试 restart ..."
+        sudo systemctl restart caddy && ok "Caddy 已重启" || { err "Caddy 启动失败,请查看 journalctl -u caddy"; return 1; }
+    fi
+}
+
+main() {
+    info "== 安装/配置 Caddy 反向代理 =="
+    [[ -n "${CADDY_DOMAIN}" ]] || { err "必须提供 CADDY_DOMAIN(对外域名)"; exit 1; }
+    ensure_caddy
+    write_site
+    ensure_import
+    validate_reload
+    cat <<EOF
+
+${c_green}Caddy 反向代理配置完成${c_reset}
+  域名          : ${CADDY_DOMAIN}
+  TLS 模式      : ${CADDY_TLS_MODE}
+  站点文件      : ${CADDY_SITES_DIR}/fwh-toolbox.caddy
+  前端入口      : https://${CADDY_DOMAIN}/fwh-toolbox/
+  后端 API      : https://${CADDY_DOMAIN}/fwh-toolbox/api/ (-> localhost:${CADDY_BACKEND_PORT})
+$( [[ "${CADDY_INCLUDE_CASDOOR}" == "y" ]] && echo "  Casdoor       : https://${CADDY_DOMAIN}/casdoor/ (-> localhost:${CADDY_CASDOOR_PORT})" )
+$( [[ "${CADDY_INCLUDE_MINIO}" == "y" ]] && echo "  MinIO 控制台  : https://${CADDY_DOMAIN}/fwh-minio/ (-> localhost:${CADDY_MINIO_CONSOLE_PORT})" )
+  详情参考      : deploy/DEPLOYMENT.md 第 9 章
+EOF
+}
+
+main "$@"

+ 187 - 0
deploy/scripts/install_casdoor.sh

@@ -0,0 +1,187 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - Casdoor 安装脚本(Docker 方式)
+# =============================================================================
+# 作用:以 Docker Compose 部署 Casdoor(casbin/casdoor),并:
+#   1. 在本地 PostgreSQL 中创建 casdoor 角色与 casdoor 数据库
+#   2. 生成 docker-compose.yml 与 conf/app.conf
+#   3. 首次启动时用 --createDatabase=true 让 Casdoor 建表并写入初始数据
+#   4. 常规启动容器(端口 8010->8000,与线上一致)
+#
+# 前置条件:
+#   - Docker + docker compose 插件已安装(可用 install_deps.sh docker)
+#   - PostgreSQL 已安装并运行(本机 :5432)
+#
+# 环境变量:
+#   CASDOOR_DIR         Casdoor 部署目录        (默认 /home/$USER/docker/casdoor)
+#   CASDOOR_PORT        对外端口(->容器8000)     (默认 8010)
+#   CASDOOR_PG_HOST     PG 主机(容器内视角)      (默认 host.docker.internal)
+#   CASDOOR_PG_PORT     PG 端口                 (默认 5432)
+#   CASDOOR_PG_USER     Casdoor 使用的 PG 角色   (默认 casdoor)
+#   CASDOOR_PG_PASSWORD Casdoor PG 角色密码      (默认 casdoor)
+#   CASDOOR_DB_NAME     Casdoor 数据库名         (默认 casdoor)
+#   CASDOOR_ORIGIN      Casdoor 对外访问地址     (默认 http://localhost:8010)
+# =============================================================================
+set -euo pipefail
+
+CASDOOR_DIR="${CASDOOR_DIR:-$HOME/docker/casdoor}"
+# TODO: [确认] Casdoor 对外端口(默认 8010,需与反向代理 /casdoor 一致)
+CASDOOR_PORT="${CASDOOR_PORT:-8010}"
+CASDOOR_PG_HOST="${CASDOOR_PG_HOST:-host.docker.internal}"
+CASDOOR_PG_PORT="${CASDOOR_PG_PORT:-5432}"
+CASDOOR_PG_USER="${CASDOOR_PG_USER:-casdoor}"
+# TODO: [必填] Casdoor 数据库口令——请通过环境变量传入强口令,勿使用默认 casdoor
+CASDOOR_PG_PASSWORD="${CASDOOR_PG_PASSWORD:-casdoor}"
+CASDOOR_DB_NAME="${CASDOOR_DB_NAME:-casdoor}"
+# TODO: [必填] Casdoor 对外访问地址(origin)——填成实际域名,如 https://<域名>/casdoor
+CASDOOR_ORIGIN="${CASDOOR_ORIGIN:-http://localhost:${CASDOOR_PORT}}"
+
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
+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; }
+
+# docker compose 命令探测(插件 or 独立二进制)
+compose() {
+    if docker compose version >/dev/null 2>&1; then
+        docker compose "$@"
+    elif command -v docker-compose >/dev/null 2>&1; then
+        docker-compose "$@"
+    else
+        err "未找到 docker compose,请先安装(install_deps.sh docker)"; return 1
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 1. PostgreSQL:建角色 + 建库
+# ---------------------------------------------------------------------------
+provision_pg() {
+    info "在 PostgreSQL 中准备 Casdoor 角色与数据库 ..."
+    command -v psql >/dev/null 2>&1 || { err "未找到 psql,请先安装 PostgreSQL"; return 1; }
+
+    # 角色(幂等)
+    sudo -u postgres psql -v ON_ERROR_STOP=1 \
+        -v role="${CASDOOR_PG_USER}" -v pwd="${CASDOOR_PG_PASSWORD}" <<'SQL'
+SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'role', :'pwd')
+WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'role')
+\gexec
+SQL
+
+    # 数据库(幂等,owner=角色)
+    sudo -u postgres psql -v ON_ERROR_STOP=1 \
+        -v db="${CASDOOR_DB_NAME}" -v role="${CASDOOR_PG_USER}" <<'SQL'
+SELECT format('CREATE DATABASE %I OWNER %I ENCODING ''UTF8''', :'db', :'role')
+WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db')
+\gexec
+SQL
+    ok "PostgreSQL:角色 ${CASDOOR_PG_USER} / 数据库 ${CASDOOR_DB_NAME} 就绪"
+
+    # 允许容器经 host-gateway 连接本机 PG:确保监听地址与 pg_hba 允许
+    warn "请确认 PostgreSQL 允许来自 Docker 网段的连接(listen_addresses 与 pg_hba.conf)。"
+    warn "常见做法:postgresql.conf 设 listen_addresses='*';pg_hba.conf 增加 host ${CASDOOR_DB_NAME} ${CASDOOR_PG_USER} 172.16.0.0/12 scram-sha-256,然后 systemctl reload postgresql。"
+}
+
+# ---------------------------------------------------------------------------
+# 2. 生成 compose 与 app.conf
+# ---------------------------------------------------------------------------
+write_files() {
+    info "生成 Casdoor 部署文件到 ${CASDOOR_DIR} ..."
+    mkdir -p "${CASDOOR_DIR}/conf"
+
+    # 数据源:Casdoor postgres driver 使用 keyword=value DSN;末尾 dbname 由 dbName 指定
+    local dsn="user=${CASDOOR_PG_USER} password=${CASDOOR_PG_PASSWORD} host=${CASDOOR_PG_HOST} port=${CASDOOR_PG_PORT} sslmode=disable dbname="
+
+    cat > "${CASDOOR_DIR}/docker-compose.yml" <<EOF
+services:
+  casdoor:
+    restart: always
+    image: casbin/casdoor:latest
+    entrypoint: /bin/sh -c './server'
+    ports:
+      - "${CASDOOR_PORT}:8000"
+    environment:
+      RUNNING_IN_DOCKER: "true"
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    volumes:
+      - ./conf:/conf/
+EOF
+
+    cat > "${CASDOOR_DIR}/conf/app.conf" <<EOF
+appname = casdoor
+httpport = 8000
+runmode = prod
+copyrequestbody = true
+driverName = postgres
+dataSourceName = ${dsn}
+dbName = ${CASDOOR_DB_NAME}
+tableNamePrefix =
+showSql = false
+redisEndpoint =
+defaultStorageProvider =
+isCloudIntranet = false
+authState = "casdoor"
+verificationCodeTimeout = 10
+initScore = 0
+logPostOnly = true
+isUsernameLowered = false
+origin = ${CASDOOR_ORIGIN}
+originFrontend =
+staticBaseUrl = "https://cdn.casbin.org"
+isDemoMode = false
+batchSize = 100
+enableGzip = true
+ldapServerPort = 389
+radiusServerPort = 1812
+radiusDefaultOrganization = "built-in"
+quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
+logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
+initDataNewOnly = false
+initDataFile = "./init_data.json"
+frontendBaseDir = "../cc_0"
+EOF
+    ok "已生成 docker-compose.yml 与 conf/app.conf"
+}
+
+# ---------------------------------------------------------------------------
+# 3. 首次建表 + 启动
+# ---------------------------------------------------------------------------
+start_casdoor() {
+    cd "${CASDOOR_DIR}"
+
+    # 首次:让 Casdoor 建表并写入初始数据(--createDatabase=true)
+    info "首次初始化 Casdoor 数据表(--createDatabase=true)..."
+    docker run --rm \
+        --add-host host.docker.internal:host-gateway \
+        -v "${CASDOOR_DIR}/conf:/conf/" \
+        casbin/casdoor:latest \
+        /bin/sh -c './server --createDatabase=true' || \
+        warn "初始化返回非零(若为“表已存在”可忽略)"
+
+    info "启动 Casdoor 容器 ..."
+    compose up -d
+    sleep 3
+    compose ps
+    ok "Casdoor 已启动:${CASDOOR_ORIGIN}(默认管理员 admin/123)"
+}
+
+main() {
+    info "== 安装 Casdoor(Docker)=="
+    command -v docker >/dev/null 2>&1 || { err "未找到 docker,请先安装(install_deps.sh docker)"; exit 1; }
+    provision_pg
+    write_files
+    start_casdoor
+    cat <<EOF
+
+${c_green}Casdoor 部署完成${c_reset}
+  目录        : ${CASDOOR_DIR}
+  访问地址    : ${CASDOOR_ORIGIN}
+  管理员      : admin / 123(首次登录后请立即修改)
+  后续配置    : 见 deploy/DEPLOYMENT.md 的《Casdoor 配置》章节
+                (创建组织 organization_franklinwh_toolbox、应用 app-franklinwh-toolbox,
+                 取得 clientId/clientSecret/证书,并配置反向代理 /casdoor)
+EOF
+}
+
+main "$@"

+ 248 - 0
deploy/scripts/install_deps.sh

@@ -0,0 +1,248 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - 依赖检测与安装脚本(Ubuntu)
+# =============================================================================
+# 作用:检测并(按需)安装本项目所需的数据库引擎与运行时组件:
+#   - PostgreSQL 14+
+#   - MongoDB 8.0
+#   - Redis 7.x
+#   - MinIO(对象存储)+ mc(MinIO Client)
+#   - Docker + Docker Compose 插件(Casdoor 需要)
+#   - Go 1.26 / Node 22 + pnpm(仅“构建”时需要,可选)
+#
+# 特点:
+#   * 每个组件都是“先检测,缺失才安装”,可重复执行(幂等)
+#   * 支持仅检测:DETECT_ONLY=1 ./install_deps.sh
+#   * 可指定要处理的组件:./install_deps.sh postgres mongo redis minio docker
+#     不带参数时处理默认集合:postgres mongo redis minio docker mc
+#
+# 环境变量:
+#   MINIO_API_PORT       MinIO S3 API 端口   (默认 9198)
+#   MINIO_CONSOLE_PORT   MinIO 控制台端口     (默认 9199)
+#   MINIO_ROOT_USER      MinIO 管理员用户     (默认 minioadmin)
+#   MINIO_ROOT_PASSWORD  MinIO 管理员密码     (默认 minioadmin)
+#   GO_VERSION           Go 版本             (默认 1.26.4)
+# =============================================================================
+set -euo pipefail
+
+MINIO_API_PORT="${MINIO_API_PORT:-9198}"
+MINIO_CONSOLE_PORT="${MINIO_CONSOLE_PORT:-9199}"
+# TODO: [必填] MinIO 管理员账户——请通过环境变量传入强口令,勿使用默认 minioadmin/minioadmin
+MINIO_ROOT_USER="${MINIO_ROOT_USER:-minioadmin}"
+MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-minioadmin}"
+# TODO: [确认] Go 版本(仅在需要安装 Go 时用到)
+GO_VERSION="${GO_VERSION:-1.26.4}"
+DETECT_ONLY="${DETECT_ONLY:-0}"
+
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
+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; }
+
+has() { command -v "$1" >/dev/null 2>&1; }
+
+# 仅检测时的统一处理:打印状态并返回,跳过安装
+_detect_guard() {
+    # $1=组件名 $2=已安装(0/1) $3=版本串
+    if [[ "$2" == "1" ]]; then
+        ok "$1 已安装:$3"
+        return 0   # 已装 -> 调用方 return
+    fi
+    if [[ "${DETECT_ONLY}" == "1" ]]; then
+        warn "$1 未安装(仅检测模式,跳过安装)"
+        return 0
+    fi
+    return 1        # 未装且需要安装 -> 调用方继续执行安装
+}
+
+apt_update_once() {
+    if [[ -z "${_APT_UPDATED:-}" ]]; then
+        sudo apt-get update -y
+        _APT_UPDATED=1
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# PostgreSQL
+# ---------------------------------------------------------------------------
+ensure_postgres() {
+    local ver=""
+    has psql && ver="$(psql --version 2>/dev/null | awk '{print $3}')"
+    _detect_guard "PostgreSQL" "$([[ -n $ver ]] && echo 1 || echo 0)" "${ver}" && return 0
+    info "安装 PostgreSQL ..."
+    apt_update_once
+    sudo apt-get install -y postgresql postgresql-contrib
+    sudo systemctl enable --now postgresql
+    ok "PostgreSQL 安装完成:$(psql --version | awk '{print $3}')"
+}
+
+# ---------------------------------------------------------------------------
+# MongoDB 8.0
+# ---------------------------------------------------------------------------
+ensure_mongo() {
+    local ver=""
+    has mongod && ver="$(mongod --version 2>/dev/null | awk 'NR==1{print $3}')"
+    _detect_guard "MongoDB" "$([[ -n $ver ]] && echo 1 || echo 0)" "${ver}" && return 0
+    info "安装 MongoDB 8.0 ..."
+    local codename; codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-jammy}")"
+    curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc \
+        | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor --yes
+    echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu ${codename}/mongodb-org/8.0 multiverse" \
+        | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list >/dev/null
+    sudo apt-get update -y
+    sudo apt-get install -y mongodb-org
+    sudo systemctl enable --now mongod
+    ok "MongoDB 安装完成"
+}
+
+# ---------------------------------------------------------------------------
+# Redis
+# ---------------------------------------------------------------------------
+ensure_redis() {
+    local ver=""
+    has redis-server && ver="$(redis-server --version 2>/dev/null | sed -E 's/.*v=([0-9.]+).*/\1/')"
+    _detect_guard "Redis" "$([[ -n $ver ]] && echo 1 || echo 0)" "${ver}" && return 0
+    info "安装 Redis ..."
+    apt_update_once
+    sudo apt-get install -y redis-server
+    sudo systemctl enable --now redis-server
+    ok "Redis 安装完成"
+}
+
+# ---------------------------------------------------------------------------
+# MinIO + mc
+# ---------------------------------------------------------------------------
+ensure_minio() {
+    local ver=""
+    has minio && ver="installed"
+    if _detect_guard "MinIO" "$([[ -n $ver ]] && echo 1 || echo 0)" "$(command -v minio || true)"; then
+        return 0
+    fi
+    info "安装 MinIO(二进制 + systemd)..."
+    sudo wget -q -O /usr/local/bin/minio https://dl.min.io/server/minio/release/linux-amd64/minio
+    sudo chmod +x /usr/local/bin/minio
+    id minio-user >/dev/null 2>&1 || sudo useradd -r minio-user -s /sbin/nologin
+    sudo mkdir -p /var/lib/minio/data
+    sudo chown -R minio-user:minio-user /var/lib/minio
+
+    sudo tee /etc/default/minio >/dev/null <<EOF
+MINIO_VOLUMES="/var/lib/minio/data"
+MINIO_OPTS="--address :${MINIO_API_PORT} --console-address :${MINIO_CONSOLE_PORT}"
+MINIO_ROOT_USER="${MINIO_ROOT_USER}"
+MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD}"
+EOF
+
+    sudo tee /etc/systemd/system/minio.service >/dev/null <<'EOF'
+[Unit]
+Description=MinIO
+Documentation=https://docs.min.io
+Wants=network-online.target
+After=network-online.target
+AssertFileIsExecutable=/usr/local/bin/minio
+
+[Service]
+Type=notify
+WorkingDirectory=/usr/local
+User=minio-user
+Group=minio-user
+EnvironmentFile=-/etc/default/minio
+ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
+Restart=always
+LimitNOFILE=1048576
+TasksMax=infinity
+OOMScoreAdjust=-1000
+
+[Install]
+WantedBy=multi-user.target
+EOF
+    sudo systemctl daemon-reload
+    sudo systemctl enable --now minio
+    ok "MinIO 安装完成(API :${MINIO_API_PORT} / 控制台 :${MINIO_CONSOLE_PORT})"
+}
+
+ensure_mc() {
+    local ver=""
+    has mc && ver="installed"
+    _detect_guard "mc(MinIO Client)" "$([[ -n $ver ]] && echo 1 || echo 0)" "$(command -v mc || true)" && return 0
+    info "安装 mc(MinIO Client)..."
+    sudo wget -q -O /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc
+    sudo chmod +x /usr/local/bin/mc
+    ok "mc 安装完成"
+}
+
+# ---------------------------------------------------------------------------
+# Docker + Compose 插件(Casdoor 需要)
+# ---------------------------------------------------------------------------
+ensure_docker() {
+    local ver=""
+    has docker && ver="$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ,)"
+    _detect_guard "Docker" "$([[ -n $ver ]] && echo 1 || echo 0)" "${ver}" && return 0
+    info "安装 Docker CE + compose 插件 ..."
+    apt_update_once
+    sudo apt-get install -y ca-certificates curl gnupg
+    sudo install -m 0755 -d /etc/apt/keyrings
+    curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
+        | sudo gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
+    sudo chmod a+r /etc/apt/keyrings/docker.gpg
+    local codename; codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-jammy}")"
+    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${codename} stable" \
+        | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
+    sudo apt-get update -y
+    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+    sudo systemctl enable --now docker
+    ok "Docker 安装完成:$(docker --version)"
+}
+
+# ---------------------------------------------------------------------------
+# Go / Node(仅构建时需要)
+# ---------------------------------------------------------------------------
+ensure_go() {
+    local ver=""
+    has go && ver="$(go version 2>/dev/null | awk '{print $3}')"
+    _detect_guard "Go" "$([[ -n $ver ]] && echo 1 || echo 0)" "${ver}" && return 0
+    info "安装 Go ${GO_VERSION} ..."
+    wget -q "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -O /tmp/go.tgz
+    sudo rm -rf /usr/local/go
+    sudo tar -C /usr/local -xzf /tmp/go.tgz
+    rm -f /tmp/go.tgz
+    echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh >/dev/null
+    export PATH="$PATH:/usr/local/go/bin"
+    ok "Go 安装完成:$(go version | awk '{print $3}')"
+}
+
+ensure_node() {
+    local ver=""
+    has node && ver="$(node -v 2>/dev/null)"
+    _detect_guard "Node.js" "$([[ -n $ver ]] && echo 1 || echo 0)" "${ver}" && { has pnpm || { [[ ${DETECT_ONLY} == 1 ]] || { sudo npm install -g pnpm@8; ok "pnpm 安装完成"; }; }; return 0; }
+    info "安装 Node.js 22 + pnpm ..."
+    curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
+    sudo apt-get install -y nodejs
+    sudo npm install -g pnpm@8
+    ok "Node/pnpm 安装完成:$(node -v) / pnpm $(pnpm -v)"
+}
+
+# ---------------------------------------------------------------------------
+# 主流程
+# ---------------------------------------------------------------------------
+main() {
+    local comps=("$@")
+    [[ ${#comps[@]} -eq 0 ]] && comps=(postgres mongo redis minio mc docker)
+    [[ "${DETECT_ONLY}" == "1" ]] && info "== 仅检测模式 ==" || info "== 检测并安装缺失组件 =="
+    for c in "${comps[@]}"; do
+        case "${c}" in
+            postgres|pg) ensure_postgres ;;
+            mongo|mongodb) ensure_mongo ;;
+            redis) ensure_redis ;;
+            minio) ensure_minio ;;
+            mc) ensure_mc ;;
+            docker) ensure_docker ;;
+            go) ensure_go ;;
+            node) ensure_node ;;
+            *) warn "未知组件:${c}(可选:postgres mongo redis minio mc docker go node)" ;;
+        esac
+    done
+    ok "依赖处理完成"
+}
+
+main "$@"

+ 192 - 0
deploy/scripts/install_kiro_cli.sh

@@ -0,0 +1,192 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - Kiro CLI 安装脚本
+# =============================================================================
+# 作用:为后端 AI 对话功能安装 kiro-cli。后端通过 ACP 协议以子进程方式拉起
+#   `kiro-cli`,并降权到 config.env 中的 KIRO_RUN_AS_USER(HOME=KIRO_HOME)。
+#   因此 kiro-cli 必须:
+#     1. 安装到可被该用户执行的位置(默认装到该用户的 ~/.local/bin);
+#     2. 该用户完成 `kiro-cli login`(AWS Builder ID,交互式)后方可使用。
+#
+# 安装方式(优先级):
+#   1. ZIP 安装包(离线/可控,默认):下载官方 kirocli-<arch>-linux[-musl].zip,
+#      解压后以目标用户身份运行 install.sh --no-confirm。
+#   2. 官方一键脚本(KIRO_INSTALL_METHOD=curl):curl -fsSL https://cli.kiro.dev/install | bash
+#
+# 前置条件:curl、unzip(缺失会尝试用 apt 安装)
+#
+# 环境变量:
+#   KIRO_RUN_AS_USER      安装并运行 kiro-cli 的系统用户 (默认当前用户)
+#   KIRO_HOME             该用户 HOME(决定 ~/.kiro 配置位置) (默认该用户家目录)
+#   KIRO_INSTALL_METHOD   zip | curl                          (默认 zip)
+#   KIRO_LIBC             gnu | musl(zip 方式的 C 库变体)     (默认 gnu)
+#   KIRO_ZIP_URL          自定义 ZIP 下载地址(覆盖自动推断)   (默认空)
+#   KIRO_DO_LOGIN         安装后是否立即交互式登录 (y/n)        (默认 n)
+# =============================================================================
+set -euo pipefail
+
+KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-$(id -un)}"
+KIRO_HOME="${KIRO_HOME:-}"
+KIRO_INSTALL_METHOD="${KIRO_INSTALL_METHOD:-zip}"
+KIRO_LIBC="${KIRO_LIBC:-gnu}"
+KIRO_ZIP_URL="${KIRO_ZIP_URL:-}"
+KIRO_DO_LOGIN="${KIRO_DO_LOGIN:-n}"
+
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
+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; }
+
+has() { command -v "$1" >/dev/null 2>&1; }
+
+# 解析目标用户 HOME(KIRO_HOME 优先,否则取该用户 passwd 家目录)
+resolve_home() {
+    if [[ -n "${KIRO_HOME}" ]]; then
+        echo "${KIRO_HOME}"; return 0
+    fi
+    getent passwd "${KIRO_RUN_AS_USER}" | cut -d: -f6
+}
+
+# 以目标用户身份执行命令(设置正确的 HOME/USER),当前用户即目标用户时直接执行
+as_target_user() {
+    local home; home="$(resolve_home)"
+    if [[ "${KIRO_RUN_AS_USER}" == "$(id -un)" ]]; then
+        HOME="${home}" bash -lc "$*"
+    else
+        sudo -u "${KIRO_RUN_AS_USER}" -H env HOME="${home}" bash -lc "$*"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 0. 前置依赖
+# ---------------------------------------------------------------------------
+ensure_prereqs() {
+    local need=()
+    has curl  || need+=(curl)
+    has unzip || need+=(unzip)
+    if [[ ${#need[@]} -gt 0 ]]; then
+        info "安装依赖:${need[*]}"
+        if has apt-get; then
+            sudo apt-get update -y
+            sudo apt-get install -y "${need[@]}"
+        else
+            err "缺少 ${need[*]},且系统无 apt-get,请手动安装后重试"
+            return 1
+        fi
+    fi
+    if ! getent passwd "${KIRO_RUN_AS_USER}" >/dev/null; then
+        err "系统用户不存在:${KIRO_RUN_AS_USER}(请先创建,或修正 KIRO_RUN_AS_USER)"
+        return 1
+    fi
+}
+
+# 已装则跳过(检测目标用户可见的 kiro-cli)
+already_installed() {
+    if as_target_user 'command -v kiro-cli >/dev/null 2>&1'; then
+        local ver; ver="$(as_target_user 'kiro-cli version 2>/dev/null | head -n1' || true)"
+        ok "kiro-cli 已安装(用户 ${KIRO_RUN_AS_USER}):${ver:-unknown}"
+        return 0
+    fi
+    return 1
+}
+
+# 推断 ZIP 下载地址
+resolve_zip_url() {
+    if [[ -n "${KIRO_ZIP_URL}" ]]; then echo "${KIRO_ZIP_URL}"; return 0; fi
+    local arch; arch="$(uname -m)"
+    local platform
+    case "${arch}" in
+        x86_64|amd64)  platform="x86_64-linux" ;;
+        aarch64|arm64) platform="aarch64-linux" ;;
+        *) err "未知架构:${arch}(可用 KIRO_ZIP_URL 手动指定下载地址)"; return 1 ;;
+    esac
+    [[ "${KIRO_LIBC}" == "musl" ]] && platform="${platform}-musl"
+    echo "https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-${platform}.zip"
+}
+
+# ---------------------------------------------------------------------------
+# 1a. ZIP 方式安装
+# ---------------------------------------------------------------------------
+install_via_zip() {
+    local url; url="$(resolve_zip_url)" || return 1
+    info "下载 Kiro CLI 安装包:${url}"
+    local tmp; tmp="$(mktemp -d)"
+    # 以目标用户身份下载/解压/安装,确保产物落到其家目录且属主正确
+    local home; home="$(resolve_home)"
+    sudo chown "${KIRO_RUN_AS_USER}" "${tmp}" 2>/dev/null || true
+    as_target_user "cd '${tmp}' && \
+        curl --proto '=https' --tlsv1.2 -sSf '${url}' -o kirocli.zip && \
+        unzip -q kirocli.zip && \
+        bash ./kirocli/install.sh --no-confirm"
+    rm -rf "${tmp}"
+    ok "Kiro CLI 安装完成(ZIP 方式)"
+}
+
+# ---------------------------------------------------------------------------
+# 1b. 官方一键脚本安装
+# ---------------------------------------------------------------------------
+install_via_curl() {
+    info "使用官方一键脚本安装 Kiro CLI ..."
+    as_target_user "curl -fsSL https://cli.kiro.dev/install | bash"
+    ok "Kiro CLI 安装完成(curl 方式)"
+}
+
+# ---------------------------------------------------------------------------
+# 2. 校验
+# ---------------------------------------------------------------------------
+verify() {
+    info "校验 kiro-cli 可用性(用户 ${KIRO_RUN_AS_USER})..."
+    if as_target_user 'command -v kiro-cli >/dev/null 2>&1'; then
+        local ver; ver="$(as_target_user 'kiro-cli version 2>/dev/null | head -n1' || true)"
+        ok "kiro-cli 可用:${ver:-installed}"
+    else
+        warn "在登录 shell 中未找到 kiro-cli。安装脚本可能已把它装到 ~/.local/bin,"
+        warn "需确保该目录在 PATH 中(安装脚本一般会写入 shell 配置,重登后生效)。"
+        warn "后端拉起 kiro-cli 时依赖 PATH/HOME,请确认 KIRO_HOME 与该用户一致。"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 3. 登录(可选,交互式)
+# ---------------------------------------------------------------------------
+login() {
+    [[ "${KIRO_DO_LOGIN}" == "y" ]] || return 0
+    info "启动 kiro-cli 登录(AWS Builder ID,按提示在浏览器完成认证)..."
+    as_target_user "kiro-cli login" || warn "登录未完成,可稍后以 ${KIRO_RUN_AS_USER} 身份运行 'kiro-cli login'"
+}
+
+main() {
+    info "== 安装 Kiro CLI(AI 对话依赖)=="
+    info "运行用户=${KIRO_RUN_AS_USER}  HOME=$(resolve_home)  方式=${KIRO_INSTALL_METHOD}"
+    ensure_prereqs
+
+    if already_installed; then
+        verify; login
+        return 0
+    fi
+
+    case "${KIRO_INSTALL_METHOD}" in
+        zip)  install_via_zip ;;
+        curl) install_via_curl ;;
+        *) err "未知 KIRO_INSTALL_METHOD:${KIRO_INSTALL_METHOD}(可选 zip|curl)"; exit 1 ;;
+    esac
+
+    verify
+    login
+
+    cat <<EOF
+
+${c_green}Kiro CLI 安装步骤结束${c_reset}
+  运行用户    : ${KIRO_RUN_AS_USER}
+  HOME        : $(resolve_home)
+  配置目录    : $(resolve_home)/.kiro
+  重要后续    :
+    1) 若尚未登录,请以该用户身份运行: sudo -u ${KIRO_RUN_AS_USER} -H kiro-cli login
+       (AWS Builder ID,交互式浏览器认证;远程无浏览器时按提示复制 URL 完成)
+    2) 确认 config.env 的 KIRO_RUN_AS_USER=${KIRO_RUN_AS_USER}、KIRO_HOME 与此处一致
+    3) 确认 kiro-cli 在该用户 PATH 中(一般为 ~/.local/bin),后端方能成功拉起
+EOF
+}
+
+main "$@"

+ 703 - 0
deploy/scripts/setup.sh

@@ -0,0 +1,703 @@
+#!/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"
+    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; }"
+    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
+    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 "$@"

+ 171 - 0
deploy/sql/init_pgsql.sql

@@ -0,0 +1,171 @@
+-- =============================================================================
+-- FranklinWH Toolbox - PostgreSQL 从零初始化脚本
+-- =============================================================================
+-- 作用:
+--   1. 创建业务角色 fwh_toolbox
+--   2. 创建业务数据库 fwh_toolbox_db(owner = fwh_toolbox)
+--   3. 创建 schema:original / alarm_snapshot / public
+--   4. 创建后端“显式管理”的两张表(alarm_snapshot.*)
+--
+-- 说明:
+--   * original schema 下的“站点原始数据表”由后端在运行时按需动态建表
+--     (表名为雪花ID的十六进制串,如 6901c3b1d05017860aaf87e3),
+--     因此本脚本只创建 schema,不预建这些动态表。
+--   * alarm_snapshot 下两张表结构来自线上库 pg_dump,后端不做 AutoMigrate,
+--     需要本脚本预先建好。
+--
+-- 使用方式(作为 postgres 超级用户执行):
+--   sudo -u postgres psql -v fwh_pwd=你的密码 -f init_pgsql.sql
+--   密码直接传入、无需加引号(脚本内部用 %L 做安全转义)。
+--   若不传 fwh_pwd,默认使用 fwh_toolbox(仅供本地开发,请勿用于生产)。
+-- =============================================================================
+
+\set ON_ERROR_STOP on
+
+-- 默认密码(可通过 -v fwh_pwd=xxx 覆盖,无需自行加引号)
+\if :{?fwh_pwd}
+\else
+  \set fwh_pwd fwh_toolbox
+\endif
+
+-- ---------------------------------------------------------------------------
+-- 1. 角色
+--    使用 format(... %L ...) 对密码做安全的字面量转义,支持含特殊字符的密码。
+-- ---------------------------------------------------------------------------
+SELECT format('CREATE ROLE fwh_toolbox LOGIN PASSWORD %L', :'fwh_pwd')
+WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fwh_toolbox')
+\gexec
+
+-- ---------------------------------------------------------------------------
+-- 2. 数据库(owner = fwh_toolbox)
+--    CREATE DATABASE 不能在事务块或 DO 块中执行,用 \gexec 条件创建
+-- ---------------------------------------------------------------------------
+SELECT 'CREATE DATABASE fwh_toolbox_db OWNER fwh_toolbox ENCODING ''UTF8'''
+WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'fwh_toolbox_db')
+\gexec
+
+-- 切换到业务库继续初始化
+\connect fwh_toolbox_db
+
+-- ---------------------------------------------------------------------------
+-- 3. Schema
+-- ---------------------------------------------------------------------------
+CREATE SCHEMA IF NOT EXISTS original       AUTHORIZATION fwh_toolbox;
+CREATE SCHEMA IF NOT EXISTS alarm_snapshot AUTHORIZATION fwh_toolbox;
+-- public schema 默认存在,owner 为 postgres,授予业务角色使用权
+GRANT USAGE, CREATE ON SCHEMA public TO fwh_toolbox;
+
+-- ---------------------------------------------------------------------------
+-- 4. alarm_snapshot 表(后端不做 AutoMigrate,需预建)
+-- ---------------------------------------------------------------------------
+
+-- 4.1 站点告警快照数据
+CREATE TABLE IF NOT EXISTS alarm_snapshot.site_alarm_snapshot_data (
+    id             bigint                          NOT NULL,
+    sn             character varying(32)           NOT NULL,
+    file_record_id bigint                          NOT NULL,
+    log_time       timestamp(0) without time zone  NOT NULL,
+    json_data      jsonb                           NOT NULL,
+    zone_info      text
+);
+
+COMMENT ON TABLE  alarm_snapshot.site_alarm_snapshot_data                IS '站点告警快照数据';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_data.sn             IS '站点SN';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_data.file_record_id IS '文件记录表的id';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_data.log_time       IS '日志记录时间(UTC时间)';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_data.json_data      IS 'json数据(二进制)';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_data.zone_info      IS '时区信息';
+
+-- 自增身份列(幂等:仅在未定义 identity 时添加)
+DO $$
+BEGIN
+    IF NOT EXISTS (
+        SELECT 1 FROM pg_attribute
+        WHERE attrelid = 'alarm_snapshot.site_alarm_snapshot_data'::regclass
+          AND attname  = 'id'
+          AND attidentity <> ''
+    ) THEN
+        ALTER TABLE alarm_snapshot.site_alarm_snapshot_data
+            ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
+                SEQUENCE NAME alarm_snapshot.site_alarm_snapshot_data_id_seq
+                START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1
+            );
+    END IF;
+END$$;
+
+-- 约束(幂等)
+DO $$
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_alarm_snapshot_data_id_pkey') THEN
+        ALTER TABLE ONLY alarm_snapshot.site_alarm_snapshot_data
+            ADD CONSTRAINT site_alarm_snapshot_data_id_pkey PRIMARY KEY (id);
+    END IF;
+    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_alarm_snapshot_data_sn_file_record_id_log_time_unique') THEN
+        ALTER TABLE ONLY alarm_snapshot.site_alarm_snapshot_data
+            ADD CONSTRAINT site_alarm_snapshot_data_sn_file_record_id_log_time_unique
+            UNIQUE (sn, file_record_id, log_time);
+    END IF;
+END$$;
+
+-- 4.2 站点告警快照文件处理记录
+CREATE TABLE IF NOT EXISTS alarm_snapshot.site_alarm_snapshot_file_record (
+    id            bigint                          NOT NULL,
+    code_type     integer,
+    file_name     text                            NOT NULL,
+    file_url      text                            NOT NULL,
+    sn            character varying(32)           NOT NULL,
+    log_time      date                            NOT NULL,
+    start_time    timestamp(0) without time zone  NOT NULL,
+    end_time      timestamp(0) without time zone  NOT NULL,
+    complete_time timestamp(0) without time zone  NOT NULL,
+    state         smallint DEFAULT 0              NOT NULL
+);
+
+COMMENT ON TABLE  alarm_snapshot.site_alarm_snapshot_file_record               IS '站点告警快照的文件处理记录';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_file_record.sn            IS 'site SN';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_file_record.log_time      IS '日志记录日期(format YYYY-MM-DD)';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_file_record.start_time    IS '记录开始时间(format YYYY-MM-DD HH:mm:SS)';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_file_record.end_time      IS '记录结束时间(format YYYY-MM-DD HH:mm:SS)';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_file_record.complete_time IS '记录上传完成时间(format YYYY-MM-DD HH:mm:SS)';
+COMMENT ON COLUMN alarm_snapshot.site_alarm_snapshot_file_record.state         IS '文件分析状态(0-分析中;1-完成;-1-失败)';
+
+DO $$
+BEGIN
+    IF NOT EXISTS (
+        SELECT 1 FROM pg_attribute
+        WHERE attrelid = 'alarm_snapshot.site_alarm_snapshot_file_record'::regclass
+          AND attname  = 'id'
+          AND attidentity <> ''
+    ) THEN
+        ALTER TABLE alarm_snapshot.site_alarm_snapshot_file_record
+            ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
+                SEQUENCE NAME alarm_snapshot.site_alarm_snapshot_file_record_id_seq
+                START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1
+            );
+    END IF;
+END$$;
+
+DO $$
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_alarm_snapshot_file_record_id_pkey') THEN
+        ALTER TABLE ONLY alarm_snapshot.site_alarm_snapshot_file_record
+            ADD CONSTRAINT site_alarm_snapshot_file_record_id_pkey PRIMARY KEY (id);
+    END IF;
+    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'site_alarm_snapshot_file_record_file_url_unique') THEN
+        ALTER TABLE ONLY alarm_snapshot.site_alarm_snapshot_file_record
+            ADD CONSTRAINT site_alarm_snapshot_file_record_file_url_unique UNIQUE (file_url);
+    END IF;
+END$$;
+
+-- ---------------------------------------------------------------------------
+-- 5. 归属与授权
+-- ---------------------------------------------------------------------------
+ALTER TABLE alarm_snapshot.site_alarm_snapshot_data        OWNER TO fwh_toolbox;
+ALTER TABLE alarm_snapshot.site_alarm_snapshot_file_record OWNER TO fwh_toolbox;
+
+GRANT ALL ON ALL TABLES    IN SCHEMA alarm_snapshot TO fwh_toolbox;
+GRANT ALL ON ALL SEQUENCES IN SCHEMA alarm_snapshot TO fwh_toolbox;
+GRANT ALL ON ALL TABLES    IN SCHEMA original       TO fwh_toolbox;
+GRANT ALL ON ALL SEQUENCES IN SCHEMA original       TO fwh_toolbox;
+
+\echo '✅ PostgreSQL 初始化完成:数据库 fwh_toolbox_db / schema original,alarm_snapshot / alarm_snapshot 表已就绪'