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
@@ -0,0 +1,164 @@
1
+ # 配置加密(ENC 密文)运维手册
2
+
3
+ > 模型:**仓库 / 传输层明文维护,服务器侧按需落盘为密文**。开发机零密钥、零加密操作 ——
4
+ > dev 一律明文(security.encrypt=false,无 `.develop.secrets`,改密 = 改 yaml + 重启);
5
+ > prod 的 `config.<env>.yaml` 也明文维护,ENC(...) 加密只发生在服务器:首部署 `install.sh`
6
+ > 询问是否开启,开启后由部署脚本生成本机根密钥并把部署位 `config.yaml` 的敏感值就地加密,
7
+ > 此后每次部署自动加密新增明文。运行时解密:`src/common/config-crypto.ts` + `configuration.ts`。
8
+
9
+ ---
10
+
11
+ ## 0. 总开关 security.encrypt
12
+
13
+ yaml 顶层的开关,声明本环境是否强制配置加密:
14
+
15
+ ```yaml
16
+ security: # 写在 config.base.yaml(全环境共享;部署位 config.yaml 可覆盖)
17
+ encrypt: false # 模板恒 false(仓库层明文);服务器开启加密时 install.sh 把部署位副本置 true
18
+ encryptedFields: # 受控字段清单(dot-path)
19
+ - jwt.secret
20
+ - admin.password
21
+ - sso.ticketKey
22
+ - appSign.cryptKey
23
+ ```
24
+
25
+ - **true**:`security.encryptedFields` 清单内的字段**非空就必须是 `ENC(...)` 密文**,启动校验,
26
+ 漏填明文直接报错拒启(fail-fast)。校验在 base+overlay 合并之后做 —— base 与环境 overlay
27
+ (部署位 config.yaml)**同强制**,防「以为加密了实际裸奔」。空占位(`''`)不拦,与
28
+ 「占位可直跑」一致。
29
+ - **false**(模板默认):全部接受明文;已写的 `ENC(...)` 仍会照常解密(向后兼容),故切换开关无需改值。
30
+ - `appSign.signKey` 不在强制清单(与前端 bundle 逐字一致,刻意明文,见 §2)。
31
+ - **清单可在 yaml 配置**:dist 是混淆后的产物,新增敏感字段只改 yaml 的 `encryptedFields` 即可,
32
+ 无需重编译;缺省回落内置默认四项。
33
+ - **老配置缺 security 段按 true**(历史口径,防升级回退)。
34
+
35
+ ## 1. 加密在哪儿发生(生命周期一图流)
36
+
37
+ ```
38
+ 开发机 服务器(APP_ROOT)
39
+ ────── ─────────────────────
40
+ config.<env>.yaml 明文维护
41
+ │
42
+ pnpm publish(tar/scp,与现状同通道;
43
+ 包内密钥不再随行,敏感值明文)
44
+ ▼
45
+ 首部署 install.sh 交互:是否开启配置加密?
46
+ ├─ Y(默认)→ 生成本机根密钥 → APP_ROOT/.prod.secrets(600,
47
+ │ 现场打印一次,存密码管理器)→ 把播种的部署位
48
+ │ config.yaml 敏感值【就地加密】为 ENC(...)+
49
+ │ 置 security.encrypt: true
50
+ └─ n → 全明文运行,什么都不生成
51
+ ▼
52
+ 日常轮换:改部署位 config.yaml 明文(如换 admin.password)
53
+ → 重跑 ./install.sh(= 再发布)→ 自动检测非空明文、
54
+ 用本机根密钥就地重新加密 → 服务重启生效
55
+ (服务器上永远不需要手工跑 config-crypt)
56
+ ```
57
+
58
+ - 就地加密/自动重加密由 `ops/ensure-encrypt.cjs` 完成,`ops/lib-encrypt.sh` 编排:
59
+ 首部署交互选「开启」给 mode install 置 `CONFIG_ENCRYPT_INIT=1`;`$APP_ROOT/.prod.secrets`
60
+ 存在 = 加密模式(此后每次 install/version 切换都自动跑 relock);不存在 = 明文模式(零动作)。
61
+ - 三模式的 node 执行器:systemd = `NODE_BIN`、pm2 = PATH、docker = 一次性容器
62
+ (镜像构建后 `docker run --rm --user` 跑,写回文件的宿主属主不变)。
63
+ - **防呆闭环**:开启加密后手改明文直接重启会被启动校验拦下,提示「重跑 ./install.sh」——
64
+ 引导用户走自动加密路径,不放松校验。
65
+ - **老加密部署升级**:既有 ENC 密文 + 服务器 `.prod.secrets` 的部署,新版本行为不变
66
+ (relock 用同一把钥匙校验既有密文、加密新明文),升级无感。
67
+
68
+ ## 2. 哪些字段受控
69
+
70
+ | 字段 | 位置 | 说明 |
71
+ |---|---|---|
72
+ | `jwt.secret` | 环境 overlay(部署位 config.yaml) | JWT 签名密钥 |
73
+ | `admin.password` | 同上 | 超管口令(启动 reconcile 到 DB) |
74
+ | `appSign.cryptKey` | 同上 | 大屏内容加密密钥(预留) |
75
+ | `sso.ticketKey` | 同上 | SSO 票据密钥(默认关闭) |
76
+ | `appSign.signKey` | ❌ 恒明文 | **不加密**:须与前端 `VITE_APP_SIGN_KEY` bundle 逐字一致(前端可见),加密无意义且易错位 |
77
+
78
+ > 机制是通用的:最终配置树里任意字符串值写成 `ENC(...)` 都会被解密。新增敏感字段加进
79
+ > `security.encryptedFields` 清单即可(加密、校验、清单解析三处自动生效)。
80
+
81
+ ## 3. 工具
82
+
83
+ ### 3.1 服务器自动:`ops/ensure-encrypt.cjs`(零依赖,主路径)
84
+
85
+ ```bash
86
+ CONFIG_SECRET_KEY=<根密钥> node ops/ensure-encrypt.cjs relock $APP_ROOT/config
87
+ node ops/ensure-encrypt.cjs genkey
88
+ ```
89
+
90
+ 每次 install.sh 自动调用,做三件事:① 校验既有 ENC(...) 用当前根密钥能解(密钥错配
91
+ fail-fast);② 清单内的**非空明文**就地加密(行级改写,注释逐字节保留);③ 部署位
92
+ config.yaml 的 `security.encrypt` 置 true。日常操作不直接跑它 —— 改明文后重跑
93
+ `./install.sh` 即可。
94
+
95
+ ### 3.2 手动排障:`scripts/config-crypt.cjs`(单值加解密)
96
+
97
+ ```bash
98
+ node scripts/config-crypt.cjs genkey # 生成新密钥(仅手动场景;install.sh 自动生成的密钥在服务器)
99
+ node scripts/config-crypt.cjs decrypt "ENC(...)" <key> # 解密校验 / 排障
100
+ node scripts/config-crypt.cjs encrypt "<明文>" <key> # 手动产出 ENC(...)(不建议:改明文 + 重跑 ./install.sh 更省事)
101
+ ```
102
+
103
+ > 每次加密 IV 随机,**同一明文每次密文不同**,属正常。
104
+
105
+ ## 4. 密钥管理
106
+
107
+ | 环境 | 密钥 | 存放 | 说明 |
108
+ |---|---|---|---|
109
+ | **dev** | 无 | — | 明文运行(security.encrypt=false),不涉及密钥 |
110
+ | **每个部署(服务器)** | 部署机本地生成 | `$APP_ROOT/.prod.secrets`(600,仅属主可读) | 首部署选「开启加密」时生成、现场打印一次(存密码管理器);systemd 经 EnvironmentFile、pm2 经 ecosystem env、docker 经 `run -e` 注入进程。密钥与密文同机演化 —— 不存在「本地加密密钥与服务器密文错配」问题 |
111
+
112
+ ## 5. 首次部署 / 密钥注入(按模式)
113
+
114
+ `install.sh` 首部署在「选模式 → 选环境 overlay → 确认端口」之后多问一句**是否开启配置加密**
115
+ (默认 Y)。三种部署方式由各 mode 的 install.sh 统一处理(`ops/lib-encrypt.sh`):
116
+ - **docker**:镜像构建后经一次性容器执行加密(宿主常无 node);根密钥由 `run-container.sh`
117
+ 以 `docker run -e` 注入容器。
118
+ - **systemd**:unit 以 `EnvironmentFile=$APP_ROOT/.prod.secrets` 注入(600,仅属主可读,
119
+ 不直接写进 unit —— unit 在 /etc/systemd 下可被多读)。
120
+ - **pm2**:ecosystem env 注入。
121
+
122
+ 明文模式(选 n)三模式都不注密钥;但部署位 `config.yaml` 带 `ENC(...)` 而无密钥 = 部署不
123
+ 完整,mode install 直接拦下。
124
+
125
+ ### 三模式统一:改部署位 config.yaml 重跑 ./install.sh 即轮换
126
+ 业务配置(含全部密码类条目)一律在 `$APP_ROOT/config/config.yaml`:
127
+ - 改**明文** → 重跑 release 部署(`./install.sh <包>` 或再 publish)→ 自动加密 → 新值生效;
128
+ - 临时验证可用 `./versionswitch.sh` 重派发(同样触发 relock)。
129
+ - 直接 `./start.sh` 重启**不**触发加密:若 config.yaml 有明文且 security.encrypt=true,
130
+ 启动校验会拦下并提示重跑 `./install.sh`。
131
+ - 回滚:`./versionswitch.sh`(relock 幂等,不影响)。
132
+
133
+ ### 老部署升级迁移
134
+ - 旧模型「包内随行 `.prod.secrets`」:新包不再带该文件;服务器既有的 `.prod.secrets` 为权威,
135
+ 继续有效(relock 用它维护既有 ENC 密文)。
136
+ - 旧版 `.prod.docker.env` 里的业务条目(`JWT_SECRET` / `ADMIN_PASSWORD` 等)新版本**不再读取**,
137
+ 迁入 `$APP_ROOT/config/config.yaml` 对应位(install.sh 检测并提示)。
138
+
139
+ ## 6. 排障
140
+
141
+ | 现象 | 原因 | 处理 |
142
+ |---|---|---|
143
+ | 启动报「以下敏感字段为明文,必须写 ENC(...) 密文」 | 加密模式下部署位 config.yaml 被手改成明文,直接重启 | 重跑 `./install.sh`(自动加密);或确要明文:config.yaml 的 security.encrypt 置 false |
144
+ | 启动报「缺少 CONFIG_SECRET_KEY 环境变量」 | 密钥没注入(如容器重建丢 env) | 按部署模式注入;缺 `$APP_ROOT/.prod.secrets` 则重跑 `./install.sh` |
145
+ | 部署/启动报「既有密文用当前根密钥解不开」 | 根密钥被换过 / 密文来自别的部署 | 核对 `.prod.secrets`;用 `config-crypt.cjs decrypt` 验证单条;必要时改明文 + 重跑 `./install.sh`(以现密钥全量重加密) |
146
+ | 改了 config.yaml 明文但服务没起 | 明文被防呆拦下 | 同第一行:重跑 `./install.sh` |
147
+ | 登录一直失败 | admin 解出的还是旧口令,reconcile 把 DB 刷回旧值 | 确认 config.yaml 的 admin.password 改的是新值并重跑部署 |
148
+
149
+ ## 7. 轮换根密钥本身(CONFIG_SECRET_KEY)
150
+
151
+ 密钥换了,用它加密的 `ENC(...)` 都得**重新加密**。新模型下很简单:备份好 `.prod.secrets` →
152
+ 删掉它 → 把部署位 config.yaml 的敏感值改回**明文**(或用旧密钥 `decrypt` 出明文)→ 重跑
153
+ `./install.sh`(生成新密钥 + 全量重加密 + 现场打印新密钥)。密钥轮换低频,但不再是
154
+ 「每条密文手工过一遍」。
155
+
156
+ ## 8. 安全边界(诚实声明)
157
+
158
+ - ✅ 服务器侧:部署位配置、release 包残留在服务器上的部分中敏感值均为密文(开启加密后);
159
+ 密钥 600、仅属主可读,与配置文件分离。
160
+ - ⚠️ 已知取舍(2026-10 模型收口的决策):**仓库、本地构建与 tar/scp 传输层是明文** ——
161
+ prod 凭证以明文存在于生成项目的 git 历史、buildops 包与上传通道中。这是拿「仓库泄露
162
+ 风险」换「开发零密钥、维护零操作」;对高敏感项目请自建加密前置(如在打包前自行加密)。
163
+ - ❌ 不防:能登服务器读 env(`docker inspect`、`/proc/<pid>/environ`)或解密后内存的人。
164
+ 密钥与配置分离只是抬高门槛,不是绝对防护。
@@ -18,8 +18,8 @@
18
18
  |---|---|
