@zhin.js/core 1.0.17 → 1.0.19
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/CHANGELOG.md +22 -0
- package/REFACTORING_COMPLETE.md +178 -0
- package/REFACTORING_STATUS.md +263 -0
- package/lib/adapter.d.ts +44 -19
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +81 -50
- package/lib/adapter.js.map +1 -1
- package/lib/bot.d.ts +7 -12
- package/lib/bot.d.ts.map +1 -1
- package/lib/built/adapter-process.d.ts +36 -0
- package/lib/built/adapter-process.d.ts.map +1 -0
- package/lib/built/adapter-process.js +77 -0
- package/lib/built/adapter-process.js.map +1 -0
- package/lib/built/command.d.ts +46 -0
- package/lib/built/command.d.ts.map +1 -0
- package/lib/built/command.js +54 -0
- package/lib/built/command.js.map +1 -0
- package/lib/built/component.d.ts +42 -0
- package/lib/built/component.d.ts.map +1 -0
- package/lib/built/component.js +66 -0
- package/lib/built/component.js.map +1 -0
- package/lib/built/config.d.ts +31 -0
- package/lib/built/config.d.ts.map +1 -0
- package/lib/built/config.js +141 -0
- package/lib/built/config.js.map +1 -0
- package/lib/built/cron.d.ts +53 -0
- package/lib/built/cron.d.ts.map +1 -0
- package/lib/built/cron.js +79 -0
- package/lib/built/cron.js.map +1 -0
- package/lib/built/database.d.ts +17 -0
- package/lib/built/database.d.ts.map +1 -0
- package/lib/built/database.js +28 -0
- package/lib/built/database.js.map +1 -0
- package/lib/{permissions.d.ts → built/permission.d.ts} +5 -10
- package/lib/built/permission.d.ts.map +1 -0
- package/lib/{permissions.js → built/permission.js} +11 -10
- package/lib/built/permission.js.map +1 -0
- package/lib/command.d.ts +7 -7
- package/lib/command.d.ts.map +1 -1
- package/lib/command.js +5 -15
- package/lib/command.js.map +1 -1
- package/lib/component.d.ts.map +1 -1
- package/lib/component.js.map +1 -1
- package/lib/cron.d.ts +1 -0
- package/lib/cron.d.ts.map +1 -1
- package/lib/cron.js +2 -0
- package/lib/cron.js.map +1 -1
- package/lib/index.d.ts +11 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +14 -4
- package/lib/index.js.map +1 -1
- package/lib/jsx-runtime.d.ts +2 -2
- package/lib/message.d.ts +2 -2
- package/lib/message.d.ts.map +1 -1
- package/lib/plugin.d.ts +164 -51
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +521 -150
- package/lib/plugin.js.map +1 -1
- package/lib/prompt.d.ts +1 -1
- package/lib/prompt.d.ts.map +1 -1
- package/lib/prompt.js +2 -1
- package/lib/prompt.js.map +1 -1
- package/lib/types.d.ts +33 -33
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts +16 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +166 -66
- package/lib/utils.js.map +1 -1
- package/package.json +19 -9
- package/src/adapter.ts +131 -80
- package/src/bot.ts +8 -13
- package/src/built/adapter-process.ts +77 -0
- package/src/built/command.ts +102 -0
- package/src/built/component.ts +111 -0
- package/src/built/config.ts +126 -0
- package/src/built/cron.ts +140 -0
- package/src/built/database.ts +38 -0
- package/src/{permissions.ts → built/permission.ts} +9 -12
- package/src/command.ts +11 -20
- package/src/component.ts +0 -1
- package/src/cron.ts +2 -0
- package/src/index.ts +15 -5
- package/src/message.ts +2 -2
- package/src/plugin.ts +671 -202
- package/src/prompt.ts +4 -3
- package/src/types.ts +41 -35
- package/src/utils.ts +418 -296
- package/test/minimal-bot.ts +31 -0
- package/test/stress-test.ts +123 -0
- package/tests/command.test.ts +47 -44
- package/ASYNC-JSX-SUPPORT.md +0 -173
- package/lib/app.d.ts +0 -191
- package/lib/app.d.ts.map +0 -1
- package/lib/app.js +0 -604
- package/lib/app.js.map +0 -1
- package/lib/config.d.ts +0 -54
- package/lib/config.d.ts.map +0 -1
- package/lib/config.js +0 -308
- package/lib/config.js.map +0 -1
- package/lib/log-transport.d.ts +0 -37
- package/lib/log-transport.d.ts.map +0 -1
- package/lib/log-transport.js +0 -136
- package/lib/log-transport.js.map +0 -1
- package/lib/permissions.d.ts.map +0 -1
- package/lib/permissions.js.map +0 -1
- package/src/app.ts +0 -772
- package/src/config.ts +0 -397
- package/src/log-transport.ts +0 -163
- package/tests/app.test.ts +0 -265
- package/tests/permissions.test.ts +0 -358
- package/tests/plugin.test.ts +0 -234
- package/tests/prompt.test.ts +0 -223
package/src/config.ts
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
5
|
-
import { parse as parseToml } from "toml";
|
|
6
|
-
import { config as loadDotenv } from "dotenv";
|
|
7
|
-
import { Schema } from '@zhin.js/hmr';
|
|
8
|
-
import type { AppConfig, DefineConfig } from "./types.js";
|
|
9
|
-
import { LogLevel } from "@zhin.js/logger";
|
|
10
|
-
import { EventEmitter } from "node:events";
|
|
11
|
-
|
|
12
|
-
export interface ConfigOptions {
|
|
13
|
-
configPath?: string;
|
|
14
|
-
envPath?: string;
|
|
15
|
-
envOverride?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 支持的配置文件格式
|
|
20
|
-
*/
|
|
21
|
-
export type ConfigFormat = "json" | "yaml" | "yml" | "toml" | "js" | "ts";
|
|
22
|
-
export class Config<T extends object = object> extends EventEmitter {
|
|
23
|
-
#filepath: string;
|
|
24
|
-
#schema: Schema<Partial<T>>;
|
|
25
|
-
#data: T;
|
|
26
|
-
constructor(filename: string, schema: Schema<Partial<T>>, initialConfig: T) {
|
|
27
|
-
super();
|
|
28
|
-
const ext = path.extname(filename).toLowerCase();
|
|
29
|
-
if (!Config.supportedExtensions.includes(ext)) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`不支持的配置文件格式: ${ext},支持的格式有 ${Config.supportedExtensions.join(
|
|
32
|
-
"/"
|
|
33
|
-
)}`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
const fullpath = path.resolve(process.cwd(), filename);
|
|
37
|
-
if (!fs.existsSync(fullpath)) {
|
|
38
|
-
Config.save(fullpath, initialConfig);
|
|
39
|
-
}
|
|
40
|
-
this.#filepath = fullpath;
|
|
41
|
-
this.#schema = schema;
|
|
42
|
-
this.#data = initialConfig;
|
|
43
|
-
this.#load(this.#data);
|
|
44
|
-
}
|
|
45
|
-
get filepath() {
|
|
46
|
-
return this.#filepath;
|
|
47
|
-
}
|
|
48
|
-
get config() {
|
|
49
|
-
return Config.proxyResult(this.#data,()=>{Config.save(this.#filepath, this.#data);});
|
|
50
|
-
}
|
|
51
|
-
set config(newConfig: T) {
|
|
52
|
-
const beforeConfig = this.#data;
|
|
53
|
-
this.#data = newConfig;
|
|
54
|
-
this.emit("change", beforeConfig, this.#data);
|
|
55
|
-
Config.save(this.#filepath, this.#data);
|
|
56
|
-
}
|
|
57
|
-
#load(before: T) {
|
|
58
|
-
// 加载配置文件
|
|
59
|
-
Config.load(this.#filepath, this.#schema)
|
|
60
|
-
.then((data) => {
|
|
61
|
-
this.#data = data as T;
|
|
62
|
-
if (JSON.stringify(before) !== JSON.stringify(this.#data)) {
|
|
63
|
-
this.emit("change", before, this.#data);
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
reload() {
|
|
68
|
-
this.#load(this.#data);
|
|
69
|
-
}
|
|
70
|
-
get<K extends Config.Paths<T>>(key: K): Config.Value<T, K> {
|
|
71
|
-
const lastKey = /\.?([^.]*)$/.exec(key)?.[1];
|
|
72
|
-
if (!lastKey) throw new Error(`无法获取配置项: ${key}`);
|
|
73
|
-
const obj = Config.getNestedObject(this.#data, key);
|
|
74
|
-
const result= Reflect.get(obj, lastKey);
|
|
75
|
-
return Config.proxyResult(result,()=>{Config.save(this.#filepath, this.#data);}) as Config.Value<T, K>;
|
|
76
|
-
}
|
|
77
|
-
set<K extends Config.Paths<T>>(key: K, value: Config.Value<T, K>): void {
|
|
78
|
-
const prop = /\.?([^.]*)$/.exec(key)?.[1];
|
|
79
|
-
if (!prop) throw new Error(`无法设置配置项: ${key}`);
|
|
80
|
-
const obj = Config.getNestedObject(this.#data, key);
|
|
81
|
-
const beforeConfig = this.#data;
|
|
82
|
-
Reflect.set(obj, prop, value);
|
|
83
|
-
Config.save(this.#filepath, this.#data);
|
|
84
|
-
this.emit("change", beforeConfig, this.#data);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
export namespace Config {
|
|
88
|
-
export const supportedExtensions = [
|
|
89
|
-
".json",
|
|
90
|
-
".yaml",
|
|
91
|
-
".yml",
|
|
92
|
-
".toml",
|
|
93
|
-
".js",
|
|
94
|
-
".ts",
|
|
95
|
-
];
|
|
96
|
-
export function proxyResult<T>(result:T,onSet:(value:T)=>void):T{
|
|
97
|
-
if((typeof result!=='object' && typeof result!=='string') || result===null) return result;
|
|
98
|
-
if(typeof result==='string') return replaceEnvVars(result) as T;
|
|
99
|
-
return new Proxy(result as (T & object),{
|
|
100
|
-
get(target,prop,receiver){
|
|
101
|
-
const value=Reflect.get(target,prop,receiver)
|
|
102
|
-
return proxyResult(value as T,onSet);
|
|
103
|
-
},
|
|
104
|
-
set(target,prop,value,receiver){
|
|
105
|
-
const result=Reflect.set(target,prop,value,receiver)
|
|
106
|
-
onSet(value);
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* 配置文件元数据,用于记录原始格式信息
|
|
113
|
-
*/
|
|
114
|
-
interface ConfigMetadata {
|
|
115
|
-
usesFunction: boolean; // 是否使用函数导出
|
|
116
|
-
originalContent?: string; // 原始文件内容
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 存储配置文件的元数据
|
|
120
|
-
const configMetadataMap = new Map<string, ConfigMetadata>();
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 智能保存配置文件,保留原有格式
|
|
124
|
-
*/
|
|
125
|
-
export function save<T extends object>(filePath: string, config: T): void {
|
|
126
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
127
|
-
const metadata = configMetadataMap.get(filePath);
|
|
128
|
-
let content: string;
|
|
129
|
-
|
|
130
|
-
switch (ext) {
|
|
131
|
-
case ".json":
|
|
132
|
-
content = JSON.stringify(config, null, 2);
|
|
133
|
-
break;
|
|
134
|
-
case ".yaml":
|
|
135
|
-
case ".yml":
|
|
136
|
-
content = stringifyYaml(config, { indent: 2 });
|
|
137
|
-
break;
|
|
138
|
-
case ".toml":
|
|
139
|
-
throw new Error("暂不支持保存 TOML 格式的配置文件");
|
|
140
|
-
case ".js":
|
|
141
|
-
case ".ts":
|
|
142
|
-
// 智能保存 JS/TS 配置
|
|
143
|
-
content = saveJsConfig(filePath, config, metadata);
|
|
144
|
-
break;
|
|
145
|
-
default:
|
|
146
|
-
throw new Error(`不支持的配置文件格式: ${ext}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* 智能保存 JS/TS 配置文件,保留函数格式和环境变量
|
|
154
|
-
*/
|
|
155
|
-
function saveJsConfig<T extends object>(
|
|
156
|
-
filePath: string,
|
|
157
|
-
config: T,
|
|
158
|
-
metadata?: ConfigMetadata
|
|
159
|
-
): string {
|
|
160
|
-
const ext = path.extname(filePath);
|
|
161
|
-
const usesFunction = metadata?.usesFunction ?? false;
|
|
162
|
-
const originalContent = metadata?.originalContent;
|
|
163
|
-
|
|
164
|
-
if (usesFunction && originalContent) {
|
|
165
|
-
// 如果使用函数导出,尝试保留原有格式并更新配置
|
|
166
|
-
return updateJsConfigWithFunction(originalContent, config, ext === ".ts");
|
|
167
|
-
} else {
|
|
168
|
-
// 简单对象导出
|
|
169
|
-
const typeAnnotation = ext === ".ts" ? ": DefineConfig<AppConfig>" : "";
|
|
170
|
-
return `export default ${JSON.stringify(
|
|
171
|
-
config,
|
|
172
|
-
null,
|
|
173
|
-
2
|
|
174
|
-
)}${typeAnnotation};`;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* 更新使用函数导出的配置文件
|
|
180
|
-
* 保留环境变量访问模式和原有代码结构
|
|
181
|
-
*/
|
|
182
|
-
function updateJsConfigWithFunction<T extends object>(
|
|
183
|
-
originalContent: string,
|
|
184
|
-
newConfig: T,
|
|
185
|
-
isTypeScript: boolean
|
|
186
|
-
): string {
|
|
187
|
-
// 简单的字符串替换策略:
|
|
188
|
-
// 1. 找到 return 语句中的对象
|
|
189
|
-
// 2. 替换对象内容,但保留环境变量访问
|
|
190
|
-
|
|
191
|
-
const returnMatch = originalContent.match(
|
|
192
|
-
/return\s*({[\s\S]*?})\s*[;}]?\s*$/m
|
|
193
|
-
);
|
|
194
|
-
if (!returnMatch) {
|
|
195
|
-
// 如果找不到 return 语句,回退到简单格式
|
|
196
|
-
const typeAnnotation = isTypeScript ? ": DefineConfig<AppConfig>" : "";
|
|
197
|
-
return `export default ${JSON.stringify(
|
|
198
|
-
newConfig,
|
|
199
|
-
null,
|
|
200
|
-
2
|
|
201
|
-
)}${typeAnnotation};`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 保留原有的函数签名和代码结构
|
|
205
|
-
// 只更新 return 语句中的对象
|
|
206
|
-
const configString = formatConfigObject(newConfig, 2);
|
|
207
|
-
const beforeReturn = originalContent.substring(0, returnMatch.index! + 6); // "return"
|
|
208
|
-
|
|
209
|
-
return `${beforeReturn.trim()} ${configString}\n});`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* 格式化配置对象为字符串,保留环境变量访问模式
|
|
214
|
-
*/
|
|
215
|
-
function formatConfigObject(obj: any, indent: number = 0): string {
|
|
216
|
-
const spaces = " ".repeat(indent);
|
|
217
|
-
const nextSpaces = " ".repeat(indent + 2);
|
|
218
|
-
|
|
219
|
-
if (obj === null || obj === undefined) {
|
|
220
|
-
return String(obj);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (typeof obj === "string") {
|
|
224
|
-
for(const [key,value] of Object.entries(process.env)){
|
|
225
|
-
if(obj===value && value!=='zhin') return `process.env.${key}`;
|
|
226
|
-
}
|
|
227
|
-
return JSON.stringify(obj);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (typeof obj === "number" || typeof obj === "boolean") {
|
|
231
|
-
return String(obj);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (Array.isArray(obj)) {
|
|
235
|
-
if (obj.length === 0) return "[]";
|
|
236
|
-
const items = obj
|
|
237
|
-
.map((item) => `${nextSpaces}${formatConfigObject(item, indent + 2)}`)
|
|
238
|
-
.join(",\n");
|
|
239
|
-
return `[\n${items}\n${spaces}]`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (typeof obj === "object") {
|
|
243
|
-
const entries = Object.entries(obj);
|
|
244
|
-
if (entries.length === 0) return "{}";
|
|
245
|
-
|
|
246
|
-
const props = entries
|
|
247
|
-
.map(([key, value]) => {
|
|
248
|
-
const needsQuotes = /[^a-zA-Z0-9_$]/.test(key);
|
|
249
|
-
const keyStr = needsQuotes ? `'${key}'` : key;
|
|
250
|
-
return `${nextSpaces}${keyStr}: ${formatConfigObject(
|
|
251
|
-
value,
|
|
252
|
-
indent + 2
|
|
253
|
-
)}`;
|
|
254
|
-
})
|
|
255
|
-
.join(",\n");
|
|
256
|
-
|
|
257
|
-
return `{\n${props}\n${spaces}}`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return JSON.stringify(obj);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* 记录配置文件的元数据
|
|
265
|
-
*/
|
|
266
|
-
export function setMetadata(
|
|
267
|
-
filePath: string,
|
|
268
|
-
metadata: ConfigMetadata
|
|
269
|
-
): void {
|
|
270
|
-
configMetadataMap.set(filePath, metadata);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* 获取配置文件的元数据
|
|
275
|
-
*/
|
|
276
|
-
export function getMetadata(filePath: string): ConfigMetadata | undefined {
|
|
277
|
-
return configMetadataMap.get(filePath);
|
|
278
|
-
}
|
|
279
|
-
export async function load<T extends object>(
|
|
280
|
-
filePath: string,
|
|
281
|
-
schema: Schema<T>
|
|
282
|
-
): Promise<T> {
|
|
283
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
284
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
285
|
-
let rawConfig: any;
|
|
286
|
-
let usesFunction = false;
|
|
287
|
-
|
|
288
|
-
switch (ext) {
|
|
289
|
-
case ".json":
|
|
290
|
-
rawConfig = JSON.parse(content);
|
|
291
|
-
break;
|
|
292
|
-
case ".yaml":
|
|
293
|
-
case ".yml":
|
|
294
|
-
rawConfig = parseYaml(content);
|
|
295
|
-
break;
|
|
296
|
-
case ".toml":
|
|
297
|
-
rawConfig = parseToml(content);
|
|
298
|
-
break;
|
|
299
|
-
case ".js":
|
|
300
|
-
case ".ts":
|
|
301
|
-
// 使用动态导入加载 JS/TS 模块
|
|
302
|
-
const fileUrl = pathToFileURL(path.resolve(filePath)).href;
|
|
303
|
-
const module = await import(`${fileUrl}?t=${Date.now()}`);
|
|
304
|
-
// 支持 ES 模块的 default 导出和 CommonJS 模块
|
|
305
|
-
rawConfig = module.default || module;
|
|
306
|
-
if (typeof rawConfig === "function") {
|
|
307
|
-
usesFunction = true;
|
|
308
|
-
rawConfig = await rawConfig(
|
|
309
|
-
(process.env || {}) as Record<string, string>
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
// 记录元数据
|
|
313
|
-
setMetadata(filePath, { usesFunction, originalContent: content });
|
|
314
|
-
break;
|
|
315
|
-
default:
|
|
316
|
-
throw new Error(`不支持的配置文件格式: ${ext}`);
|
|
317
|
-
}
|
|
318
|
-
return schema(rawConfig);
|
|
319
|
-
}
|
|
320
|
-
export type Value<
|
|
321
|
-
T,
|
|
322
|
-
K extends Paths<T>
|
|
323
|
-
> = K extends `${infer Key}.${infer Rest}`
|
|
324
|
-
? Key extends keyof T
|
|
325
|
-
? Rest extends Paths<T[Key]>
|
|
326
|
-
? Value<T[Key], Rest>
|
|
327
|
-
: never
|
|
328
|
-
: never
|
|
329
|
-
: K extends keyof T
|
|
330
|
-
? T[K]
|
|
331
|
-
: never;
|
|
332
|
-
|
|
333
|
-
export type Paths<
|
|
334
|
-
T,
|
|
335
|
-
Prefix extends string = "",
|
|
336
|
-
Depth extends any[] = []
|
|
337
|
-
> = Depth["length"] extends 10
|
|
338
|
-
? never
|
|
339
|
-
: {
|
|
340
|
-
[K in keyof T]: T[K] extends object
|
|
341
|
-
?
|
|
342
|
-
| `${Prefix}${K & string}`
|
|
343
|
-
| Paths<T[K], `${Prefix}${K & string}.`, [...Depth, 1]>
|
|
344
|
-
: `${Prefix}${K & string}`;
|
|
345
|
-
}[keyof T];
|
|
346
|
-
export function getNestedObject<T>(obj: T, path: string): any {
|
|
347
|
-
const parts = path.split(".");
|
|
348
|
-
let current: any = obj;
|
|
349
|
-
|
|
350
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
351
|
-
const part = parts[i];
|
|
352
|
-
if (!(part in current)) {
|
|
353
|
-
current[part] = {};
|
|
354
|
-
}
|
|
355
|
-
current = current[part];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return current;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* 替换字符串中的环境变量
|
|
364
|
-
*/
|
|
365
|
-
function replaceEnvVars(str: string): string{
|
|
366
|
-
if (typeof str !== 'string') return str;
|
|
367
|
-
return (str as string).replace(/^\$\{([^}]+)\}$/, (match, content) => {
|
|
368
|
-
// 解析环境变量名和默认值
|
|
369
|
-
const colonIndex = content.indexOf(":-");
|
|
370
|
-
let envName: string;
|
|
371
|
-
let defaultValue: string | undefined;
|
|
372
|
-
|
|
373
|
-
if (colonIndex !== -1) {
|
|
374
|
-
// 格式: VAR_NAME:-default_value
|
|
375
|
-
envName = content.slice(0, colonIndex);
|
|
376
|
-
defaultValue = content.slice(colonIndex + 2);
|
|
377
|
-
} else {
|
|
378
|
-
// 格式: VAR_NAME
|
|
379
|
-
envName = content;
|
|
380
|
-
defaultValue = undefined;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const envValue = process.env[envName];
|
|
384
|
-
|
|
385
|
-
if (envValue !== undefined) {
|
|
386
|
-
return envValue;
|
|
387
|
-
} else if (defaultValue !== undefined) {
|
|
388
|
-
return defaultValue;
|
|
389
|
-
} else {
|
|
390
|
-
return match;
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function defineConfig<T extends DefineConfig<AppConfig>>(config: T): T {
|
|
396
|
-
return config;
|
|
397
|
-
}
|
package/src/log-transport.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { LogTransport } from '@zhin.js/logger'
|
|
2
|
-
import { App } from './app.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 数据库日志传输器
|
|
6
|
-
* 将日志存储到数据库,并自动清理旧日志
|
|
7
|
-
*/
|
|
8
|
-
export class DatabaseLogTransport implements LogTransport {
|
|
9
|
-
private app: App
|
|
10
|
-
private stripAnsiRegex = /\x1b\[[0-9;]*m/g
|
|
11
|
-
private cleanupTimer?: NodeJS.Timeout
|
|
12
|
-
private maxDays: number
|
|
13
|
-
private maxRecords: number
|
|
14
|
-
private cleanupInterval: number
|
|
15
|
-
|
|
16
|
-
constructor(app: App) {
|
|
17
|
-
this.app = app
|
|
18
|
-
|
|
19
|
-
// 从配置读取日志清理策略
|
|
20
|
-
const logConfig = app['config']?.log || {}
|
|
21
|
-
this.maxDays = logConfig.maxDays || 7 // 默认保留 7 天
|
|
22
|
-
this.maxRecords = logConfig.maxRecords || 10000 // 默认最多 10000 条
|
|
23
|
-
this.cleanupInterval = logConfig.cleanupInterval || 24 // 默认 24 小时清理一次
|
|
24
|
-
|
|
25
|
-
// 启动定时清理
|
|
26
|
-
this.startCleanup()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 启动定时清理任务
|
|
31
|
-
*/
|
|
32
|
-
private startCleanup(): void {
|
|
33
|
-
// 立即执行一次清理
|
|
34
|
-
this.cleanupOldLogs().catch(err => {
|
|
35
|
-
this.app.logger.error('[DatabaseLogTransport] Initial cleanup failed:', err.message)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// 设置定时任务
|
|
39
|
-
this.cleanupTimer = setInterval(() => {
|
|
40
|
-
this.cleanupOldLogs().catch(err => {
|
|
41
|
-
this.app.logger.error('[DatabaseLogTransport] Scheduled cleanup failed:', err.message)
|
|
42
|
-
})
|
|
43
|
-
}, this.cleanupInterval * 60 * 60 * 1000) // 转换为毫秒
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 清理旧日志
|
|
48
|
-
*/
|
|
49
|
-
private async cleanupOldLogs(): Promise<void> {
|
|
50
|
-
if (!this.app.database) {
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const LogModel = this.app.database.model('SystemLog')
|
|
56
|
-
if (!LogModel) {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 1. 按时间清理:删除超过 maxDays 天的日志
|
|
61
|
-
const cutoffDate = new Date()
|
|
62
|
-
cutoffDate.setDate(cutoffDate.getDate() - this.maxDays)
|
|
63
|
-
|
|
64
|
-
const deletedByDate = await LogModel
|
|
65
|
-
.delete({ timestamp: { $lt: cutoffDate } })
|
|
66
|
-
|
|
67
|
-
// 2. 按数量清理:如果日志总数超过 maxRecords,删除最旧的
|
|
68
|
-
const total = await LogModel.select()
|
|
69
|
-
const totalCount = total.length
|
|
70
|
-
|
|
71
|
-
if (totalCount > this.maxRecords) {
|
|
72
|
-
const excessCount = totalCount - this.maxRecords
|
|
73
|
-
|
|
74
|
-
// 查找最旧的 excessCount 条日志的 ID
|
|
75
|
-
const oldestLogs = await LogModel
|
|
76
|
-
.select('id','timestamp')
|
|
77
|
-
.orderBy('timestamp', 'ASC')
|
|
78
|
-
.limit(excessCount)
|
|
79
|
-
|
|
80
|
-
const idsToDelete = oldestLogs.map((log: any) => log.id)
|
|
81
|
-
|
|
82
|
-
if (idsToDelete.length > 0) {
|
|
83
|
-
await LogModel
|
|
84
|
-
.delete({ id: { $in: idsToDelete } })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
this.app.logger.info(
|
|
89
|
-
`[DatabaseLogTransport] Log cleanup completed. ` +
|
|
90
|
-
`Deleted ${deletedByDate || 0} logs older than ${this.maxDays} days. ` +
|
|
91
|
-
`Current total: ${Math.max(0, totalCount - (deletedByDate || 0))} logs.`
|
|
92
|
-
)
|
|
93
|
-
} catch (error) {
|
|
94
|
-
// 静默处理错误
|
|
95
|
-
this.app.logger.debug('[DatabaseLogTransport] Cleanup error:', (error as Error).message,(error as Error).stack)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 停止清理任务
|
|
101
|
-
*/
|
|
102
|
-
public stopCleanup(): void {
|
|
103
|
-
if (this.cleanupTimer) {
|
|
104
|
-
clearInterval(this.cleanupTimer)
|
|
105
|
-
this.cleanupTimer = undefined
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 移除 ANSI 颜色代码
|
|
111
|
-
*/
|
|
112
|
-
private stripAnsi(str: string): string {
|
|
113
|
-
return str.replace(this.stripAnsiRegex, '')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
write(message: string): void {
|
|
117
|
-
// 移除 ANSI 颜色代码
|
|
118
|
-
const cleanMessage = this.stripAnsi(message)
|
|
119
|
-
|
|
120
|
-
// 解析日志消息
|
|
121
|
-
// 格式: [09-08 04:07:55.852] [INFO] [MyApp]: message
|
|
122
|
-
const logRegex = /\[[\d-]+ [\d:.]+\] \[(\w+)\] \[([^\]]+)\]: ([\s\S]+)/
|
|
123
|
-
const match = cleanMessage.match(logRegex)
|
|
124
|
-
|
|
125
|
-
if (match) {
|
|
126
|
-
const [, level, name, msg] = match
|
|
127
|
-
const source = name.split(':')[0] // 取第一部分作为 source
|
|
128
|
-
|
|
129
|
-
// 异步存储到数据库,不阻塞日志输出
|
|
130
|
-
this.saveToDatabase(level.toLowerCase(), name, msg.trim(), source).catch(err => {
|
|
131
|
-
// 避免日志存储失败导致应用崩溃
|
|
132
|
-
console.error('[DatabaseLogTransport] Failed to save log:', err.message)
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 保存日志到数据库
|
|
139
|
-
*/
|
|
140
|
-
private async saveToDatabase(level: string, name: string, message: string, source: string): Promise<void> {
|
|
141
|
-
if (!this.app.database) {
|
|
142
|
-
return // 没有数据库则跳过
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const LogModel = this.app.database.model('SystemLog')
|
|
147
|
-
if (!LogModel) {
|
|
148
|
-
return // 模型不存在则跳过
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
await LogModel.create({
|
|
152
|
-
level,
|
|
153
|
-
name,
|
|
154
|
-
message,
|
|
155
|
-
source,
|
|
156
|
-
timestamp: new Date()
|
|
157
|
-
})
|
|
158
|
-
} catch (error) {
|
|
159
|
-
// 静默处理错误,避免干扰主流程
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|