create-ssnode-cli 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/index.js +17 -21
  3. package/package.json +1 -1
  4. package/secrets.js +32 -34
  5. package/templates/backend/Dockerfile +18 -8
  6. package/templates/backend/config/config.base.yaml +130 -0
  7. package/templates/backend/config/config.develop.yaml +8 -75
  8. package/templates/backend/config/config.prod.yaml +12 -67
  9. package/templates/backend/docker-compose.yml +30 -22
  10. package/templates/backend/ops/docker/config.sh +30 -15
  11. package/templates/backend/ops/docker/install.sh +25 -34
  12. package/templates/backend/ops/docker/run-container.sh +100 -0
  13. package/templates/backend/ops/docker/start.sh +37 -8
  14. package/templates/backend/ops/docker/stop.sh +1 -1
  15. package/templates/backend/ops/ensure-encrypt.cjs +217 -0
  16. package/templates/backend/ops/install.sh +249 -148
  17. package/templates/backend/ops/lib-encrypt.sh +98 -0
  18. package/templates/backend/ops/lib.sh +22 -0
  19. package/templates/backend/ops/pm2/config.sh +11 -6
  20. package/templates/backend/ops/pm2/install.sh +27 -3
  21. package/templates/backend/ops/pm2/start.sh +1 -1
  22. package/templates/backend/ops/start.sh +7 -5
  23. package/templates/backend/ops/status.sh +7 -5
  24. package/templates/backend/ops/stop.sh +7 -5
  25. package/templates/backend/ops/systemd/config.sh +13 -6
  26. package/templates/backend/ops/systemd/install.sh +28 -1
  27. package/templates/backend/ops/systemd/start.sh +1 -1
  28. package/templates/backend/ops/versionswitch.sh +34 -122
  29. package/templates/backend/package-lock.json +7561 -0
  30. package/templates/backend/package.json +3 -1
  31. package/templates/backend/scripts/build.cjs +104 -10
  32. package/templates/backend/scripts/config-crypt.cjs +108 -0
  33. package/templates/backend/scripts/lib/ssh.cjs +72 -3
  34. package/templates/backend/scripts/publish.cjs +17 -9
  35. package/templates/backend/scripts/pulldocker.cjs +208 -0
  36. package/templates/backend/scripts/pushdocker.cjs +323 -0
  37. package/templates/backend/scripts/rollback.cjs +6 -3
  38. package/templates/backend/scripts/server.config.example.cjs +43 -15
  39. package/templates/backend/scripts/slowsql-report.mjs +2 -2
  40. package/templates/backend/src/common/api-log/api-log.service.ts +1 -1
  41. package/templates/backend/src/common/config-crypto.ts +133 -0
  42. package/templates/backend/src/config/config.interface.ts +22 -3
  43. package/templates/backend/src/config/configuration.ts +173 -86
  44. package/templates/backend/src/database/migrations/0001-init.migration.ts +1 -1
  45. package/templates/backend/src/database/slow-sql.logger.ts +1 -1
  46. package/templates/backend/src/database/sqlite.config.ts +2 -2
  47. package/templates/backend/src/main.ts +3 -3
  48. package/templates/backend/src/modules/ops/ops-probe.interface.ts +3 -3
  49. package/templates/backend/src/modules/ops/ops.module.ts +2 -11
  50. package/templates/backend/src/modules/user/user.service.ts +2 -2
  51. package/templates/docs/config-encryption.md +164 -0
  52. package/templates/root/AGENTS.md +5 -4
  53. package/templates/root/CLAUDE.md +1 -1
  54. package/templates/root/README.md +25 -15
  55. package/templates/root/_gitignore +5 -0
  56. package/templates/root/scripts/publish.cjs +20 -2
  57. package/templates/root/scripts/rollback.cjs +20 -2
  58. package/templates/web/config/router.config.ts +8 -0
  59. package/templates/web/package.json +5 -1
  60. package/templates/web/scripts/compress-src-images.cjs +332 -0
  61. package/templates/web/scripts/lib/interactive.cjs +134 -0
  62. package/templates/web/scripts/publish.cjs +299 -200
  63. package/templates/web/scripts/rollback.cjs +110 -41
  64. package/templates/web/scripts/server.config.example.json +17 -0
  65. package/templates/web/src/layouts/Background.module.less +0 -4
  66. package/templates/web/src/layouts/Background.tsx +48 -74
  67. package/templates/web/src/layouts/components/PageTabs/SortableTab.tsx +133 -0
  68. package/templates/web/src/layouts/components/PageTabs/index.module.less +105 -0
  69. package/templates/web/src/layouts/components/PageTabs/index.tsx +110 -0
  70. package/templates/web/src/layouts/components/PageTabs/usePageTabs.ts +224 -0
  71. package/templates/web/src/pages/Backend/Image/index.tsx +4 -4
  72. package/templates/web/src/pages/Maintenance/index.module.less +177 -0
  73. package/templates/web/src/pages/Maintenance/index.tsx +142 -0
  74. package/templates/web/src/services/api.ts +4 -0
  75. package/templates/web/src/services/app-request.ts +8 -1
  76. package/templates/web/src/services/maintenance.ts +67 -0
  77. package/templates/web/src/services/request.ts +5 -0
  78. package/transforms.js +21 -28
  79. package/templates/backend/ops/docker/.env.example +0 -40
  80. package/templates/backend/ops/sqlite/README.md +0 -57
  81. package/templates/backend/ops/sqlite/backup.sh +0 -65
  82. package/templates/backend/ops/sqlite/config.sh +0 -91
  83. package/templates/backend/ops/sqlite/healthcheck.sh +0 -25
  84. package/templates/backend/ops/sqlite/restore.sh +0 -105
  85. package/templates/backend/src/modules/ops/sqlite-backup-list.probe.ts +0 -100
  86. package/templates/backend/src/modules/ops/sqlite-backup-log.probe.ts +0 -63
  87. package/templates/backend/src/modules/ops/sqlite-backup.probe.ts +0 -144
@@ -3,8 +3,10 @@
3
3
  # Description backend 顶层部署编排器(服务器侧、纯 shell、单一真相源)
4
4
  ###
5
5
  # 开发机只负责 buildops + tar + scp 上传 release 包,然后 ssh 触发本脚本。
6
- # 真正的部署力学(releases 轮转 / current 装填 / 密钥注入 / 派发进程安装)全在本脚本,
6
+ # 真正的部署力学(releases 轮转 / current 装填 / 派发进程安装)全在本脚本,
7
7
  # 不再由开发机 node-ssh 远程执行 —— 部署模型与运维现场一致,可端到端验证实际问题。
8
+ # 密钥检测不在本脚本职责内:敏感值校验一律由应用启动 fail-fast 兜底(暴露后用
9
+ # ./versionswitch.sh 回滚),ops 只做环境判定 / 依赖安装 / 服务启停。
8
10
  #
