Explorar el Código

feat:完善部署脚本

Jerry Liang hace 1 mes
padre
commit
2446668487

+ 29 - 8
deploy/CHECKLIST.md

@@ -19,8 +19,11 @@ grep -rn 'TODO' deploy/scripts deploy/config.env.example
 | # | 项目 | 位置 | 说明 |
 |---|------|------|------|
 | ☐ | **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` | 后端访问对象存储用 |
+| ☐ | **MinIO 管理员账户** | `install_deps.sh` / `provision_minio.sh` 的 `MINIO_ROOT_USER/PASSWORD` | 勿用默认 `minioadmin/minioadmin` |
+| ☐ | **MinIO 业务密钥(后端)** | `config.env:MINIO_ACCESS_KEY / MINIO_SECRET_KEY` + `provision_minio.sh:MINIO_ACCESS_KEY/SECRET_KEY` | 账号 `fwh-toolbox`,写 `fwh-toolbox*` 桶;两处须一致 |
+| ☐ | **MinIO AI 密钥(MCP)** | `config.mcp.env:MINIO_ACCESS_KEY / MINIO_SECRET_KEY` + `provision_minio.sh:MINIO_AI_ACCESS_KEY/SECRET_KEY` | 账号 `fwh-toolbox-ai`,写 `fwh-toolbox-ai*` 桶;默认账号=密码 |
+| ☐ | **postgres 超级用户密码** | `install_pgadmin.sh:POSTGRES_PASSWORD` | 供 pgAdmin4 登录连接;勿用默认占位 |
+| ☐ | **pgAdmin4 登录密码** | `install_pgadmin.sh:PGADMIN_PASSWORD` | Web 登录口令(邮箱 `liang_jlyx@163.com`) |
 | ☐ | **FWH 生产账号** | `config.env:FWH_USER / FWH_PASSWORD` | 密码填明文,后端自动 MD5 |
 | ☐ | **Casdoor 数据库口令** | `install_casdoor.sh:CASDOOR_PG_PASSWORD` | 勿用默认 `casdoor` |
 
@@ -36,6 +39,7 @@ grep -rn 'TODO' deploy/scripts deploy/config.env.example
 | # | 项目 | 位置 | 说明 |
 |---|------|------|------|
 | ☐ | **对外域名** | Caddyfile 的 `你的域名` | 反向代理入口 |
+| ☐ | **Caddy DNS 凭据** | `install_caddy.sh:LIBDNS_SPACESHIP_APIKEY/APISECRET` | spaceship DNS-01 签证书;写入 systemd 服务,勿提交仓库 |
 | ☐ | **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 |
 
@@ -62,6 +66,8 @@ grep -rn 'TODO' deploy/scripts deploy/config.env.example
 | ☐ | 运行环境 | `dev` | `config.env:APP_ENV`(生产改 `pro`) |
 | ☐ | Mongo 库名 | `FwhToolboxDb` | `config.env:MONGO_DSN`(开发 `FwhToolboxDbDev`) |
 | ☐ | MinIO bucket | `fwh-toolbox` | `config.env:MINIO_BUCKET_NAME`(与建桶一致) |
+| ☐ | MinIO AI bucket | `fwh-toolbox-ai-file` | `provision_minio.sh:MINIO_AI_BUCKET` / `filestore.AIFileBucket` |
+| ☐ | pgAdmin4 端口 | `5480`(仅绑 `127.0.0.1`) | `install_pgadmin.sh:PGADMIN_PORT/PGADMIN_BIND` |
 | ☐ | Kiro 用户/HOME | `jerry-remote` | `config.env:KIRO_RUN_AS_USER/KIRO_HOME` |
 
 ### A6. 可选功能
@@ -99,12 +105,22 @@ grep -rn 'TODO' deploy/scripts deploy/config.env.example
 
 ### 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/`
+- ☐ `PG_FWH_PASSWORD='强口令' deploy/scripts/init_databases.sh`(PG 建表 + Mongo/Redis 检查,不含 MinIO)
+- ☐ 验证:`psql -d fwh_toolbox_db -c "\dt alarm_snapshot.*"`
+- ☐ 创建 MinIO 桶 + 业务账号与策略:`MINIO_ROOT_USER=... MINIO_ROOT_PASSWORD=... MINIO_SECRET_KEY='强口令' MINIO_AI_SECRET_KEY='强口令' deploy/scripts/provision_minio.sh`
+- ☐ 验证:`mc ls fwhlocal/`(桶 `fwh-toolbox`、`fwh-toolbox-ai-file`)、`mc admin user list fwhlocal`(用户 `fwh-toolbox`、`fwh-toolbox-ai`)
+
+### B4.1 pgAdmin4(可选,数据库 Web 管理)
+
+- ☐ `POSTGRES_PASSWORD='强口令' PGADMIN_PASSWORD='强口令' deploy/scripts/install_pgadmin.sh`
+- ☐ 按脚本提示确认 `pg_hba.conf` 允许 Docker 网段以 `scram-sha-256` 连接后 `systemctl reload postgresql`
+- ☐ 访问 `http://127.0.0.1:5480`,用 `liang_jlyx@163.com` 登录,新建 Server 连本机 PG(用户 `postgres`)
+- ☐ 确认未把 `5480` 暴露公网(如需远程,置于反向代理 + 访问控制之后)
 
 ### B5. 配置后端
 
 - ☐ `cp deploy/config.env.example service/config.env`
+- ☐ `cp deploy/config.mcp.env.example service/config.mcp.env`(MCP 用 `fwh-toolbox-ai` 账号)
 - ☐ 按 A 节逐项填写 `config.env`(搜索文件内 `TODO`)
 - ☐ 放置 MQTT 证书到 `service/cert/pro/`(及 `test/`)
 
@@ -116,9 +132,11 @@ grep -rn 'TODO' deploy/scripts deploy/config.env.example
 
 ### B7. 反向代理(Caddy)
 
-- ☐ 安装 Caddy 2
-- ☐ 配置站点:`/fwh-toolbox/*`→静态、`/fwh-toolbox/api/*`→`localhost:8301`、`/casdoor/*`→`localhost:8010`
-- ☐ 把 `你的域名` 替换为实际域名,`systemctl reload caddy`
+- ☐ 部署自编译 Caddy + 生成站点配置:`CADDY_DOMAIN='你的域名' LIBDNS_SPACESHIP_APIKEY='xxx' LIBDNS_SPACESHIP_APISECRET='xxx' deploy/scripts/install_caddy.sh`
+- ☐ 确认二进制 `deploy/bin/caddy` 含 spaceship 插件(脚本会校验),拷至 `/usr/bin/caddy`
+- ☐ 站点:`/fwh-toolbox/*`→静态 8300、`/fwh-toolbox/api/*`→`localhost:8301`、`/casdoor/*`→`localhost:8010`
+- ☐ 校验并重载:`caddy validate --config /etc/caddy/Caddyfile` + `systemctl reload caddy`
+- ☐ 确认 systemd 服务 `/lib/systemd/system/caddy.service` 权限 600(含明文 DNS token)
 
 ### B8. 验证
 
@@ -133,7 +151,10 @@ grep -rn 'TODO' deploy/scripts deploy/config.env.example
 
 ## C. 安全收尾
 
-- ☐ 所有默认口令(minioadmin / casdoor / admin@123 等)均已改为强口令
+- ☐ 所有默认口令(minioadmin / casdoor / admin@123 / pgAdmin 与 postgres 占位口令等)均已改为强口令
 - ☐ `config.env` 权限收紧,且**未**提交到 Git(含密钥/证书)
 - ☐ 数据库/MinIO/Redis 仅监听本地或受信网段
+- ☐ pgAdmin4(`5480`)未暴露公网;postgres 超级用户密码仅用于受控管理
+- ☐ MinIO 业务账号最小权限:后端用 `fwh-toolbox`、MCP 用 `fwh-toolbox-ai`,未用 root 跑业务
+- ☐ Caddy systemd 服务含明文 DNS token,权限 600 且未提交仓库
 - ☐ 备份策略:PostgreSQL(`pg_dump`)、MongoDB(`mongodump`)、MinIO 数据目录

+ 111 - 19
deploy/DEPLOYMENT.md

@@ -29,6 +29,8 @@
       `app-franklinwh-toolbox`,若新建应用需把 clientId/secret/证书同步到源码
       (`service/utils/casdoor/casdoor.go`、`src/utils/casdoor/casdoor.ts`)后重新构建。
 - [ ] **域名** 反向代理 Caddyfile 中的 `你的域名`、Casdoor `origin/回调 URL`。
+- [ ] **Caddy DNS 凭据** `LIBDNS_SPACESHIP_APIKEY/APISECRET`(spaceship DNS-01 签证书用,
+      写入 systemd 服务,勿提交仓库)。
 - [ ] **远端 MySQL** `config.env` 的 `MYSQL_DSN`(用到站点信息查询功能时必填)。
 
 **需确认(有默认值,请核对是否符合你的环境)**
@@ -89,7 +91,7 @@ chmod +x setup.sh
 
 - **本地构建 + 远程安装**:前端/后端在运行脚本的机器上构建,再上传到远程,
   因此**远程主机无需安装 Go/Node**。
-- 脚本**只做建库建表建桶与服务部署**,不自动安装数据库引擎;数据库软件的
+- 脚本**只做建库建表、MinIO 建桶建账号与服务部署**,不自动安装数据库引擎;数据库软件的
   安装仍请参考下文第 3、4 章(可在目标机预先装好 PostgreSQL/MongoDB/Redis/MinIO)。
 - 反向代理(Caddy)需按第 9 章手动配置。
 
