chanjs 2.7.3 → 2.7.5
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.
- package/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/Container.js +77 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +327 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +104 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +224 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +75 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +7 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +174 -197
- package/package.json +11 -3
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +69 -142
- package/security/keywords.js +32 -136
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +57 -196
- package/storage/index.js +3 -6
- package/storage/redis.js +123 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +21 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +94 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- package/schedule/schedule.js +0 -491
package/App.js
DELETED
|
@@ -1,533 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
// 引入基础组件
|
|
6
|
-
import DatabaseManager from "./base/Database.js";
|
|
7
|
-
|
|
8
|
-
// 引入配置和工具
|
|
9
|
-
import { Paths } from "./config/index.js";
|
|
10
|
-
import { loadConfig, loaderSort, loadController } from "./loader/index.js";
|
|
11
|
-
import { Cors, setBody, setCookie, setFavicon, setHeader, setStatic, setTemplate, waf, compress, log } from "./middleware/index.js";
|
|
12
|
-
import { notFoundResponse, parseDatabaseError, errorResponse } from "./response/index.js";
|
|
13
|
-
import { store } from "./storage/index.js";
|
|
14
|
-
import { schedule } from "./schedule/schedule.js";
|
|
15
|
-
import { sse } from "./realtime/sse.js";
|
|
16
|
-
import { websocket } from "./realtime/websocket.js";
|
|
17
|
-
import { importFile } from "./global/import.js";
|
|
18
|
-
|
|
19
|
-
import "./global/index.js";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Chan 应用核心类
|
|
23
|
-
* @class Chan
|
|
24
|
-
* @description 管理应用程序的路由、中间件、数据库连接和服务
|
|
25
|
-
* @example
|
|
26
|
-
* const app = new Chan();
|
|
27
|
-
* await app.start();
|
|
28
|
-
*/
|
|
29
|
-
class Chan {
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 构造函数
|
|
33
|
-
* @constructor
|
|
34
|
-
* @description 初始化 Express 应用和核心组件
|
|
35
|
-
*/
|
|
36
|
-
constructor() {
|
|
37
|
-
this.app = express();
|
|
38
|
-
// 数据库连接管理
|
|
39
|
-
this.dbManager = new DatabaseManager();
|
|
40
|
-
// 应用路由
|
|
41
|
-
this.router = express.Router();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 启动应用程序
|
|
46
|
-
* @async
|
|
47
|
-
* @description 执行完整的启动流程
|
|
48
|
-
* 包括:初始化配置、数据库、扩展、中间件、路由、错误处理
|
|
49
|
-
* @returns {Promise<void>}
|
|
50
|
-
*/
|
|
51
|
-
async start() {
|
|
52
|
-
//加载配置
|
|
53
|
-
await this.config();
|
|
54
|
-
//暴露 Paths 到 Chan 静态属性(DI:业务侧通过 Chan.paths 或 this.paths 访问)
|
|
55
|
-
Chan.paths = Paths;
|
|
56
|
-
//初始化 Redis 适配器(WAF 封禁 / Rate Limit 共享存储)
|
|
57
|
-
await this.loadRedis();
|
|
58
|
-
//加载数据库
|
|
59
|
-
await this.loadDB();
|
|
60
|
-
//加载扩展方法
|
|
61
|
-
await this.loadExtend();
|
|
62
|
-
//加载中间件
|
|
63
|
-
await this.loadAppMiddleware();
|
|
64
|
-
//设置app
|
|
65
|
-
this.setApp();
|
|
66
|
-
//加载路由
|
|
67
|
-
await this.loadRouter();
|
|
68
|
-
await this.loadCommonRouter();
|
|
69
|
-
this.useRouter();
|
|
70
|
-
//404 500 处理(放在最后,确保所有路由都已注册)
|
|
71
|
-
this.setErrorHandler();
|
|
72
|
-
//执行 beforeStart 钩子(用户自定义的初始化逻辑)
|
|
73
|
-
await this._runBeforeStart();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 注册 beforeStart 钩子
|
|
78
|
-
* @param {Function} fn - 钩子函数(支持 async)
|
|
79
|
-
* @description 在 start() 流程末尾、run() 之前执行
|
|
80
|
-
* 适合做:缓存预热、定时任务注册、健康检查初始化等
|
|
81
|
-
* @example
|
|
82
|
-
* chan.beforeStart(async () => {
|
|
83
|
-
* schedule.every('clean-log', 60000, cleanLogs);
|
|
84
|
-
* });
|
|
85
|
-
*/
|
|
86
|
-
beforeStart(fn) {
|
|
87
|
-
if (typeof fn !== 'function') {
|
|
88
|
-
throw new Error('beforeStart 参数必须是函数');
|
|
89
|
-
}
|
|
90
|
-
if (!this._beforeStartHooks) this._beforeStartHooks = [];
|
|
91
|
-
this._beforeStartHooks.push(fn);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 执行 beforeStart 钩子
|
|
96
|
-
* @private
|
|
97
|
-
*/
|
|
98
|
-
async _runBeforeStart() {
|
|
99
|
-
if (!this._beforeStartHooks || !this._beforeStartHooks.length) return;
|
|
100
|
-
for (const fn of this._beforeStartHooks) {
|
|
101
|
-
try {
|
|
102
|
-
await fn();
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error('[Chan] beforeStart 钩子执行失败:', err.message);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 优雅停机
|
|
111
|
-
* @async
|
|
112
|
-
* @description 关闭 HTTP 服务、定时任务、数据库连接、Redis
|
|
113
|
-
* 监听 SIGTERM / SIGINT 信号自动触发
|
|
114
|
-
* @returns {Promise<void>}
|
|
115
|
-
*/
|
|
116
|
-
async shutdown() {
|
|
117
|
-
if (this._shuttingDown) return;
|
|
118
|
-
this._shuttingDown = true;
|
|
119
|
-
console.log('[Chan] 开始优雅停机...');
|
|
120
|
-
|
|
121
|
-
// 5 秒强制退出兜底(原 30s 过长,连接已主动销毁,5s 足够收尾)
|
|
122
|
-
const forceExitTimer = setTimeout(() => {
|
|
123
|
-
console.error('[Chan] 优雅停机超时(5s),强制退出');
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}, 5000);
|
|
126
|
-
forceExitTimer.unref?.();
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
// 1. 停止接收新请求 + 主动销毁所有 keep-alive 连接
|
|
130
|
-
// 说明:server.close() 只停止 listen,会等所有现有连接关闭才回调,
|
|
131
|
-
// 浏览器默认 keep-alive 会导致 30s 超时,所以必须主动 destroy。
|
|
132
|
-
if (this.server) {
|
|
133
|
-
if (this._sockets && this._sockets.size > 0) {
|
|
134
|
-
for (const socket of this._sockets) {
|
|
135
|
-
try { socket.destroy(); } catch (e) { /* 忽略已关闭的 socket */ }
|
|
136
|
-
}
|
|
137
|
-
this._sockets.clear();
|
|
138
|
-
}
|
|
139
|
-
await new Promise(resolve => this.server.close(resolve));
|
|
140
|
-
console.log('[Chan] HTTP 服务已关闭');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 2. 关闭实时通信(SSE / WebSocket,先于定时任务关闭,避免推送失败)
|
|
144
|
-
try {
|
|
145
|
-
sse.shutdown();
|
|
146
|
-
} catch (e) {
|
|
147
|
-
console.error('[Chan] 关闭 SSE 失败:', e.message);
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
websocket.shutdown();
|
|
151
|
-
} catch (e) {
|
|
152
|
-
console.error('[Chan] 关闭 WebSocket 失败:', e.message);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 3. 停止所有定时任务
|
|
156
|
-
try {
|
|
157
|
-
schedule.shutdown();
|
|
158
|
-
} catch (e) {
|
|
159
|
-
console.error('[Chan] 停止定时任务失败:', e.message);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 4. 关闭数据库连接
|
|
163
|
-
try {
|
|
164
|
-
await this.dbManager.closeAll();
|
|
165
|
-
} catch (e) {
|
|
166
|
-
console.error('[Chan] 关闭数据库失败:', e.message);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 5. 关闭 Redis / 存储适配层
|
|
170
|
-
try {
|
|
171
|
-
await store.close();
|
|
172
|
-
} catch (e) {
|
|
173
|
-
console.error('[Chan] 关闭存储适配层失败:', e.message);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
console.log('[Chan] 优雅停机完成');
|
|
177
|
-
} finally {
|
|
178
|
-
clearTimeout(forceExitTimer);
|
|
179
|
-
process.exit(0);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* 注册进程信号监听器(自动触发优雅停机)
|
|
185
|
-
* @private
|
|
186
|
-
*/
|
|
187
|
-
_registerSignalHandlers() {
|
|
188
|
-
if (this._signalRegistered) return;
|
|
189
|
-
this._signalRegistered = true;
|
|
190
|
-
|
|
191
|
-
const handler = async (signal) => {
|
|
192
|
-
// 幂等守卫:首次信号触发停机,后续信号直接忽略,避免日志噪音
|
|
193
|
-
if (this._shuttingDown) {
|
|
194
|
-
// 第二次 ctrl+c:直接强制退出,让用户能快速中断
|
|
195
|
-
console.error(`[Chan] 再次收到 ${signal},强制退出`);
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
console.log(`[Chan] 收到 ${signal} 信号`);
|
|
199
|
-
await this.shutdown();
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
process.on('SIGTERM', () => handler('SIGTERM'));
|
|
203
|
-
process.on('SIGINT', () => handler('SIGINT'));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 加载配置
|
|
208
|
-
* @async
|
|
209
|
-
* @returns {Promise<void>}
|
|
210
|
-
* @description 加载应用配置并设置到全局变量
|
|
211
|
-
*/
|
|
212
|
-
async config() {
|
|
213
|
-
let config = await loadConfig();
|
|
214
|
-
Chan.config = config;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* 初始化存储适配层
|
|
219
|
-
* @async
|
|
220
|
-
* @description
|
|
221
|
-
* 根据 config.REDIS_ENABLED 决定使用 Redis 还是内存模式
|
|
222
|
-
* - Redis 启用且连接成功 → 使用 Redis 后端
|
|
223
|
-
* - Redis 未启用或连接失败 → 降级到内存模式(Map+TTL,上限 10 万条)
|
|
224
|
-
* WAF 封禁状态、Rate Limit 计数都依赖此适配层
|
|
225
|
-
*/
|
|
226
|
-
async loadRedis() {
|
|
227
|
-
const config = Chan.config || {};
|
|
228
|
-
try {
|
|
229
|
-
await store.init({
|
|
230
|
-
REDIS_ENABLED: config.REDIS_ENABLED,
|
|
231
|
-
REDIS: config.REDIS,
|
|
232
|
-
});
|
|
233
|
-
} catch (err) {
|
|
234
|
-
console.warn(`[Chan] 存储适配层初始化失败,使用内存模式: ${err.message}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 加载数据库
|
|
240
|
-
* @async
|
|
241
|
-
* @returns {Promise<void>}
|
|
242
|
-
* @description 根据配置初始化所有数据库连接
|
|
243
|
-
*/
|
|
244
|
-
async loadDB() {
|
|
245
|
-
const dbList = Chan.config?.db || [];
|
|
246
|
-
for (const [index, item] of dbList.entries()) {
|
|
247
|
-
const key = item.key || String(index);
|
|
248
|
-
try {
|
|
249
|
-
// 已通过 dbList.entries() 拿到 item,无需再做 if (!dbConfig) return 死代码判断
|
|
250
|
-
const connection = this.dbManager.add(key, item, { isDefault: index === 0 });
|
|
251
|
-
if (index === 0) {
|
|
252
|
-
Chan.db = this.dbManager.get();
|
|
253
|
-
}
|
|
254
|
-
} catch (error) {
|
|
255
|
-
console.error(`[DB] 数据库 ${key} 初始化失败: ${error.message}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
console.log(`[DB] 初始化完成,已加载 ${dbList.length} 个数据库`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* 加载扩展模块
|
|
264
|
-
* @async
|
|
265
|
-
* @returns {Promise<void>}
|
|
266
|
-
* @description 加载 common、helper、extend 目录下的扩展模块
|
|
267
|
-
*/
|
|
268
|
-
async loadExtend() {
|
|
269
|
-
const extensions = [
|
|
270
|
-
{ _path: Paths.commonPath, key: "common" },
|
|
271
|
-
{ _path: Paths.helperPath, key: "helper" },
|
|
272
|
-
{ _path: Paths.extendPath, key: "extend" },
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
for (const { _path, key } of extensions) {
|
|
276
|
-
await this.loadFn(_path, key);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (!Chan.helper) {
|
|
280
|
-
Chan.helper = {};
|
|
281
|
-
}
|
|
282
|
-
Chan.helper.loadController = loadController;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* 加载模块文件
|
|
287
|
-
* @async
|
|
288
|
-
* @param {string} _path - 模块目录路径
|
|
289
|
-
* @param {string} key - 模块键名
|
|
290
|
-
* @returns {Promise<void>}
|
|
291
|
-
* @description 从指定目录加载所有 JS 模块并合并到 Chan 静态属性
|
|
292
|
-
*/
|
|
293
|
-
async loadFn(_path, key) {
|
|
294
|
-
if (fs.existsSync(_path)) {
|
|
295
|
-
const files = fs.readdirSync(_path).filter((file) => file.endsWith(".js"));
|
|
296
|
-
for (const file of files) {
|
|
297
|
-
const filePath = path.join(_path, file);
|
|
298
|
-
let helperModule = await importFile(filePath);
|
|
299
|
-
if (!Chan[key]) {
|
|
300
|
-
Chan[key] = {};
|
|
301
|
-
}
|
|
302
|
-
Object.assign(Chan[key], helperModule);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* 初始化中间件
|
|
309
|
-
* @async
|
|
310
|
-
* @returns {Promise<void>}
|
|
311
|
-
* @description 配置并注册所有应用中间件
|
|
312
|
-
*/
|
|
313
|
-
async loadAppMiddleware() {
|
|
314
|
-
const config = Chan.config;
|
|
315
|
-
const {
|
|
316
|
-
views = [],
|
|
317
|
-
NODE_ENV = "dev",
|
|
318
|
-
APP_NAME = "ChanCMS",
|
|
319
|
-
APP_VERSION = "1.0.0",
|
|
320
|
-
cookieKey,
|
|
321
|
-
BODY_LIMIT = "10mb",
|
|
322
|
-
statics = [],
|
|
323
|
-
cors = {},
|
|
324
|
-
PROXY = "false",
|
|
325
|
-
logger = {},
|
|
326
|
-
waf: wafConfig = { enabled: false }
|
|
327
|
-
} = config;
|
|
328
|
-
|
|
329
|
-
await waf(this.app, wafConfig);
|
|
330
|
-
setFavicon(this.app);
|
|
331
|
-
setStatic(this.app, statics);
|
|
332
|
-
// 响应压缩中间件:放在 setStatic 之后(静态资源已由 express.static 处理流式响应)、Cors 之前
|
|
333
|
-
// 跳过 SSE / WebSocket 升级请求,仅对可压缩文本类型且 > 1KB 的响应体压缩
|
|
334
|
-
this.app.use(compress());
|
|
335
|
-
setCookie(this.app, cookieKey);
|
|
336
|
-
setBody(this.app, BODY_LIMIT);
|
|
337
|
-
Cors(this.app, cors);
|
|
338
|
-
log(this.app, logger);
|
|
339
|
-
setTemplate(this.app, { views, NODE_ENV });
|
|
340
|
-
setHeader(this.app, { APP_NAME, APP_VERSION });
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* 应用配置
|
|
345
|
-
* @private
|
|
346
|
-
* @description 设置 Express 应用配置
|
|
347
|
-
* trust proxy 根据 config.PROXY 动态设置,避免默认 true 导致 X-Forwarded-For 伪造
|
|
348
|
-
*/
|
|
349
|
-
setApp() {
|
|
350
|
-
const proxyConfig = Chan.config?.PROXY;
|
|
351
|
-
if (proxyConfig === 'true' || proxyConfig === true) {
|
|
352
|
-
// 信任所有代理(仅在反代可信时使用)
|
|
353
|
-
this.app.set("trust proxy", true);
|
|
354
|
-
} else if (proxyConfig === 'loopback' || !proxyConfig) {
|
|
355
|
-
// 默认仅信任本机回环代理
|
|
356
|
-
this.app.set("trust proxy", "loopback");
|
|
357
|
-
} else {
|
|
358
|
-
// 其他值按字面传入(IP / CIDR / 跳数)
|
|
359
|
-
this.app.set("trust proxy", proxyConfig);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* 应用路由
|
|
365
|
-
* @private
|
|
366
|
-
* @description 将所有路由应用到 Express 应用
|
|
367
|
-
*/
|
|
368
|
-
useRouter() {
|
|
369
|
-
this.app.use(this.router);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* 应用错误处理404 500
|
|
374
|
-
* @private
|
|
375
|
-
* @description 配置全局错误处理中间件
|
|
376
|
-
*/
|
|
377
|
-
setErrorHandler() {
|
|
378
|
-
//404(标准 Express 中间件签名,保留 next 参数)
|
|
379
|
-
this.app.use((req, res, next) => {
|
|
380
|
-
res.status(404).json(notFoundResponse(req));
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// 500
|
|
384
|
-
this.app.use((err, req, res, next) => {
|
|
385
|
-
if (res.headersSent) return next(err);
|
|
386
|
-
|
|
387
|
-
const errorInfo = err?.stack ? this._parseErrorStack(err.stack) : { message: err?.message || err };
|
|
388
|
-
const requestInfo = `${req.method} ${req.originalUrl}`;
|
|
389
|
-
console.error(`[Global Error] ${requestInfo} - ${errorInfo.file}:${errorInfo.line || "?"} - ${errorInfo.message}`);
|
|
390
|
-
|
|
391
|
-
const parsedError = parseDatabaseError(err);
|
|
392
|
-
console.error(`[Global Error Details] Database Error - ${parsedError.msg}`);
|
|
393
|
-
res.status(parsedError.statusCode).json(errorResponse(err, req));
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* 解析错误堆栈
|
|
399
|
-
* @private
|
|
400
|
-
* @param {string} stack - 错误堆栈字符串
|
|
401
|
-
* @returns {Object} 解析后的错误信息
|
|
402
|
-
* @description 从错误堆栈中提取文件名、行号、列号等信息
|
|
403
|
-
*/
|
|
404
|
-
_parseErrorStack(stack) {
|
|
405
|
-
if (!stack) return { message: '未知错误' };
|
|
406
|
-
|
|
407
|
-
const stackLines = stack.split('\n');
|
|
408
|
-
if (stackLines.length < 2) {
|
|
409
|
-
return { message: stackLines[0] || '未知错误' };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const errorLine = stackLines[1].trim();
|
|
413
|
-
const match = errorLine.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
|
|
414
|
-
|
|
415
|
-
if (match) {
|
|
416
|
-
return {
|
|
417
|
-
message: stackLines[0] || '未知错误',
|
|
418
|
-
function: match[1],
|
|
419
|
-
file: match[2],
|
|
420
|
-
line: match[3],
|
|
421
|
-
column: match[4]
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const fileMatch = errorLine.match(/at\s+(.+?):(\d+):(\d+)/);
|
|
426
|
-
if (fileMatch) {
|
|
427
|
-
return {
|
|
428
|
-
message: stackLines[0] || '未知错误',
|
|
429
|
-
file: fileMatch[1],
|
|
430
|
-
line: fileMatch[2],
|
|
431
|
-
column: fileMatch[3]
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return { message: stackLines[0] || '未知错误', file: errorLine };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* 加载模块路由
|
|
442
|
-
* @async
|
|
443
|
-
* @returns {Promise<void>}
|
|
444
|
-
* @description 从 app/modules 目录加载所有模块的路由
|
|
445
|
-
*/
|
|
446
|
-
async loadRouter() {
|
|
447
|
-
const configPath = path.join(Paths.appPath, "modules");
|
|
448
|
-
if (fs.existsSync(configPath)) {
|
|
449
|
-
const dirs = loaderSort(Chan.config.modules);
|
|
450
|
-
for (const item of dirs) {
|
|
451
|
-
let routerFn = await importFile(path.join(Paths.modulesPath, item, "router.js"));
|
|
452
|
-
// 为每个模块创建子路由并添加前缀
|
|
453
|
-
const subRouter = express.Router();
|
|
454
|
-
routerFn(this.app, subRouter, Chan.config);
|
|
455
|
-
this.app.use(`/${item}`, subRouter);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* 加载公共路由
|
|
462
|
-
* @async
|
|
463
|
-
* @returns {Promise<void>}
|
|
464
|
-
* @description 加载 app/router.js 中的公共路由
|
|
465
|
-
*/
|
|
466
|
-
async loadCommonRouter() {
|
|
467
|
-
let router = await importFile(path.join(Paths.appPath, "router.js"));
|
|
468
|
-
if (router) {
|
|
469
|
-
router(this.app, this.router, Chan.config);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* 准备启动
|
|
475
|
-
* @param {Function} [cb] - 回调函数
|
|
476
|
-
* @description 启动 HTTP 服务,并按配置挂载 WebSocket(SSE 由业务侧通过 sse.middleware() 注册路由)
|
|
477
|
-
*/
|
|
478
|
-
run(cb) {
|
|
479
|
-
this.server = this.app.listen(Chan.config.PORT, () => {
|
|
480
|
-
console.log(`Server running on port ${Chan.config.PORT}`);
|
|
481
|
-
cb?.(Chan.config.PORT);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// 跟踪所有 HTTP socket,用于优雅停机时主动销毁(避免 keep-alive 卡住 server.close)
|
|
485
|
-
this._sockets = new Set();
|
|
486
|
-
this.server.on('connection', (socket) => {
|
|
487
|
-
this._sockets.add(socket);
|
|
488
|
-
socket.on('close', () => this._sockets.delete(socket));
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
// 按配置挂载 WebSocket(config.WEBSOCKET.enabled = true 时启用)
|
|
492
|
-
this._attachWebSocket();
|
|
493
|
-
// 注册 SIGTERM / SIGINT 信号处理,支持优雅停机
|
|
494
|
-
this._registerSignalHandlers();
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* 挂载 WebSocket 服务到 HTTP server
|
|
499
|
-
* @private
|
|
500
|
-
* @description 根据 config.WEBSOCKET 配置自动挂载,业务侧通过 Chan.websocket 使用
|
|
501
|
-
* @example
|
|
502
|
-
* // config.js
|
|
503
|
-
* module.exports = {
|
|
504
|
-
* WEBSOCKET: { enabled: true, path: '/ws', heartbeatInterval: 30000 }
|
|
505
|
-
* };
|
|
506
|
-
* // 业务侧
|
|
507
|
-
* import { websocket } from 'chanjs';
|
|
508
|
-
* websocket.on('chat', (socket, payload) => { ... });
|
|
509
|
-
*/
|
|
510
|
-
_attachWebSocket() {
|
|
511
|
-
const wsConfig = Chan.config?.WEBSOCKET || {};
|
|
512
|
-
if (!wsConfig.enabled) return;
|
|
513
|
-
|
|
514
|
-
try {
|
|
515
|
-
websocket.attach(this.server, {
|
|
516
|
-
path: wsConfig.path || '/ws',
|
|
517
|
-
heartbeatInterval: wsConfig.heartbeatInterval,
|
|
518
|
-
verifyClient: wsConfig.verifyClient,
|
|
519
|
-
onConnection: wsConfig.onConnection,
|
|
520
|
-
});
|
|
521
|
-
// 暴露到 Chan 静态属性,便于业务侧通过 import Chan 访问
|
|
522
|
-
Chan.websocket = websocket;
|
|
523
|
-
} catch (err) {
|
|
524
|
-
console.error('[Chan] WebSocket 挂载失败:', err.message);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// 不再使用 global.Chan 全局污染
|
|
530
|
-
// 业务侧通过 `import Chan from 'chanjs'` 获取同一个类引用(ESM 模块缓存保证单例)
|
|
531
|
-
// 框架内部直接使用类名 Chan(同一模块作用域)
|
|
532
|
-
|
|
533
|
-
export default Chan;
|