@daiyu-5577/buildsystem 1.0.0

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 (50) hide show
  1. package/README.md +485 -0
  2. package/dist/artifact.js +59 -0
  3. package/dist/artifact.js.map +1 -0
  4. package/dist/cli.js +25 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/build.js +39 -0
  7. package/dist/commands/build.js.map +1 -0
  8. package/dist/commands/server.js +28 -0
  9. package/dist/commands/server.js.map +1 -0
  10. package/dist/config.js +42 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/cron.js +163 -0
  13. package/dist/cron.js.map +1 -0
  14. package/dist/crypto.js +40 -0
  15. package/dist/crypto.js.map +1 -0
  16. package/dist/custError.js +10 -0
  17. package/dist/custError.js.map +1 -0
  18. package/dist/database/index.js +64 -0
  19. package/dist/database/index.js.map +1 -0
  20. package/dist/database/types.js +8 -0
  21. package/dist/database/types.js.map +1 -0
  22. package/dist/fsHelpers.js +93 -0
  23. package/dist/fsHelpers.js.map +1 -0
  24. package/dist/git.js +67 -0
  25. package/dist/git.js.map +1 -0
  26. package/dist/logger.js +80 -0
  27. package/dist/logger.js.map +1 -0
  28. package/dist/notifier.js +66 -0
  29. package/dist/notifier.js.map +1 -0
  30. package/dist/pm.js +21 -0
  31. package/dist/pm.js.map +1 -0
  32. package/dist/queue.js +273 -0
  33. package/dist/queue.js.map +1 -0
  34. package/dist/server/index.js +189 -0
  35. package/dist/server/index.js.map +1 -0
  36. package/dist/server/routes.js +395 -0
  37. package/dist/server/routes.js.map +1 -0
  38. package/dist/server/websocket.js +230 -0
  39. package/dist/server/websocket.js.map +1 -0
  40. package/dist/static/web/assets/index-BzSDdMyu.js +304 -0
  41. package/dist/static/web/assets/index-DptBjpyJ.css +1 -0
  42. package/dist/static/web/index.html +13 -0
  43. package/dist/types.js +33 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/utils.js +66 -0
  46. package/dist/utils.js.map +1 -0
  47. package/dist/web/assets/index-Ds-OtVY9.js +76 -0
  48. package/dist/web/assets/index-F5jE8yX9.css +1 -0
  49. package/dist/web/index.html +13 -0
  50. package/package.json +61 -0