@@ -329,6 +331,39 @@ sudo wget -O /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/m
 sudo chmod +x /usr/local/bin/mc
 ```
 
+#### 4.4.1 账号与权限模型
+
+MinIO 采用「一个管理员 + 两个业务账号」的划分,业务账号最小权限、按桶名前缀授权:
+
+| 账号 | 用途 | 对 `fwh-toolbox*` 桶 | 对 `fwh-toolbox-ai*` 桶 | 对其余桶 |
+|------|------|----------------------|-------------------------|----------|
+| `MINIO_ROOT_USER`(root) | 建桶 / 建用户 / 设策略(全局管理) | 全权 | 全权 | 全权 |
+| `fwh-toolbox` | **后端主服务**(`config.env`) | 读写(含删除) | 读写(含删除)※ | 只读 |
+| `fwh-toolbox-ai` | **MCP Server**(`config.mcp.env`) | 只读 | 读写(含删除) | 只读 |
+
+> ※ `fwh-toolbox-ai-file` 同时匹配 `fwh-toolbox*` 与 `fwh-toolbox-ai*` 两个前缀,
+> 因此后端账号 `fwh-toolbox` 对 AI 桶也有写权限——这是前缀通配策略的必然结果。
+> MCP 工具本身仅做只读访问(`list/read object`),只读权限即可满足。
+
+两个业务账号由脚本 `deploy/scripts/provision_minio.sh` 用 root 通过 `mc admin`
+创建(建桶 → 建策略 → 建用户 → 绑定策略,均幂等):
+
+```bash
+MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' \
+MINIO_SECRET_KEY='业务强密码' MINIO_AI_SECRET_KEY='AI强密码' \
+  ./deploy/scripts/provision_minio.sh
+```
+
+对应关系(务必一致):
+
+- 后端 `config.env` 的 `MINIO_ACCESS_KEY/MINIO_SECRET_KEY` = `provision_minio.sh` 的
+  `MINIO_ACCESS_KEY/MINIO_SECRET_KEY`(默认 `fwh-toolbox`);
+- MCP `config.mcp.env` 的 `MINIO_ACCESS_KEY/MINIO_SECRET_KEY` = `provision_minio.sh` 的
+  `MINIO_AI_ACCESS_KEY/MINIO_AI_SECRET_KEY`(默认账号=密码=`fwh-toolbox-ai`)。
+
+> `setup.sh` 会分别生成 `config.env`(后端)与 `config.mcp.env`(MCP)两份配置,
+> MCP 那份的 MinIO 凭据默认使用 `fwh-toolbox-ai`。
+
 ### 4.5 远端 MySQL
 
 **新服务器不需要安装 MySQL**。它连接的是 FranklinWH 的远程生产库
@@ -378,20 +413,37 @@ sudo -u postgres psql -d fwh_toolbox_db -c "\dt alarm_snapshot.*"
 
 ### 5.2 一键初始化所有本地数据库
 
-仓库提供 `deploy/scripts/init_databases.sh`,会完成 PostgreSQL 初始化、
-MongoDB/Redis 连通性检查、MinIO bucket 创建:
+仓库提供 `deploy/scripts/init_databases.sh`,完成 PostgreSQL 初始化与
+MongoDB/Redis 连通性检查(**不涉及 MinIO**):
 
 ```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
 ```
 
+> MinIO 的建桶、业务账号与最小权限策略**全部**由下面的 `provision_minio.sh`
+> 负责,`init_databases.sh` 不再处理 MinIO。
+
+#### 5.2.1 创建 MinIO 业务账号与策略
+
+见 4.4.1 的账号模型。用 root 账户运行(幂等:建桶 → 建策略 → 建用户 → 绑定):
+
+```bash
+cd deploy/scripts
+chmod +x provision_minio.sh
+
+MINIO_ROOT_USER='你的MinIO管理员' MINIO_ROOT_PASSWORD='你的MinIO密码' \
+MINIO_SECRET_KEY='业务账号强密码' \
+MINIO_AI_SECRET_KEY='AI账号强密码' \
+  ./provision_minio.sh
+```
+
+完成后确保:`config.env` 用 `fwh-toolbox`(`MINIO_ACCESS_KEY/SECRET_KEY`),
+`config.mcp.env` 用 `fwh-toolbox-ai`(默认账号=密码=`fwh-toolbox-ai`)。
+
 ### 5.3 MongoDB
 
 无需手动建集合——集合会在后端首次写入时自动创建。生产环境使用库名
@@ -548,23 +600,48 @@ cd deploy/scripts
 
 ## 9. 反向代理(Caddy)
 
-安装 Caddy 2:
+本项目使用**自编译的 Caddy**(内置 `dns.providers.spaceship` 插件,用于 DNS-01 签发
+证书),二进制随仓库提供于 `deploy/bin/caddy`。**不要用 `apt install caddy`**(官方包
+不含 spaceship 插件)。
+
+推荐用脚本一键部署(部署二进制 + systemd 服务 + 生成站点配置):
 
 ```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
