@zhin.js/http 1.0.4 → 1.0.6
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/app/index.ts +732 -0
- package/lib/index.d.ts +5 -5
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +237 -105
- package/lib/index.js.map +1 -1
- package/lib/router.d.ts.map +1 -1
- package/lib/router.js.map +1 -1
- package/package.json +7 -3
- package/CHANGELOG.md +0 -32
- package/src/index.ts +0 -556
- package/tsconfig.json +0 -23
- /package/{src → app}/router.ts +0 -0
package/app/index.ts
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppConfig,
|
|
3
|
+
register,
|
|
4
|
+
defineSchema,
|
|
5
|
+
Schema,
|
|
6
|
+
usePlugin,
|
|
7
|
+
useApp,
|
|
8
|
+
useDatabase,
|
|
9
|
+
} from "@zhin.js/core";
|
|
10
|
+
import { createServer, Server } from "http";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import Koa from "koa";
|
|
13
|
+
import auth from "koa-basic-auth";
|
|
14
|
+
import KoaBodyParser from "koa-bodyparser";
|
|
15
|
+
import { Router } from "./router.js";
|
|
16
|
+
import * as process from "process";
|
|
17
|
+
import { config } from "node:process";
|
|
18
|
+
|
|
19
|
+
export * from "./router.js";
|
|
20
|
+
|
|
21
|
+
declare module "@zhin.js/types" {
|
|
22
|
+
interface GlobalContext {
|
|
23
|
+
koa: Koa;
|
|
24
|
+
router: Router;
|
|
25
|
+
server: Server;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const plugin = usePlugin();
|
|
29
|
+
const schema = defineSchema(
|
|
30
|
+
Schema.object({
|
|
31
|
+
port: Schema.number("port").default(8086).description("HTTP 服务端口"),
|
|
32
|
+
username: Schema.string("username").description(
|
|
33
|
+
"HTTP 基本认证用户名, 默认为当前系统用户名"
|
|
34
|
+
),
|
|
35
|
+
password: Schema.string("password").description(
|
|
36
|
+
"HTTP 基本认证密码, 默认为随机生成的6位字符串"
|
|
37
|
+
),
|
|
38
|
+
base: Schema.string("base")
|
|
39
|
+
.default("/api")
|
|
40
|
+
.description("HTTP 路由前缀, 默认为 /api"),
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
const koa = new Koa();
|
|
44
|
+
const server = createServer(koa.callback());
|
|
45
|
+
const router = new Router(server, { prefix: process.env.routerPrefix || "" });
|
|
46
|
+
// 获取当前计算机登录用户名
|
|
47
|
+
const getCurrentUsername = () => {
|
|
48
|
+
try {
|
|
49
|
+
return os.userInfo().username;
|
|
50
|
+
} catch {
|
|
51
|
+
return "admin"; // 如果获取失败,使用默认用户名
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 生成6位随机密码
|
|
56
|
+
const generateRandomPassword = () => {
|
|
57
|
+
const chars =
|
|
58
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
59
|
+
let result = "";
|
|
60
|
+
for (let i = 0; i < 6; i++) {
|
|
61
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
port = 8086,
|
|
68
|
+
username = getCurrentUsername(),
|
|
69
|
+
password = generateRandomPassword(),
|
|
70
|
+
base = "/api",
|
|
71
|
+
} = schema(plugin.config, "http");
|
|
72
|
+
const app = useApp();
|
|
73
|
+
|
|
74
|
+
koa.use(
|
|
75
|
+
auth({
|
|
76
|
+
name: username,
|
|
77
|
+
pass: password,
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// API 路由
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
// 系统状态 API
|
|
85
|
+
router.get(`${base}/system/status`, async (ctx) => {
|
|
86
|
+
try {
|
|
87
|
+
ctx.body = {
|
|
88
|
+
success: true,
|
|
89
|
+
data: {
|
|
90
|
+
uptime: process.uptime(),
|
|
91
|
+
memory: process.memoryUsage(),
|
|
92
|
+
cpu: process.cpuUsage(),
|
|
93
|
+
platform: process.platform,
|
|
94
|
+
nodeVersion: process.version,
|
|
95
|
+
pid: process.pid,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
ctx.status = 500;
|
|
101
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 健康检查 API
|
|
106
|
+
router.get(`${base}/health`, async (ctx) => {
|
|
107
|
+
ctx.body = {
|
|
108
|
+
success: true,
|
|
109
|
+
status: "ok",
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 统计信息 API
|
|
115
|
+
router.get(`${base}/stats`, async (ctx) => {
|
|
116
|
+
try {
|
|
117
|
+
// 统计插件数量
|
|
118
|
+
const pluginCount = app.dependencyList.length;
|
|
119
|
+
const activePluginCount = app.dependencyList.filter(
|
|
120
|
+
(dep) => (dep as any).mounted
|
|
121
|
+
).length;
|
|
122
|
+
|
|
123
|
+
// 统计机器人数量
|
|
124
|
+
let botCount = 0;
|
|
125
|
+
let onlineBotCount = 0;
|
|
126
|
+
for (const context of app.contextList) {
|
|
127
|
+
const adapter = app.getContext(context.name);
|
|
128
|
+
if (adapter && typeof adapter === "object" && "bots" in adapter) {
|
|
129
|
+
const adapterBots = (adapter as any).bots;
|
|
130
|
+
if (adapterBots && adapterBots instanceof Map) {
|
|
131
|
+
botCount += adapterBots.size;
|
|
132
|
+
for (const bot of adapterBots.values()) {
|
|
133
|
+
if (bot.$connected) onlineBotCount++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 统计命令和组件
|
|
140
|
+
let commandCount = 0;
|
|
141
|
+
let componentCount = 0;
|
|
142
|
+
for (const dep of app.dependencyList) {
|
|
143
|
+
commandCount += dep.commands.length;
|
|
144
|
+
componentCount += dep.components.size;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
ctx.body = {
|
|
148
|
+
success: true,
|
|
149
|
+
data: {
|
|
150
|
+
plugins: { total: pluginCount, active: activePluginCount },
|
|
151
|
+
bots: { total: botCount, online: onlineBotCount },
|
|
152
|
+
commands: commandCount,
|
|
153
|
+
components: componentCount,
|
|
154
|
+
uptime: process.uptime(),
|
|
155
|
+
memory: process.memoryUsage().heapUsed / 1024 / 1024, // MB
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
ctx.status = 500;
|
|
160
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 插件管理 API
|
|
165
|
+
router.get(`${base}/plugins`, async (ctx) => {
|
|
166
|
+
try {
|
|
167
|
+
// 获取详细的插件数据
|
|
168
|
+
const plugins = app.dependencyList.map((dep) => {
|
|
169
|
+
return {
|
|
170
|
+
name: dep.name,
|
|
171
|
+
status: (dep as any).mounted ? "active" : "inactive",
|
|
172
|
+
commandCount: dep.commands.length,
|
|
173
|
+
componentCount: dep.components.size,
|
|
174
|
+
middlewareCount: dep.middlewares.length,
|
|
175
|
+
contextCount: dep.contexts.size,
|
|
176
|
+
description: (dep as any).description || "无描述",
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
ctx.body = { success: true, data: plugins, total: plugins.length };
|
|
181
|
+
} catch (error) {
|
|
182
|
+
ctx.status = 500;
|
|
183
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 插件详情 API
|
|
188
|
+
router.get("/api/plugins/:name", async (ctx) => {
|
|
189
|
+
try {
|
|
190
|
+
const pluginName = ctx.params.name;
|
|
191
|
+
const plugin = app.dependencyList.find((dep) => dep.name === pluginName);
|
|
192
|
+
|
|
193
|
+
if (!plugin) {
|
|
194
|
+
ctx.status = 404;
|
|
195
|
+
ctx.body = { success: false, error: "插件不存在" };
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 获取命令详情
|
|
200
|
+
const commands = plugin.commands.map((cmd) => ({
|
|
201
|
+
name: cmd.pattern,
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
// 获取组件详情
|
|
205
|
+
const components = Array.from(plugin.components.entries()).map(
|
|
206
|
+
([name, comp]) => ({
|
|
207
|
+
name,
|
|
208
|
+
props: (comp as any).props || {},
|
|
209
|
+
type: typeof comp,
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// 获取中间件详情
|
|
214
|
+
const middlewares = plugin.middlewares.map((_, index) => ({
|
|
215
|
+
id: `middleware-${index}`,
|
|
216
|
+
type: "function",
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
// 获取上下文详情
|
|
220
|
+
const contexts = Array.from(plugin.contexts.entries()).map(
|
|
221
|
+
([name, ctx]) => ({
|
|
222
|
+
name,
|
|
223
|
+
description: (ctx as any).description || "无描述",
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// 获取定时任务详情
|
|
228
|
+
const crons = plugin.crons.map((cron, index) => ({
|
|
229
|
+
id: `cron-${index}`,
|
|
230
|
+
pattern: (cron as any).pattern || "unknown",
|
|
231
|
+
running: (cron as any).running || false,
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
// 获取数据模型详情
|
|
235
|
+
const definitions = Array.from(plugin.definitions.entries()).map(
|
|
236
|
+
([name, definition]) => ({
|
|
237
|
+
name,
|
|
238
|
+
fields: Object.keys(definition),
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
ctx.body = {
|
|
243
|
+
success: true,
|
|
244
|
+
data: {
|
|
245
|
+
name: plugin.name,
|
|
246
|
+
filename: plugin.filename,
|
|
247
|
+
status: (plugin as any).mounted ? "active" : "inactive",
|
|
248
|
+
description: (plugin as any).description || "无描述",
|
|
249
|
+
commands,
|
|
250
|
+
components,
|
|
251
|
+
middlewares,
|
|
252
|
+
contexts,
|
|
253
|
+
crons,
|
|
254
|
+
definitions,
|
|
255
|
+
statistics: {
|
|
256
|
+
commandCount: commands.length,
|
|
257
|
+
componentCount: components.length,
|
|
258
|
+
middlewareCount: middlewares.length,
|
|
259
|
+
contextCount: contexts.length,
|
|
260
|
+
cronCount: crons.length,
|
|
261
|
+
definitionCount: definitions.length,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
ctx.status = 500;
|
|
267
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 适配器和机器人状态 API
|
|
272
|
+
router.get(`${base}/bots`, async (ctx) => {
|
|
273
|
+
try {
|
|
274
|
+
const bots: any[] = [];
|
|
275
|
+
|
|
276
|
+
// 遍历所有上下文,查找适配器
|
|
277
|
+
for (const context of app.contextList) {
|
|
278
|
+
const adapter = app.getContext(context.name);
|
|
279
|
+
if (adapter && typeof adapter === "object" && "bots" in adapter) {
|
|
280
|
+
const adapterBots = (adapter as any).bots;
|
|
281
|
+
if (adapterBots && adapterBots instanceof Map) {
|
|
282
|
+
for (const [botName, bot] of adapterBots.entries()) {
|
|
283
|
+
bots.push({
|
|
284
|
+
name: botName,
|
|
285
|
+
adapter: context.name,
|
|
286
|
+
connected: bot.$connected || false,
|
|
287
|
+
status: bot.$connected ? "online" : "offline",
|
|
288
|
+
// 移除 config 字段以保护隐私
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
ctx.body = { success: true, data: bots, total: bots.length };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
ctx.status = 500;
|
|
298
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 框架配置信息 API
|
|
303
|
+
router.get(`${base}/config`, async (ctx) => {
|
|
304
|
+
try {
|
|
305
|
+
const config = app.getConfig();
|
|
306
|
+
|
|
307
|
+
ctx.body = { success: true, data: config };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
ctx.status = 500;
|
|
310
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// 获取所有插件的 Schema API
|
|
315
|
+
router.get(`${base}/schemas`, async (ctx) => {
|
|
316
|
+
try {
|
|
317
|
+
const schemas: Record<string, any> = {};
|
|
318
|
+
|
|
319
|
+
// 获取 App 的 Schema
|
|
320
|
+
const appSchema = app.schema.toJSON();
|
|
321
|
+
if (appSchema) {
|
|
322
|
+
schemas["app"] = appSchema;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 获取所有插件的 Schema
|
|
326
|
+
for (const plugin of app.dependencyList) {
|
|
327
|
+
const schema = plugin.schema.toJSON();
|
|
328
|
+
if (schema) {
|
|
329
|
+
schemas[plugin.name] = schema;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
ctx.body = {
|
|
334
|
+
success: true,
|
|
335
|
+
data: schemas,
|
|
336
|
+
total: Object.keys(schemas).length,
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
ctx.status = 500;
|
|
340
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 获取单个插件的 Schema API
|
|
345
|
+
router.get(`${base}/schemas/:name`, async (ctx) => {
|
|
346
|
+
try {
|
|
347
|
+
const { name } = ctx.params;
|
|
348
|
+
|
|
349
|
+
if (name === "app") {
|
|
350
|
+
const schema = app.schema?.toJSON();
|
|
351
|
+
if (!schema) {
|
|
352
|
+
ctx.status = 404;
|
|
353
|
+
ctx.body = { success: false, error: "App schema not found" };
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
ctx.body = { success: true, data: schema };
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const plugin = app.findPluginByName(name);
|
|
361
|
+
if (!plugin) {
|
|
362
|
+
ctx.status = 404;
|
|
363
|
+
ctx.body = { success: false, error: `Plugin ${name} not found` };
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const schema = plugin.schema.toJSON();
|
|
368
|
+
if (!schema) {
|
|
369
|
+
ctx.status = 404;
|
|
370
|
+
ctx.body = {
|
|
371
|
+
success: false,
|
|
372
|
+
error: `Schema for plugin ${name} not found`,
|
|
373
|
+
};
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
ctx.body = { success: true, data: schema };
|
|
378
|
+
} catch (error) {
|
|
379
|
+
ctx.status = 500;
|
|
380
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// 获取插件配置 API
|
|
385
|
+
router.get(`${base}/config/:name`, async (ctx) => {
|
|
386
|
+
try {
|
|
387
|
+
const { name } = ctx.params;
|
|
388
|
+
|
|
389
|
+
if (name === "app") {
|
|
390
|
+
const config = app.getConfig();
|
|
391
|
+
ctx.body = { success: true, data: config };
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const plugin = app.findPluginByName(name);
|
|
396
|
+
if (!plugin) {
|
|
397
|
+
ctx.status = 404;
|
|
398
|
+
ctx.body = { success: false, error: `Plugin ${name} not found` };
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const config = plugin.config;
|
|
403
|
+
ctx.body = { success: true, data: config };
|
|
404
|
+
} catch (error) {
|
|
405
|
+
ctx.status = 500;
|
|
406
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// 更新插件配置 API
|
|
411
|
+
router.post(`${base}/config/:name`, async (ctx) => {
|
|
412
|
+
try {
|
|
413
|
+
const { name } = ctx.params;
|
|
414
|
+
const newConfig = ctx.request.body;
|
|
415
|
+
|
|
416
|
+
if (name === "app") {
|
|
417
|
+
app.config = newConfig as AppConfig;
|
|
418
|
+
ctx.body = {
|
|
419
|
+
success: true,
|
|
420
|
+
message: "App configuration updated successfully",
|
|
421
|
+
data: app.getConfig(),
|
|
422
|
+
};
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const plugin = app.findPluginByName(name);
|
|
427
|
+
if (!plugin) {
|
|
428
|
+
ctx.status = 404;
|
|
429
|
+
ctx.body = { success: false, error: `Plugin ${name} not found` };
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
plugin.config = newConfig as Record<string, any>;
|
|
434
|
+
ctx.body = {
|
|
435
|
+
success: true,
|
|
436
|
+
message: `Plugin ${name} configuration updated successfully`,
|
|
437
|
+
data: plugin.config,
|
|
438
|
+
};
|
|
439
|
+
} catch (error) {
|
|
440
|
+
ctx.status = 500;
|
|
441
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// 消息发送 API
|
|
446
|
+
router.post(`${base}/message/send`, async (ctx) => {
|
|
447
|
+
try {
|
|
448
|
+
const body = ctx.request.body as any;
|
|
449
|
+
const { context, bot, id, type, content } = body;
|
|
450
|
+
|
|
451
|
+
if (!context || !bot || !id || !type || !content) {
|
|
452
|
+
ctx.status = 400;
|
|
453
|
+
ctx.body = {
|
|
454
|
+
success: false,
|
|
455
|
+
error: "Missing required fields: context, bot, id, type, content",
|
|
456
|
+
};
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 模拟发送消息(实际环境中会调用应用实例的sendMessage方法)
|
|
461
|
+
// console.log 已替换为注释
|
|
462
|
+
|
|
463
|
+
ctx.body = {
|
|
464
|
+
success: true,
|
|
465
|
+
message: "Message sent successfully",
|
|
466
|
+
data: {
|
|
467
|
+
context,
|
|
468
|
+
bot,
|
|
469
|
+
id,
|
|
470
|
+
type,
|
|
471
|
+
content,
|
|
472
|
+
timestamp: new Date().toISOString(),
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
} catch (error) {
|
|
476
|
+
ctx.status = 500;
|
|
477
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// 日志 API - 获取日志
|
|
482
|
+
router.get(`${base}/logs`, async (ctx) => {
|
|
483
|
+
try {
|
|
484
|
+
const database = useDatabase();
|
|
485
|
+
if (!database) {
|
|
486
|
+
ctx.status = 503;
|
|
487
|
+
ctx.body = { success: false, error: "Database not available" };
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 获取查询参数
|
|
492
|
+
const limit = parseInt(ctx.query.limit as string) || 100;
|
|
493
|
+
const level = ctx.query.level as string;
|
|
494
|
+
|
|
495
|
+
const LogModel = database.model("SystemLog");
|
|
496
|
+
if (!LogModel) {
|
|
497
|
+
ctx.status = 500;
|
|
498
|
+
ctx.body = { success: false, error: "SystemLog model not found" };
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 查询日志
|
|
503
|
+
let selection = LogModel.select();
|
|
504
|
+
|
|
505
|
+
// 按级别过滤
|
|
506
|
+
if (level && level !== "all") {
|
|
507
|
+
selection = selection.where({ level });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 按时间倒序,限制数量
|
|
511
|
+
const logs = await selection.orderBy("timestamp", "DESC").limit(limit);
|
|
512
|
+
|
|
513
|
+
// 格式化返回数据
|
|
514
|
+
const formattedLogs = logs.map((log: any) => ({
|
|
515
|
+
level: log.level,
|
|
516
|
+
name: log.name,
|
|
517
|
+
message: log.message,
|
|
518
|
+
source: log.source,
|
|
519
|
+
timestamp:
|
|
520
|
+
log.timestamp instanceof Date
|
|
521
|
+
? log.timestamp.toISOString()
|
|
522
|
+
: log.timestamp,
|
|
523
|
+
}));
|
|
524
|
+
|
|
525
|
+
ctx.body = {
|
|
526
|
+
success: true,
|
|
527
|
+
data: formattedLogs,
|
|
528
|
+
total: formattedLogs.length,
|
|
529
|
+
};
|
|
530
|
+
} catch (error) {
|
|
531
|
+
ctx.status = 500;
|
|
532
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// 日志 API - 清空日志
|
|
537
|
+
router.delete(`${base}/logs`, async (ctx) => {
|
|
538
|
+
try {
|
|
539
|
+
const database = useDatabase();
|
|
540
|
+
if (!database) {
|
|
541
|
+
ctx.status = 503;
|
|
542
|
+
ctx.body = { success: false, error: "Database not available" };
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const LogModel = database.model("SystemLog");
|
|
547
|
+
if (!LogModel) {
|
|
548
|
+
ctx.status = 500;
|
|
549
|
+
ctx.body = { success: false, error: "SystemLog model not found" };
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 删除所有日志
|
|
554
|
+
await (LogModel as any).delete({});
|
|
555
|
+
|
|
556
|
+
ctx.body = {
|
|
557
|
+
success: true,
|
|
558
|
+
message: "日志已清空",
|
|
559
|
+
};
|
|
560
|
+
} catch (error) {
|
|
561
|
+
ctx.status = 500;
|
|
562
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// 日志 API - 获取日志统计
|
|
567
|
+
router.get(`${base}/logs/stats`, async (ctx) => {
|
|
568
|
+
try {
|
|
569
|
+
const database = useDatabase();
|
|
570
|
+
if (!database) {
|
|
571
|
+
ctx.status = 503;
|
|
572
|
+
ctx.body = { success: false, error: "Database not available" };
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const LogModel = database.model("SystemLog");
|
|
577
|
+
if (!LogModel) {
|
|
578
|
+
ctx.status = 500;
|
|
579
|
+
ctx.body = { success: false, error: "SystemLog model not found" };
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 获取总日志数
|
|
584
|
+
const total = await LogModel.select();
|
|
585
|
+
const totalCount = total.length;
|
|
586
|
+
// 获取各级别日志数
|
|
587
|
+
const levels = ["info", "warn", "error"];
|
|
588
|
+
const levelCounts: Record<string, number> = {};
|
|
589
|
+
|
|
590
|
+
for (const level of levels) {
|
|
591
|
+
const count = await LogModel.select().where({ level });
|
|
592
|
+
levelCounts[level] = count.length;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 获取最旧日志时间
|
|
596
|
+
const oldestLog = await LogModel.select("timestamp")
|
|
597
|
+
.orderBy("timestamp", "ASC")
|
|
598
|
+
.limit(1);
|
|
599
|
+
|
|
600
|
+
const oldestTimestamp =
|
|
601
|
+
oldestLog.length > 0
|
|
602
|
+
? oldestLog[0].timestamp instanceof Date
|
|
603
|
+
? oldestLog[0].timestamp.toISOString()
|
|
604
|
+
: oldestLog[0].timestamp
|
|
605
|
+
: null;
|
|
606
|
+
|
|
607
|
+
ctx.body = {
|
|
608
|
+
success: true,
|
|
609
|
+
data: {
|
|
610
|
+
total: totalCount,
|
|
611
|
+
byLevel: levelCounts,
|
|
612
|
+
oldestTimestamp,
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
} catch (error) {
|
|
616
|
+
ctx.status = 500;
|
|
617
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// 日志 API - 清理旧日志(手动触发)
|
|
622
|
+
router.post(`${base}/logs/cleanup`, async (ctx) => {
|
|
623
|
+
try {
|
|
624
|
+
const database = useDatabase();
|
|
625
|
+
if (!database) {
|
|
626
|
+
ctx.status = 503;
|
|
627
|
+
ctx.body = { success: false, error: "Database not available" };
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const LogModel = database.model("SystemLog");
|
|
632
|
+
if (!LogModel) {
|
|
633
|
+
ctx.status = 500;
|
|
634
|
+
ctx.body = { success: false, error: "SystemLog model not found" };
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const { days, maxRecords } = (ctx.request.body as any) || {};
|
|
639
|
+
|
|
640
|
+
let deletedCount = 0;
|
|
641
|
+
|
|
642
|
+
// 按天数清理
|
|
643
|
+
if (days && typeof days === "number" && days > 0) {
|
|
644
|
+
const cutoffDate = new Date();
|
|
645
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
646
|
+
|
|
647
|
+
const deleted = await LogModel.delete({ timestamp: { $lt: cutoffDate } });
|
|
648
|
+
|
|
649
|
+
deletedCount += deleted || 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// 按数量清理
|
|
653
|
+
if (maxRecords && typeof maxRecords === "number" && maxRecords > 0) {
|
|
654
|
+
const totalCount = await (LogModel as any).select();
|
|
655
|
+
|
|
656
|
+
if (totalCount.length > maxRecords) {
|
|
657
|
+
const excessCount = totalCount - maxRecords;
|
|
658
|
+
|
|
659
|
+
const oldestLogs = await LogModel.select("id", "timestamp")
|
|
660
|
+
.orderBy("timestamp", "ASC")
|
|
661
|
+
.limit(excessCount);
|
|
662
|
+
|
|
663
|
+
const idsToDelete = oldestLogs.map((log: any) => log.id);
|
|
664
|
+
|
|
665
|
+
if (idsToDelete.length > 0) {
|
|
666
|
+
const deleted = await (LogModel as any)
|
|
667
|
+
.delete()
|
|
668
|
+
.where({ id: { $in: idsToDelete } });
|
|
669
|
+
|
|
670
|
+
deletedCount += deleted || 0;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
ctx.body = {
|
|
676
|
+
success: true,
|
|
677
|
+
message: `已清理 ${deletedCount} 条日志`,
|
|
678
|
+
deletedCount,
|
|
679
|
+
};
|
|
680
|
+
} catch (error) {
|
|
681
|
+
ctx.status = 500;
|
|
682
|
+
ctx.body = { success: false, error: (error as Error).message };
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// ============================================================================
|
|
687
|
+
// 上下文注册
|
|
688
|
+
// ============================================================================
|
|
689
|
+
|
|
690
|
+
register({
|
|
691
|
+
name: "server",
|
|
692
|
+
description: "http server",
|
|
693
|
+
mounted(p) {
|
|
694
|
+
return new Promise<Server>((resolve) => {
|
|
695
|
+
server.listen(
|
|
696
|
+
{
|
|
697
|
+
host: "0.0.0.0",
|
|
698
|
+
port: port,
|
|
699
|
+
},
|
|
700
|
+
() => {
|
|
701
|
+
const address = server.address();
|
|
702
|
+
if (!address) return;
|
|
703
|
+
const visitAddress =
|
|
704
|
+
typeof address === "string"
|
|
705
|
+
? address
|
|
706
|
+
: `${address.address}:${address.port}`;
|
|
707
|
+
p.logger.info(`server is running at http://${visitAddress}`);
|
|
708
|
+
p.logger.info("your username is:", username);
|
|
709
|
+
p.logger.info("your password is:", password);
|
|
710
|
+
resolve(server);
|
|
711
|
+
}
|
|
712
|
+
);
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
dispose(s) {
|
|
716
|
+
s.close();
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
register({
|
|
721
|
+
name: "koa",
|
|
722
|
+
description: "koa instance",
|
|
723
|
+
value: koa,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
register({
|
|
727
|
+
name: "router",
|
|
728
|
+
description: "koa router",
|
|
729
|
+
value: router,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
koa.use(KoaBodyParser()).use(router.routes()).use(router.allowedMethods());
|