9
11
  # current 形态(按模式):
10
12
  # systemd/pm2:current = 软链 → releases/<ver>-<ts>(Capistrano 式;releases 只放业务服务包,
@@ -13,41 +15,75 @@
13
15
  # ops/ 共享:ops/ 不进 releases,统一落 APP_ROOT/ops/(与 install.sh 同级、跨版本共享);
14
16
  # 各 mode 的 config.sh 持久化(运维改的 NODE_BIN/RUN_USER 等跨版本保留)。
15
17
  #
16
- # 两种场景:
17
- # 1) 首次部署(bootstrap):APP_ROOT 未 baked。交互确认 APP_ROOT 与模式 → 建 releases/data/logs
18
- # → 拷顶层脚本到 APP_ROOT 并 baked → 落 release 到 releases/<ver>-<ts>(或 docker current)
19
- # → 落 ops 到 APP_ROOT/ops/ → 种 .env(docker,非空阻断)→ 软链 current(systemd/pm2)→ 派发 mode install。
20
- # 2) 二次部署:APP_ROOT 已 baked。传新 release(tar/目录)→ 停旧 → 落新 release →
21
- # overlay ops(保留 config.sh)→ 检测 config 非空(systemd/pm2 用 release 自带 config/config.prod.yaml)
22
- # → 装依赖 → 改链 go-live → 轮转 → 重派发。
18
+ # 顶层脚本零配置(不 baked):运行时以脚本所在目录为 APP_ROOT、MODE 读 $APP_ROOT/.mode ——
19
+ # APP_ROOT 整体搬迁(换盘/换机)后开箱即用,发布 overlay 自然收敛。
20
+ #
21
+ # 部署位配置(APP_ROOT/config/,跨版本共享 —— 配置不再随 release 走):
22
+ # config.base.yaml 全量默认;每次发布用包内版本覆盖刷新(开发权威:新增键/注释随发布到位)
23
+ # config.yaml 环境 overlay 固定名副本:首部署从包内所选 overlay 播种(config.prod.yaml /
24
+ # config.<env>.yaml → 此名),此后发布永不覆盖 —— 运维/开发在部署位维护本
25
+ # 环境真值(多发布环境 = 各 APP_ROOT 各自的 config.yaml,同一份构建产物通用)。
26
+ # 运行时读取:docker 由 run-container.sh 只读挂载 /app/config;systemd/pm2 由 unit/ecosystem
27
+ # 注入 CONFIG_DIR=$APP_ROOT/config(见 configuration.ts)。改配置 + ./start.sh 即生效,三模式一致。
28
+ #
29
+ # 两种场景(判定:脚本所在目录有无 .mode):
30
+ # 1) 首次部署(bootstrap,无 .mode):交互确认 APP_ROOT 与模式 → 选环境 overlay → 确认端口 →
31
+ # 选是否开启配置加密 → 建 releases/data/logs → 拷顶层脚本到 APP_ROOT → 落 release 到
32
+ # releases/<ver>-<ts>(或 docker current)→ 落 ops 到 APP_ROOT/ops/ → 播种/刷新 APP_ROOT/config
33
+ # → 移 .prod.docker.env(纯部署件)入 APP_ROOT 根(包根 config/ 原件随移删除,不在服务器
34
+ # 留双份)→ 软链 current(systemd/pm2)→ 派发 mode install(mode install 经 ops/lib-encrypt.sh
35
+ # 按加密选择生成/维护根密钥并就地加密部署位 config.yaml)。
36
+ # 2) 二次部署(有 .mode):传新 release(tar/目录)→ 停旧 → 落新 release →
37
+ # overlay ops(保留 config.sh)→ 刷新部署位 base(不动 config.yaml)→ 移 env 文件入根
38
+ # 对齐包内版本(原件删除) → 装依赖 → 改链 go-live → 轮转 → 重派发(mode install 顺带
39
+ # relock:有根密钥则自动加密部署位 config.yaml 新明文)。
23
40
  #
24
41
  # 用法:
25
42
  # 首次: bash install.sh # 在解压后的 release 包根里跑(脚本现位于包根,非 ops/)
26
- # 二次: bash install.sh <release.tar.gz|dir> # 在 APP_ROOT 下跑(MODE 已 baked,缺省读 .mode)
43
+ # 二次: bash install.sh <release.tar.gz|dir> # 在 APP_ROOT 下跑(.mode 已就位)
27
44
  # 切版本: bash install.sh # 在 APP_ROOT 下裸跑(无参):扫 releases/ 找新丢的
28
45
  # buildops 产物包目录并部署切 current(等价显式传 <包目录>,部署后清掉暂存包)
29
46
  set -euo pipefail
30
47
 
31
- # ── baked 字段(首次为空;bootstrap 拷自身到 APP_ROOT 时 sed 改写为字面量) ──
32
- APP_ROOT="${APP_ROOT:-}"
33
- MODE="${MODE:-}"
34
48
  # 轮转保留数:此处只存显式值(--keep / env),不预设缺省 —— 缺省在轮转前由
35
49
  # resolve_keep_versions 从 ops/<mode>/config.sh 取(运维改那处,跨版本保留)。
36
50
  KEEP_VERSIONS="${KEEP_VERSIONS:-}"
37
51
 
38
52
  # 脚本所在目录(首次=release 包根,即 install.sh 所在的包根;二次=APP_ROOT)
39
53
  DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
54
+
55
+ # ── 部署状态自定位(零配置):脚本所在目录有 .mode = 已完成首次部署(即 APP_ROOT);
56
+ # 无 = 还在 release 包根,进 bootstrap。MODE 恒读 .mode,不 bake。 ──
57
+ APP_ROOT=""
58
+ MODE=""
59
+ if [[ -f "$DIR/.mode" ]]; then
60
+ APP_ROOT="$DIR"
61
+ MODE="$(cat "$DIR/.mode" 2>/dev/null || true)"
62
+ fi
40
63
  RELEASE_DIR_DEFAULT="$DIR" # 首次=release 包根(install.sh 现位于包根,非 ops/)
41
64
  RELEASE_DIR=""
42
65
  APP_DIR="" # releases/<version>/(dist/package.json/Dockerfile/config 所在)
43
66
  VER=""
44
67
  STAGE_CLEANUP=""
45
68
 
46
- # 本次部署选定端口(systemd/pm2=yaml 权威,docker=.env PORT 权威)。
47
- # 首部署:prompt_port 必赋值,ensure_secrets 据此写 .env PORT(覆盖 .env.example 的 3001 缺省)。
48
- # 二次部署:仅显式改端口时赋值;沿用当前则留空,ensure_secrets 不动 .env PORT(保留运维护过的值)。
69
+ # 本次部署选定的环境 overlay 名(仅 bootstrap 赋值,如 prod / demo;二次部署留空 = 部署位
70
+ # 环境已定,config.yaml 不再动)。ensure_secrets 据此播种 APP_ROOT/config/config.yaml。
71
+ CHOSEN_ENV=""
72
+
73
+ # 本次部署选定端口(部署位权威:systemd/pm2=APP_ROOT/config/config.yaml 的 app.port,
74
+ # docker=.prod.docker.env 的 PORT)。首部署 prompt_port 必赋值,ensure_secrets 据此写入
75
+ # 播种的 config.yaml 与 .prod.docker.env PORT。二次部署恒留空(换端口改部署位 + ./start.sh)。
49
76
  DEPLOY_PORT=""