+cd deploy/scripts
+chmod +x install_caddy.sh
+CADDY_DOMAIN='你的域名' \
+LIBDNS_SPACESHIP_APIKEY='你的APIKey' \
+LIBDNS_SPACESHIP_APISECRET='你的APISecret' \
+  ./install_caddy.sh
 ```
 
+脚本会:
+
+1. 拷贝 `deploy/bin/caddy` -> `/usr/bin/caddy`,并校验含 spaceship 模块;
+2. 生成 systemd 服务 `/lib/systemd/system/caddy.service`(`User=caddy`、
+   `run --environ`,把 DNS token 写入 `Environment=`,文件权限 600);
+3. 生成站点文件 `/etc/caddy/sites/fwh-toolbox.caddy`(TLS 走 spaceship DNS-01),
+   并确保主 `/etc/caddy/Caddyfile` 含 `import sites/*.caddy`;
+4. `validate` 后 `reload`/`restart`。
+
 在站点 Caddyfile 中加入以下片段(`你的域名` 替换为实际域名)。前端静态走 8300,
 API 走 8301(与 `VITE_API_URL=/fwh-toolbox/api` 对应):
 
+> **配置路径参考**(与 `install_caddy.sh` 一致,线上同此结构):
+> - 主配置:`/etc/caddy/Caddyfile`(含 `import sites/*.caddy`);
+> - 站点片段:`/etc/caddy/sites/fwh-toolbox.caddy`。
+
 ```caddyfile
 你的域名 {
-    tls internal        # 生产用真实证书时替换为 ACME/DNS 配置
+    tls {
+        dns spaceship {
+            api_key    {env.LIBDNS_SPACESHIP_APIKEY}
+            api_secret {env.LIBDNS_SPACESHIP_APISECRET}
+        }
+        propagation_timeout 10m
+        propagation_delay 5m
+        resolvers 8.8.8.8
+    }
     encode zstd gzip
 
     # fwh-toolbox 前端 + API
@@ -586,6 +663,9 @@ API 走 8301(与 `VITE_API_URL=/fwh-toolbox/api` 对应):
 }
 ```
 
+> DNS token 经 systemd `Environment=` 注入,配置里只用 `{env.XXX}` 引用,不写明文。
+> 服务文件含明文 token,权限已设 600,**切勿提交到仓库**。
+
 > 线上实际由 Apache 在 8300 托管静态、Caddy 统一入口反代;也可让 Caddy 直接
 > `file_server` 托管 `/var/www/fwh-toolbox/fwh-toolbox`,二选一即可。
 
@@ -593,7 +673,7 @@ API 走 8301(与 `VITE_API_URL=/fwh-toolbox/api` 对应):
 
 ```bash
 sudo systemctl reload caddy
-# 或 caddy reload --config /etc/caddy/Caddyfile
+# 或 /usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
 ```
 
 ---
@@ -791,7 +871,11 @@ service/cmd/showTodayErrorLog.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/init_databases.sh` | 初始化本地数据库(PG 建表 + Mongo/Redis 连通性检查;不含 MinIO) |
+| `deploy/scripts/provision_minio.sh` | 创建 MinIO 业务账号与最小权限策略(`fwh-toolbox` / `fwh-toolbox-ai`) |
+| `deploy/scripts/install_pgadmin.sh` | 给 `postgres` 设密码 + Docker 部署 Web 版 pgAdmin4 |
+| `deploy/scripts/install_caddy.sh` | 部署自编译 Caddy(spaceship DNS)+ systemd 服务 + 生成站点配置 |
+| `deploy/bin/caddy` | 自编译 Caddy 二进制(内置 `dns.providers.spaceship` 插件) |
 | `deploy/scripts/deploy.sh` | 构建并部署前端/后端(`all`/`frontend`/`backend`) |
 | `deploy/config.env.example` | 后端配置模板 |
 | `service/cmd/deploy_toolbox.sh` | 仅构建部署后端主程序(历史脚本) |
@@ -814,8 +898,12 @@ MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' \
 # 2) 安装 Casdoor(认证服务,Docker)
 CASDOOR_PG_PASSWORD='强密码' ./install_casdoor.sh
 
-# 3) 初始化数据库(建库/建表/建桶)
-PG_FWH_PASSWORD='强密码' MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' ./init_databases.sh
+# 3) 初始化数据库(建库/建表 + Mongo/Redis 检查;不含 MinIO)
+PG_FWH_PASSWORD='强密码' ./init_databases.sh
+
+# 3.1) 创建 MinIO 桶 + 业务账号 + 策略(fwh-toolbox / fwh-toolbox-ai)
+MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' \
+  MINIO_SECRET_KEY='业务强密码' MINIO_AI_SECRET_KEY='AI强密码' ./provision_minio.sh
 
 # 4) 配置后端(回到仓库根目录)
 cd ../..
@@ -825,5 +913,9 @@ mkdir -p service/cert/pro service/cert/test                                 # 
 # 5) 部署前后端
 ./deploy/scripts/deploy.sh all
 
-# 6) 配置并重载 Caddy(第 9 章)+ 在 Casdoor 建组织/应用(第 10 章),完成
+# 6) 部署 Caddy(自编译 + spaceship DNS)
+CADDY_DOMAIN='你的域名' LIBDNS_SPACESHIP_APIKEY='xxx' LIBDNS_SPACESHIP_APISECRET='xxx' \
+  ./deploy/scripts/install_caddy.sh
+
+# 7) 在 Casdoor 建组织/应用(第 10 章),完成
 ```

BIN
deploy/bin/caddy


+ 2 - 2
deploy/config.env.example

@@ -37,9 +37,9 @@ MYSQL_DSN="USER:PASSWORD@tcp(HOST:PORT)/home_energy_storage_server?charset=utf8m
 # TODO: [确认] MinIO S3 API 地址
 MINIO_ENDPOINT="localhost:9198"
 # TODO: [必填] MinIO Access Key(须与 MinIO 服务实际配置一致)
-MINIO_ACCESS_KEY="CHANGE_ME"
+MINIO_ACCESS_KEY="fwh-toolbox"
 # TODO: [必填] MinIO Secret Key
-MINIO_SECRET_KEY="CHANGE_ME"
+MINIO_SECRET_KEY="fwh-toolbox"
 # TODO: [确认] 业务 bucket 名(需与 init_databases.sh 创建的 bucket 一致)
 MINIO_BUCKET_NAME="fwh-toolbox"
 

+ 5 - 2
deploy/config.mcp.env.example

@@ -23,9 +23,12 @@ REDIS_ADDR="localhost:6379"
 MYSQL_DSN="USER:PASSWORD@tcp(HOST:PORT)/home_energy_storage_server?charset=utf8mb4&parseTime=True&loc=Local"
 
 # ---------- MinIO(本地对象存储)----------
+# MCP 使用 AI 专用账号 fwh-toolbox-ai(由 provision_minio.sh 创建):
+#   对 fwh-toolbox-ai* 桶可读写、对其余桶只读;MCP 工具本身仅做只读访问。
+#   须与 provision_minio.sh 的 MINIO_AI_ACCESS_KEY/MINIO_AI_SECRET_KEY 一致。
 MINIO_ENDPOINT="localhost:9198"
-MINIO_ACCESS_KEY="CHANGE_ME"
-MINIO_SECRET_KEY="CHANGE_ME"
+MINIO_ACCESS_KEY="fwh-toolbox-ai"
+MINIO_SECRET_KEY="fwh-toolbox-ai"
 MINIO_BUCKET_NAME="fwh-toolbox"
 
 # ---------- FWH API(生产)----------

+ 7 - 33
deploy/scripts/init_databases.sh

@@ -6,10 +6,11 @@
 #   1. PostgreSQL:创建角色 / 数据库 / schema / alarm_snapshot 表
 #   2. MongoDB   :无需建表(集合首次写入自动创建),此处仅做连通性检查
 #   3. Redis     :仅做连通性检查
-#   4. MinIO     :创建业务 bucket(需要 mc 客户端)
+#
+# MinIO 不在本脚本范围内:建桶 + 业务账号 + 策略统一由 provision_minio.sh 负责。
 #
 # 前置条件:相关服务已安装并运行(见 docs/DEPLOYMENT.md 第 3 章)。
-# 本脚本只做“建库建表建桶”,不负责安装数据库软件。
+# 本脚本只做“建库建表”,不负责安装数据库软件、不处理 MinIO。
 #
 # 用法:
 #   ./init_databases.sh                     # 使用默认值
@@ -17,11 +18,6 @@
 #
 # 可用环境变量(均有默认值):
 #   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)
 # =============================================================================
@@ -30,17 +26,9 @@ 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
+# [必填] PostgreSQL 业务角色 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}"
@@ -99,31 +87,17 @@ check_redis() {
 }
 
 # ---------------------------------------------------------------------------
-# 4. MinIO bucket
+# 4. MinIO
+#    建桶 + 业务账号 + 策略统一由 provision_minio.sh 负责,本脚本不再处理 MinIO。
 # ---------------------------------------------------------------------------
-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 "===== 全部完成 ====="
+    echo "提示:MinIO 建桶 + 业务账号 + 策略请运行 provision_minio.sh"
 }
 
 main "$@"

+ 114 - 73
deploy/scripts/install_caddy.sh

@@ -2,43 +2,52 @@
 # =============================================================================
 # 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。
+# 作用:部署自编译 Caddy(含 spaceship DNS 插件)并配置本项目站点:
+#   1. 拷贝项目内自编译二进制 deploy/bin/caddy -> /usr/bin/caddy
+#   2. 安装/更新 systemd 服务(参考 caddy-new.service:注入 spaceship DNS token)
+#   3. 生成站点 Caddyfile(TLS 走 spaceship DNS-01,风格同线上 002-*.caddy):
+#        - 前端静态:/fwh-toolbox/*        -> localhost:${CADDY_FRONTEND_PORT}(8300)
+#        - 后端 API :/fwh-toolbox/api/*   -> localhost:${CADDY_BACKEND_PORT}(8301)
+#        - (可选)Casdoor:/casdoor/*      -> localhost:${CADDY_CASDOOR_PORT}(8010)
+#        - (可选)MinIO 控制台:/fwh-minio/* -> localhost:${CADDY_MINIO_CONSOLE_PORT}(9199)
 #
 # 站点文件默认写入 /etc/caddy/sites/,并确保主 /etc/caddy/Caddyfile 里存在
 # `import sites/*.caddy`(若缺失则自动追加),最后 validate + reload。
 #
 # 前置条件:
-#   - Ubuntu/Debian(apt)。其他发行版请参考 DEPLOYMENT.md 第 9 章手动安装。
+#   - 项目内已放置自编译 caddy 二进制:deploy/bin/caddy(内置 dns.providers.spaceship)
+#   - DNS 托管在 spaceship,并已申请 API key/secret
 #
 # 环境变量:
 #   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_BIN_SRC             自编译二进制来源路径   (默认 <脚本目录>/../bin/caddy)
+#   CADDY_BIN_DST             目标机二进制安装路径   (默认 /usr/bin/caddy)
+#   CADDY_SERVICE_FILE        systemd 服务文件路径   (默认 /lib/systemd/system/caddy.service)
+#   LIBDNS_SPACESHIP_APIKEY    spaceship DNS API Key(必填,写入 systemd 服务)
+#   LIBDNS_SPACESHIP_APISECRET spaceship DNS API Secret(必填,写入 systemd 服务)
+#   CADDY_DNS_RESOLVER         DNS 传播检查解析器     (默认 8.8.8.8)
+#   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)
+#   CADDY_STATIC_ROOT          file_server 的静态根目录  (默认 /var/www/fwh-toolbox/fwh-toolbox)
 # =============================================================================
 set -euo pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
 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_BIN_SRC="${CADDY_BIN_SRC:-${SCRIPT_DIR}/../bin/caddy}"
+CADDY_BIN_DST="${CADDY_BIN_DST:-/usr/bin/caddy}"
+CADDY_SERVICE_FILE="${CADDY_SERVICE_FILE:-/lib/systemd/system/caddy.service}"
+LIBDNS_SPACESHIP_APIKEY="${LIBDNS_SPACESHIP_APIKEY:-}"
+LIBDNS_SPACESHIP_APISECRET="${LIBDNS_SPACESHIP_APISECRET:-}"
+CADDY_DNS_RESOLVER="${CADDY_DNS_RESOLVER:-8.8.8.8}"
 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}"
@@ -59,57 +68,88 @@ err()  { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; }
 has() { command -v "$1" >/dev/null 2>&1; }
 
 # ---------------------------------------------------------------------------
-# 1. 安装 Caddy(缺失才装)
+# 1. 部署自编译 Caddy 二进制(deploy/bin/caddy -> /usr/bin/caddy)
 # ---------------------------------------------------------------------------
 ensure_caddy() {
-    if has caddy; then
-        ok "Caddy 已安装:$(caddy version 2>/dev/null | head -n1)"
-        return 0
+    if [[ ! -f "${CADDY_BIN_SRC}" ]]; then
+        err "未找到自编译二进制:${CADDY_BIN_SRC}"
+        err "请将含 spaceship 插件的 caddy 放到该路径(见 deploy/bin/caddy)。"
+        return 1
     fi
-    info "安装 Caddy 2 ..."
-    if ! has apt-get; then
-        err "当前系统未提供 apt-get,请参考 DEPLOYMENT.md 第 9 章手动安装 Caddy。"
+    info "部署自编译 Caddy:${CADDY_BIN_SRC} -> ${CADDY_BIN_DST}"
+    # 若正在运行,先停以便覆盖二进制
+    sudo systemctl stop caddy >/dev/null 2>&1 || true
+    sudo install -m 0755 "${CADDY_BIN_SRC}" "${CADDY_BIN_DST}"
+    # 校验必需的 spaceship DNS 模块存在
+    if ! "${CADDY_BIN_DST}" list-modules 2>/dev/null | grep -q '^dns.providers.spaceship$'; then
+        err "已安装的 caddy 缺少 dns.providers.spaceship 模块,无法用 spaceship DNS 签证书。"
         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
+    ok "Caddy 就绪:$(${CADDY_BIN_DST} version 2>/dev/null | head -n1)"
+}
+
+# ---------------------------------------------------------------------------
+# 1b. 安装/更新 systemd 服务(注入 spaceship DNS token)
+#     参考 caddy-new.service:User=caddy,run --environ,Environment 注入 token
+# ---------------------------------------------------------------------------
+install_service() {
+    [[ -n "${LIBDNS_SPACESHIP_APIKEY}" ]]    || { err "缺少 LIBDNS_SPACESHIP_APIKEY";    return 1; }
+    [[ -n "${LIBDNS_SPACESHIP_APISECRET}" ]] || { err "缺少 LIBDNS_SPACESHIP_APISECRET"; return 1; }
+
+    # 确保 caddy 用户存在
+    id caddy >/dev/null 2>&1 || sudo useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin caddy || true
+    sudo mkdir -p /var/log/caddy /var/lib/caddy
+    sudo chown -R caddy:caddy /var/log/caddy /var/lib/caddy 2>/dev/null || true
+
+    info "写入 systemd 服务:${CADDY_SERVICE_FILE}"
+    sudo tee "${CADDY_SERVICE_FILE}" >/dev/null <<EOF
+# caddy.service —— 由 install_caddy.sh 生成(自编译 caddy + spaceship DNS)
+[Unit]
+Description=Caddy
+Documentation=https://caddyserver.com/docs/
+After=network.target network-online.target
+Requires=network-online.target
+
+[Service]
+Environment="LIBDNS_SPACESHIP_APIKEY=${LIBDNS_SPACESHIP_APIKEY}"
+Environment="LIBDNS_SPACESHIP_APISECRET=${LIBDNS_SPACESHIP_APISECRET}"
+Type=notify
+User=caddy
+Group=caddy
+ExecStart=${CADDY_BIN_DST} run --environ --config ${CADDY_MAIN_FILE}
+ExecReload=${CADDY_BIN_DST} reload --config ${CADDY_MAIN_FILE} --force
+TimeoutStopSec=5s
+LimitNOFILE=1048576
+PrivateTmp=true
+ProtectSystem=full
+AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
+
+[Install]
+WantedBy=multi-user.target
+EOF
+    # 服务文件含明文 token,收紧权限
+    sudo chmod 600 "${CADDY_SERVICE_FILE}"
+    sudo systemctl daemon-reload
     sudo systemctl enable caddy >/dev/null 2>&1 || true
-    ok "Caddy 安装完成:$(caddy version 2>/dev/null | head -n1)"
+    ok "systemd 服务已安装(token 已注入,文件权限 600)"
 }
 
 # ---------------------------------------------------------------------------
-# 2. 组装 tls 指令块
+# 2. 组装 tls 指令块(spaceship DNS-01,风格同线上 002-*.caddy)
+#    token 通过 systemd Environment 注入,此处用 {env.XXX} 引用,不写明文。
 # ---------------------------------------------------------------------------
 _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
+    cat <<EOF
+	tls {
+		dns spaceship {
+			api_key    {env.LIBDNS_SPACESHIP_APIKEY}
+			api_secret {env.LIBDNS_SPACESHIP_APISECRET}
+		}
+		propagation_timeout 10m
+		propagation_delay 5m
+		resolvers ${CADDY_DNS_RESOLVER}
+	}
+EOF
 }
 
 # ---------------------------------------------------------------------------
@@ -120,10 +160,7 @@ write_site() {
 
     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'
+    local tls_block; tls_block="$(_tls_block)" || return 1
 
     info "生成站点配置:${site_file}"
 
@@ -186,7 +223,7 @@ EOF
     # 写入站点文件(用 sudo tee,内容经 heredoc 组装)
     sudo tee "${site_file}" >/dev/null <<EOF
 ${CADDY_DOMAIN} {
-${tls_line}	encode zstd gzip
+${tls_block}	encode zstd gzip
 
         # 前端 redirect(补斜杠)
         redir /fwh-toolbox /fwh-toolbox/ 301
@@ -223,11 +260,11 @@ ensure_import() {
 # ---------------------------------------------------------------------------
 validate_reload() {
     info "校验 Caddy 配置 ..."
-    if sudo caddy validate --config "${CADDY_MAIN_FILE}" >/dev/null 2>&1; then
+    if sudo "${CADDY_BIN_DST}" validate --config "${CADDY_MAIN_FILE}" >/dev/null 2>&1; then
         ok "配置校验通过"
     else
         warn "配置校验未通过,输出详细信息:"
-        sudo caddy validate --config "${CADDY_MAIN_FILE}" || true
+        sudo "${CADDY_BIN_DST}" validate --config "${CADDY_MAIN_FILE}" || true
         err "请检查后手动 reload:sudo systemctl reload caddy"
         return 1
     fi
@@ -242,9 +279,10 @@ validate_reload() {
 }
 
 main() {
-    info "== 安装/配置 Caddy 反向代理 =="
+    info "== 部署自编译 Caddy 反向代理 =="
     [[ -n "${CADDY_DOMAIN}" ]] || { err "必须提供 CADDY_DOMAIN(对外域名)"; exit 1; }
     ensure_caddy
+    install_service
     write_site
     ensure_import
     validate_reload
@@ -252,12 +290,15 @@ main() {
 
 ${c_green}Caddy 反向代理配置完成${c_reset}
   域名          : ${CADDY_DOMAIN}
-  TLS 模式      : ${CADDY_TLS_MODE}
+  二进制        : ${CADDY_BIN_DST}(自编译,含 spaceship DNS 插件)
+  systemd 服务  : ${CADDY_SERVICE_FILE}
+  TLS           : spaceship DNS-01(token 经 systemd Environment 注入)
   站点文件      : ${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})" )
+  提示          : systemd 服务含明文 DNS token,已设权限 600;勿提交到仓库。
   详情参考      : deploy/DEPLOYMENT.md 第 9 章
 EOF
 }

+ 2 - 2
deploy/scripts/install_deps.sh

@@ -20,7 +20,7 @@
 # 环境变量:
 #   MINIO_API_PORT       MinIO S3 API 端口   (默认 9198)
 #   MINIO_CONSOLE_PORT   MinIO 控制台端口     (默认 9199)
-#   MINIO_ROOT_USER      MinIO 管理员用户     (默认 minioadmin)
+#   MINIO_ROOT_USER      MinIO 管理员用户     (默认 jerry)
 #   MINIO_ROOT_PASSWORD  MinIO 管理员密码     (默认 minioadmin)
 #   GO_VERSION           Go 版本             (默认 1.26.4)
 # =============================================================================
@@ -29,7 +29,7 @@ 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_USER="${MINIO_ROOT_USER:-jerry}"
 MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-minioadmin}"
 # TODO: [确认] Go 版本(仅在需要安装 Go 时用到)
 GO_VERSION="${GO_VERSION:-1.26.4}"

+ 181 - 0
deploy/scripts/install_pgadmin.sh

@@ -0,0 +1,181 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - pgAdmin4(Web)部署脚本
+# =============================================================================
+# 作用:
+#   1. 给现有 PostgreSQL 超级用户 postgres 设置登录密码(供 pgAdmin 连接用)
+#   2. 以 Docker Compose 部署 Web 版 pgAdmin4(dpage/pgadmin4)
+#   3. 检测 pg_hba.conf / listen_addresses,打印让容器经 TCP 连本机 PG 所需的配置
+#      (本脚本【不】自动修改系统配置文件,仅给出提示,改动交由你确认执行)
+#
+# 前置条件:
+#   - Docker + docker compose 插件已安装(可用 install_deps.sh docker)
+#   - PostgreSQL 已安装并运行(本机 :5432),且当前用户可 sudo -u postgres psql
+#
+# 用法:
+#   POSTGRES_PASSWORD='强密码' PGADMIN_PASSWORD='强密码' ./install_pgadmin.sh
+#
+# 可用环境变量(均有默认值):
+#   POSTGRES_PASSWORD   PostgreSQL 超级用户 postgres 的新密码 (默认 change_me_strong)
+#   PGADMIN_EMAIL       pgAdmin4 Web 登录邮箱                  (默认 liang_jlyx@163.com)
+#   PGADMIN_PASSWORD    pgAdmin4 Web 登录密码                  (默认 change_me_strong)
+#   PGADMIN_PORT        pgAdmin4 对外端口                       (默认 5480)
+#   PGADMIN_BIND        端口绑定地址(默认仅本机,防公网暴露)    (默认 127.0.0.1)
+#   PGADMIN_DIR         pgAdmin 部署目录                        (默认 /home/$USER/docker/pgadmin)
+#   PG_HOST             容器视角的 PG 主机                       (默认 host.docker.internal)
+#   PG_PORT             PG 端口                                 (默认 5432)
+# =============================================================================
+set -euo pipefail
+
+# TODO: [必填] postgres 超级用户新密码——请通过环境变量传入强口令,勿用默认
+POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-change_me_strong}"
+PGADMIN_EMAIL="${PGADMIN_EMAIL:-liang_jlyx@163.com}"
+# TODO: [必填] pgAdmin4 Web 登录密码——请通过环境变量传入强口令,勿用默认
+PGADMIN_PASSWORD="${PGADMIN_PASSWORD:-change_me_strong}"
+PGADMIN_PORT="${PGADMIN_PORT:-5480}"
+PGADMIN_BIND="${PGADMIN_BIND:-127.0.0.1}"
+PGADMIN_DIR="${PGADMIN_DIR:-$HOME/docker/pgadmin}"
+PG_HOST="${PG_HOST:-host.docker.internal}"
+PG_PORT="${PG_PORT:-5432}"
+
+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. 给 postgres 超级用户设置密码
+#    使用 psql 变量 + %L 转义,安全支持含特殊字符的密码。
+# ---------------------------------------------------------------------------
+set_postgres_password() {
+    info "为 PostgreSQL 超级用户 postgres 设置密码 ..."
+    command -v psql >/dev/null 2>&1 || { err "未找到 psql,请先安装 PostgreSQL"; return 1; }
+
+    if [[ "${POSTGRES_PASSWORD}" == "change_me_strong" ]]; then
+        warn "当前使用默认占位密码 change_me_strong,请务必改用强口令后重跑:"
+        warn "  POSTGRES_PASSWORD='你的强口令' ./install_pgadmin.sh"
+    fi
+
+    sudo -u postgres psql -v ON_ERROR_STOP=1 -v pwd="${POSTGRES_PASSWORD}" <<'SQL'
+SELECT format('ALTER ROLE postgres WITH PASSWORD %L', :'pwd')
+\gexec
+SQL
+    ok "postgres 密码已设置"
+}
+
+# ---------------------------------------------------------------------------
+# 2. 检测 pg_hba.conf / listen_addresses(只提示,不自动改)
+# ---------------------------------------------------------------------------
+check_pg_network() {
+    info "检查 PostgreSQL 是否允许容器经 TCP 连接(密码认证)..."
+
+    local hba conf listen
+    hba="$(sudo -u postgres psql -tAc 'SHOW hba_file;' 2>/dev/null || true)"
+    conf="$(sudo -u postgres psql -tAc 'SHOW config_file;' 2>/dev/null || true)"
+    listen="$(sudo -u postgres psql -tAc 'SHOW listen_addresses;' 2>/dev/null || true)"
+
+    info "  hba_file        : ${hba:-未知}"
+    info "  config_file     : ${conf:-未知}"
+    info "  listen_addresses: ${listen:-未知}"
+
+    if [[ "${listen}" != "*" && "${listen}" != *"0.0.0.0"* ]]; then
+        warn "listen_addresses 当前为 '${listen}',容器可能无法经 host-gateway 连入。"
+        warn "如需允许,请在 ${conf:-postgresql.conf} 设置: listen_addresses = '*'"
+    fi
+
+    cat <<EOF
+${c_yellow}[WARN]${c_reset} 若要用 postgres + 密码从 pgAdmin 容器连入,请确认 ${hba:-pg_hba.conf} 中
+       有一条允许 Docker 网段、且认证方式为 scram-sha-256 的 host 记录,例如:
+
+    # 允许 Docker 默认网段以密码方式连接(按需收窄网段)
+    host    all    all    172.16.0.0/12    scram-sha-256
+
+       修改后执行: sudo systemctl reload postgresql
+       (本脚本不会自动修改系统配置文件。)
+EOF
+}
+
+# ---------------------------------------------------------------------------
+# 3. 生成 compose 并启动 pgAdmin4
+# ---------------------------------------------------------------------------
+deploy_pgadmin() {
+    info "生成 pgAdmin4 部署文件到 ${PGADMIN_DIR} ..."
+    mkdir -p "${PGADMIN_DIR}"
+
+    if [[ "${PGADMIN_PASSWORD}" == "change_me_strong" ]]; then
+        warn "pgAdmin 登录密码为默认占位 change_me_strong,请改用强口令:"
+        warn "  PGADMIN_PASSWORD='你的强口令' ./install_pgadmin.sh"
+    fi
+
+    cat > "${PGADMIN_DIR}/docker-compose.yml" <<EOF
+services:
+  pgadmin:
+    image: dpage/pgadmin4:latest
+    restart: always
+    ports:
+      # 默认仅绑定本机,避免公网直接暴露;如需内网访问改 PGADMIN_BIND
+      - "${PGADMIN_BIND}:${PGADMIN_PORT}:80"
+    environment:
+      PGADMIN_DEFAULT_EMAIL: "${PGADMIN_EMAIL}"
+      PGADMIN_DEFAULT_PASSWORD: "${PGADMIN_PASSWORD}"
+      # 关闭主密码,简化单人使用;多人共享请置为 True
+      PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    volumes:
+      - pgadmin_data:/var/lib/pgadmin
+
+volumes:
+  pgadmin_data:
+EOF
+    ok "已生成 docker-compose.yml"
+
+    info "启动 pgAdmin4 容器 ..."
+    ( cd "${PGADMIN_DIR}" && compose up -d )
+    sleep 2
+    ( cd "${PGADMIN_DIR}" && compose ps )
+    ok "pgAdmin4 已启动"
+}
+
+main() {
+    info "== 部署 pgAdmin4(Web)+ 设置 postgres 密码 =="
+    command -v docker >/dev/null 2>&1 || { err "未找到 docker,请先安装(install_deps.sh docker)"; exit 1; }
+
+    set_postgres_password
+    check_pg_network
+    deploy_pgadmin
+
+    cat <<EOF
+
+${c_green}pgAdmin4 部署完成${c_reset}
+  部署目录    : ${PGADMIN_DIR}
+  访问地址    : http://${PGADMIN_BIND}:${PGADMIN_PORT}
+  登录邮箱    : ${PGADMIN_EMAIL}
+  登录密码    : (见环境变量 PGADMIN_PASSWORD)
+
+  在 pgAdmin 里新建 Server 连接本机 PostgreSQL:
+    Host name/address : ${PG_HOST}     # 容器视角,经 host-gateway 连宿主机
+    Port              : ${PG_PORT}
+    Username          : postgres
+    Password          : (见环境变量 POSTGRES_PASSWORD)
+
+  ${c_yellow}安全提示${c_reset}:
+    - postgres 是超级用户,切勿把 ${PGADMIN_PORT} 端口直接暴露到公网;
+      建议置于反向代理(如 Caddy) + 访问控制之后(参考 install_caddy.sh)。
+    - 若连接报认证失败,多为 pg_hba.conf 未启用 scram-sha-256,见上文提示。
+EOF
+}
+
+main "$@"

+ 219 - 0
deploy/scripts/provision_minio.sh

@@ -0,0 +1,219 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - MinIO 业务账号与策略配置脚本
+# =============================================================================
+# 作用(使用 root 管理员账户,通过 mc admin 完成):
+#   1. 创建业务 bucket:fwh-toolbox、fwh-toolbox-ai-file
+#   2. 创建两条自定义策略(基于桶名前缀通配):
+#        - fwh-toolbox-rw    :对 fwh-toolbox*      前缀桶可读写(含删),其余桶只读
+#        - fwh-toolbox-ai-rw :对 fwh-toolbox-ai*   前缀桶可读写(含删),其余桶只读
+#   3. 创建两个业务用户并绑定对应策略:
+#        - fwh-toolbox     (后端主业务,用 MINIO_ACCESS_KEY/MINIO_SECRET_KEY)
+#        - fwh-toolbox-ai  (AI 文件,用 MINIO_AI_ACCESS_KEY/MINIO_AI_SECRET_KEY)
+#
+# 账号职责:
+#   root 管理员   -> 建桶/建用户/设策略(全局管理)
+#   fwh-toolbox   -> 写 fwh-toolbox* 桶,读其余所有桶
+#   fwh-toolbox-ai-> 写 fwh-toolbox-ai* 桶,读其余所有桶
+#   注意:fwh-toolbox-ai-file 同时匹配 fwh-toolbox* 与 fwh-toolbox-ai*,
+#         因此两个用户对该桶均有写权限(前缀通配的必然结果)。
+#
+# 前置条件:
+#   - MinIO 已运行;mc(MinIO Client)已安装(见 install_deps.sh)
+#
+# 用法:
+#   MINIO_ROOT_USER='admin' MINIO_ROOT_PASSWORD='强密码' \
+#   MINIO_ACCESS_KEY='fwh-toolbox' MINIO_SECRET_KEY='强密码' \
+#   MINIO_AI_ACCESS_KEY='fwh-toolbox-ai' MINIO_AI_SECRET_KEY='强密码' \
+#     ./provision_minio.sh
+#
+# 可用环境变量(均有默认值):
+#   MINIO_ALIAS          mc alias 名                          (默认 fwhlocal)
+#   MINIO_ENDPOINT       MinIO S3 API 地址                    (默认 http://localhost:9198)
+#   MINIO_ROOT_USER      MinIO 管理员用户                      (默认 jerry)
+#   MINIO_ROOT_PASSWORD  MinIO 管理员密码                      (默认 minioadmin)
+#   MINIO_ACCESS_KEY     业务用户 AK                           (默认 fwh-toolbox)
+#   MINIO_SECRET_KEY     业务用户 SK                           (默认 fwh-toolbox)
+#   MINIO_AI_ACCESS_KEY  AI 用户 AK                            (默认 fwh-toolbox-ai)
+#   MINIO_AI_SECRET_KEY  AI 用户 SK                            (默认 fwh-toolbox-ai)
+#   MINIO_BUCKET         主业务 bucket 名                       (默认 fwh-toolbox)
+#   MINIO_AI_BUCKET      AI 文件 bucket 名                      (默认 fwh-toolbox-ai-file)
+# =============================================================================
+set -euo pipefail
+
+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:-jerry}"
+MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-minioadmin}"
+# TODO: [必填] 业务用户 AK/SK——须与 config.env 的 MINIO_ACCESS_KEY/MINIO_SECRET_KEY 一致
+MINIO_ACCESS_KEY="${MINIO_ACCESS_KEY:-fwh-toolbox}"
+MINIO_SECRET_KEY="${MINIO_SECRET_KEY:-fwh-toolbox}"
+# TODO: [必填] AI 用户 AK/SK
+MINIO_AI_ACCESS_KEY="${MINIO_AI_ACCESS_KEY:-fwh-toolbox-ai}"
+MINIO_AI_SECRET_KEY="${MINIO_AI_SECRET_KEY:-fwh-toolbox-ai}"
+MINIO_BUCKET="${MINIO_BUCKET:-fwh-toolbox}"
+MINIO_AI_BUCKET="${MINIO_AI_BUCKET:-fwh-toolbox-ai-file}"
+
+# 策略名与前缀(如需改动桶前缀在此调整)
+POLICY_BIZ="fwh-toolbox-rw"
+POLICY_AI="fwh-toolbox-ai-rw"
+PREFIX_BIZ="fwh-toolbox"        # 写权限前缀:fwh-toolbox*
+PREFIX_AI="fwh-toolbox-ai"      # 写权限前缀:fwh-toolbox-ai*
+
+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; }
+
+TMP_DIR="$(mktemp -d)"
+cleanup() { rm -rf "${TMP_DIR}"; }
+trap cleanup EXIT
+
+# ---------------------------------------------------------------------------
+# 0. 预检
+# ---------------------------------------------------------------------------
+precheck() {
+    command -v mc >/dev/null 2>&1 || { err "未找到 mc(MinIO Client),请先安装(install_deps.sh mc)"; exit 1; }
+
+    for kv in \
+        "MINIO_ROOT_PASSWORD:${MINIO_ROOT_PASSWORD}:minioadmin" \
+        "MINIO_SECRET_KEY:${MINIO_SECRET_KEY}:fwh-toolbox" \
+        "MINIO_AI_SECRET_KEY:${MINIO_AI_SECRET_KEY}:fwh-toolbox-ai"; do
+        local name="${kv%%:*}"; local rest="${kv#*:}"; local val="${rest%%:*}"; local def="${rest##*:}"
+        if [[ "${val}" == "${def}" ]]; then
+            warn "${name} 仍为默认弱口令(${def}),生产环境请通过环境变量传入强口令。"
+        fi
+    done
+}
+
+# ---------------------------------------------------------------------------
+# 1. 设置 root alias
+# ---------------------------------------------------------------------------
+set_alias() {
+    info "配置 mc alias(root): ${MINIO_ALIAS} -> ${MINIO_ENDPOINT}"
+    mc alias set "${MINIO_ALIAS}" "${MINIO_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" >/dev/null
+    ok "alias 已配置"
+}
+
+# ---------------------------------------------------------------------------
+# 2. 建桶(幂等)
+# ---------------------------------------------------------------------------
+ensure_buckets() {
+    local b
+    for b in "${MINIO_BUCKET}" "${MINIO_AI_BUCKET}"; do
+        if mc ls "${MINIO_ALIAS}/${b}" >/dev/null 2>&1; then
+            ok "bucket 已存在:${b}"
+        else
+            mc mb "${MINIO_ALIAS}/${b}"
+            ok "bucket 已创建:${b}"
+        fi
+    done
+}
+
+# 生成一条策略 JSON:$1=写前缀 -> 该前缀桶可读写(含删),其余所有桶只读
+# 用法:write_policy_json <prefix> <out_file>
+write_policy_json() {
+    local prefix="$1" out="$2"
+    cat > "${out}" <<JSON
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Sid": "ReadAllBuckets",
+      "Effect": "Allow",
+      "Action": ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"],
+      "Resource": ["arn:aws:s3:::*"]
+    },
+    {
+      "Sid": "WriteOwnPrefixBuckets",
+      "Effect": "Allow",
+      "Action": [
+        "s3:PutObject",
+        "s3:DeleteObject",
+        "s3:AbortMultipartUpload",
+        "s3:ListMultipartUploadParts"
+      ],
+      "Resource": ["arn:aws:s3:::${prefix}*/*"]
+    },
+    {
+      "Sid": "ManageOwnPrefixBuckets",
+      "Effect": "Allow",
+      "Action": ["s3:ListBucketMultipartUploads"],
+      "Resource": ["arn:aws:s3:::${prefix}*"]
+    }
+  ]
+}
+JSON
+}
+
+# ---------------------------------------------------------------------------
+# 3. 创建/更新策略(幂等:先尝试 create,已存在则用最新文件覆盖)
+# ---------------------------------------------------------------------------
+ensure_policy() {
+    local name="$1" file="$2"
+    if mc admin policy info "${MINIO_ALIAS}" "${name}" >/dev/null 2>&1; then
+        info "策略已存在,更新:${name}"
+        # 不同 mc 版本子命令略有差异:优先 create(覆盖),回退 add
+        mc admin policy create "${MINIO_ALIAS}" "${name}" "${file}" >/dev/null 2>&1 \
+            || mc admin policy add "${MINIO_ALIAS}" "${name}" "${file}" >/dev/null
+    else
+        info "创建策略:${name}"
+        mc admin policy create "${MINIO_ALIAS}" "${name}" "${file}" >/dev/null 2>&1 \
+            || mc admin policy add "${MINIO_ALIAS}" "${name}" "${file}" >/dev/null
+    fi
+    ok "策略就绪:${name}"
+}
+
+# ---------------------------------------------------------------------------
+# 4. 创建用户(幂等)并绑定策略
+# ---------------------------------------------------------------------------
+ensure_user() {
+    local ak="$1" sk="$2" policy="$3"
+    if mc admin user info "${MINIO_ALIAS}" "${ak}" >/dev/null 2>&1; then
+        ok "用户已存在:${ak}"
+    else
+        mc admin user add "${MINIO_ALIAS}" "${ak}" "${sk}" >/dev/null
+        ok "用户已创建:${ak}"
+    fi
+    # 绑定策略(幂等:attach 已绑定会报错,忽略即可)
+    mc admin policy attach "${MINIO_ALIAS}" "${policy}" --user "${ak}" >/dev/null 2>&1 \
+        || mc admin policy set "${MINIO_ALIAS}" "${policy}" "user=${ak}" >/dev/null 2>&1 \
+        || true
+    ok "已绑定策略 ${policy} -> 用户 ${ak}"
+}
+
+main() {
+    echo "===== FranklinWH Toolbox MinIO 账号与策略配置 ====="
+    precheck
+    set_alias
+    ensure_buckets
+
+    write_policy_json "${PREFIX_BIZ}" "${TMP_DIR}/${POLICY_BIZ}.json"
+    write_policy_json "${PREFIX_AI}"  "${TMP_DIR}/${POLICY_AI}.json"
+    ensure_policy "${POLICY_BIZ}" "${TMP_DIR}/${POLICY_BIZ}.json"
+    ensure_policy "${POLICY_AI}"  "${TMP_DIR}/${POLICY_AI}.json"
+
+    ensure_user "${MINIO_ACCESS_KEY}"    "${MINIO_SECRET_KEY}"    "${POLICY_BIZ}"
+    ensure_user "${MINIO_AI_ACCESS_KEY}" "${MINIO_AI_SECRET_KEY}" "${POLICY_AI}"
+
+    cat <<EOF
+
+${c_green}MinIO 配置完成${c_reset}
+  Endpoint : ${MINIO_ENDPOINT}
+  Buckets  : ${MINIO_BUCKET}, ${MINIO_AI_BUCKET}
+
+  账号与权限:
+    root(${MINIO_ROOT_USER})           -> 全局管理
+    ${MINIO_ACCESS_KEY}                 -> 策略 ${POLICY_BIZ}:写 ${PREFIX_BIZ}* 桶(含删),读其余所有桶
+    ${MINIO_AI_ACCESS_KEY}              -> 策略 ${POLICY_AI}:写 ${PREFIX_AI}* 桶(含删),读其余所有桶
+
+  ${c_yellow}提示${c_reset}:${MINIO_AI_BUCKET} 同时匹配 ${PREFIX_BIZ}* 与 ${PREFIX_AI}*,
+        故 ${MINIO_ACCESS_KEY} 与 ${MINIO_AI_ACCESS_KEY} 对该桶均可写。
+  请确保 config.env 中 MINIO_ACCESS_KEY/MINIO_SECRET_KEY 与此处业务用户一致。
+EOF
+}
+
+main "$@"