19
19
  | `pnpm dev` | 并行起 web(6177)+ backend(3001) |
20
20
  | `pnpm build` / `pnpm lint` | 全量构建 / 全量 lint |
21
- | `pnpm --filter web dev` / `pnpm --filter backend dev` | 单起一端 |
22
- | `pnpm --filter backend migration:generate src/database/migrations/<name>` | 新增实体后生成迁移(必须,见硬约束 ①) |
21
+ | `pnpm --filter ./web dev` / `pnpm --filter ./backend dev` | 单起一端 |
22
+ | `pnpm --filter ./backend migration:generate src/database/migrations/<name>` | 新增实体后生成迁移(必须,见硬约束 ①) |
23
23
  | `pnpm publish` / `pnpm rollback` | 发布 / 回滚(`--only web\|backend` 单发);会 SSH 真实服务器,见「禁区」 |
24
24
 
25
25
  dev 下 vite proxy 把 `/api`、`/ws`、`/static/uploads` 转给 `127.0.0.1:3001`,前端同源请求。
@@ -34,7 +34,8 @@ backend/src/common/ guard / filter / interceptor(响应包装、双轨鉴权
34
34
  backend/src/modules/ 业务模块:auth、user、audit、ops、upload、websocket、page-data(示例)、screen-config、image(示例)
35
35
  backend/src/entities/ TypeORM 实体
36
36
  backend/src/database/ migrations/ + sqlite.config.ts(显式 import 注册)
37
- backend/config/ config.develop.yaml / config.prod.yaml
37
+ backend/config/ config.base.yaml(全量默认)+ config.<env>.yaml 环境 overlay(develop/prod/新环境
38
+ 只增一份;部署位 = 服务器 APP_ROOT/config/config.yaml,install.sh 播种)
38
39
  backend/ops/ 服务器侧部署 shell(install.sh 为单一真相源)
39
40
  scripts/ 根级发布/回滚薄编排
40
41
  ```
@@ -48,7 +49,7 @@ scripts/ 根级发布/回滚薄编排
48
49
  5. **prod 建表只靠迁移**:`synchronize:false`;develop 是 `synchronize:true` 本地免迁移,别被它骗了。
49
50
  6. **web 不引 TS**:web 是 jsconfig 工程,后端才是 TS;类型前后端各自声明,不建 `packages/shared`,不用 `workspace:*`。
50
51
  7. **两个请求实例禁止复用**:`request.ts`(后台,带 Authorization)与 `app-request.ts`(大屏,三头签名)独立,复用会互相头污染。
51
- 8. **DB 文件名程序定死**:`template.prod.sqlite`/`template.dev.sqlite`,改名要同步 `sqlite.config.ts`、`configuration.ts`、`ops/sqlite/config.sh`、ops 探针四处。
52
+ 8. **DB 文件名程序定死**:`template.prod.sqlite`/`template.dev.sqlite`,改名要同步 `sqlite.config.ts`、`configuration.ts` 两处。
52
53
  9. **better-sqlite3 v13 无预编译二进制**:需 python3/make/g++ 源码编译(Dockerfile 已装齐);本地装依赖报 node-gyp 错先查这个。
53
54
  10. **backend/package-lock.json 必须是真 npm lockfile**(Dockerfile `npm ci` 用;buildops 自动生成/刷新——缺失或依赖不同步时在隔离目录跑 `npm install --package-lock-only` 拷回,无需手工;在 `backend/` 下裸跑 npm 会撞 workspace 根探测/pnpm 符号链污染,详见 `build.cjs::ensureLockfile` 注释与 README「第 4 步」)。
54
55
 
@@ -11,7 +11,7 @@
11
11
  ## Claude Code 专属约定
12
12
 
13
13
  - **方案先行**:非平凡改动(新功能/重构/修 bug)先给方案和根因分析,经用户确认再动手;贴错误栈或要求「接入/修复/调整」时同样适用。
14
- - **改动后自查**:`pnpm lint` 过了不算完,后端改动跑一次 `pnpm --filter backend build` 确认 tsc+terser 产物能出(tsc 会做全量类型检查;迁移显式注册的问题只有 build+prod 路径暴露)。
14
+ - **改动后自查**:`pnpm lint` 过了不算完,后端改动跑一次 `pnpm --filter ./backend build` 确认 tsc+terser 产物能出(tsc 会做全量类型检查;迁移显式注册的问题只有 build+prod 路径暴露)。
15
15
  - **不要主动 publish**:发布/回滚走真实 SSH,只在用户明确说「发版/回滚」时执行。
16
16
  - **web 是 JS 不是 TS**:在 `web/` 下写代码用 JS + jsconfig 路径别名,别顺手加 TS 语法;`backend/` 才是 TS。
17
17
  - **git 提交**:只在用户要求时提交;type 前缀 + 中文描述(如 `feat: xxx`)。
@@ -11,7 +11,7 @@
11
11
  - **web**(`web/`)— Vite + React18 + AntD5 + Recoil + Echarts,dev 端口 6177。
12
12
  - **backend**(`backend/`)— NestJS + SQLite(better-sqlite3)+ TypeORM + yaml 多环境,dev 端口 3001,全局前缀 `/api`。
13
13
 
14
- 根命令:`pnpm dev`(web+backend 并行)/ `pnpm build` / `pnpm lint` / `pnpm publish`(发 web+backend;`--only web|backend` 单发)/ `pnpm rollback`。
14
+ 根命令:`pnpm dev`(web+backend 并行)/ `pnpm build` / `pnpm lint` / `pnpm publish`(发 web+backend;`--only web|backend` 单发;`--env <名>` 发指定服务器)/ `pnpm rollback`。
15
15
 
16
16
  dev 时 vite proxy 把 `/api`、`/ws`、`/static/uploads` 转给 `localhost:3001`;前端同源请求,`web/public/env.config.js` 的 `requestBaseUrl` 留空。
17
17
 
@@ -21,29 +21,37 @@ dev 时 vite proxy 把 `/api`、`/ws`、`/static/uploads` 转给 `localhost:3001
21
21
 
22
22
  ### 1. 包名 / 标题(身份)— ✅ 已由脚手架完成
23
23
  - 根 `package.json`:`name`、`description`
24
- - `web/package.json`:`name`、`description`(同时影响 `pnpm --filter web` 与 `backend/scripts/build.cjs` 的 `safeName`)
24
+ - `web/package.json`:`name`、`description`(影响 `backend/scripts/build.cjs` 的 `safeName`)
25
25
  - `backend/package.json`:`name`、`description`
26
26
  - `web/config/project.config.ts`:`title`(后台顶栏 + 登录页品牌)
27
27
  - `backend/src/main.ts`:Swagger `.setTitle(...)`
28
- - 根 `scripts/publish.cjs`、`rollback.cjs`:若改了 web/backend 包名,同步改 `--filter` 名
28
+ - 根 `scripts/publish.cjs`、`rollback.cjs`:`--filter` 用 `./web`/`./backend` 路径形态,改包名无需同步
29
29
 
30
30
  ### 2. 密钥(占位值,务必替换)— ✅ 已由脚手架生成并前后端成对注入
31
31
  dev/prod 两套密钥已生成并写入下列位置(超管密码见生成时控制台输出):
32
32
  - **大屏签名密钥** `appSign.signKey`(HMAC,前后端逐字一致):
33
33
  - `web/.env.development` + `web/.env.production` 的 `VITE_APP_SIGN_KEY`
34
- - `backend/config/config.develop.yaml` + `config.prod.yaml` 的 `appSign.signKey`
34
+ - `backend/config/config.develop.yaml` + `config.prod.yaml`(环境 overlay)的 `appSign.signKey`
35
35
  - ⚠️ **signKey 会内联进公开的前端 bundle**(任何人可从 dist 提取),签名只防随手调用、不是真实鉴权;
36
36
  大屏若含敏感数据,须在后端叠加真实防护(IP 白名单/网关鉴权)
37
- - **JWT secret**:backend 两份 yaml 的 `jwt.secret`
38
- - **超管密码**:backend 两份 yaml 的 `admin.password`(启动 reconcile 到 DB)
39
- - **SSO 票据密钥** `sso.ticketKey`:backend 两份 yaml(默认关闭,见下「单点登录」)
40
- - **docker 部署**:`backend/ops/docker/.env.example` 的 `JWT_SECRET`/`ADMIN_PASSWORD`/`APP_SIGN_KEY`(拷为服务器 `.env` 后改真值)
37
+ - **服务端秘密**(jwt.secret / admin.password / appSign.cryptKey / sso.ticketKey,五处中的后四项):**明文写入**环境 overlay yaml
38
+ (dev 开箱即跑、prod 明文维护,手册见 `docs/config-encryption.md`)—— ENC(...) 加密只在服务器侧按需发生:
39
+ 服务器**首部署时 install.sh 会询问是否开启配置加密**,选开启则本机生成根密钥(`APP_ROOT/.prod.secrets`,600,
40
+ 现场打印一次存密码管理器)并把部署位 config.yaml 的敏感值就地加密;此后每次部署自动加密新增明文
41
+ (**日常轮换 = 改部署位明文 + 重跑 ./install.sh**,服务器上不需要手工跑 config-crypt)
42
+ - **超管密码**:明文在 yaml(见生成时控制台输出,启动 reconcile 到 DB;服务器加密后落盘为 ENC 密文)
43
+ - **SSO 票据密钥** `sso.ticketKey`:明文在 backend 环境 overlay yaml(默认关闭,见下「单点登录」)
44
+ - **部署配置模型(单一真相源 = yaml)**:配置 = `config.base.yaml`(全量默认)+ 环境 overlay(`config.develop.yaml` /
45
+ `config.prod.yaml`,新发布环境 = 复制一份改值)。buildops 打包随行(密钥不随包),首部署 install.sh 选定 overlay 播种为服务器
46
+ 部署位 `APP_ROOT/config/config.yaml`(此后发布不覆盖,运维/开发在部署位维护该环境真值;改它 + 重跑 ./install.sh 即生效,
47
+ 三模式一致)。`.prod.docker.env` 由 buildops **自动生成**,只含部署件(镜像/tag/端口/数据目录),**不含任何密码**
41
48
 
42
49
  **单点登录(/api/ssologin,默认关闭)**:接入第三方门户时,改 backend yaml `sso.enabled: true` 并把 `sso.ticketKey` 交给接入方;接入方生成跳转 URL `?user=<用户名>&ts=<unix秒>&nonce=<16位hex>&sign=<hex>`,其中 `sign = HMAC-SHA256('<username>\n<ts>\n<nonce>', ticketKey)`。nonce 一次性消费(防票据重放)。未开启时接口返回 404,签发的 token 角色为 `web`(最小权限)。
43
50
 
44
51
  ### 3. 部署凭证(本地、不入库)
45
52
  - `web/scripts/server.config.json`:从 `server.config.example.json` 拷,填 SSH host/账号/密码/部署路径(已 `.gitignore`)
46
53
  - `backend/scripts/server.config.cjs`:从 `server.config.example.cjs` 拷,填 SSH + `backendServiceDir`(已 `.gitignore`)
54
+ - **多服务器**(各机独立实例,互不共享数据):web 侧给每台机器加一个 key(如 `machine-b`),backend 侧在 `targets` 加同名目标;`pnpm publish -- --env machine-b` 即 web+backend 同发该机,`--env` 缺失或目标不存在会 fail-fast(绝不静默回落默认机)
47
55
 
48
56
  ### 4. backend lockfile(自动生成,无需手工)
49
57
  模板**未带** `backend/package-lock.json`(供 Dockerfile `npm ci` 的真 npm lockfile)——`pnpm buildops` 会自动生成/刷新:缺失或与 package.json 依赖不同步时,自动在一次性隔离目录跑 `npm install --package-lock-only` 并把产物拷回 `backend/`(不能直接在 `backend/` 下跑:npm 会爬 workspace 根、读 pnpm 符号链,崩或产出伪 lock)。异常时的手工兜底:
@@ -81,17 +89,17 @@ mkdir -p /tmp/be-lock && cp backend/package.json /tmp/be-lock/ \
81
89
  ### 数据库(TypeORM + SQLite)
82
90
  - prod `synchronize:false`,靠迁移建表;develop `synchronize:true`。
83
91
  - 迁移 `backend/src/database/migrations/*.ts`;`sqlite.config.ts` 的 `migrations` 用**显式 import**(非 glob——单文件 bundle 下 glob 落空致 prod 不建表)。
84
- - 新增实体后 `pnpm --filter backend migration:generate src/database/migrations/xxxx`。
92
+ - 新增实体后 `pnpm --filter ./backend migration:generate src/database/migrations/xxxx`。
85
93
  - WAL + PRAGMA 加固:`journal_mode=WAL / synchronous=NORMAL / busy_timeout=5000 / foreign_keys=ON`。
86
- - DB 文件名程序定死:`template.prod.sqlite`/`template.dev.sqlite`(改名要同步 `sqlite.config.ts`、`configuration.ts`、`ops/sqlite/config.sh`、ops 探针)。
94
+ - DB 文件名程序定死:`template.prod.sqlite`/`template.dev.sqlite`(改名要同步 `sqlite.config.ts`、`configuration.ts`、`scripts/data-source.ts`;`config.*.yaml` / `docker-compose.yml` 注释里的文件名示例同)。
87
95
 
88
96
  ### 后台主题(Airtable 风格)
89
97
  仅作用于 `/background` 后台路由树;大屏端不套此主题。入口:`web/src/styles/theme.ts`(`airtableTheme`)→ `Background.tsx` 的 `<ConfigProvider>`。
90
98
  UI 规范见 `docs/airtable/DESIGN.md`;后期想换自己的风格:替换/修改 `theme.ts` 的 token 即可,该规范文档可删。
91
99
 
92
100
  ### backend 构建(tsc + terser 混淆)
93
- - `pnpm --filter backend build` = 两步管线:`nest build`(tsc,含全量类型检查)→ dist/ 目录树(入口仍 `dist/main.js`)→ `terser` 逐文件 compress + mangle(变量名混淆,部署产物防看),自动 bump version。
94
- - `pnpm --filter backend buildops` = build 之后组装运维包到 `backend/release/`。
101
+ - `pnpm --filter ./backend build` = 两步管线:`nest build`(tsc,含全量类型检查)→ dist/ 目录树(入口仍 `dist/main.js`)→ `terser` 逐文件 compress + mangle(变量名混淆,部署产物防看),自动 bump version。
102
+ - `pnpm --filter ./backend buildops` = build 之后组装运维包到 `backend/release/`。
95
103
  - **混淆边界**:`mangle.keep_fnames` 必须开——Swagger 的 operationId/schema 名从类名读,混淆类名会废掉 `/docs`;Nest DI 走构造函数引用不受影响。属性名不 mangle(保 yaml/DTO/装饰器元数据)。
96
104
  - **泄露面**:运维包/镜像内的 dist 剥离 `.js.map` 与 `.d.ts`(源码映射与接口结构不外发);`.js.map` 仅本地排障用(terser 已链到源 ts 的映射,`node --enable-source-maps` 可栈回源)。
97
105
  - **关键 gotcha**:`backend/package-lock.json` 必须是真 npm lockfile(buildops 自动生成,见上方第 4 步;裸 `npm install` 造成的 pnpm 符号链污染会被拦截)。
@@ -100,11 +108,13 @@ UI 规范见 `docs/airtable/DESIGN.md`;后期想换自己的风格:替换/修改
100
108
  ### 部署 + 发布/回滚
101
109
  **职责下沉**:发布/回滚下沉到各服务;backend 把部署力学整体下沉到服务器侧纯 shell(`backend/ops/install.sh` 单一真相源)。
102
110
  - 根 `scripts/publish.cjs`/`rollback.cjs`:薄编排,spawn 子服务,TTY 透传。
103
- - **web**(`web/scripts/`):SSH 上传 dist + 版本备份链 `dist_bak`/`dist_last_bak.tar.gz`,配置 `web/scripts/server.config.json`(本地、不入库)。
104
- - **backend**(`backend/scripts/` + `backend/ops/`):buildops → tar → scp → 触发 `install.sh`。服务器侧支持 docker/systemd/pm2 三模式,Capistrano 式 `releases/<ver>-<ts>` + `current` 软链原子切换,`versionswitch.sh` 可升可降。SQLite 定时备份由宿主 cron 调 `backend/ops/sqlite/backup.sh`。
111
+ - **web**(`web/scripts/`):SSH 上传 dist + 版本历史两级备份(`dist_bak`/`dist_last_bak.tar.gz`)+ 多版本归档 `dist_releases/`(每次发布存 `v<版本>_<时间戳>.tar.gz`,`keepVersions` 控制保留份数,默认 3)。TTY 下交互:多目标菜单 `key(host)`、发布前选版本策略(自增默认/维持/自定义)、回滚罗列全部归档版本选目标;`--env <名>` 显式指定则跳过交互,CI(非 TTY)自动回落 deploy+自增+备份链回退。配置 `web/scripts/server.config.json`(本地、不入库)。
112
+ - **backend**(`backend/scripts/` + `backend/ops/`):buildops → tar → scp → 触发 `install.sh`。服务器侧支持 docker/systemd/pm2 三模式,Capistrano 式 `releases/<ver>-<ts>` + `current` 软链原子切换,`versionswitch.sh` 可升可降。
113
+ - **backend 反向拉取**(`pnpm --filter ./backend pulldocker`):从服务器拉回 docker 镜像(docker save)+ 部署配置(`current/` + 根部 `.mode`/`.prod.secrets`[加密部署才有]/`.prod.docker.env` + 部署位 `config/`,data/ 可选)合并压缩下载到本地 `backend/release/`,用于迁移备份;迁移机 `docker load` + 解包即起。多服务器同 publish 口径:`--env <名>` 选 `server.config.cjs` targets,不传且配了 targets 时交互列选。
114
+ - **backend 迁移推送**(`pnpm --filter ./backend pushdocker`):pulldocker 的对偶——把拉取归档(或解包目录,`--from <绝对路径>` 或交互输入)推到目标服务器,docker load 镜像 → 按 install.sh 的 docker 部署力学落位(current 重建 + 顶层脚本 + 根部 env/密钥,data/ 可选)→ 跑 `start.sh` 起容器(run-container.sh 单一真相源,零 docker run 参数漂移)。裸机直接引导落位;已部署机交互确认后覆盖(旧根部文件/data 备份 `.bak.<ts>`)。目标机选择同 pulldocker 口径。
105
115
 
106
116
  ### 运维监控(ops 模块)
107
- 后台 `/background/yunwei/ops` 只读展示 SQLite 备份状态,走 JWT 鉴权。`OpsProbe` 接口 + 探针数组注入,新增探针 = 新文件 + `ops.module.ts` providers 加一行,前端零改动。
117
+ 后台 `/background/yunwei/ops` 只读展示运维探针信息(当前含后台版本号),走 JWT 鉴权。`OpsProbe` 接口 + 探针数组注入,新增探针 = 新文件 + `ops.module.ts` providers 加一行,前端零改动。
108
118
 
109
119
  ---
110
120
 
@@ -42,6 +42,11 @@ backend/release
42
42
  # backend buildops release 产物(releases/<ver>-<ts>,本地生成、不入库)
43
43
  backend/rel
44
44
 
45
+ # 配置加密根密钥真值(旧模型遗留文件名,现已改由服务器首部署生成 APP_ROOT/.prod.secrets ——
46
+ # 开发机不再有该文件;保留 ignore 条目作防御网,防手工重建时误提交)
47
+ backend/config/.develop.secrets
48
+ backend/config/.prod.secrets
49
+
45
50
  # deploy config (contains server credentials / sign key)
46
51
  scripts/deploy.config.json
47
52
  # backend 部署 SSH 凭证(含服务器密码/IP,本地填写、不入库)
@@ -22,6 +22,8 @@
22
22
  * isTTY 为真,raw 模式交互正常,backend 侧无需改动。
23
23
  */
24
24
  const { spawnSync } = require('child_process');
25
+ const fs = require('fs');
26
+ const path = require('path');
25
27
 
26
28
  const argv = process.argv.slice(2);
27
29
 
@@ -55,7 +57,23 @@ function run(cmd) {
55
57
  }
56
58
  }