package/README.md ADDED
@@ -0,0 +1,485 @@
1
+ # BuildSystem
2
+
3
+ 基于 Git 变更的自动化构建系统,提供 CLI 构建命令和 Web 可视化构建管理界面,支持增量构建、并发控制、实时消息推送、版本回退、Webhook 通知和多用户权限管理。
4
+
5
+ ## 功能概览
6
+
7
+ - **CLI 构建** — 命令行一键触发构建,支持指定分支、目录、commit 区间
8
+ - **Web 管理界面** — 实时构建状态监控、聊天室、日志查看、版本回退、用户权限管理
9
+ - **增量构建** — 通过 `git diff` 识别变更项目,仅构建受影响的包
10
+ - **并发控制** — 可配置最大并发构建数(`MAX_CONCURRENCY`)
11
+ - **实时推送** — Socket.IO 双向通信,构建日志实时流式推送
12
+ - **Webhook 通知** — 构建完成自动通知外部系统
13
+ - **版本回退** — 构建前自动备份,支持一键回退
14
+ - **定时清理** — Cron 定时清理旧日志和备份
15
+ - **多用户认证** — Token 加密鉴权,支持邀请注册
16
+ - **角色权限** — 三级角色体系(Super / Admin / User),细粒度权限控制
17
+
18
+ ## 目录结构
19
+
20
+ ```
21
+ buildSystem/
22
+ ├── src/ # 后端(Node.js + TypeScript + ESM)
23
+ │ ├── cli.ts # CLI 入口(commander)
24
+ │ ├── commands/
25
+ │ │ ├── build.ts # build 命令:纯 CLI 构建
26
+ │ │ └── server.ts # server 命令:启动 Web 服务
27
+ │ ├── server/
28
+ │ │ ├── index.ts # Express + Socket.IO 服务主类
29
+ │ │ ├── routes.ts # REST API 路由(含用户管理接口)
30
+ │ │ └── websocket.ts # WebSocket 处理器 & 命令系统
31
+ │ ├── queue.ts # 构建任务队列(并发控制、增量构建)
32
+ │ ├── git.ts # Git 异步操作(checkout/pull/diff/clean)
33
+ │ ├── pm.ts # 包管理器检测(npm/yarn/pnpm)
34
+ │ ├── artifact.ts # 构建产物备份与复制
35
+ │ ├── notifier.ts # 通知系统(Socket.IO + Webhook)
36
+ │ ├── cron.ts # Cron 定时任务
37
+ │ ├── crypto.ts # AES-256-CBC 加解密
38
+ │ ├── database/
39
+ │ │ ├── index.ts # JSON 文件数据库
40
+ │ │ └── types.ts # 数据库类型(User/Message/Invite)
41
+ │ ├── config.ts # 集中配置(环境变量 + 路径)
42
+ │ ├── types.ts # 公共类型定义
43
+ │ ├── logger.ts # Winston 日志(http / socket 分离)
44
+ │ ├── fsHelpers.ts # 文件系统工具
45
+ │ ├── utils.ts # 通用工具函数
46
+ │ └── custError.ts # 自定义错误
47
+
48
+ ├── web/ # 前端(React + Vite + Less)
49
+ │ ├── src/
50
+ │ │ ├── main.tsx # 入口(Router + AuthGuard)
51
+ │ │ ├── pages/
52
+ │ │ │ ├── Home.tsx # 主页(构建面板 + 聊天室 + 用户操作)
53
+ │ │ │ ├── Login.tsx # 登录/注册页
54
+ │ │ │ ├── InviteModal.tsx # 邀请码弹窗
55
+ │ │ │ ├── PermissionModal.tsx # 权限管理弹窗
56
+ │ │ │ ├── UserInfoModal.tsx # 用户信息弹窗
57
+ │ │ │ └── *.module.less # 页面级样式
58
+ │ │ ├── components/
59
+ │ │ │ ├── Modal/ # 模态框体系(StackModal/Toast/Confirm/ImagePreview)
60
+ │ │ │ ├── Select/ # 下拉选择器
61
+ │ │ │ └── UserAvatar/ # 用户头像下拉组件
62
+ │ │ ├── hooks/
63
+ │ │ │ ├── useSocket.ts # Socket.IO 连接 + 消息管理
64
+ │ │ │ └── useScrollToBottom.ts
65
+ │ │ ├── utils/
66
+ │ │ │ └── api.ts # API 封装(带 token)
67
+ │ │ └── index.css # 全局样式 & CSS 变量
68
+ │ └── vite.config.ts
69
+
70
+ ├── .buildsystemCatch/ # 运行时缓存(自动创建)
71
+ │ ├── webBackup/ # 构建产物备份
72
+ │ ├── buildLog/ # 构建日志
73
+ │ ├── database/ # JSON 数据库
74
+ │ ├── log/ # HTTP 日志
75
+ │ └── images/ # 聊天图片
76
+
77
+ ├── .env.example # 环境变量模板
78
+ ├── package.json
79
+ └── tsconfig.json
80
+ ```
81
+
82
+ ## 架构
83
+
84
+ ```
85
+ ┌──────────────────────────────────────────────────────────────┐
86
+ │ Browser │
87
+ │ ┌──────────┐ ┌──────────┐ ┌───────────────────────────┐ │
88
+ │ │ Login │ │ Home │ │ Modal/Select/UserAvatar │ │
89
+ │ │ (React) │ │ (React) │ │ (React Components) │ │
90
+ │ └────┬─────┘ └────┬─────┘ └─────────────┬─────────────┘ │
91
+ │ │ │ │ │
92
+ │ └──────┬───────┴───────────────────────┘ │
93
+ │ │ REST API + Socket.IO │
94
+ └──────────────┼────────────────────────────────────────────────┘
95
+
96
+ ┌──────────────┼────────────────────────────────────────────────┐
97
+ │ BuildServer (Express) │
98
+ │ ┌───────────┴───────────┐ │
99
+ │ │ routes.ts (REST API) │ websocket.ts (Socket.IO) │
100
+ │ │ - 构建任务管理 │ - 实时消息推送 │
101
+ │ │ - 用户与权限管理 │ - 聊天室 & 快捷命令 │
102
+ │ │ - 邀请码管理 │ - 构建日志订阅 │
103
+ │ └───────────┬───────────┘──────────┬─────────────────────────┘
104
+ │ │ │ │
105
+ │ ┌───────────┴───────────────────────┴────────────────────────┐ │
106
+ │ │ BuildTaskQueue │ │
107
+ │ │ ┌─────────┐ ┌──────────┐ ┌───────────────────────────┐ │ │
108
+ │ │ │ git.ts │ │ pm.ts │ │ artifact.ts │ │ │
109
+ │ │ │ checkout │ │ npm ci │ │ backup / copy / restore │ │ │
110
+ │ │ │ pull │ │ yarn │ │ │ │ │
111
+ │ │ │ diff │ │ pnpm │ └───────────────────────────┘ │ │
112
+ │ │ └─────────┘ └──────────┘ │ │
113
+ │ └───────────────────────────────────────────────────────────┘ │
114
+ │ ┌─────────────┐ ┌───────────┐ ┌──────────────────────────┐ │
115
+ │ │ notifier.ts │ │ cron.ts │ │ database/ │ │
116
+ │ │ webhook │ │ 定时清理 │ │ User / Message / Invite │ │
117
+ │ └─────────────┘ └───────────┘ └──────────────────────────┘ │
118
+ └────────────────────────────────────────────────────────────────┘
119
+ ```
120
+
121
+ ## 快速开始
122
+
123
+ ### 前置要求
124
+
125
+ - Node.js >= 18
126
+ - 项目根目录包含 `package.json`(含 `build` 脚本)
127
+
128
+ ### 安装
129
+
130
+ ```bash
131
+ # 克隆项目
132
+ git clone <repo-url>
133
+ cd buildSystem
134
+
135
+ # 安装后端依赖
136
+ npm install
137
+
138
+ # 安装前端依赖
139
+ cd web && npm install && cd ..
140
+
141
+ # 构建前端产物
142
+ npm run build:web
143
+ ```
144
+
145
+ ### 使用方式
146
+
147
+ #### 1. Web 服务模式(推荐)
148
+
149
+ ```bash
150
+ # 开发模式(前后端分别启动)
151
+ npm run dev:server # 启动后端 :3000
152
+ npm run dev:web # 启动前端 :5173(代理 API/WS 到 :3000)
153
+
154
+ # 生产模式
155
+ npm run build # 编译 TypeScript
156
+ npm run build:web # 构建前端到 ../dist/web
157
+ node dist/cli.js server # 启动服务 :3000
158
+ ```
159
+
160
+ 启动后访问 `http://localhost:3000/page` 进入 Web 界面。
161
+
162
+ 首次使用时,以超级管理员身份生成邀请码(邀请码 → 点击"生成邀请码"),然后将邀请码分发给新用户用于注册。
163
+
164
+ ##### 托管额外网站(websitePath / websiteRoot)
165
+
166
+ 服务支持在指定 URL 路径下托管静态网站,例如将 Vue / React 构建后的站点挂载到 `/web` 路径:
167
+
168
+ ```bash
169
+ node dist/cli.js server \
170
+ --website-path /web \
171
+ --website-root /path/dist
172
+ ```
173
+
174
+ | 参数 | 说明 |
175
+ |------|------|
176
+ | `--website-root` | 网站静态文件所在的绝对路径(如 Vite 构建输出的 `dist` 目录) |
177
+ | `--website-path` | URL 匹配前缀,请求此路径时将返回 `website-root` 下的文件 |
178
+
179
+ 工作原理:
180
+ 1. 客户端访问 `/web` `/web/login`(无扩展名),服务端查找 `/path/dist/index.html` 并返回,支持 React Router History 模式,vite baseRoute 为 `/web`
181
+ 2. 如果 `website-root` 下的静态页面存在多层级,例如 `path/dist/page/index.html`,客户端需访问 `/web/page` `/web/page/login`(无扩展名),仅使用 `/web` 无法找到下面层级的路径文件,服务端查找 `/path/dist/page/index.html` 并返回,支持 React Router History 模式, vite baseRoute 为 `/web`
182
+ 3. 带有扩展名(如 `.js`, `.css`, `.png`),直接由 `express.static` 提供静态资源
183
+
184
+ #### 2. CLI 构建模式
185
+
186
+ ```bash
187
+ # 编译
188
+ npm run build
189
+
190
+ # 构建指定分支
191
+ node dist/cli.js build -b master
192
+
193
+ # 构建指定分支 + 指定目录
194
+ node dist/cli.js build -b dev -p packages/app1 packages/app2
195
+
196
+ # 增量构建(根据 commit 区间自动识别变更项目)
197
+ node dist/cli.js build -b master -c abc123..def456
198
+
199
+ # 构建并通知
200
+ node dist/cli.js build -b master -n https://hooks.example.com/notify
201
+ ```
202
+
203
+ #### 3. Server 命令参数
204
+
205
+ ```
206
+ node dist/cli.js server [options]
207
+ --port <number> 端口号(默认 3000)
208
+ --website-root <string> 网站静态文件根目录
209
+ --website-path <string> URL 匹配路径前缀(挂载点)
210
+ ```
211
+
212
+ ### Nginx 反向代理配置
213
+
214
+ 当自定义了 `BASE_ROUTE` 环境变量,并通过nginx匹配该路径`location BASE_ROUTE`反向代理到后端服务时,前端访问`/buildsystem/login`无法匹配到nginx路径。因此需要通过反向代理将所有匹配的路径统一转发到后端服务:
215
+
216
+ ```nginx
217
+ server {
218
+ listen 80;
219
+ server_name example.com;
220
+
221
+ # 匹配 page 路径下的所有请求,全部转发到后端
222
+ location /buildsystem/ {
223
+ proxy_pass http://127.0.0.1:3000;
224
+ proxy_http_version 1.1;
225
+ proxy_set_header Upgrade $http_upgrade;
226
+ proxy_set_header Connection "upgrade";
227
+ proxy_set_header Host $host;
228
+ proxy_set_header X-Real-IP $remote_addr;
229
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
230
+ }
231
+ }
232
+ ```
233
+
234
+ 如果使用 `--website-path /app` 托管另一个站点,则需添加对应的 location block:
235
+
236
+ ```nginx
237
+ location /app/ {
238
+ proxy_pass http://127.0.0.1:3000;
239
+ # ... 同上 header
240
+ }
241
+ ```
242
+
243
+ 这样无论前端如何路由,所有请求都会到达 Express,由 `findIndexHtml` 或 `express.static` 正确处理 SPA 的 fallback 逻辑。
244
+
245
+ ### Nginx 多层级目录场景
246
+
247
+ 当后端托管多个网站,且每个网站存在子目录路径时,Nginx 需要为每个挂载点分别配置反向代理:
248
+
249
+ ```nginx
250
+ server {
251
+ listen 80;
252
+ server_name example.com;
253
+
254
+ # ── 配置 1:构建系统管理后台(必须放在最前面,因为前缀最长) ──
255
+ location /buildsystem/ {
256
+ proxy_pass http://127.0.0.1:3000;
257
+ proxy_http_version 1.1;
258
+ # Socket.IO 需要 WebSocket 升级头
259
+ proxy_set_header Upgrade $http_upgrade;
260
+ proxy_set_header Connection "upgrade";
261
+ proxy_set_header Host $host;
262
+ proxy_set_header X-Real-IP $remote_addr;
263
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
264
+ # 防止 Nginx 缓存动态 API 响应
265
+ proxy_no_cache 1;
266
+ proxy_cache_bypass 1;
267
+ }
268
+
269
+ # ── 配置 2:Vue 应用(嵌套在 /app/vue 下,支持 History 模式) ──
270
+ location /app/vue/ {
271
+ proxy_pass http://127.0.0.1:3000;
272
+ proxy_http_version 1.1;
273
+ proxy_set_header Upgrade $http_upgrade;
274
+ proxy_set_header Connection "upgrade";
275
+ proxy_set_header Host $host;
276
+ proxy_set_header X-Real-IP $remote_addr;
277
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
278
+ }
279
+
280
+ # ── 配置 3:React 应用(嵌套在 /app/react 下) ──
281
+ location /app/react/ {
282
+ proxy_pass http://127.0.0.1:3000;
283
+ proxy_http_version 1.1;
284
+ proxy_set_header Upgrade $http_upgrade;
285
+ proxy_set_header Connection "upgrade";
286
+ proxy_set_header Host $host;
287
+ proxy_set_header X-Real-IP $remote_addr;
288
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
289
+ }
290
+ }
291
+ ```
292
+
293
+ ### `--website-path` 与 `--website-root` 配置
294
+
295
+ 通过 `server --website-path /app --website-root <path>` 可在指定 URL 路径下托管任意静态网站:
296
+
297
+ ```bash
298
+ node dist/cli.js server \
299
+ --website-path /app \
300
+ --website-root /data/sites/app
301
+ ```
302
+
303
+ | 参数 | 作用 | 示例 |
304
+ |------|------|------|
305
+ | `--website-path` | URL 匹配前缀(挂载点) | `/app` |
306
+ | `--website-root` | 静态文件实际所在的**绝对路径** | `/data/sites/app` |
307
+
308
+ 访问 `http://example.com/app` → 返回 `/data/sites/app/index.html`
309
+ 访问 `http://example.com/app/login` → 返回 `/data/sites/app/login/index.html`
310
+
311
+ #### 多层级目录结构
312
+
313
+ 假设 `website-root` 目录如下:
314
+
315
+ ```
316
+ /data/sites/app/
317
+ ├── index.html # /app → 命中
318
+ ├── login/index.html # /app/login → 命中
319
+ ├── vue/dist/index.html # /app/vue → 命中
320
+ │ └── assets/
321
+ └── react/dist/index.html # /app/react → 命中
322
+ └── assets/
323
+ ```
324
+
325
+ 只需启动一次服务,所有子路径自动生效。无需为每个站点单独配置 `--website-path`。
326
+
327
+ | 客户端访问 | 查找逻辑 | 结果 |
328
+ |-----------|---------|------|
329
+ | `/app` | `/data/sites/app/index.html` | ✅ |
330
+ | `/app/login` | `/data/sites/app/login/index.html` | ✅ |
331
+ | `/app/vue/assets/logo.png` | `express.static` 直接提供静态资源 | ✅ |
332
+ | `/app/vue/non-existent` | 逐级 fallback 查找 `index.html` | 找到最近的父级或根 |
333
+
334
+ #### `findIndexHtml` 逐级查找算法
335
+
336
+ 当请求无文件扩展名(SPA History 模式)时,服务端从长到短逐级尝试匹配:
337
+
338
+ ```
339
+ 请求路径: /app/vue/deep/page
340
+ websiteRoot: /data/sites/app
341
+
342
+ 1. /data/sites/app + /app/vue/deep/page + /index.html → 不存在
343
+ 2. /app/vue/deep + /index.html → 不存在
344
+ 3. /app/vue + /index.html → ✅ 命中
345
+ ```
346
+
347
+ - `--website-path` 仅作为 Express 的 `use` 前缀过滤,不参与最终文件路径拼接
348
+ - 拼接公式:`websiteRoot + 请求路径的前缀 + /index.html`
349
+ - 逐级向上 fallback 直到根目录的 `index.html`
350
+
351
+ ### 注意事项
352
+
353
+ | 要点 | 说明 |
354
+ |------|------|
355
+ | 位置顺序 | `location` 按**前缀匹配优先级**从高到低排列,较长前缀应放在较之前,否则短前缀会先匹配拦截请求 |
356
+ | `/buildsystem/` | 这是前端 Vite 构建产物所在的路径,对应代码中 `base: '/buildsystem'` |
357
+ | `/app/vue/`、`/app/react/` | 通过 `server --website-path /app` 启动后,所有 `/app/**` 请求都会被 Express 代理处理 |
358
+ | WebSocket 头 | Socket.IO 依赖 `Upgrade` + `Connection` 头部做协议升级,务必添加这两行 |
359
+
360
+ ## Vite 配置详解
361
+
362
+ 前端使用 Vite 6 开发,配置文件位于 `web/vite.config.ts`:
363
+
364
+ ```ts
365
+ import { defineConfig } from 'vite'
366
+ import react from '@vitejs/plugin-react'
367
+
368
+ export default defineConfig({
369
+ base: '/buildsystem', // ① 前端资源路径前缀,与 Nginx 的 location 和 Express 的静态文件路径对应
370
+ plugins: [react()], // ② React JSX 转换插件
371
+ server: {
372
+ port: 5173, // ③ 开发服务器端口
373
+ proxy: { // ④ 开发环境代理:将 /api、/ws 请求转发到后端 :3000
374
+ '/api': { // - /api → Express REST API(用户认证、任务管理等)
375
+ target: 'http://localhost:3000',
376
+ changeOrigin: true, // - 修改 Host 头为目标地址,避免 CORS 问题
377
+ },
378
+ '/ws': { // - /ws → Socket.IO 实时通信端点
379
+ target: 'http://localhost:3000',
380
+ ws: true, // - 启用 WebSocket 代理
381
+ },
382
+ },
383
+ },
384
+ build: {
385
+ outDir: '../dist/web', // ⑤ 生产构建输出目录(相对于 web/ 即项目根目录的 dist/web)
386
+ emptyOutDir: true, // ⑥ 构建前先清空输出目录,避免残留旧文件
387
+ },
388
+ })
389
+ ```
390
+
391
+ | 字段 | 作用 | 注意事项 |
392
+ |------|------|----------|
393
+ | `base` | 定义前端资源(JS/CSS/HTML)访问路径的前缀 | 设为 `/buildsystem` 表示所有请求 URL 以 `/buildsystem/` 开头;若改为 `/` 则表示直接部署在域名根路径下 |
394
+ | `server.proxy` | 仅开发环境生效,解决前后端同源策略 | 生产环境需通过 Nginx 反向代理或后端统一端口解决 |
395
+ | `build.outDir` | 指定编译产出放置位置 | 默认会覆盖整个目录,配合 `emptyOutDir` 保证干净构建 |
396
+ | `build.emptyOutDir` | 确保每次构建都是全新产物 | 如果希望保留某些文件可设为 `false`,但通常建议开启 |
397
+
398
+ ## 不同 `base` 值对应的部署方式
399
+
400
+ | `base` 值 | Nginx location | Express 启动参数 | 适用场景 |
401
+ |-----------|----------------|------------------|----------|
402
+ | `/buildsystem` | `location /buildsystem/` | 无需额外参数 | 子路径部署,与其他站点共存 |
403
+ | `/` | `location /` | `--website-path /` | 独立域名部署 |
404
+ | `/web/` | `location /web/` | `--website-path /web` | 自定义前缀 |
405
+
406
+ ## 环境变量
407
+
408
+ | 变量 | 说明 | 默认值 |
409
+ |------|------|--------|
410
+ | `NODE_ENV` | 运行环境 | `development` |
411
+ | `BASE_ROUTE` | REST API 路由前缀(即 express 中 `/api/...` 的前缀) | 空 |
412
+ | `CATCH_ROOT` | 缓存根目录 | `.buildsystemCatch` |
413
+ | `DIST_ROOT` | 构建产物目录 | `dist` |
414
+ | `MAX_CONCURRENCY` | 最大并发构建数 | `1` |
415
+ | `CRYPTO_SALT` | AES 加密 salt | 生产环境必须设置 |
416
+ | `CRYPTO_SECRET` | AES 加密 secret | 生产环境必须设置 |
417
+ | `WEBHOOK_URL` | Webhook 通知地址 | 空 |
418
+
419
+ ## 用户权限体系
420
+
421
+ 系统采用三级角色体系:
422
+
423
+ | 角色 | 权限 |
424
+ |------|------|
425
+ | **Super** | 全部权限:可以分配 Super/Admin/User 角色、生成邀请码 |
426
+ | **Admin** | 管理权限:可以分配 Admin/User 角色、查看所有构建 |
427
+ | **User** | 普通用户:仅可查看构建状态、发送聊天消息 |
428
+
429
+ ### 邀请注册流程
430
+
431
+ 1. 管理员(Super/Admin)生成邀请码
432
+ 2. 将邀请码分享给新用户
433
+ 3. 用户在登录页切换至"注册"Tab,输入用户名、密码和邀请码完成注册
434
+
435
+ ### 权限管理
436
+
437
+ - 点击右上角头像打开下拉菜单,选择"权限管理"(需 Admin 或以上角色)
438
+ - 搜索用户后通过下拉框修改其角色
439
+ - Admin 不能降级或提升 Super 角色的用户
440
+
441
+ ## API
442
+
443
+ 启动 Server 后,主要接口位于 `/api/` 路径下(若设置了 `BASE_ROUTE`,则为 `<BASE_ROUTE>/api/...`):
444
+
445
+ | 方法 | 路径 | 说明 |
446
+ |------|------|------|
447
+ | POST | `/api/login` | 用户登录 |
448
+ | POST | `/api/register` | 用户注册(需邀请码) |
449
+ | POST | `/api/addTask` | 添加构建任务 |
450
+ | POST | `/api/cancelTask` | 取消构建任务 |
451
+ | GET | `/api/getAllTask` | 获取任务列表 |
452
+ | GET | `/api/getAllPackage` | 获取可构建项目列表 |
453
+ | GET | `/api/refreshPackages` | 刷新可构建项目列表(Admin/Super) |
454
+ | GET | `/api/getAllLog` | 获取日志列表 |
455
+ | GET | `/api/getAllBackup` | 获取备份列表 |
456
+ | POST | `/api/restore` | 版本回退 |
457
+ | GET | `/api/getNoticeList` | 获取通知配置 |
458
+ | POST | `/api/notify` | Webhook 回调(构建通知) |
459
+ | GET | `/api/getUserInfo` | 获取当前/指定用户信息 |
460
+ | GET | `/api/getAllUsers` | 获取所有用户列表(Admin/Super) |
461
+ | POST | `/api/updateUserRole` | 更新用户角色(Admin/Super) |
462
+ | GET | `/api/inviteCodes` | 获取当前用户的邀请码列表(Super) |
463
+ | POST | `/api/generateInviteCode` | 生成新邀请码(Super) |
464
+
465
+ ## 构建流程
466
+
467
+ ```
468
+ 添加任务 → 入队列 → 并发调度 → 执行:
469
+ 1. git checkout <branch> + git pull
470
+ 2. 检测包管理器 → npm ci / yarn install / pnpm install
471
+ 3. npm run build
472
+ 4. 备份旧产物 → 复制新产物到 dist/
473
+ 5. git clean(还原工作区)
474
+ → 成功/失败 → Webhook 通知 + Socket.IO 推送
475
+ ```
476
+
477
+ ## 技术栈
478
+
479
+ **后端**:Node.js · TypeScript · ESM · Express 5 · Socket.IO · Commander · Winston · dayjs · fs-extra
480
+
481
+ **前端**:React 19 · Vite 6 · TypeScript · React Router 6 · Socket.IO Client · Less (CSS Modules)
482
+
483
+ ## License
484
+
485
+ MIT
@@ -0,0 +1,59 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ /**
11
+ * 构建产物管理 – 复制、备份、恢复
12
+ */
13
+ import fse from 'fs-extra';
14
+ import path from 'node:path';
15
+ import { logger } from './logger.js';
16
+ import { webDistPath, catchWebDistPath } from './config.js';
17
+ /** 异步复制构建产物 */
18
+ export function copyDist(src, dest) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ try {
21
+ yield fse.ensureDir(dest);
22
+ yield fse.copy(src, dest, { overwrite: true });
23
+ logger.info(`copyDist: copied ${src} → ${dest}`);
24
+ }
25
+ catch (e) {
26
+ logger.error(`copyDist error: ${e}`);
27
+ throw e;
28
+ }
29
+ });
30
+ }
31
+ /** 异步备份构建产物,路径格式: {branch}_{packagePath}_{timestamp} */
32
+ export function backupDist(target, backupPath) {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ try {
35
+ yield fse.ensureDir(backupPath);
36
+ if (fse.existsSync(target)) {
37
+ yield fse.copy(target, backupPath, { overwrite: true });
38
+ logger.info(`backupDist: backup ${target} --> ${backupPath}`);
39
+ }
40
+ return backupPath;
41
+ }
42
+ catch (e) {
43
+ logger.error(`backupDist error: ${e}`);
44
+ throw e;
45
+ }
46
+ });
47
+ }
48
+ /** 从备份恢复构建产物 */
49
+ export function restoreDist(fileName) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ const absPath = path.join(catchWebDistPath, fileName);
52
+ if (!fse.existsSync(absPath)) {
53
+ throw new Error(`${absPath} not found`);
54
+ }
55
+ yield fse.copy(absPath, webDistPath, { overwrite: true });
56
+ logger.info(`restoreDist: restored ${absPath} → ${webDistPath}`);
57
+ });
58
+ }
59
+ //# sourceMappingURL=artifact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact.js","sourceRoot":"","sources":["../src/artifact.ts"],"names":[],"mappings":";;;;;;;;;AAAA;;GAEG;AACH,OAAO,GAAG,MAAM,UAAU,CAAA;AAC1B,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAG3D,eAAe;AACf,MAAM,UAAgB,QAAQ,CAAC,GAAW,EAAE,IAAY;;QACtD,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;YACzB,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC9C,MAAM,CAAC,IAAI,CAAC,oBAAoB,GAAG,MAAM,IAAI,EAAE,CAAC,CAAA;QAClD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAA;YACpC,MAAM,CAAC,CAAA;QACT,CAAC;IACH,CAAC;CAAA;AAED,wDAAwD;AACxD,MAAM,UAAgB,UAAU,CAAC,MAAc,EAAE,UAAkB;;QACjE,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;YAC/B,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;gBACvD,MAAM,CAAC,IAAI,CAAC,sBAAsB,MAAM,QAAQ,UAAU,EAAE,CAAC,CAAA;YAC/D,CAAC;YACD,OAAO,UAAU,CAAA;QACnB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAA;YACtC,MAAM,CAAC,CAAA;QACT,CAAC;IACH,CAAC;CAAA;AAED,gBAAgB;AAChB,MAAM,UAAgB,WAAW,CAAC,QAAgB;;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAA;QACrD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,YAAY,CAAC,CAAA;QACzC,CAAC;QACD,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACzD,MAAM,CAAC,IAAI,CAAC,yBAAyB,OAAO,MAAM,WAAW,EAAE,CAAC,CAAA;IAClE,CAAC;CAAA"}
package/dist/cli.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Command } from 'commander';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { registerBuildCommand } from './commands/build.js';
7
+ import { registerServerCommand } from './commands/server.js';
8
+ const __dirname = new URL('.', import.meta.url).pathname;
9
+ let packageJsonPath = path.join(__dirname, '../package.json');
10
+ let packageJson = {};
11
+ try {
12
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
13
+ }
14
+ catch (_a) {
15
+ console.log('package.json 读取失败');
16
+ }
17
+ const program = new Command();
18
+ program
19
+ .name('buildSystem')
20
+ .description((packageJson === null || packageJson === void 0 ? void 0 : packageJson.description) || 'CLI tool to build projects based on git changes')
21
+ .version((packageJson === null || packageJson === void 0 ? void 0 : packageJson.version) || '1.0.0');
22
+ registerBuildCommand(program);
23
+ registerServerCommand(program);
24
+ program.parse();
25
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAA;AACtB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;AAExD,IAAI,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAA;AAC7D,IAAI,WAAW,GAAwB,EAAE,CAAA;AAEzC,IAAI,CAAC;IACH,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAA;AACrE,CAAC;AAAC,WAAM,CAAC;IACP,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;AAClC,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,WAAW,KAAI,iDAAiD,CAAC;KAC1F,OAAO,CAAC,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,OAAO,KAAI,OAAO,CAAC,CAAA;AAE3C,oBAAoB,CAAC,OAAO,CAAC,CAAA;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAE9B,OAAO,CAAC,KAAK,EAAE,CAAA"}
@@ -0,0 +1,39 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { BuildTaskQueue } from '../queue.js';
11
+ import chalk from 'chalk';
12
+ export function registerBuildCommand(program) {
13
+ program
14
+ .command('build')
15
+ .description('build website')
16
+ .requiredOption('-b, --branch <string>', '选择构建分支')
17
+ .option('-p, --packagePaths <string...>', '指定构建目录', ['.'])
18
+ .option('-c, --commit <string>', '选择构建commit...commit区间,用于monorepo项目', '')
19
+ .option('-n, --notify <string>', '构建消息通知地址', '')
20
+ .action((options) => __awaiter(this, void 0, void 0, function* () {
21
+ const { packagePaths = ['.'], branch, commit, notify } = options;
22
+ const queue = new BuildTaskQueue();
23
+ for (const pkg of packagePaths) {
24
+ try {
25
+ yield queue.addTask({
26
+ packagePath: pkg,
27
+ branch,
28
+ commit,
29
+ notify,
30
+ buildSourceType: 'buildCommand',
31
+ });
32
+ }
33
+ catch (error) {
34
+ console.error(chalk.red(`Error: ${error.message}`));
35
+ }
36
+ }
37
+ }));
38
+ }
39
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":";;;;;;;;;AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,eAAe,CAAC;SAC5B,cAAc,CAAC,uBAAuB,EAAE,QAAQ,CAAC;SACjD,MAAM,CAAC,gCAAgC,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;SACzD,MAAM,CAAC,uBAAuB,EAAE,oCAAoC,EAAE,EAAE,CAAC;SACzE,MAAM,CAAC,uBAAuB,EAAE,UAAU,EAAE,EAAE,CAAC;SAC/C,MAAM,CAAC,CAAO,OAAO,EAAE,EAAE;QACxB,MAAM,EAAE,YAAY,GAAG,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;QAChE,MAAM,KAAK,GAAG,IAAI,cAAc,EAAE,CAAA;QAClC,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,OAAO,CAAC;oBAClB,WAAW,EAAE,GAAG;oBAChB,MAAM;oBACN,MAAM;oBACN,MAAM;oBACN,eAAe,EAAE,cAAc;iBAChC,CAAC,CAAA;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,UAAW,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YAChE,CAAC;QACH,CAAC;IACH,CAAC,CAAA,CAAC,CAAA;AACN,CAAC"}
@@ -0,0 +1,28 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import BuildServer from '../server/index.js';
11
+ export function registerServerCommand(program) {
12
+ program
13
+ .command('server')
14
+ .description('start build service')
15
+ .option('-p, --port [number]', 'select port', '3000')
16
+ .option('--website-root [string]', 'root path of the website file', '')
17
+ .option('--website-path [string]', 'path of the website match request path', '')
18
+ .action((options) => __awaiter(this, void 0, void 0, function* () {
19
+ const { port, websiteRoot, websitePath } = options;
20
+ const server = new BuildServer({
21
+ port: Number(port),
22
+ websiteRoot,
23
+ websitePath,
24
+ });
25
+ yield server.start();
26
+ }));
27
+ }
28
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":";;;;;;;;;AAIA,OAAO,WAAW,MAAM,oBAAoB,CAAA;AAE5C,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,qBAAqB,CAAC;SAClC,MAAM,CAAC,qBAAqB,EAAE,aAAa,EAAE,MAAM,CAAC;SACpD,MAAM,CAAC,yBAAyB,EAAE,+BAA+B,EAAE,EAAE,CAAC;SACtE,MAAM,CAAC,yBAAyB,EAAE,wCAAwC,EAAE,EAAE,CAAC;SAC/E,MAAM,CAAC,CAAO,OAAO,EAAE,EAAE;QACxB,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,OAAO,CAAA;QAClD,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC;YAC7B,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;YAClB,WAAW;YACX,WAAW;SACZ,CAAC,CAAA;QACF,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;IACtB,CAAC,CAAA,CAAC,CAAA;AACN,CAAC"}