+ 197 - 40
deploy/scripts/setup.sh

@@ -31,6 +31,7 @@ PROJECT_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"         # 仓库根目录
 SERVICE_DIR="${PROJECT_ROOT}/service"
 SQL_FILE="${DEPLOY_DIR}/sql/init_pgsql.sql"
 CONFIG_EXAMPLE="${DEPLOY_DIR}/config.env.example"
+MCP_CONFIG_EXAMPLE="${DEPLOY_DIR}/config.mcp.env.example"
 
 APP_NAME="fwh-toolbox-server"
 MCP_NAME="fwh-mcp-server"
@@ -66,6 +67,23 @@ ask_secret() {
     printf -v "${__var}" '%s' "${__ans}"
 }
 
+# 强制输入密码(不回显、无默认,必须非空且两次一致):ask_secret_required VAR "提示"
+ask_secret_required() {
+    local __var="$1" __prompt="$2" __a __b
+    while :; do
+        read -r -s -p "$(echo -e "${c_bold}${__prompt}${c_reset}: ")" __a; echo
+        if [[ -z "${__a}" ]]; then
+            warn "不能为空,请重新输入。"; continue
+        fi
+        read -r -s -p "$(echo -e "${c_bold}${__prompt}(再输一次确认)${c_reset}: ")" __b; echo
+        if [[ "${__a}" != "${__b}" ]]; then
+            warn "两次输入不一致,请重新输入。"; continue
+        fi
+        break
+    done
+    printf -v "${__var}" '%s' "${__a}"
+}
+
 # 是/否确认:ask_yn "提示" "y|n(默认)"  -> 返回 0=yes 1=no
 ask_yn() {
     local __prompt="$1" __default="${2:-y}" __ans
@@ -83,7 +101,7 @@ require_cmd() {
 # 全局变量(向导填写)
 # ---------------------------------------------------------------------------
 MODE=""                 # local | remote
-DO_DEPS="y"; DO_CASDOOR="n"; DO_INITDB="y"; DO_BACKEND="y"; DO_FRONTEND="y"; DO_GENCONF="y"; DO_CADDY="n"; DO_KIRO="n"
+DO_DEPS="y"; DO_CASDOOR="n"; DO_INITDB="y"; DO_BACKEND="y"; DO_FRONTEND="y"; DO_GENCONF="y"; DO_CADDY="n"; DO_KIRO="n"; DO_PGADMIN="n"; DO_MINIOACCT="y"
 
 # 远程连接
 R_HOST=""; R_PORT="22"; R_USER=""; R_SSH_KEY=""
@@ -101,8 +119,14 @@ PG_HOST="localhost"; PG_PORT="5432"; PG_DB="fwh_toolbox_db"; PG_USER="fwh_toolbo
 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 管理员改强口令
+MINIO_ENDPOINT="localhost:9198"; MINIO_ACCESS_KEY="fwh-toolbox"; MINIO_SECRET_KEY="fwh-toolbox"; MINIO_BUCKET="fwh-toolbox"  # TODO: [确认] MinIO 业务 AK/SK(默认 fwh-toolbox)
+MINIO_AI_ACCESS_KEY="fwh-toolbox-ai"; MINIO_AI_SECRET_KEY="fwh-toolbox-ai"; MINIO_AI_BUCKET="fwh-toolbox-ai-file"  # MCP 用 AI 账号(provision_minio.sh 创建)
+MINIO_ROOT_USER="jerry"; MINIO_ROOT_PASSWORD=""  # TODO: [必填] MinIO 管理员密码向导内强制输入
+
+# pgAdmin4(数据库 Web 管理,可选)
+PGADMIN_PORT="5480"; PGADMIN_BIND="127.0.0.1"; PGADMIN_EMAIL="liang_jlyx@163.com"
+PGADMIN_PASSWORD=""                                      # TODO: [必填] 向导内强制输入
+POSTGRES_ADMIN_PASSWORD=""                               # TODO: [必填] 向导内强制输入
 
 # Casdoor
 CASDOOR_DIR=""; CASDOOR_PORT="8010"
@@ -111,9 +135,7 @@ CASDOOR_ORIGIN=""                                         # TODO: [必填] Casdo
 
 # 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 模式证书/私钥路径
+LIBDNS_SPACESHIP_APIKEY=""; LIBDNS_SPACESHIP_APISECRET="" # TODO: [必填] spaceship DNS API 凭据(向导内输入,写入 systemd 服务)
 CADDY_FRONTEND_PORT="8300"; CADDY_BACKEND_PORT="8301"
 CADDY_INCLUDE_CASDOOR="n"; CADDY_INCLUDE_MINIO="n"
 CADDY_SERVE_STATIC="n"                                   # 由 Caddy 直接 file_server 托管前端
@@ -125,6 +147,7 @@ KIRO_DO_LOGIN="n"                                        # 安装后是否立即
 
 # 生成的配置文件(本地临时路径)
 GEN_CONFIG=""
+GEN_MCP_CONFIG=""
 
 # ---------------------------------------------------------------------------
 # 步骤 0:选择模式与步骤
@@ -157,6 +180,8 @@ wizard_intro() {
     fi
     ask_yn "⑦ 安装并配置 Caddy 反向代理(生成站点 Caddyfile 并 reload)?" "n" && DO_CADDY="y" || DO_CADDY="n"
     ask_yn "⑧ 安装 Kiro CLI(后端 AI 对话依赖,需以运行用户登录 Builder ID)?" "n" && DO_KIRO="y" || DO_KIRO="n"
+    ask_yn "⑨ 部署 pgAdmin4(Docker,数据库 Web 管理,并给 postgres 设密码)?" "n" && DO_PGADMIN="y" || DO_PGADMIN="n"
+    ask_yn "⑩ 创建 MinIO 业务账号与策略(fwh-toolbox / fwh-toolbox-ai)?" "y" && DO_MINIOACCT="y" || DO_MINIOACCT="n"
 }
 
 # ---------------------------------------------------------------------------
@@ -247,10 +272,10 @@ wizard_params() {
         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"
+    if [[ "${DO_INITDB}" == "y" || "${DO_DEPS}" == "y" || "${DO_MINIOACCT}" == "y" ]]; then
+        title "MinIO 管理员账户(安装/建桶/建账号用)"
+        ask MINIO_ROOT_USER "MinIO 管理员用户" "jerry"
+        ask_secret_required MINIO_ROOT_PASSWORD "MinIO 管理员密码(必填)"
     fi
 
     if [[ "${DO_CASDOOR}" == "y" ]]; then
@@ -264,20 +289,33 @@ wizard_params() {
         ask CASDOOR_ORIGIN "Casdoor 对外访问地址(origin)" "http://localhost:${CASDOOR_PORT}"
     fi
 
+    if [[ "${DO_PGADMIN}" == "y" ]]; then
+        title "pgAdmin4(数据库 Web 管理)"
+        ask PGADMIN_PORT "pgAdmin4 对外端口" "5480"
+        ask PGADMIN_BIND "pgAdmin4 绑定地址(默认仅本机,防公网暴露)" "127.0.0.1"
+        ask PGADMIN_EMAIL "pgAdmin4 登录邮箱" "liang_jlyx@163.com"
+        ask_secret_required PGADMIN_PASSWORD "pgAdmin4 登录密码(必填)"
+        ask_secret_required POSTGRES_ADMIN_PASSWORD "postgres 超级用户新密码(供 pgAdmin 连接,必填)"
+    fi
+
+    if [[ "${DO_MINIOACCT}" == "y" ]]; then
+        title "MinIO 业务账号(后端 fwh-toolbox / MCP fwh-toolbox-ai)"
+        # 业务账号:默认 fwh-toolbox;若已在生成 config 时填过则复用,此处不重复询问
+        [[ -n "${MINIO_ACCESS_KEY}" ]] || ask MINIO_ACCESS_KEY "后端业务 Access Key" "fwh-toolbox"
+        [[ -n "${MINIO_SECRET_KEY}" ]] || ask_secret MINIO_SECRET_KEY "后端业务 Secret Key" "fwh-toolbox"
+        # AI 账号(供 MCP 使用)
+        ask MINIO_AI_ACCESS_KEY "AI Access Key(MCP 用)" "fwh-toolbox-ai"
+        ask_secret MINIO_AI_SECRET_KEY "AI Secret Key(MCP 用,默认与账号一致)" "fwh-toolbox-ai"
+        ask MINIO_AI_BUCKET "AI 文件 bucket 名" "fwh-toolbox-ai-file"
+    fi
+
     if [[ "${DO_CADDY}" == "y" ]]; then
         title "Caddy 反向代理"
         ask CADDY_DOMAIN "对外域名 (如 lmt.zenpower.cc)"
         [[ -n "${CADDY_DOMAIN}" ]] || { err "Caddy 域名不能为空"; exit 1; }
-        echo "TLS 模式: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
+        echo "TLS 走 spaceship DNS-01(自编译 caddy 内置该插件),请输入 spaceship API 凭据:"
+        ask_secret_required LIBDNS_SPACESHIP_APIKEY "spaceship API Key(必填)"
+        ask_secret_required LIBDNS_SPACESHIP_APISECRET "spaceship API Secret(必填)"
         ask CADDY_FRONTEND_PORT "前端静态端口" "8300"
         ask CADDY_BACKEND_PORT "后端 API 端口" "${APP_PORT}"
         ask_yn "由 Caddy 直接 file_server 托管前端静态(否则反代到前端端口)?" "n" \
@@ -379,6 +417,70 @@ PY
         sed -i -E "s#^KIRO_RUN_AS_USER=.*#KIRO_RUN_AS_USER=${RUN_USER}#" "${GEN_CONFIG}"
     fi
     ok "已生成配置:${GEN_CONFIG}"
+
+    # -----------------------------------------------------------------------
+    # 单独生成 config.mcp.env(基于 config.mcp.env.example)
+    #   与后端共享库/缓存/MinIO endpoint/MySQL 等,但 MinIO 使用 AI 专用账号
+    #   fwh-toolbox-ai(对 fwh-toolbox-ai* 桶可读写、其余桶只读;MCP 工具仅只读)。
+    # -----------------------------------------------------------------------
+    if [[ -f "${MCP_CONFIG_EXAMPLE}" ]]; then
+        GEN_MCP_CONFIG="$(mktemp /tmp/fwh_config.mcp.env.XXXXXX)"
+        cp "${MCP_CONFIG_EXAMPLE}" "${GEN_MCP_CONFIG}"
+
+        # MCP 侧 MinIO 使用 AI 账号;默认账号=密码=fwh-toolbox-ai(可用环境变量覆盖)
+        local mcp_ak="${MINIO_AI_ACCESS_KEY:-fwh-toolbox-ai}"
+        local mcp_sk="${MINIO_AI_SECRET_KEY:-fwh-toolbox-ai}"
+
+        if command -v python3 >/dev/null 2>&1; then
+            APP_ENV="${APP_ENV}" PG_DSN="${pg_dsn}" MONGO_DSN="${MONGO_DSN}" \
+            REDIS_ADDR="${REDIS_ADDR}" MYSQL_DSN="${MYSQL_DSN}" \
+            MINIO_ENDPOINT="${MINIO_ENDPOINT}" MINIO_ACCESS_KEY="${mcp_ak}" \
+            MINIO_SECRET_KEY="${mcp_sk}" MINIO_BUCKET="${MINIO_BUCKET}" \
+            python3 - "${GEN_MCP_CONFIG}" <<'PY'
+import os, re, sys
+path = sys.argv[1]
+repl = {
+    "APP_ENV":            os.environ["APP_ENV"],
+    "POSTGRES_DSN":       os.environ["PG_DSN"],
+    "MONGO_DSN":          os.environ["MONGO_DSN"],
+    "REDIS_ADDR":         os.environ["REDIS_ADDR"],
+    "MINIO_ENDPOINT":     os.environ["MINIO_ENDPOINT"],
+    "MINIO_ACCESS_KEY":   os.environ["MINIO_ACCESS_KEY"],
+    "MINIO_SECRET_KEY":   os.environ["MINIO_SECRET_KEY"],
+    "MINIO_BUCKET_NAME":  os.environ["MINIO_BUCKET"],
+}
+mysql = os.environ.get("MYSQL_DSN", "")
+if mysql:
+    repl["MYSQL_DSN"] = mysql
+
+lines = open(path, encoding="utf-8").read().splitlines()
+out = []
+for line in lines:
+    m = re.match(r'^(\s*)([A-Z0-9_]+)\s*=', line)
+    if m and m.group(2) in repl:
+        out.append(f'{m.group(2)}="{repl[m.group(2)]}"')
+    else:
+        out.append(line)
+open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
+print("config.mcp.env 已生成")
+PY
+        else
+            warn "未找到 python3,改用 sed 简易替换(含特殊字符时请手工核对)"
+            _sed_mcp() { sed -i -E "s#^(${1})=.*#${1}=\"${2//#/\\#}\"#" "${GEN_MCP_CONFIG}"; }
+            _sed_mcp APP_ENV "${APP_ENV}"
+            _sed_mcp POSTGRES_DSN "${pg_dsn}"
+            _sed_mcp MONGO_DSN "${MONGO_DSN}"
+            _sed_mcp REDIS_ADDR "${REDIS_ADDR}"
+            [[ -n "${MYSQL_DSN}" ]] && _sed_mcp MYSQL_DSN "${MYSQL_DSN}"
+            _sed_mcp MINIO_ENDPOINT "${MINIO_ENDPOINT}"
+            _sed_mcp MINIO_ACCESS_KEY "${mcp_ak}"
+            _sed_mcp MINIO_SECRET_KEY "${mcp_sk}"
+            _sed_mcp MINIO_BUCKET_NAME "${MINIO_BUCKET}"
+        fi
+        ok "已生成配置:${GEN_MCP_CONFIG}"
+    else
+        warn "未找到 ${MCP_CONFIG_EXAMPLE},跳过 config.mcp.env 生成"
+    fi
 }
 
 # ---------------------------------------------------------------------------
@@ -409,11 +511,17 @@ 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"
+    ssh_run "rm -rf ${REMOTE_STAGE} && mkdir -p ${REMOTE_STAGE}/sql ${REMOTE_STAGE}/cert ${REMOTE_STAGE}/scripts ${REMOTE_STAGE}/bin"
     # 辅助脚本(依赖安装/ Casdoor)
     scp_to "${SCRIPT_DIR}/install_deps.sh" "${REMOTE_STAGE}/scripts/"
     scp_to "${SCRIPT_DIR}/install_casdoor.sh" "${REMOTE_STAGE}/scripts/"
     scp_to "${SCRIPT_DIR}/install_caddy.sh" "${REMOTE_STAGE}/scripts/"
+    # 自编译 caddy 二进制(仅在需要装 Caddy 时上传,约 45MB)
+    if [[ "${DO_CADDY}" == "y" && -f "${SCRIPT_DIR}/../bin/caddy" ]]; then
+        scp_to "${SCRIPT_DIR}/../bin/caddy" "${REMOTE_STAGE}/bin/"
+    fi
+    scp_to "${SCRIPT_DIR}/install_pgadmin.sh" "${REMOTE_STAGE}/scripts/"
+    scp_to "${SCRIPT_DIR}/provision_minio.sh" "${REMOTE_STAGE}/scripts/"
     scp_to "${SCRIPT_DIR}/install_kiro_cli.sh" "${REMOTE_STAGE}/scripts/"
     ssh_run "chmod +x ${REMOTE_STAGE}/scripts/*.sh"
     # SQL 与二进制、前端、配置、证书
@@ -430,7 +538,12 @@ prepare_remote_stage() {
         rm -f /tmp/fwh-dist.tgz
     fi
     [[ -n "${GEN_CONFIG}" ]] && scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.env"
-    [[ -n "${GEN_CONFIG}" ]] && scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.mcp.env"
+    if [[ -n "${GEN_MCP_CONFIG}" ]]; then
+        scp_to "${GEN_MCP_CONFIG}" "${REMOTE_STAGE}/config.mcp.env"
+    elif [[ -n "${GEN_CONFIG}" ]]; then
+        # 回退:未单独生成 MCP 配置时沿用后端配置
+        scp_to "${GEN_CONFIG}" "${REMOTE_STAGE}/config.mcp.env"
+    fi
     ok "产物已上传到远程 ${REMOTE_STAGE}"
 }
 
@@ -450,8 +563,8 @@ step_deps() {
     # 需要的组件:若要装 Casdoor 则一并确保 docker
     # uv 作为 MCP/AI 对话运行时依赖(uvx 拉起 MCP server),默认一并检测安装
     local comps="postgres mongo redis minio mc uv"
-    [[ "${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}"
+    [[ "${DO_CASDOOR}" == "y" || "${DO_PGADMIN}" == "y" ]] && comps="${comps} docker"
+    run "MINIO_API_PORT='${api_port}' MINIO_ROOT_USER='${MINIO_ROOT_USER}' MINIO_ROOT_PASSWORD='$(printf '%s' "${MINIO_ROOT_PASSWORD}")' bash '${base}/install_deps.sh' ${comps}"
     ok "依赖检测/安装完成"
 }
 
@@ -469,6 +582,39 @@ step_casdoor() {
     ok "Casdoor 安装步骤结束(后续配置见 DEPLOYMENT.md)"
 }
 
+# ---------------------------------------------------------------------------
+# 部署 pgAdmin4(Docker)+ 给 postgres 设密码
+# ---------------------------------------------------------------------------
+step_pgadmin() {
+    [[ "${DO_PGADMIN}" == "y" ]] || return 0
+    title "部署 pgAdmin4(Web)"
+    local base; base="$(_scripts_base)"
+    run "POSTGRES_PASSWORD='$(printf '%s' "${POSTGRES_ADMIN_PASSWORD}")' \
+         PGADMIN_EMAIL='${PGADMIN_EMAIL}' \
+         PGADMIN_PASSWORD='$(printf '%s' "${PGADMIN_PASSWORD}")' \
+         PGADMIN_PORT='${PGADMIN_PORT}' PGADMIN_BIND='${PGADMIN_BIND}' \
+         PG_PORT='${PG_PORT}' \
+         bash '${base}/install_pgadmin.sh'"
+    ok "pgAdmin4 部署步骤结束(访问 http://${PGADMIN_BIND}:${PGADMIN_PORT})"
+}
+
+# ---------------------------------------------------------------------------
+# 创建 MinIO 业务账号与策略(fwh-toolbox / fwh-toolbox-ai)
+# ---------------------------------------------------------------------------
+step_minioacct() {
+    [[ "${DO_MINIOACCT}" == "y" ]] || return 0
+    title "创建 MinIO 业务账号与策略"
+    local base; base="$(_scripts_base)"
+    local minio_ep="http://${MINIO_ENDPOINT}"
+    run "MINIO_ENDPOINT='${minio_ep}' \
+         MINIO_ROOT_USER='${MINIO_ROOT_USER}' MINIO_ROOT_PASSWORD='$(printf '%s' "${MINIO_ROOT_PASSWORD}")' \
+         MINIO_ACCESS_KEY='${MINIO_ACCESS_KEY:-fwh-toolbox}' MINIO_SECRET_KEY='$(printf '%s' "${MINIO_SECRET_KEY:-fwh-toolbox}")' \
+         MINIO_AI_ACCESS_KEY='${MINIO_AI_ACCESS_KEY}' MINIO_AI_SECRET_KEY='$(printf '%s' "${MINIO_AI_SECRET_KEY}")' \
+         MINIO_BUCKET='${MINIO_BUCKET}' MINIO_AI_BUCKET='${MINIO_AI_BUCKET}' \
+         bash '${base}/provision_minio.sh'"
+    ok "MinIO 业务账号步骤结束(后端 ${MINIO_ACCESS_KEY:-fwh-toolbox} / MCP ${MINIO_AI_ACCESS_KEY})"
+}
+
 # ---------------------------------------------------------------------------
 # 安装/配置 Caddy 反向代理
 # ---------------------------------------------------------------------------
@@ -476,9 +622,9 @@ 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}' \
+    run "CADDY_DOMAIN='${CADDY_DOMAIN}' \
+         LIBDNS_SPACESHIP_APIKEY='$(printf '%s' "${LIBDNS_SPACESHIP_APIKEY}")' \
+         LIBDNS_SPACESHIP_APISECRET='$(printf '%s' "${LIBDNS_SPACESHIP_APISECRET}")' \
          CADDY_FRONTEND_PORT='${CADDY_FRONTEND_PORT}' CADDY_BACKEND_PORT='${CADDY_BACKEND_PORT}' \
          CADDY_INCLUDE_CASDOOR='${CADDY_INCLUDE_CASDOOR}' CADDY_CASDOOR_PORT='${CASDOOR_PORT}' \
          CADDY_INCLUDE_MINIO='${CADDY_INCLUDE_MINIO}' CADDY_MINIO_CONSOLE_PORT='9199' \
@@ -508,8 +654,7 @@ step_kiro() {
 step_initdb() {
     [[ "${DO_INITDB}" == "y" ]] || return 0
     title "初始化数据库"
-    local sql_path minio_ep
-    minio_ep="http://${MINIO_ENDPOINT}"
+    local sql_path
     if [[ "${MODE}" == "remote" ]]; then
         sql_path="${REMOTE_STAGE}/sql/init_pgsql.sql"
     else
@@ -525,13 +670,7 @@ step_initdb() {
     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"
+    # 注:MinIO 建桶 + 业务账号 + 策略统一由 step_minioacct(provision_minio.sh)负责,此处不再建桶。
     ok "数据库初始化步骤结束"
 }
 
@@ -559,7 +698,11 @@ step_backend() {
         sudo chmod +x "${server_dir}/${APP_NAME}" "${server_dir}/${MCP_NAME}"
         [[ -d "${SERVICE_DIR}/cert" ]] && sudo cp -r "${SERVICE_DIR}/cert" "${server_dir}/" || true
         [[ -n "${GEN_CONFIG}" ]] && sudo cp "${GEN_CONFIG}" "${server_dir}/config.env" || true
-        [[ -n "${GEN_CONFIG}" ]] && sudo cp "${GEN_CONFIG}" "${server_dir}/config.mcp.env" || true
+        if [[ -n "${GEN_MCP_CONFIG}" ]]; then
+            sudo cp "${GEN_MCP_CONFIG}" "${server_dir}/config.mcp.env" || true
+        elif [[ -n "${GEN_CONFIG}" ]]; then
+            sudo cp "${GEN_CONFIG}" "${server_dir}/config.mcp.env" || true
+        fi
     fi
 
     # 写入 systemd 服务
@@ -609,6 +752,7 @@ step_frontend() {
 # ---------------------------------------------------------------------------
 cleanup() {
     [[ -n "${GEN_CONFIG}" && -f "${GEN_CONFIG}" ]] && rm -f "${GEN_CONFIG}"
+    [[ -n "${GEN_MCP_CONFIG}" && -f "${GEN_MCP_CONFIG}" ]] && rm -f "${GEN_MCP_CONFIG}"
     if [[ "${MODE}" == "remote" ]]; then
         ssh_run "rm -rf ${REMOTE_STAGE}" >/dev/null 2>&1 || true
     fi
@@ -619,12 +763,12 @@ summary() {
     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 "已执行步骤  : $( [[ ${DO_DEPS} == y ]] && echo -n '依赖 ' )$( [[ ${DO_CASDOOR} == y ]] && echo -n 'Casdoor ' )$( [[ ${DO_INITDB} == y ]] && echo -n '初始化DB ' )$( [[ ${DO_BACKEND} == y ]] && echo -n '后端 ' )$( [[ ${DO_FRONTEND} == y ]] && echo -n '前端 ' )$( [[ ${DO_CADDY} == y ]] && echo -n 'Caddy ' )$( [[ ${DO_KIRO} == y ]] && echo -n 'KiroCLI ' )$( [[ ${DO_PGADMIN} == y ]] && echo -n 'pgAdmin4 ' )$( [[ ${DO_MINIOACCT} == y ]] && echo -n 'MinIO账号 ' )"
     echo
     echo "后续:"
     if [[ "${DO_CADDY}" == "y" ]]; then
         echo "  * Caddy 已配置:https://${CADDY_DOMAIN}/fwh-toolbox/ (站点文件 /etc/caddy/sites/fwh-toolbox.caddy)"
-        [[ "${CADDY_TLS_MODE}" == "internal" ]] && echo "    注意:TLS=internal 为自签证书,浏览器会告警,生产请改用 acme/manual。"
+        echo "    TLS 走 spaceship DNS-01;token 已写入 systemd 服务(/lib/systemd/system/caddy.service,权限 600)。"
     else
         echo "  * 配置并重载反向代理 Caddy(见 deploy/DEPLOYMENT.md 第 9 章)"
     fi
@@ -643,6 +787,17 @@ summary() {
         fi
         echo "    确认 config.env 的 KIRO_RUN_AS_USER=${RUN_USER}、KIRO_HOME=/home/${RUN_USER} 一致,后端方能拉起 AI 对话。"
     fi
+    if [[ "${DO_PGADMIN}" == "y" ]]; then
+        echo "  * pgAdmin4 已部署:http://${PGADMIN_BIND}:${PGADMIN_PORT}(登录 ${PGADMIN_EMAIL})"
+        echo "    新建 Server 连本机 PG:主机 host.docker.internal、端口 ${PG_PORT}、用户 postgres。"
+        echo "    若连接报认证失败,多为 pg_hba.conf 未启用 scram-sha-256,见脚本提示。"
+        echo "    安全:勿把 ${PGADMIN_PORT} 暴露公网,建议置于反向代理 + 访问控制之后。"
+    fi
+    if [[ "${DO_MINIOACCT}" == "y" ]]; then
+        echo "  * MinIO 业务账号已创建:后端 ${MINIO_ACCESS_KEY:-fwh-toolbox}(写 fwh-toolbox* 桶)、"
+        echo "    MCP ${MINIO_AI_ACCESS_KEY}(写 fwh-toolbox-ai* 桶,其余只读)。"
+        echo "    确认 config.env 用后端账号、config.mcp.env 用 AI 账号;核对:mc admin user list fwhlocal"
+    fi
     echo "  * 验证:curl http://127.0.0.1:${APP_PORT}/swagger/index.html"
     if [[ "${MODE}" == "remote" ]]; then
         echo "  * 查看后端日志:ssh ${R_USER}@${R_HOST} 'journalctl -u ${APP_NAME} -f'"
@@ -687,7 +842,7 @@ main() {
 
     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}"
+    echo "步骤:deps=${DO_DEPS} casdoor=${DO_CASDOOR} initdb=${DO_INITDB} backend=${DO_BACKEND} frontend=${DO_FRONTEND} genconf=${DO_GENCONF} caddy=${DO_CADDY} kiro=${DO_KIRO} pgadmin=${DO_PGADMIN} minio-acct=${DO_MINIOACCT}"
     ask_yn "确认开始执行?" "y" || { warn "已取消"; exit 0; }
 
     generate_config
@@ -697,6 +852,8 @@ main() {
     step_deps
     step_casdoor
     step_initdb
+    step_minioacct
+    step_pgadmin
     step_backend
     step_frontend
     step_caddy