57
59
 
58
- if (doWeb) run(`pnpm --filter web run publish${rest}`);
59
- if (doBackend) run(`pnpm --filter backend run publish${rest}`);
60
+ // 预检:pnpm `--filter` 无匹配时只打一句提示、退出码仍为 0(实测),只看退出码会「假成功」
61
+ // (打出 ✓ 发布完成 但实际啥也没发)。用 fs 显式校验子包存在且声明了目标 script。
62
+ // filter 用路径(./web、./backend)而非包名:包名改名后 filter 不漂移失效。
63
+ function ensureTarget(dir, script) {
64
+ const pkgPath = path.join(__dirname, '..', dir, 'package.json');
65
+ if (!fs.existsSync(pkgPath)) {
66
+ console.error(`✗ 找不到 ${dir}/package.json(需在项目根目录运行)`);
67
+ process.exit(1);
68
+ }
69
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
70
+ if (!pkg.scripts || !pkg.scripts[script]) {
71
+ console.error(`✗ ${dir}/package.json 未声明 "${script}" 脚本,无法发布`);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ if (doWeb) { ensureTarget('web', 'publish'); run(`pnpm --filter ./web run publish${rest}`); }
77
+ if (doBackend) { ensureTarget('backend', 'publish'); run(`pnpm --filter ./backend run publish${rest}`); }
60
78
 
61
79
  console.log('\n✓ 发布完成');
@@ -21,6 +21,8 @@
21
21
  * node,isTTY 为真,raw 模式交互正常,backend 侧无需改动。与 scripts/publish.cjs 同款修复。
22
22
  */
23
23
  const { spawnSync } = require('child_process');
24
+ const fs = require('fs');
25
+ const path = require('path');
24
26
 
25
27
  const argv = process.argv.slice(2);
26
28
 
@@ -53,7 +55,23 @@ function run(cmd) {
53
55
  }
54
56
  }
55
57
 
56
- if (doWeb) run(`pnpm --filter web run rollback${rest}`);
57
- if (doBackend) run(`pnpm --filter backend run rollback${rest}`);
58
+ // 预检:pnpm `--filter` 无匹配时只打一句提示、退出码仍为 0(实测),只看退出码会「假成功」
59
+ // (打出 ✓ 回滚完成 但实际啥也没滚)。用 fs 显式校验子包存在且声明了目标 script。
60
+ // filter 用路径(./web、./backend)而非包名:包名改名后 filter 不漂移失效。
61
+ function ensureTarget(dir, script) {
62
+ const pkgPath = path.join(__dirname, '..', dir, 'package.json');
63
+ if (!fs.existsSync(pkgPath)) {
64
+ console.error(`✗ 找不到 ${dir}/package.json(需在项目根目录运行)`);
65
+ process.exit(1);
66
+ }
67
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
68
+ if (!pkg.scripts || !pkg.scripts[script]) {
69
+ console.error(`✗ ${dir}/package.json 未声明 "${script}" 脚本,无法回滚`);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ if (doWeb) { ensureTarget('web', 'rollback'); run(`pnpm --filter ./web run rollback${rest}`); }
75
+ if (doBackend) { ensureTarget('backend', 'rollback'); run(`pnpm --filter ./backend run rollback${rest}`); }
58
76
 
59
77
  console.log('\n✓ 回滚完成');
@@ -23,6 +23,14 @@ const RouterConfig: RouteConfigItem[] = [
23
23
  hideInMenu: true,
24
24
  component: './pages/Login/index'
25
25
  },
26
+ {
27
+ // 维护页:接口 502(后端不可用/发版重启)时由请求层自动整页跳入,
28
+ // 页面自带 /api/version 探活自动恢复,不需要菜单入口
29
+ name: '系统维护中',
30
+ path: '/maintenance',
31
+ hideInMenu: true,
32
+ component: './pages/Maintenance/index'
33
+ },
26
34
  {
27
35
  name: '系统',
28
36
  path: '/',
@@ -27,12 +27,16 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@ant-design/icons": "5.3.0",
30
+ "@dnd-kit/core": "^6.3.1",
31
+ "@dnd-kit/sortable": "^10.0.0",
32
+ "@dnd-kit/utilities": "^3.2.2",
30
33
  "antd": "5.14.2",
31
34
  "axios": "1.20.0",
32
35
  "dayjs": "1.11.10",
33
36
  "echarts": "6.1.0",
34
37
  "exceljs": "4.4.0",
35
38
  "js-sha256": "^1.0.0",
39
+ "keepalive-for-react": "^4.0.3",
36
40
  "lodash": "4.18.1",
37
41
  "qs": "6.16.0",
38
42
  "react": "18.2.0",
@@ -74,4 +78,4 @@
74
78
  "vite-plugin-chunk-split": "0.5.0",
75
79
  "vite-plugin-svgr": "^5.2.0"
76
80
  }
77
- }
81
+ }