50
77
 
78
+ # 首部署是否开启配置加密(1/0,prompt_encrypt 置位;dispatch_install 导出给 mode install,
79
+ # 由 ops/lib-encrypt.sh 消费:生成服务器根密钥 + 就地加密部署位 config.yaml)。
80
+ # 二次部署恒保持 0 —— 加密状态由 $APP_ROOT/.prod.secrets 的存在性自表述(存在 = 加密模式)。
81
+ CONFIG_ENCRYPT_INIT=0
82
+
83
+ # 本次部署是否走了「播种 config.yaml」路径(ensure_secrets 置位;deploy 完成输出末尾据此提示
84
+ # 老部署升级的真值迁移 —— 播种的是包内 overlay,不含旧部署在服务器上维护的环境真值)。
85
+ SEEDED_CONFIG=0
86
+
51
87
  # 本次部署的工作目录(docker=current;systemd/pm2=releases/<id>,最后软链 current 指向)
52
88
  DEPLOY_TARGET=""
53
89
  IS_SYMLINK=0
@@ -96,7 +132,7 @@ resolve_app_dir() {
96
132
  printf '%s' "$(cd "$vdir" && pwd)"
97
133
  }
98
134
 
99
- # ── APP_ROOT 裸跑 ./install.sh(无参、APP_ROOT 已 baked)时:扫 APP_ROOT/releases/ 找
135
+ # ── APP_ROOT 裸跑 ./install.sh(无参、已完成首次部署)时:扫 APP_ROOT/releases/ 找
100
136
  # 运维新丢进来的 buildops 产物包并部署,等价于正常二次部署显式传 <包目录>。
101
137
  # 产物包判据:目录自身含内层 releases/(正常部署版本目录 <ver>-<ts> 只有 dist/package.json/
102
138
  # config;ops 不进 releases、在 APP_ROOT/ops/ 共享,故正常部署目录无内层 releases/)。多个包则交互列选;��署后由 cleanup_staging 清掉暂存包目录。
