befly 3.17.0 → 3.17.2
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/README.md +6 -0
- package/apis/admin/cacheRefresh.js +122 -0
- package/apis/admin/del.js +34 -0
- package/apis/admin/detail.js +23 -0
- package/apis/admin/ins.js +69 -0
- package/apis/admin/list.js +28 -0
- package/apis/admin/upd.js +95 -0
- package/apis/api/all.js +24 -0
- package/apis/api/list.js +31 -0
- package/apis/auth/login.js +123 -0
- package/apis/auth/sendSmsCode.js +24 -0
- package/apis/dashboard/configStatus.js +39 -0
- package/apis/dashboard/environmentInfo.js +43 -0
- package/apis/dashboard/performanceMetrics.js +20 -0
- package/apis/dashboard/permissionStats.js +27 -0
- package/apis/dashboard/serviceStatus.js +75 -0
- package/apis/dashboard/systemInfo.js +19 -0
- package/apis/dashboard/systemOverview.js +30 -0
- package/apis/dashboard/systemResources.js +106 -0
- package/apis/dict/all.js +23 -0
- package/apis/dict/del.js +16 -0
- package/apis/dict/detail.js +27 -0
- package/apis/dict/ins.js +51 -0
- package/apis/dict/items.js +30 -0
- package/apis/dict/list.js +36 -0
- package/apis/dict/upd.js +74 -0
- package/apis/dictType/all.js +16 -0
- package/apis/dictType/del.js +38 -0
- package/apis/dictType/detail.js +20 -0
- package/apis/dictType/ins.js +37 -0
- package/apis/dictType/list.js +26 -0
- package/apis/dictType/upd.js +51 -0
- package/apis/email/config.js +25 -0
- package/apis/email/logList.js +23 -0
- package/apis/email/send.js +66 -0
- package/apis/email/verify.js +21 -0
- package/apis/loginLog/list.js +23 -0
- package/apis/menu/all.js +41 -0
- package/apis/menu/list.js +25 -0
- package/apis/operateLog/list.js +23 -0
- package/apis/role/all.js +21 -0
- package/apis/role/apiSave.js +43 -0
- package/apis/role/apis.js +22 -0
- package/apis/role/del.js +49 -0
- package/apis/role/detail.js +32 -0
- package/apis/role/ins.js +46 -0
- package/apis/role/list.js +27 -0
- package/apis/role/menuSave.js +42 -0
- package/apis/role/menus.js +22 -0
- package/apis/role/save.js +40 -0
- package/apis/role/upd.js +50 -0
- package/apis/sysConfig/all.js +16 -0
- package/apis/sysConfig/del.js +36 -0
- package/apis/sysConfig/get.js +49 -0
- package/apis/sysConfig/ins.js +50 -0
- package/apis/sysConfig/list.js +24 -0
- package/apis/sysConfig/upd.js +62 -0
- package/checks/api.js +55 -0
- package/checks/config.js +107 -0
- package/checks/hook.js +38 -0
- package/checks/menu.js +58 -0
- package/checks/plugin.js +38 -0
- package/checks/table.js +78 -0
- package/configs/beflyConfig.json +61 -0
- package/configs/beflyMenus.json +85 -0
- package/configs/constConfig.js +34 -0
- package/configs/regexpAlias.json +55 -0
- package/hooks/auth.js +34 -0
- package/hooks/cors.js +39 -0
- package/hooks/parser.js +90 -0
- package/hooks/permission.js +71 -0
- package/hooks/validator.js +43 -0
- package/index.js +326 -0
- package/lib/cacheHelper.js +483 -0
- package/lib/cacheKeys.js +42 -0
- package/lib/connect.js +120 -0
- package/lib/dbHelper/builders.js +698 -0
- package/lib/dbHelper/context.js +131 -0
- package/lib/dbHelper/dataOps.js +505 -0
- package/lib/dbHelper/execute.js +65 -0
- package/lib/dbHelper/index.js +27 -0
- package/lib/dbHelper/transaction.js +43 -0
- package/lib/dbHelper/util.js +58 -0
- package/lib/dbHelper/validate.js +549 -0
- package/lib/emailHelper.js +110 -0
- package/lib/logger.js +604 -0
- package/lib/redisHelper.js +684 -0
- package/lib/sqlBuilder/batch.js +113 -0
- package/lib/sqlBuilder/check.js +150 -0
- package/lib/sqlBuilder/compiler.js +347 -0
- package/lib/sqlBuilder/errors.js +60 -0
- package/lib/sqlBuilder/index.js +218 -0
- package/lib/sqlBuilder/parser.js +296 -0
- package/lib/sqlBuilder/util.js +260 -0
- package/lib/validator.js +303 -0
- package/package.json +19 -12
- package/paths.js +112 -0
- package/plugins/cache.js +16 -0
- package/plugins/config.js +11 -0
- package/plugins/email.js +27 -0
- package/plugins/logger.js +20 -0
- package/plugins/mysql.js +36 -0
- package/plugins/redis.js +34 -0
- package/plugins/tool.js +155 -0
- package/router/api.js +140 -0
- package/router/static.js +71 -0
- package/sql/admin.sql +18 -0
- package/sql/api.sql +12 -0
- package/sql/dict.sql +13 -0
- package/sql/dictType.sql +12 -0
- package/sql/emailLog.sql +20 -0
- package/sql/loginLog.sql +25 -0
- package/sql/menu.sql +12 -0
- package/sql/operateLog.sql +22 -0
- package/sql/role.sql +14 -0
- package/sql/sysConfig.sql +16 -0
- package/sync/api.js +93 -0
- package/sync/cache.js +13 -0
- package/sync/dev.js +171 -0
- package/sync/menu.js +99 -0
- package/tables/admin.json +56 -0
- package/tables/api.json +26 -0
- package/tables/dict.json +30 -0
- package/tables/dictType.json +24 -0
- package/tables/emailLog.json +61 -0
- package/tables/loginLog.json +86 -0
- package/tables/menu.json +24 -0
- package/tables/operateLog.json +68 -0
- package/tables/role.json +32 -0
- package/tables/sysConfig.json +43 -0
- package/utils/calcPerfTime.js +13 -0
- package/utils/cors.js +17 -0
- package/utils/deepMerge.js +78 -0
- package/utils/fieldClear.js +65 -0
- package/utils/formatYmdHms.js +23 -0
- package/utils/formatZodIssues.js +109 -0
- package/utils/getClientIp.js +47 -0
- package/utils/importDefault.js +51 -0
- package/utils/is.js +462 -0
- package/utils/loggerUtils.js +185 -0
- package/utils/processInfo.js +39 -0
- package/utils/regexpUtil.js +52 -0
- package/utils/response.js +114 -0
- package/utils/scanFiles.js +124 -0
- package/utils/scanSources.js +68 -0
- package/utils/sortModules.js +75 -0
- package/utils/toSessionTtlSeconds.js +14 -0
- package/utils/util.js +374 -0
- package/befly.js +0 -16413
- package/befly.min.js +0 -72
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 邮件发送工具类 - JavaScript 版本
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Logger } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 邮件发送工具类
|
|
9
|
+
*/
|
|
10
|
+
export class EmailHelper {
|
|
11
|
+
/**
|
|
12
|
+
* 创建 EmailHelper 实例
|
|
13
|
+
* @param config - 邮件配置
|
|
14
|
+
*/
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 发送邮件
|
|
21
|
+
* @param options - 邮件选项
|
|
22
|
+
* @returns 是否发送成功
|
|
23
|
+
*/
|
|
24
|
+
async send(options) {
|
|
25
|
+
const config = this.getConfig();
|
|
26
|
+
|
|
27
|
+
if (!config || !config.host || !config.user) {
|
|
28
|
+
Logger.warn("邮件未配置,已禁用发送");
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await this.doSend(config, options);
|
|
34
|
+
if (result.success) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Logger.warn("发送邮件失败", { err: result.error });
|
|
39
|
+
return false;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
Logger.error("发送邮件异常", error);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取邮件配置
|
|
48
|
+
* @returns 完整配置
|
|
49
|
+
*/
|
|
50
|
+
getConfig() {
|
|
51
|
+
if (!this.config) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = Object.assign({}, this.config);
|
|
56
|
+
result.pass = this.config.pass || process.env.SMTP_PASS;
|
|
57
|
+
result.ssl = Boolean(this.config.ssl);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 实际发送邮件
|
|
63
|
+
* @param config - 邮件配置
|
|
64
|
+
* @param options - 邮件选项
|
|
65
|
+
* @returns 发送结果
|
|
66
|
+
*/
|
|
67
|
+
async doSend(config, options) {
|
|
68
|
+
let emailService;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// 动态加载 emailjs
|
|
72
|
+
const emailjs = await import("emailjs");
|
|
73
|
+
emailService = emailjs.server.connect({
|
|
74
|
+
user: config.user,
|
|
75
|
+
password: config.pass,
|
|
76
|
+
host: config.host,
|
|
77
|
+
port: config.port || 25,
|
|
78
|
+
ssl: config.ssl,
|
|
79
|
+
timeout: config.timeout || 10000
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return { success: false, error: error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await emailService.send({
|
|
87
|
+
text: options.text || options.html,
|
|
88
|
+
from: options.from || config.from || config.user,
|
|
89
|
+
to: options.to,
|
|
90
|
+
cc: options.cc,
|
|
91
|
+
bcc: options.bcc,
|
|
92
|
+
subject: options.subject,
|
|
93
|
+
attachment: options.attachments
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { success: true };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { success: false, error: error };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 创建邮件工具实例
|
|
105
|
+
* @param config - 邮件配置
|
|
106
|
+
* @returns EmailHelper 实例
|
|
107
|
+
*/
|
|
108
|
+
export function createEmailHelper(config) {
|
|
109
|
+
return new EmailHelper(config);
|
|
110
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志系统 - Bun 环境自定义实现(替换 pino / pino-roll)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createWriteStream, existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { stat } from "node:fs/promises";
|
|
7
|
+
import { isAbsolute as nodePathIsAbsolute, join as nodePathJoin, resolve as nodePathResolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { formatYmdHms } from "../utils/formatYmdHms.js";
|
|
10
|
+
import { buildSensitiveKeyMatcher, sanitizeLogObject } from "../utils/loggerUtils.js";
|
|
11
|
+
import { isFiniteNumber, isNumber, isPlainObject, isString } from "../utils/is.js";
|
|
12
|
+
import { normalizePositiveInt } from "../utils/util.js";
|
|
13
|
+
|
|
14
|
+
// 注意:Logger 可能在运行时/测试中被 process.chdir() 影响。
|
|
15
|
+
// 为避免相对路径的 logs 目录随着 cwd 变化,使用模块加载时的初始 cwd 作为锚点。
|
|
16
|
+
const INITIAL_CWD = process.cwd();
|
|
17
|
+
const RUNTIME_LOG_DIR = nodePathResolve(INITIAL_CWD, "logs");
|
|
18
|
+
|
|
19
|
+
const BUILTIN_SENSITIVE_KEYS = ["*password*", "pass", "pwd", "*token*", "access_token", "refresh_token", "accessToken", "refreshToken", "authorization", "cookie", "set-cookie", "*secret*", "apiKey", "api_key", "privateKey", "private_key"];
|
|
20
|
+
|
|
21
|
+
let sanitizeOptions = {
|
|
22
|
+
maxStringLen: 200,
|
|
23
|
+
maxArrayItems: 500,
|
|
24
|
+
sanitizeDepth: 5,
|
|
25
|
+
sanitizeNodes: 5000,
|
|
26
|
+
sanitizeObjectKeys: 500,
|
|
27
|
+
sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: [] })
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function buildSanitizeOptionsForWriteOptions(writeOptions) {
|
|
31
|
+
if (!writeOptions || writeOptions.truncate !== false) return sanitizeOptions;
|
|
32
|
+
|
|
33
|
+
// 仅关闭“截断”,仍保留敏感字段掩码与结构化清洗(避免泄露敏感信息)。
|
|
34
|
+
return {
|
|
35
|
+
maxStringLen: 200000,
|
|
36
|
+
maxArrayItems: 5000,
|
|
37
|
+
sanitizeDepth: sanitizeOptions.sanitizeDepth,
|
|
38
|
+
sanitizeNodes: sanitizeOptions.sanitizeNodes,
|
|
39
|
+
sanitizeObjectKeys: sanitizeOptions.sanitizeObjectKeys,
|
|
40
|
+
sensitiveKeyMatcher: sanitizeOptions.sensitiveKeyMatcher
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let mockInstance = null;
|
|
45
|
+
|
|
46
|
+
let appFileSink = null;
|
|
47
|
+
let errorFileSink = null;
|
|
48
|
+
|
|
49
|
+
let config = {
|
|
50
|
+
debug: 0,
|
|
51
|
+
dir: RUNTIME_LOG_DIR,
|
|
52
|
+
maxSize: 20,
|
|
53
|
+
runtimeEnv: "production"
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function safeWriteStderr(msg) {
|
|
57
|
+
try {
|
|
58
|
+
process.stderr.write(`${msg}\n`);
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shiftBatchFromPending(pending, maxBatchBytes) {
|
|
65
|
+
const parts = [];
|
|
66
|
+
let bytes = 0;
|
|
67
|
+
|
|
68
|
+
while (pending.length > 0) {
|
|
69
|
+
const next = pending[0];
|
|
70
|
+
const nextBytes = Buffer.byteLength(next);
|
|
71
|
+
if (parts.length > 0 && bytes + nextBytes > maxBatchBytes) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
parts.push(next);
|
|
75
|
+
bytes = bytes + nextBytes;
|
|
76
|
+
pending.shift();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { chunk: parts.join(""), bytes: bytes };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class LogFileSink {
|
|
83
|
+
constructor(options) {
|
|
84
|
+
this.prefix = options.prefix;
|
|
85
|
+
this.maxFileBytes = options.maxFileBytes;
|
|
86
|
+
|
|
87
|
+
this.stream = null;
|
|
88
|
+
this.streamDate = "";
|
|
89
|
+
this.streamIndex = 0;
|
|
90
|
+
this.streamSizeBytes = 0;
|
|
91
|
+
this.disabled = false;
|
|
92
|
+
|
|
93
|
+
this.pending = [];
|
|
94
|
+
this.pendingBytes = 0;
|
|
95
|
+
this.scheduledTimer = null;
|
|
96
|
+
this.flushing = false;
|
|
97
|
+
|
|
98
|
+
this.maxBufferBytes = 10 * 1024 * 1024;
|
|
99
|
+
this.flushDelayMs = 10;
|
|
100
|
+
this.maxBatchBytes = 64 * 1024;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
enqueue(line) {
|
|
104
|
+
if (this.disabled) return;
|
|
105
|
+
|
|
106
|
+
const bytes = Buffer.byteLength(line);
|
|
107
|
+
if (this.pendingBytes + bytes > this.maxBufferBytes) {
|
|
108
|
+
// buffer 满:统一丢弃新日志(不区分 level)
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.pending.push(line);
|
|
113
|
+
this.pendingBytes = this.pendingBytes + bytes;
|
|
114
|
+
|
|
115
|
+
this.scheduleFlush();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async flushNow() {
|
|
119
|
+
this.cancelScheduledFlush();
|
|
120
|
+
await this.flush();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async shutdown() {
|
|
124
|
+
this.cancelScheduledFlush();
|
|
125
|
+
await this.flush();
|
|
126
|
+
await this.closeStream();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
scheduleFlush() {
|
|
130
|
+
if (this.scheduledTimer) return;
|
|
131
|
+
|
|
132
|
+
this.scheduledTimer = setTimeout(() => {
|
|
133
|
+
// timer 触发时先清空句柄,避免 flush 内再次 schedule 时被认为“已安排”。
|
|
134
|
+
this.scheduledTimer = null;
|
|
135
|
+
void this.flush();
|
|
136
|
+
}, this.flushDelayMs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
cancelScheduledFlush() {
|
|
140
|
+
if (!this.scheduledTimer) return;
|
|
141
|
+
clearTimeout(this.scheduledTimer);
|
|
142
|
+
this.scheduledTimer = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async flush() {
|
|
146
|
+
if (this.flushing) return;
|
|
147
|
+
this.flushing = true;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
while (this.pending.length > 0) {
|
|
151
|
+
const batch = shiftBatchFromPending(this.pending, this.maxBatchBytes);
|
|
152
|
+
const chunk = batch.chunk;
|
|
153
|
+
const chunkBytes = Buffer.byteLength(chunk);
|
|
154
|
+
this.pendingBytes = this.pendingBytes - chunkBytes;
|
|
155
|
+
|
|
156
|
+
const ok = await this.writeChunk(chunk, chunkBytes);
|
|
157
|
+
if (!ok) {
|
|
158
|
+
// writer 已禁用/失败:清空剩余 pending
|
|
159
|
+
this.pending = [];
|
|
160
|
+
this.pendingBytes = 0;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
this.flushing = false;
|
|
166
|
+
if (this.pending.length > 0) {
|
|
167
|
+
this.scheduleFlush();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async writeChunk(chunk, chunkBytes) {
|
|
173
|
+
if (this.disabled) return false;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await this.ensureStreamReady(chunkBytes);
|
|
177
|
+
if (!this.stream) {
|
|
178
|
+
// 文件 sink 已被禁用或打开失败
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ok = this.stream.write(chunk);
|
|
183
|
+
if (!ok) {
|
|
184
|
+
await new Promise((resolve) => {
|
|
185
|
+
const stream = this.stream;
|
|
186
|
+
if (!stream) {
|
|
187
|
+
resolve();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
stream.once("drain", () => resolve());
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
this.streamSizeBytes = this.streamSizeBytes + chunkBytes;
|
|
194
|
+
return true;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
safeWriteStderr(`[Logger] file sink flush error (${this.prefix}): ${error?.message || error}`);
|
|
197
|
+
this.disabled = true;
|
|
198
|
+
await this.closeStream();
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async closeStream() {
|
|
204
|
+
if (!this.stream) return;
|
|
205
|
+
const st = this.stream;
|
|
206
|
+
this.stream = null;
|
|
207
|
+
|
|
208
|
+
await new Promise((resolve) => {
|
|
209
|
+
try {
|
|
210
|
+
st.end(() => resolve());
|
|
211
|
+
} catch {
|
|
212
|
+
resolve();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
openStream(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
this.stream = createWriteStream(filePath, { flags: "a" });
|
|
220
|
+
this.stream.on("error", (error) => {
|
|
221
|
+
safeWriteStderr(`[Logger] file sink error (${this.prefix}): ${error?.message || error}`);
|
|
222
|
+
this.disabled = true;
|
|
223
|
+
void this.closeStream();
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
safeWriteStderr(`[Logger] createWriteStream failed (${this.prefix}): ${error?.message || error}`);
|
|
227
|
+
this.disabled = true;
|
|
228
|
+
this.stream = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getFilePath(date, index) {
|
|
233
|
+
const suffix = index > 0 ? `.${index}` : "";
|
|
234
|
+
const filename = this.prefix === "app" ? `${date}${suffix}.log` : `${this.prefix}.${date}${suffix}.log`;
|
|
235
|
+
return nodePathJoin(RUNTIME_LOG_DIR, filename);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async ensureStreamReady(nextChunkBytes) {
|
|
239
|
+
const date = formatYmdHms(Reflect.construct(Date, []), "date");
|
|
240
|
+
|
|
241
|
+
// 日期变化:切新文件
|
|
242
|
+
if (this.stream && this.streamDate && date !== this.streamDate) {
|
|
243
|
+
await this.closeStream();
|
|
244
|
+
this.streamDate = "";
|
|
245
|
+
this.streamIndex = 0;
|
|
246
|
+
this.streamSizeBytes = 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 首次打开
|
|
250
|
+
if (!this.stream) {
|
|
251
|
+
const filePath = this.getFilePath(date, 0);
|
|
252
|
+
let size = 0;
|
|
253
|
+
try {
|
|
254
|
+
const st = await stat(filePath);
|
|
255
|
+
size = isNumber(st.size) ? st.size : 0;
|
|
256
|
+
} catch {
|
|
257
|
+
size = 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.streamDate = date;
|
|
261
|
+
this.streamIndex = 0;
|
|
262
|
+
this.streamSizeBytes = size;
|
|
263
|
+
|
|
264
|
+
this.openStream(filePath);
|
|
265
|
+
if (!this.stream) return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 大小滚动
|
|
269
|
+
if (this.stream && this.maxFileBytes > 0 && this.streamSizeBytes + nextChunkBytes > this.maxFileBytes) {
|
|
270
|
+
await this.closeStream();
|
|
271
|
+
this.streamIndex = this.streamIndex + 1;
|
|
272
|
+
const filePath = this.getFilePath(date, this.streamIndex);
|
|
273
|
+
this.streamDate = date;
|
|
274
|
+
this.streamSizeBytes = 0;
|
|
275
|
+
|
|
276
|
+
this.openStream(filePath);
|
|
277
|
+
if (!this.stream) return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function flush() {
|
|
283
|
+
// 测试场景:mock logger 不需要 flush
|
|
284
|
+
if (mockInstance) return;
|
|
285
|
+
|
|
286
|
+
const sinks = [];
|
|
287
|
+
if (appFileSink) sinks.push({ flush: () => (appFileSink ? appFileSink.flushNow() : Promise.resolve()) });
|
|
288
|
+
if (errorFileSink) sinks.push({ flush: () => (errorFileSink ? errorFileSink.flushNow() : Promise.resolve()) });
|
|
289
|
+
|
|
290
|
+
for (const item of sinks) {
|
|
291
|
+
try {
|
|
292
|
+
await item.flush();
|
|
293
|
+
} catch {
|
|
294
|
+
// ignore
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function shutdown() {
|
|
300
|
+
// 测试场景:mock logger 不需要 shutdown
|
|
301
|
+
if (mockInstance) return;
|
|
302
|
+
|
|
303
|
+
// 重要:shutdown 可能与后续 Logger 调用并发。
|
|
304
|
+
// 因此这里捕获“当前的旧 sink/instance 快照”,只关闭这些快照,避免把新创建的 sink 一并清掉。
|
|
305
|
+
const currentAppFileSink = appFileSink;
|
|
306
|
+
const currentErrorFileSink = errorFileSink;
|
|
307
|
+
|
|
308
|
+
const sinks = [];
|
|
309
|
+
if (currentAppFileSink) sinks.push({ shutdown: () => currentAppFileSink.shutdown() });
|
|
310
|
+
if (currentErrorFileSink) sinks.push({ shutdown: () => currentErrorFileSink.shutdown() });
|
|
311
|
+
|
|
312
|
+
for (const item of sinks) {
|
|
313
|
+
try {
|
|
314
|
+
await item.shutdown();
|
|
315
|
+
} catch {
|
|
316
|
+
// ignore
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (appFileSink === currentAppFileSink) {
|
|
321
|
+
appFileSink = null;
|
|
322
|
+
}
|
|
323
|
+
if (errorFileSink === currentErrorFileSink) {
|
|
324
|
+
errorFileSink = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// shutdown 后允许下一次重新初始化时再次校验/创建目录(测试会清理目录,避免 ENOENT)
|
|
328
|
+
// 无需缓存状态:确保目录存在是幂等的。
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function ensureLogDirExists() {
|
|
332
|
+
try {
|
|
333
|
+
if (!existsSync(RUNTIME_LOG_DIR)) {
|
|
334
|
+
mkdirSync(RUNTIME_LOG_DIR, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
// 不能在 Logger 初始化前调用 Logger 本身,直接抛错即可
|
|
338
|
+
throw new Error(`创建 logs 目录失败: ${RUNTIME_LOG_DIR}. ${error?.message || error}`, {
|
|
339
|
+
cause: error,
|
|
340
|
+
code: "runtime"
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 方案B:删除“启动时清理旧日志”功能(减少 I/O 与复杂度)。
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 配置日志
|
|
349
|
+
*/
|
|
350
|
+
export function configure(cfg) {
|
|
351
|
+
// 旧实例可能仍持有文件句柄;这里异步关闭(不阻塞主流程)
|
|
352
|
+
void shutdown();
|
|
353
|
+
|
|
354
|
+
// 方案B:每次 configure 都从默认配置重新构建(避免继承上一次配置造成测试/运行时污染)
|
|
355
|
+
config = Object.assign(
|
|
356
|
+
{
|
|
357
|
+
debug: 0,
|
|
358
|
+
dir: RUNTIME_LOG_DIR,
|
|
359
|
+
maxSize: 20,
|
|
360
|
+
runtimeEnv: "production"
|
|
361
|
+
},
|
|
362
|
+
cfg
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// 约束:日志目录始终固定在当前项目根目录 logs,不允许外部覆盖。
|
|
366
|
+
config.dir = RUNTIME_LOG_DIR;
|
|
367
|
+
|
|
368
|
+
if (cfg && !isString(cfg.runtimeEnv) && isString(cfg.runMode)) {
|
|
369
|
+
config.runtimeEnv = cfg.runMode;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// maxSize:仅按 MB 计算,且强制范围 10..100
|
|
373
|
+
{
|
|
374
|
+
const raw = config.maxSize;
|
|
375
|
+
let mb = isFiniteNumber(raw) ? raw : 20;
|
|
376
|
+
if (mb < 10) mb = 10;
|
|
377
|
+
if (mb > 100) mb = 100;
|
|
378
|
+
config.maxSize = mb;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
appFileSink = null;
|
|
382
|
+
errorFileSink = null;
|
|
383
|
+
|
|
384
|
+
// 运行时清洗/截断上限(可配置)
|
|
385
|
+
sanitizeOptions = {
|
|
386
|
+
maxStringLen: normalizePositiveInt(config.maxStringLen, 200, 20, 200000),
|
|
387
|
+
maxArrayItems: normalizePositiveInt(config.maxArrayItems, 500, 10, 5000),
|
|
388
|
+
sanitizeDepth: normalizePositiveInt(config.sanitizeDepth, 5, 1, 10),
|
|
389
|
+
sanitizeNodes: normalizePositiveInt(config.sanitizeNodes, 5000, 50, 20000),
|
|
390
|
+
sanitizeObjectKeys: normalizePositiveInt(config.sanitizeObjectKeys, 500, 10, 5000),
|
|
391
|
+
sensitiveKeyMatcher: buildSensitiveKeyMatcher({ builtinPatterns: BUILTIN_SENSITIVE_KEYS, userPatterns: config.excludeFields })
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 设置 Mock Logger(用于测试)
|
|
397
|
+
* @param mock - Mock Logger 实例(形如 {info/warn/error/debug}),传 null 清除 mock
|
|
398
|
+
*/
|
|
399
|
+
export function setMockLogger(mock) {
|
|
400
|
+
mockInstance = mock;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildJsonLine(level, timeMs, record) {
|
|
404
|
+
const base = {
|
|
405
|
+
level: level,
|
|
406
|
+
time: formatYmdHms(Reflect.construct(Date, [timeMs])),
|
|
407
|
+
pid: process.pid
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// 目标:让 level/time/timeFormat/pid/hostname 在 JSON 行首出现(更易读),同时保持 JSONL 可解析。
|
|
411
|
+
// 约束:record 不允许覆盖基础字段。
|
|
412
|
+
// 实现:先写入基础字段,再按 record 的 key 顺序追加其它字段。
|
|
413
|
+
if (record && isPlainObject(record)) {
|
|
414
|
+
const out = {};
|
|
415
|
+
out["level"] = base["level"];
|
|
416
|
+
out["time"] = base["time"];
|
|
417
|
+
out["pid"] = base["pid"];
|
|
418
|
+
|
|
419
|
+
for (const key of Object.keys(record)) {
|
|
420
|
+
if (key === "level") continue;
|
|
421
|
+
if (key === "time") continue;
|
|
422
|
+
if (key === "pid") continue;
|
|
423
|
+
out[key] = record[key];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// msg 允许保留 record.msg(若存在),否则补齐空字符串。
|
|
427
|
+
if (record.msg !== undefined) {
|
|
428
|
+
out["msg"] = record.msg;
|
|
429
|
+
} else if (out["msg"] === undefined) {
|
|
430
|
+
out["msg"] = "";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
return `${JSON.stringify(out)}\n`;
|
|
435
|
+
} catch {
|
|
436
|
+
try {
|
|
437
|
+
return `${JSON.stringify({ level: out["level"], time: out["time"], pid: out["pid"], msg: "[Unserializable log record]" })}\n`;
|
|
438
|
+
} catch {
|
|
439
|
+
return '{"msg":"[Unserializable log record]"}\n';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (base["msg"] === undefined) {
|
|
445
|
+
base["msg"] = "";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
return `${JSON.stringify(base)}\n`;
|
|
450
|
+
} catch {
|
|
451
|
+
try {
|
|
452
|
+
return `${JSON.stringify({ level: base["level"], time: base["time"], pid: base["pid"], msg: "[Unserializable log record]" })}\n`;
|
|
453
|
+
} catch {
|
|
454
|
+
return '{"msg":"[Unserializable log record]"}\n';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function ensureSinksReady(kind) {
|
|
460
|
+
ensureLogDirExists();
|
|
461
|
+
|
|
462
|
+
const maxSizeMb = isFiniteNumber(config.maxSize) ? config.maxSize : 20;
|
|
463
|
+
const maxFileBytes = Math.floor(maxSizeMb * 1024 * 1024);
|
|
464
|
+
|
|
465
|
+
if (kind === "app") {
|
|
466
|
+
if (!appFileSink) {
|
|
467
|
+
appFileSink = new LogFileSink({ prefix: "app", maxFileBytes: maxFileBytes });
|
|
468
|
+
}
|
|
469
|
+
return { fileSink: appFileSink };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!errorFileSink) {
|
|
473
|
+
errorFileSink = new LogFileSink({ prefix: "error", maxFileBytes: maxFileBytes });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { fileSink: errorFileSink };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function writeJsonl(kind, level, record, options) {
|
|
480
|
+
const sinks = ensureSinksReady(kind);
|
|
481
|
+
const time = Date.now();
|
|
482
|
+
const effectiveSanitizeOptions = buildSanitizeOptionsForWriteOptions(options);
|
|
483
|
+
const sanitizedRecord = sanitizeLogObject(record, effectiveSanitizeOptions);
|
|
484
|
+
const fileLine = buildJsonLine(level, time, sanitizedRecord);
|
|
485
|
+
sinks.fileSink.enqueue(fileLine);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 对象清洗/脱敏/截断逻辑已下沉到 utils/loggerUtils.ts(减少 logger.ts 复杂度)。
|
|
489
|
+
|
|
490
|
+
// 日志仅接受 1 个入参(任意类型)。
|
|
491
|
+
// - plain object({})直接作为 record
|
|
492
|
+
// - 其他任何类型:包装成对象再写入(避免 sink 层依赖入参类型)
|
|
493
|
+
function toRecord(input) {
|
|
494
|
+
if (isPlainObject(input)) {
|
|
495
|
+
return input;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (input instanceof Error) {
|
|
499
|
+
return { err: input, msg: input.message || input.name || "Error" };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (input === undefined) {
|
|
503
|
+
return { msg: "" };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (input === null) {
|
|
507
|
+
return { msg: "null" };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 非 plain object(数组/Date/Map/...)也走 value 包装,由 sanitize 负责做安全预览/截断。
|
|
511
|
+
if (typeof input === "object") {
|
|
512
|
+
return { value: input };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
return { msg: String(input) };
|
|
517
|
+
} catch {
|
|
518
|
+
return { msg: "[Unserializable]" };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function logWrite(level, input, options) {
|
|
523
|
+
// debug!=1/true 则不记录 debug 日志
|
|
524
|
+
if (level === "debug" && config.debug !== 1 && config.debug !== true) return;
|
|
525
|
+
|
|
526
|
+
const record0 = toRecord(input);
|
|
527
|
+
|
|
528
|
+
// 测试场景:mock logger 走同步写入,并在入口进行清洗/脱敏/截断控制
|
|
529
|
+
if (mockInstance) {
|
|
530
|
+
const effective = buildSanitizeOptionsForWriteOptions(options);
|
|
531
|
+
const sanitized = sanitizeLogObject(record0, effective);
|
|
532
|
+
if (level === "info") {
|
|
533
|
+
mockInstance.info(sanitized);
|
|
534
|
+
} else if (level === "warn") {
|
|
535
|
+
mockInstance.warn(sanitized);
|
|
536
|
+
} else if (level === "error") {
|
|
537
|
+
mockInstance.error(sanitized);
|
|
538
|
+
} else {
|
|
539
|
+
mockInstance.debug(sanitized);
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (level === "error") {
|
|
545
|
+
// error 仅写入 error 文件
|
|
546
|
+
writeJsonl("error", "error", record0, options);
|
|
547
|
+
} else {
|
|
548
|
+
writeJsonl("app", level, record0, options);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 日志实例(延迟初始化)
|
|
554
|
+
*/
|
|
555
|
+
export const Logger = {
|
|
556
|
+
info(msg, data, truncate) {
|
|
557
|
+
const options = truncate === false ? { truncate: false } : undefined;
|
|
558
|
+
if (isPlainObject(msg)) {
|
|
559
|
+
logWrite("info", msg, options);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
logWrite("info", { msg: msg, data: data }, options);
|
|
564
|
+
},
|
|
565
|
+
warn(msg, data, truncate) {
|
|
566
|
+
const options = truncate === false ? { truncate: false } : undefined;
|
|
567
|
+
if (isPlainObject(msg)) {
|
|
568
|
+
logWrite("warn", msg, options);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
logWrite("warn", { msg: msg, data: data }, options);
|
|
573
|
+
},
|
|
574
|
+
error(msg, err, data, truncate) {
|
|
575
|
+
const options = truncate === false ? { truncate: false } : undefined;
|
|
576
|
+
if (isPlainObject(msg)) {
|
|
577
|
+
logWrite("error", msg, options);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
logWrite("error", { msg: msg, err: err, data: data }, options);
|
|
582
|
+
},
|
|
583
|
+
debug(msg, data, truncate) {
|
|
584
|
+
const options = truncate === false ? { truncate: false } : undefined;
|
|
585
|
+
if (isPlainObject(msg)) {
|
|
586
|
+
logWrite("debug", msg, options);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
logWrite("debug", { msg: msg, data: data }, options);
|
|
591
|
+
},
|
|
592
|
+
async flush() {
|
|
593
|
+
await flush();
|
|
594
|
+
},
|
|
595
|
+
configure(cfg) {
|
|
596
|
+
configure(cfg);
|
|
597
|
+
},
|
|
598
|
+
setMock(mock) {
|
|
599
|
+
setMockLogger(mock);
|
|
600
|
+
},
|
|
601
|
+
async shutdown() {
|
|
602
|
+
await shutdown();
|
|
603
|
+
}
|
|
604
|
+
};
|