@@ -138,7 +174,7 @@ resolve_release() {
138
174
  local arg="${1:-}"
139
175
  if [[ -z "$arg" ]]; then
140
176
  if [[ -n "$APP_ROOT" ]]; then
141
- # 无参 + APP_ROOT 已 baked = 运维在 APP_ROOT 裸跑 ./install.sh 切版本:
177
+ # 无参 + 已完成首次部署(脚本住 APP_ROOT)= 运维裸跑 ./install.sh 切版本:
142
178
  # 扫 APP_ROOT/releases/ 找新丢进来的 buildops 产物包并部署(等价二次部署传 <包目录>)。
143
179
  resolve_dropped_package
144
180
  return
@@ -146,7 +182,7 @@ resolve_release() {
146
182
  RELEASE_DIR="$RELEASE_DIR_DEFAULT"
147
183
  [[ -d "$RELEASE_DIR/releases" ]] || die "默认 release 目录 $RELEASE_DIR 无 releases/(应在解压后的 release 包内运行,或显式传包路径)"
148
184
  elif [[ -f "$arg" && "$arg" == *.tar.gz ]]; then
149
- [[ -n "$APP_ROOT" ]] || die "传 tar 包但后台服务目录未 baked(首次部署请先在解压目录里跑一次本脚本完成引导)"
185
+ [[ -n "$APP_ROOT" ]] || die "传 tar 包但当前目录未完成首次部署(先在解压目录里裸跑 install.sh 完成引导)"
150
186
  local ts; ts="$(date +%Y%m%d-%H%M%S)"
151
187
  RELEASE_DIR="$APP_ROOT/.stage-$ts"
152
188
  rm -rf "$RELEASE_DIR"; mkdir -p "$RELEASE_DIR"
@@ -222,36 +258,78 @@ prompt_mode() {
222
258
  esac
223
259
  }
224
260
 
261
+ # ── 首部署选择环境 overlay ──
262
+ # 包内 config/ 随行 base + 全部环境 overlay(config.<env>.yaml);每个 APP_ROOT = 一个发布环境,
263
+ # 首部署选定一份,ensure_secrets 播种为部署位 APP_ROOT/config/config.yaml(此后发布不覆盖,
264
+ # 本环境真值在部署位维护)。仅一份 overlay(通常 prod)时静默采用;多份交互列选(默认 prod)。
265
+ prompt_env() {
266
+ local dir="$RELEASE_DIR/config" items=() f n pick choice i def
267
+ [[ -d "$dir" ]] || die "缺 $dir(release 包不完整)"
268
+ while IFS= read -r -d '' f; do
269
+ items+=("$(basename "$f" | sed -E 's/^config\.//; s/\.yaml$//')")
270
+ done < <(find "$dir" -maxdepth 1 -name 'config.*.yaml' ! -name 'config.base.yaml' -print0 2>/dev/null | sort -z)
271
+ n=${#items[@]}
272
+ [[ $n -gt 0 ]] || die "$dir 下无环境 overlay(config.<env>.yaml;release 包不完整)"
273
+ if [[ $n -eq 1 ]]; then
274
+ CHOSEN_ENV="${items[0]}"
275
+ ok "部署环境: $CHOSEN_ENV(包内唯一 overlay)"
276
+ return 0
277
+ fi
278
+ echo " 检测到多份环境 overlay(每个 APP_ROOT = 一个发布环境,选定后本机恒用该环境):"
279
+ i=0
280
+ for pick in "${items[@]}"; do i=$((i+1)); echo " $i) $pick"; done
281
+ def=1
282
+ for i in "${!items[@]}"; do [[ "${items[$i]}" == "prod" ]] && def=$((i+1)); done
283
+ read -rp " 选择 [${def}]: " choice || choice=""
284
+ choice="${choice:-$def}"
285
+ if [[ "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -le $n ]]; then
286
+ CHOSEN_ENV="${items[$((choice-1))]}"
287
+ else
288
+ die "无效选择: $choice"
289
+ fi
290
+ ok "部署环境: $CHOSEN_ENV"
291
+ }
292
+
225
293
  # ── 运行端口首次确认 ──
226
- # 端口 = release 自带 config.prod.yaml 的 app.port;systemd/pm2=app 直监听(yaml 为权威),
227
- # docker=宿主映射=容器内监听(.env PORT 为权威,yaml 仅作镜像内兜底默认)。
228
- # 回车=采纳当前值;输入合法端口=就地改 release 的 yaml(随包拷入 current,镜像兜底对齐),
229
- # 并记入 DEPLOY_PORT:ensure_secrets 据此把 .env 的 PORT 改为同值(docker 权威,部署即生效)。
230
- # 二次部署端口变更由 deploy() 另行确认。
294
+ # 端口 = 选定 overlay 的 app.port(未覆盖时回落 base);确认值/新值都记入 DEPLOY_PORT,
295
+ # 由 ensure_secrets 写进播种的部署位 config.yaml(systemd/pm2 权威)与 .prod.docker.env
296
+ # 的 PORT(docker 权威)。二次部署不问端口(部署位权威,换端口改部署位 + ./start.sh)。
231
297
  prompt_port() {
232
298
  local port input
233
- port="$(yaml_app_port "$APP_DIR/config/config.prod.yaml")"
299
+ port="$(yaml_app_port "$RELEASE_DIR/config/config.${CHOSEN_ENV}.yaml")"
300
+ [[ -n "$port" ]] || port="$(yaml_app_port "$RELEASE_DIR/config/config.base.yaml")"
234
301
  [[ -n "$port" ]] || port="3001"
235
- echo " 后端运行端口(来自 config.prod.yaml app.port = $port;systemd/pm2=app 直监听,docker=宿主映射)"
236
- read -rp " 回车确认此端口,或输入新端口直接改 yaml: " input || input=""
302
+ echo " 后端运行端口(来自 config.${CHOSEN_ENV}.yaml app.port = $port;systemd/pm2=app 直监听,docker=宿主映射)"
303
+ read -rp " 回车确认此端口,或输入新端口(写入部署位 config.yaml): " input || input=""
237
304
  input="${input%%$'\n'}"
238
305
  if [[ -z "$input" ]]; then
239
- ok "运行端口: $port(来自 config.prod.yaml)"
306
+ ok "运行端口: $port"
240
307
  DEPLOY_PORT="$port"
241
308
  return 0
242
309
  fi
243
- # 校验合法端口(1-65535)后就地改 release 源 yaml;与当前同值则不动
244
310
  if [[ ! "$input" =~ ^[0-9]+$ || "$input" -lt 1 || "$input" -gt 65535 ]]; then
245
- die "无效端口: $input(期望 1-65535 的数字;手动改请编辑 $APP_DIR/config/config.prod.yaml 的 app.port 后重跑)"
311
+ die "无效端口: $input(期望 1-65535 的数字)"
246
312
  fi
247
- if [[ "$input" == "$port" ]]; then
248
- ok "运行端口: $port(未变更)"
249
- DEPLOY_PORT="$port"
250
- return 0
251
- fi
252
- set_app_port "$APP_DIR/config/config.prod.yaml" "$input"
253
313
  DEPLOY_PORT="$input"
254
- ok "已改 $APP_DIR/config/config.prod.yaml app.port: $port → $input(systemd/pm2=yaml 权威;docker=.env PORT 权威,由 ensure_secrets 写入)"
314
+ ok "运行端口: $port → $input(将写入部署位 config.yaml + docker env PORT)"
315
+ }
316
+
317
+ # ── 首部署:是否开启配置加密 ──
318
+ # 加密模型(见 docs/config-encryption.md):仓库/传输层明文维护(开发机零密钥零加密操作),
319
+ # 服务器侧落盘为密文。选 Y → mode install 经 ops/lib-encrypt.sh 在本机生成根密钥
320
+ # (APP_ROOT/.prod.secrets,600,现场打印一次供抄录)并就地把播种的 config.yaml 敏感值加密;
321
+ # 此后每次部署自动把新明文就地加密(改明文 + 重跑 ./install.sh = 轮换闭环)。
322
+ # 选 n → 明文运行。二次部署不问(加密状态由 .prod.secrets 存在性自表述)。
323
+ prompt_encrypt() {
324
+ local choice
325
+ echo " 配置加密(在本机生成根密钥,把部署位 config.yaml 的敏感值加密为 ENC(...) 密文):"
326
+ echo " Y) 开启(推荐:服务器落盘为密文;此后改明文 + 重跑 ./install.sh 自动重新加密)"
327
+ echo " n) 明文运行(部署位 config.yaml 敏感值保持明文)"
328
+ read -rp " 选择 [Y]: " choice || choice=""
329
+ case "$choice" in
330
+ n|N) CONFIG_ENCRYPT_INIT=0; ok "配置加密:关闭(明文运行)" ;;
331
+ *) CONFIG_ENCRYPT_INIT=1; ok "配置加密:开启(将在部署时生成本机根密钥)" ;;
332
+ esac
255
333
  }
256
334
 
257
335
  TOP_SCRIPTS=(install.sh start.sh stop.sh versionswitch.sh status.sh)
@@ -292,7 +370,7 @@ lay_out_release() {
292
370
  }
293
371
 
294
372
  # ── lay out ops/ 到 APP_ROOT/ops/(共享、跨版本;releases/ 不放 ops) ──
295
- # 每次部署 overlay 刷新 mode 脚本 / sqlite / 文档,但各 mode 的 config.sh 持久化
373
+ # 每次部署 overlay 刷新 mode 脚本 / 文档,但各 mode 的 config.sh 持久化
296
374
  # (运维改的 NODE_BIN/RUN_USER/DATA_DIR 等跨版本不丢;首次用包内 config.sh 作种子)。
297
375
  lay_out_ops() {
298
376
  local src="$RELEASE_DIR/ops" sub name f fn
@@ -307,7 +385,7 @@ lay_out_ops() {
307
385
  [[ -e "$sub" ]] || continue
308
386
  name="$(basename "$sub")"
309
387
  if [[ -d "$sub" ]]; then
310
- # mode 目录(docker/pm2/systemd/sqlite):刷新其中脚本,保留运维护过的 config.sh
388
+ # mode 目录(docker/pm2/systemd):刷新其中脚本,保留运维护过的 config.sh
311
389
  mkdir -p "$APP_ROOT/ops/$name"
312
390
  for f in "$sub"/*; do
313
391
  [[ -e "$f" ]] || continue
@@ -324,32 +402,24 @@ lay_out_ops() {
324
402
  }
325
403
 
326
404
  # ── 刷新顶层脚本(install/start/stop/versionswitch/status.sh)到 APP_ROOT/ ──
327
- # 首次部署由 bootstrap bake 一次;二次部署用新 release 包根的同名脚本 overlay,
328
- # 让顶层脚本的修复能随 publish 下发(不再"bake 一次就冻住")。re-bake 当前
329
- # APP_ROOT/MODE(顶层脚本无运维可改字段,直接覆盖;与 lay_out_ops 保留 config.sh 不同)。
405
+ # 每次部署用新 release 包根的同名脚本 overlay,让顶层脚本的修复能随 publish 下发。
406
+ # 顶层脚本零配置(自定位 + 读 .mode,无运维可改字段),纯覆盖即可 —— 存量 baked
407
+ # 老部署也会被本步骤自然收敛到无 bake 新形态。(与 lay_out_ops 保留 config.sh 不同)
330
408
  # 安全:写 temp + mv 替换目录项,bash 持有运行脚本的旧 inode 存活至进程退出
331
409
  # (不 cp 直写同 inode —— 会 truncate 正在运行的 install.sh 致其读崩)。
332
410
  refresh_top_scripts() {
333
- [[ -n "$APP_ROOT" && -n "$MODE" && -n "$RELEASE_DIR" ]] || return 0
334
- local s src dst baked=0
411
+ [[ -n "$APP_ROOT" && -n "$RELEASE_DIR" ]] || return 0
412
+ local s src dst n=0
335
413
  for s in "${TOP_SCRIPTS[@]}"; do
336
414
  src="$RELEASE_DIR/$s"
337
415
  [[ -f "$src" ]] || continue
338
416
  dst="$APP_ROOT/$s"
339
- # 从新 release 源 bake(注入当前 APP_ROOT/MODE 字面量)→ temp → mv 替换目录项
340
- sed -e "s|^APP_ROOT=.*|APP_ROOT=\"$APP_ROOT\"|" -e "s|^MODE=.*|MODE=\"$MODE\"|" "$src" > "$dst.__new"
417
+ cp -f "$src" "$dst.__new"
341
418
  mv -f "$dst.__new" "$dst"
342
419
  chmod +x "$dst"
343
- baked=$((baked+1))
420
+ n=$((n+1))
344
421
  done
345
- [[ $baked -gt 0 ]] && ok "顶层脚本刷新:$baked 个(baked APP_ROOT=$APP_ROOT MODE=$MODE,随 release 下发)"
346
- }
347
-
348
- # ── 密钥非空检测(只拦空值,不拦占位串) ──
349
- # 取 yaml 标量值:去 "key:" 前缀、首尾空白与引号;空即未设
350
- yaml_scalar() {
351
- sed -nE "s/^[[:space:]]*$2:[[:space:]]*//p" "$1" \
352
- | sed -E "s/^['\"]?//; s/['\"]?$//; s/^[[:space:]]+//; s/[[:space:]]+$//"
422
+ [[ $n -gt 0 ]] && ok "顶层脚本刷新:$n 个(自定位零配置,随 release 下发)"
353
423
  }
354
424
 
355
425
  # 读 yaml 顶层 app: 段内 port: 的值(去引号/首尾空白)。无文件/无 app.port → 空串(不报错)。
@@ -381,58 +451,103 @@ set_app_port() {
381
451
  fi
382
452
  mv "$f.__port" "$f"
383
453
  }
384
- check_yaml_secrets() {
385
- local f="$1" bad=0 sk jwt
386
- sk="$(yaml_scalar "$f" signKey)"
387
- jwt="$(yaml_scalar "$f" secret)"
388
- [[ -z "$sk" ]] && { err "appSign.signKey 未设(空)"; bad=1; }
389
- [[ -z "$jwt" ]] && { err "jwt.secret 未设(空)"; bad=1; }
390
- [[ $bad -eq 1 ]] && die "请编辑 $f 填入生产真值后重新执行首次部署: bash $DIR/install.sh"
391
- return 0
392
- }
393
- check_env_secrets() {
394
- local f="$1" bad=0 jwt appsign
395
- jwt="$(grep -E '^JWT_SECRET=' "$f" | cut -d= -f2- || true)"
396
- appsign="$(grep -E '^APP_SIGN_KEY=' "$f" | cut -d= -f2- || true)"
397
- [[ -z "$jwt" ]] && { err "JWT_SECRET 未设(空)"; bad=1; }
398
- [[ -z "$appsign" ]] && { err "APP_SIGN_KEY 未设(空)"; bad=1; }
399
- [[ $bad -eq 1 ]] && die "请编辑 $f 填入生产真值后重新执行首次部署: bash $DIR/install.sh"
400
- return 0
401
- }
402
454
 
403
- # ── 配置/密钥(无 APP_ROOT/config.yaml 权威层;systemd/pm2 直用 release 自带 config.yaml,
404
- # app 以 cwd=current 直读、运维改 current/config.yaml + restart 即生效;docker 用 APP_ROOT/.env) ──
455
+ # ── 配置就位(播种部署位 + 移件入 APP_ROOT 根,零密钥检测)──
456
+ # 包内 config/(transport):config.base.yaml + 全部 overlay + .prod.docker.env(无密钥文件 ——
457
+ # 根密钥改为首部署时在本机生成,见 prompt_encrypt / ops/lib-encrypt.sh;敏感值在仓库与
458
+ # 传输层明文,服务器落盘为密文)。
459
+ # APP_ROOT/config/config.base.yaml 每次发布覆盖刷新(开发权威:新增键/注释随发布到位)
460
+ # APP_ROOT/config/config.yaml 首部署播种一次(选定 overlay → 此名),此后永不覆盖
461
+ # APP_ROOT/.prod.secrets 首部署选「开启加密」时由 mode install 生成(600);
462
+ # 存在即加密模式:每次部署自动把部署位新明文就地加密
463
+ # APP_ROOT/.prod.docker.env 纯部署件(buildops 自动生成),移入覆盖;PORT 沿用现值
464
+ # 拷贝成功即删包内原件(mv 语义)—— 包根 config/ 只作随行通道,不在服务器留双份
465
+ # (尤其场景三:包更名成 APP_ROOT,cleanup_staging 不清,原件会永久残留、易误改错份)。
466
+ # 不做任何密钥非空/格式检测——敏感值校验由应用启动 fail-fast 兜底(见文件头)。
405
467
  ensure_secrets() {
406
- local t="$DEPLOY_TARGET"
468
+ local src_cfg="$RELEASE_DIR/config"
469
+ [[ -d "$src_cfg" ]] || die "缺 $src_cfg(release 包不完整)"
470
+ # 场景三(包更名成 APP_ROOT):transport 目录即部署位本身,只播种、不自我拷贝/删除
471
+ local same_cfg=0
472
+ [[ "$src_cfg" -ef "$APP_ROOT/config" ]] && same_cfg=1
473
+
474
+ # 旧模型迁移提示(只提示,不自动碰密钥文件):老包随行 .prod.secrets 已不再消费,
475
+ # 服务器已有的 $APP_ROOT/.prod.secrets(旧加密部署)继续有效 —— 派发 mode install 时
476
+ # lib-encrypt 检测到它即走 relock,既有 ENC 密文与密钥配对不变。
477
+ if [[ -f "$src_cfg/.prod.secrets" ]]; then
478
+ say "检测到包内随行 .prod.secrets(旧打包模型残留,新版不再消费)—— 服务器侧 $APP_ROOT/.prod.secrets 为权威"
479
+ [[ $same_cfg -eq 1 ]] || rm -f "$src_cfg/.prod.secrets"
480
+ fi
481
+ if [[ -f "$APP_ROOT/.secrets" ]]; then
482
+ say "检测到旧 $APP_ROOT/.secrets —— 已由 .prod.secrets 取代,确认新部署运行正常后可删除"
483
+ fi
484
+
485
+ # 部署位 APP_ROOT/config/:base 刷新 + config.yaml 播种(仅缺时)
486
+ mkdir -p "$APP_ROOT/config"
487
+ if [[ $same_cfg -eq 0 ]]; then
488
+ [[ -f "$src_cfg/config.base.yaml" ]] || die "缺 $src_cfg/config.base.yaml(release 包不完整)"
489
+ cp -f "$src_cfg/config.base.yaml" "$APP_ROOT/config/config.base.yaml"
490
+ fi
491
+ if [[ ! -f "$APP_ROOT/config/config.yaml" ]]; then
492
+ local seed_src seed_name
493
+ if [[ -n "$CHOSEN_ENV" ]]; then
494
+ seed_name="config.${CHOSEN_ENV}.yaml"
495
+ seed_src="$src_cfg/${seed_name}"
496
+ else
497
+ # 二次部署但部署位缺 config.yaml(异常/老部署升级):回落包内 prod overlay
498
+ seed_name="config.prod.yaml"
499
+ seed_src="$src_cfg/${seed_name}"
500
+ [[ -f "$seed_src" ]] || { seed_name="config.base.yaml"; seed_src="$src_cfg/${seed_name}"; }
501
+ say "部署位 config.yaml 缺失,用包内 ${seed_name} 播种(老部署升级:请把原配置的环境真值合并进来)"
502
+ fi
503
+ [[ -f "$seed_src" ]] || die "缺 $seed_src(release 包不完整)"
504
+ cp -f "$seed_src" "$APP_ROOT/config/config.yaml"
505
+ SEEDED_CONFIG=1
506
+ ok "部署位配置播种: $APP_ROOT/config/config.yaml (← ${seed_name},此后发布不覆盖)"
507
+ fi
508
+ # 端口写进播种/现存 config.yaml(仅显式选定;systemd/pm2=yaml 权威;docker 由 env PORT 权威)
509
+ if [[ -n "$DEPLOY_PORT" ]]; then
510
+ set_app_port "$APP_ROOT/config/config.yaml" "$DEPLOY_PORT"
511
+ fi
512
+
407
513
  if [[ "$MODE" == "docker" ]]; then
408
- if [[ ! -f "$APP_ROOT/.env" ]]; then
409
- local seed="$RELEASE_DIR/ops/docker/.env.example"
410
- [[ -f "$seed" ]] || seed="$APP_ROOT/ops/docker/.env.example" # 共享 ops(lay_out_ops 已就位)
411
- [[ -f "$seed" ]] || die "缺 .env.example,无法种子化 $APP_ROOT/.env"
412
- cp -f "$seed" "$APP_ROOT/.env"
514
+ # docker env 文件(纯部署件,路径可外配见 ops/docker/config.sh ENV_FILE)。
515
+ # 拷贝前记下 APP_ROOT 现行 PORT:本次未显式选端口则拷贝后回写,不吞运维在发布间隔改的值。
516
+ local src_env="$src_cfg/.prod.docker.env" cur_port=""
517
+ if [[ -f "$APP_ROOT/.prod.docker.env" ]]; then
518
+ cur_port="$(sed -nE 's/^PORT=//p' "$APP_ROOT/.prod.docker.env" | head -1 | sed -E "s/^['\"]?//; s/['\"]?$//")"
519
+ fi
520
+ if [[ -f "$src_env" ]]; then
521
+ cp -f "$src_env" "$APP_ROOT/.prod.docker.env"
522
+ [[ $same_cfg -eq 1 ]] || rm -f "$src_env"
523
+ elif [[ ! -f "$APP_ROOT/.prod.docker.env" ]]; then
524
+ die "缺 $src_env 且无 $APP_ROOT/.prod.docker.env(release 包不完整)"
525
+ fi
526
+ # PORT 同步:显式选定 > 沿用现行 > 包内缺省(prompt_port 首部署必赋值 → 权威生效)
527
+ if [[ -n "$DEPLOY_PORT" ]]; then
528
+ _sed_i "$APP_ROOT/.prod.docker.env" -e "s|^PORT=.*|PORT=$DEPLOY_PORT|"
529
+ elif [[ -n "$cur_port" ]]; then
530
+ _sed_i "$APP_ROOT/.prod.docker.env" -e "s|^PORT=.*|PORT=$cur_port|"
413
531
  fi
414
- # 对齐本次部署的 IMAGE/TAG/DATA_DIR/CONTAINER_NAME(IMAGE/TAG 与 config.sh 构建一致;
415
- # --env-file 把 .env 全量灌进容器,含 PORT/密钥)。
416
- # PORT:仅当本次部署显式选定端口(DEPLOY_PORT 非空)时才写 .env。
417
- # 首部署:prompt_port 必赋值,覆盖 .env.example 的 3001 缺省 → docker 端口权威生效。
418
- # 二次部署:仅显式改端口时赋值;沿用当前则留空,保留运维护过的 .env PORT。
419
- local port_e=""
420
- [[ -n "$DEPLOY_PORT" ]] && port_e="-e s|^PORT=.*|PORT=$DEPLOY_PORT|"
421
- _sed_i "$APP_ROOT/.env" \
422
- -e "s|^TAG=.*|TAG=$VER|" \
423
- -e "s|^DATA_DIR=.*|DATA_DIR=$APP_ROOT/data|" \
424
- -e "s|^CONTAINER_NAME=.*|CONTAINER_NAME=fullstack-template-backend|" \
425
- $port_e
426
- check_env_secrets "$APP_ROOT/.env"
427
- ok "docker env: $APP_ROOT/.env (TAG=$VER)"
532
+ # 旧模型迁移检测:老 env 文件带业务配置条目(JWT_SECRET 等)—— 新版本不读这些 key,
533
+ # 静默丢弃会造成「以为配了实际没生效」,提示迁移到部署位 config.yaml
534
+ if grep -qE '^(JWT_SECRET|ADMIN_PASSWORD|SSO_TICKET_KEY|APP_SIGN_KEY|CORS_ORIGINS|TRUST_PROXY|LOG_LEVEL)=..+' \
535
+ "$APP_ROOT/.prod.docker.env" 2>/dev/null; then
536
+ say "检测到 $APP_ROOT/.prod.docker.env 带业务配置条目(JWT_SECRET 等)—— 新版本只读部署位 config.yaml,请把对应 ENC 密文/值迁入 $APP_ROOT/config/config.yaml"
537
+ fi
538
+ ok "docker env: $APP_ROOT/.prod.docker.env (纯部署件;业务配置在 APP_ROOT/config/config.yaml)"
428
539
  else
429
- # systemd/pm2:config/config.prod.yaml 随 release(current 软链指向),app 以 cwd=current
430
- # 经 resolveConfig 命中 cwd/config/config.<env>.yaml 直读。不拷贝、不注入字段(数据目录由
431
- # mode install 经 DB_DIR env 注入 = APP_ROOT/data,跨版本共享)。仅非空阻断(仓库
432
- # config.prod.yaml 已带真值则直接过)。
433
- [[ -f "$t/config/config.prod.yaml" ]] || die "缺 $t/config/config.prod.yaml(release 包不完整)"
434
- check_yaml_secrets "$t/config/config.prod.yaml"
435
- ok "config: $t/config/config.prod.yaml(release 自带,改后 restart 即生效)"
540
+ # systemd/pm2:docker env 本模式不消费,包内原件一并删除(mv 纪律,防场景三残留)。
541
+ # 运行时配置走 CONFIG_DIR=$APP_ROOT/config(unit/ecosystem 注入,mode install 生成)。
542
+ [[ $same_cfg -eq 1 ]] || rm -f "$src_cfg/.prod.docker.env"
543
+ [[ -f "$APP_ROOT/config/config.yaml" ]] || die "缺 $APP_ROOT/config/config.yaml(部署不完整)"
544
+ ok "配置: $APP_ROOT/config/config.yaml (改后 restart 即生效;跨版本共享)"
545
+ fi
546
+
547
+ # 包内 transport 清空(场景三 src 即部署位则保留;.prod.secrets 一并清理兼容老包)
548
+ if [[ $same_cfg -eq 0 ]]; then
549
+ rm -f "$src_cfg"/config.*.yaml "$src_cfg/.prod.secrets" "$src_cfg/.prod.docker.env"
550
+ rmdir "$src_cfg" 2>/dev/null || true
436
551
  fi
437
552
  }
438
553
 
@@ -449,7 +564,7 @@ dispatch_install() {
449
564
  local mode_install="$APP_ROOT/ops/$MODE/install.sh" # 共享 ops(releases 不放 ops)
450
565
  [[ -f "$mode_install" ]] || die "缺 mode 安装脚本: $mode_install(先跑 lay_out_ops 或检查后台服务目录/ops)"
451
566
  say "派发 $MODE 安装: $mode_install"
452
- export APP_ROOT MODE KEEP_VERSIONS
567
+ export APP_ROOT MODE KEEP_VERSIONS CONFIG_ENCRYPT_INIT
453
568
  bash "$mode_install"
454
569
  }
455
570
 
@@ -498,29 +613,29 @@ cleanup_staging() {
498
613
  # bootstrap
499
614
  # ════════════════════════════════════════════════════════════════
500
615
  bootstrap() {
501
- [[ -z "$APP_ROOT" ]] || die "bootstrap 不应带 baked 后台服务目录"
502
616
  prompt_app_root
503
617
  [[ -n "$MODE" ]] || prompt_mode
618
+ prompt_env
504
619
  prompt_port
620
+ prompt_encrypt
505
621
 
506
622
  say "初始化目录: $APP_ROOT"
507
623
  mkdir -p "$APP_ROOT"/{releases,data,logs}
508
624
  # current 不预建(systemd/pm2 由 go_live 建软链;docker 由 lay_out_release 建目录)
509
625
 
510
- # 拷顶层脚本到 APP_ROOT 并 bake。场景三(包更名成 APP_ROOT,DIR==APP_ROOT)时
511
- # cp 同文件会 exit1 致死(set -e) → 跳过 cp 直接就地 _sed_i bake。
512
- # bash 持有运行脚本的 open fd,rm/改自身目录项不影响读取(inode 存活至进程退出)。
626
+ # 纯拷贝顶层脚本到 APP_ROOT(不 bake:运行时自定位 + 读 .mode,零配置,APP_ROOT 搬迁可用)。
627
+ # 场景三(包更名成 APP_ROOT,DIR==APP_ROOT)时 cp 同文件会 exit1 致死(set -e)→ 跳过拷贝,
628
+ # bash 持有运行脚本的 open fd,目录项替换不影响读取(inode 存活至进程退出)。
513
629
  local s src dst same=0
514
630
  [[ "$(cd "$DIR" && pwd -P)" = "$(cd "$APP_ROOT" && pwd -P)" ]] && same=1
515
631
  for s in "${TOP_SCRIPTS[@]}"; do
516
632
  src="$DIR/$s"; dst="$APP_ROOT/$s"
517
633
  [[ -f "$src" ]] || die "缺顶层脚本 $src(release 包不完整)"
518
- [[ $same -eq 0 ]] && cp -f "$src" "$dst"
519
- _sed_i "$dst" -e "s|^APP_ROOT=.*|APP_ROOT=\"$APP_ROOT\"|" -e "s|^MODE=.*|MODE=\"$MODE\"|"
634
+ [[ $same -eq 1 ]] || cp -f "$src" "$dst"
520
635
  chmod +x "$dst"
521
636
  done
522
637
  echo "$MODE" > "$APP_ROOT/.mode"
523
- ok "顶层脚本就位:baked APP_ROOT=$APP_ROOT MODE=$MODE"
638
+ ok "顶层脚本就位:$APP_ROOT(MODE=$MODE,自定位零配置)"
524
639
 
525
640
  resolve_work_dir
526
641
  lay_out_release
@@ -540,8 +655,10 @@ bootstrap() {
540
655
  rotate_releases
541
656
  dispatch_install
542
657
  ok "首次部署完成 ✓"
543
- local disp_port; disp_port="$(yaml_app_port "$DEPLOY_TARGET/config/config.prod.yaml")"
544
- echo " 端口: ${disp_port:-3001}(来自 config.prod.yaml,nginx 反代到 http://localhost:<port>/api)"
658
+ [[ -f "$APP_ROOT/.prod.secrets" ]] \
659
+ && echo " 配置:已加密(根密钥 $APP_ROOT/.prod.secrets;改部署位 config.yaml 明文后重跑 ./install.sh 自动重新加密)"
660
+ local disp_port; disp_port="$(yaml_app_port "$APP_ROOT/config/config.yaml")"
661
+ echo " 端口: ${disp_port:-3001}(部署位 config.yaml,nginx 反代到 http://localhost:<port>/api)"
545
662
  echo " 管理: cd $APP_ROOT && ./start.sh | ./stop.sh | ./status.sh 2>/dev/null || true"
546
663
  echo " 版本切换(回滚/升级): cd $APP_ROOT && ./versionswitch.sh"
547
664
  cleanup_staging
@@ -551,43 +668,18 @@ bootstrap() {
551
668
  # deploy
552
669
  # ════════════════════════════════════════════════════════════════
553
670
  deploy() {
554
- [[ -n "$APP_ROOT" ]] || die "二次部署:后台服务目录未 baked(应在后台服务目录下的 install.sh 内运行)"
555
- [[ -n "$MODE" ]] || MODE="$(cat "$APP_ROOT/.mode" 2>/dev/null || true)"
556
- [[ -n "$MODE" ]] || die "缺 MODE(无 baked、无 .mode 文件)"
557
-
558
- # 二次部署端口确认:
559
- # systemd/pm2:yaml app.port 为权威(改新 release yaml,app cwd=current 直读)。
560
- # docker:.env PORT 为权威(改新 release yaml 作镜像兜底 + DEPLOY_PORT 由 ensure_secrets 写 .env)。
561
- # 回车=沿用当前运行端口(就地改新 release yaml 对齐,nginx 不受影响);输入合法端口=就地改 yaml
562
- # 且 docker 模式记 DEPLOY_PORT → ensure_secrets 写 .env PORT(权威)。同端口静默。
563
- local new_port cur_port dp_input
564
- new_port="$(yaml_app_port "$APP_DIR/config/config.prod.yaml")"
565
- if [[ "$MODE" == "docker" && -f "$APP_ROOT/.env" ]]; then
566
- # docker 当前运行端口权威 = .env PORT(非 yaml)
567
- cur_port="$(sed -nE 's/^PORT=//p' "$APP_ROOT/.env" | head -1 | sed -E "s/^['\"]?//; s/['\"]?$//; s/^[[:space:]]+//; s/[[:space:]]+$//")"
671
+ [[ -n "$APP_ROOT" ]] || die "二次部署:当前目录无 .mode(应在 APP_ROOT 下的 install.sh 内运行)"
672
+ [[ -n "$MODE" ]] || die "缺 MODE($APP_ROOT/.mode 不可读)"
673
+
674
+ # 端口 = 部署位权威(release 不再携带端口变更):systemd/pm2 = APP_ROOT/config/config.yaml 的
675
+ # app.port;docker = .prod.docker.env 的 PORT。换端口:改部署位 + ./start.sh(重建容器/重启进程)。
676
+ local cur_port=""
677
+ if [[ "$MODE" == "docker" && -f "$APP_ROOT/.prod.docker.env" ]]; then
678
+ cur_port="$(sed -nE 's/^PORT=//p' "$APP_ROOT/.prod.docker.env" | head -1 | sed -E "s/^['\"]?//; s/['\"]?$//; s/^[[:space:]]+//; s/[[:space:]]+$//")"
568
679
  else
569
- cur_port="$(yaml_app_port "$APP_ROOT/current/config/config.prod.yaml")"
570
- fi
571
- if [[ -n "$new_port" && -n "$cur_port" && "$new_port" != "$cur_port" ]]; then
572
- echo
573
- say "⚠ 端口不一致:当前运行端口=$cur_port,新版本 app.port=$new_port"
574
- echo " ($([[ "$MODE" == "docker" ]] && echo 'docker 权威=.env PORT' || echo 'yaml 为权威');回车=沿用当前端口 $cur_port,或输入新端口)"
575
- read -rp " 回车沿用当前端口,或输入新端口: " dp_input || dp_input=""
576
- dp_input="${dp_input%%$'\n'}"
577
- if [[ -z "$dp_input" ]]; then
578
- # 沿用当前运行端口:改新 release yaml 对齐(镜像兜底);.env PORT(docker)不动
579
- set_app_port "$APP_DIR/config/config.prod.yaml" "$cur_port"
580
- ok "运行端口对齐当前: $cur_port(已改新 release config.prod.yaml)"
581
- elif [[ ! "$dp_input" =~ ^[0-9]+$ || "$dp_input" -lt 1 || "$dp_input" -gt 65535 ]]; then
582
- die "无效端口: $dp_input(期望 1-65535 的数字;手动改请编辑 $APP_DIR/config/config.prod.yaml 的 app.port 后重跑)"
583
- else
584
- set_app_port "$APP_DIR/config/config.prod.yaml" "$dp_input"
585
- DEPLOY_PORT="$dp_input" # docker:由 ensure_secrets 写入 .env PORT(权威)
586
- ok "已改新 release app.port: $new_port → $dp_input($([[ "$MODE" == "docker" ]] && echo 'docker 写 .env PORT' || echo 'yaml 权威'),部署即生效)"
587
- fi
588
- elif [[ -n "$new_port" ]]; then
589
- say "运行端口: $new_port(与当前一致)"
680
+ cur_port="$(yaml_app_port "$APP_ROOT/config/config.yaml")"
590
681
  fi
682
+ say "运行端口: ${cur_port:-?}(部署位权威;换端口改 $APP_ROOT/config/config.yaml 的 app.port / .prod.docker.env 的 PORT,再 ./start.sh)"
591
683
 
592
684
  local cur_ver=""
593
685
  [[ -L "$APP_ROOT/current" && -f "$APP_ROOT/current/package.json" ]] && cur_ver="$(read_version "$APP_ROOT/current")" || true
@@ -621,8 +713,17 @@ deploy() {
621
713
 
622
714
  cleanup_staging
623
715
  ok "部署完成 v$VER ✓"
624
- local disp_port; disp_port="$(yaml_app_port "$DEPLOY_TARGET/config/config.prod.yaml")"
625
- echo " 端口: ${disp_port:-3001}(来自 config.prod.yaml) 状态: cd $APP_ROOT && ./ops/$MODE/status.sh"
716
+ local disp_port; disp_port="$(yaml_app_port "$APP_ROOT/config/config.yaml")"
717
+ echo " 端口: ${disp_port:-3001}(部署位 config.yaml) 状态: cd $APP_ROOT && ./ops/$MODE/status.sh"
718
+ if [[ "$SEEDED_CONFIG" = "1" ]]; then
719
+ # 老部署升级:本次 config.yaml 是包内 overlay 播种的默认值,旧部署在服务器上维护的
720
+ # 生产真值不在里面,必须手工合并一次(此后发布永不覆盖,只此一次)。
721
+ err "⚠ 本次播种了 $APP_ROOT/config/config.yaml(包内默认值)—— 老部署升级需手工合并生产真值:"
722
+ echo " 1) 旧配置的改动值:docker 看旧镜像(docker run --rm <旧IMAGE:TAG> cat /app/config/config.prod.yaml;"
723
+ echo " systemd/pm2 看 releases/ 下旧版本目录的 config/;对比合并差异项"
724
+ echo " 2) 旧 .prod.docker.env 的业务条目(JWT_SECRET 等,上方已提示):迁入 config.yaml 对应键"
725
+ echo " 3) 合并后 ./start.sh 生效;若为加密模式(.prod.secrets 存在),重跑 ./install.sh 自动加密新明文"
726
+ fi
626
727
  }
627
728
 
628
729
  # ════════════════════════════════════════════════════════════════