@koishi-ce/koishi 1.0.4 → 1.0.7
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/lib/cli/index.mjs +6 -6
- package/lib/worker/index.mjs +20 -0
- package/package.json +5 -5
- package/src/cli/start.ts +60 -37
- package/src/worker/daemon.ts +9 -3
- package/src/worker/index.ts +68 -13
- package/src/worker/logger.ts +22 -6
package/lib/cli/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { cac } from "cac";
|
|
3
3
|
import { hyphenate, isInteger } from "@koishi-ce/utils";
|
|
4
|
-
import
|
|
4
|
+
import pc from "picocolors";
|
|
5
5
|
//#region package.json
|
|
6
6
|
var package_default = {
|
|
7
7
|
name: "@koishi-ce/koishi",
|
|
8
8
|
type: "module",
|
|
9
9
|
description: "Cross-Platform Chatbot Framework Made with Love",
|
|
10
|
-
version: "1.0.
|
|
10
|
+
version: "1.0.7",
|
|
11
11
|
main: "lib/index.mjs",
|
|
12
12
|
module: "lib/index.mjs",
|
|
13
13
|
types: "lib/index.d.ts",
|
|
@@ -59,7 +59,7 @@ var package_default = {
|
|
|
59
59
|
"@koishi-ce/plugin-server": "workspace:*",
|
|
60
60
|
"@koishi-ce/utils": "workspace:*",
|
|
61
61
|
"cac": "^7.0.0",
|
|
62
|
-
"
|
|
62
|
+
"picocolors": "^1.1.1"
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
//#endregion
|
|
@@ -105,14 +105,14 @@ function createWorker(options) {
|
|
|
105
105
|
if (message.type === "start") {
|
|
106
106
|
config = message.body;
|
|
107
107
|
timer = config.heartbeatTimeout ? setTimeout(() => {
|
|
108
|
-
console.log(
|
|
108
|
+
console.log(pc.red("daemon: heartbeat timeout"));
|
|
109
109
|
child.kill("SIGKILL");
|
|
110
110
|
}, config.heartbeatTimeout) : void 0;
|
|
111
111
|
} else if (message.type === "shared") process.env["KOISHI_SHARED"] = message.body;
|
|
112
112
|
else if (message.type === "heartbeat" && timer && config.heartbeatTimeout) {
|
|
113
113
|
clearTimeout(timer);
|
|
114
114
|
timer = setTimeout(() => {
|
|
115
|
-
console.log(
|
|
115
|
+
console.log(pc.red("daemon: heartbeat timeout"));
|
|
116
116
|
child.kill("SIGKILL");
|
|
117
117
|
}, config.heartbeatTimeout);
|
|
118
118
|
}
|
|
@@ -163,7 +163,7 @@ function start_default(cli) {
|
|
|
163
163
|
cli.command("start [file]", "start a koishi bot").alias("run").allowUnknownOptions().option("--debug [namespace]", "specify debug namespace").option("--log-level [level]", "specify log level (default: 2)").option("--log-time [format]", "show timestamp in logs").action((file, options) => {
|
|
164
164
|
const { logLevel, debug, logTime, ...rest } = options;
|
|
165
165
|
if (logLevel !== void 0 && (!isInteger(logLevel) || logLevel < 0)) {
|
|
166
|
-
console.warn(`${
|
|
166
|
+
console.warn(`${pc.red("error")} log level should be a positive integer.`);
|
|
167
167
|
process.exit(1);
|
|
168
168
|
}
|
|
169
169
|
setEnvArg("KOISHI_LOG_TIME", logTime);
|
package/lib/worker/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { t as __exportAll } from "../rolldown-runtime-BBjsoOtd.mjs";
|
|
|
2
2
|
import { t as Loader } from "../src-BnUZO8R-.mjs";
|
|
3
3
|
import { Context, Logger, Schema, Time, defineProperty } from "@koishi-ce/core";
|
|
4
4
|
import Loader$1, { resolvePlugin } from "@koishi-ce/loader";
|
|
5
|
+
import pc from "picocolors";
|
|
5
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import net from "node:net";
|
|
7
8
|
import { dirname, resolve } from "node:path";
|
|
@@ -210,12 +211,31 @@ async function checkPorts(plugins, baseDir) {
|
|
|
210
211
|
process.exit(1);
|
|
211
212
|
}
|
|
212
213
|
}
|
|
214
|
+
/** 启动字符画(ANSI Shadow 字体的 KOISHI CE) */
|
|
215
|
+
const banner = [
|
|
216
|
+
"██╗ ██╗ ██████╗ ██╗███████╗██╗ ██╗██╗ ██████╗███████╗",
|
|
217
|
+
"██║ ██╔╝██╔═══██╗██║██╔════╝██║ ██║██║ ██╔════╝██╔════╝",
|
|
218
|
+
"█████╔╝ ██║ ██║██║███████╗███████║██║█████╗██║ █████╗",
|
|
219
|
+
"██╔═██╗ ██║ ██║██║╚════██║██╔══██║██║╚════╝██║ ██╔══╝",
|
|
220
|
+
"██║ ██╗╚██████╔╝██║███████║██║ ██║██║ ╚██████╗███████╗",
|
|
221
|
+
"╚═╝ ╚═╝ ╚═════╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════╝"
|
|
222
|
+
].join("\n");
|
|
223
|
+
/**
|
|
224
|
+
* 输出启动字符画。直走 stdout 而非 Logger 通道(多行消息会被日志器
|
|
225
|
+
* 统一缩进、首行混入时间戳前缀,破坏字符画对齐),因此以 isTTY 收敛:
|
|
226
|
+
* 测试与重定向/CI 等非交互环境自动跳过,不产生输出噪音。
|
|
227
|
+
*/
|
|
228
|
+
function printBanner() {
|
|
229
|
+
if (!process.stdout.isTTY) return;
|
|
230
|
+
console.log(pc.cyan(banner));
|
|
231
|
+
}
|
|
213
232
|
/** 应用启动主流程 */
|
|
214
233
|
async function start() {
|
|
215
234
|
const loader = new Loader$1();
|
|
216
235
|
await loader.init(process.env["KOISHI_CONFIG_FILE"]);
|
|
217
236
|
const config = await loader.readConfig(true);
|
|
218
237
|
prepare(config.logger);
|
|
238
|
+
printBanner();
|
|
219
239
|
await checkPorts(config.plugins ?? {}, loader.baseDir);
|
|
220
240
|
if (config.timezoneOffset !== void 0) Time.setTimezoneOffset(config.timezoneOffset);
|
|
221
241
|
if (config.stackTraceLimit !== void 0) Error.stackTraceLimit = config.stackTraceLimit;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@koishi-ce/koishi",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Cross-Platform Chatbot Framework Made with Love",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.7",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"module": "lib/index.mjs",
|
|
8
8
|
"types": "lib/index.d.ts",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@koishi-ce/core": "^1.0.
|
|
65
|
-
"@koishi-ce/loader": "^1.0.
|
|
64
|
+
"@koishi-ce/core": "^1.0.1",
|
|
65
|
+
"@koishi-ce/loader": "^1.0.6",
|
|
66
66
|
"@koishi-ce/plugin-http": "^1.0.0",
|
|
67
67
|
"@koishi-ce/plugin-proxy-agent": "^1.0.0",
|
|
68
|
-
"@koishi-ce/plugin-server": "^1.0.
|
|
68
|
+
"@koishi-ce/plugin-server": "^1.0.1",
|
|
69
69
|
"@koishi-ce/utils": "^1.0.0",
|
|
70
70
|
"cac": "^7.0.0",
|
|
71
|
-
"
|
|
71
|
+
"picocolors": "^1.1.1"
|
|
72
72
|
}
|
|
73
73
|
}
|
package/src/cli/start.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { hyphenate, isInteger } from "@koishi-ce/utils";
|
|
14
14
|
import type { CAC } from "cac";
|
|
15
|
-
import
|
|
15
|
+
import pc from "picocolors";
|
|
16
16
|
import type { Config } from "../worker/daemon.ts";
|
|
17
17
|
|
|
18
18
|
/** 子进程通过 IPC 通道发来的消息类型(并集) */
|
|
@@ -20,7 +20,9 @@ type Event = Event.Start | Event.Env | Event.Heartbeat;
|
|
|
20
20
|
/** 单个命令行选项的取值形态 */
|
|
21
21
|
type WorkerOption = boolean | string | string[] | undefined;
|
|
22
22
|
/** 传递给 worker 的完整选项表;`--` 键对应 cac 收集的透传参数 */
|
|
23
|
-
type WorkerOptions = Record<string, WorkerOption> & {
|
|
23
|
+
type WorkerOptions = Record<string, WorkerOption> & {
|
|
24
|
+
"--"?: string[];
|
|
25
|
+
};
|
|
24
26
|
|
|
25
27
|
/** 子进程 IPC 消息的具体结构定义 */
|
|
26
28
|
namespace Event {
|
|
@@ -54,7 +56,9 @@ process.env["KOISHI_SHARED"] = JSON.stringify({
|
|
|
54
56
|
* 单字母键转为 `-x`,其余转为 `--kebab-case`。
|
|
55
57
|
*/
|
|
56
58
|
function toArg(key: string) {
|
|
57
|
-
return key.length === 1
|
|
59
|
+
return key.length === 1
|
|
60
|
+
? `-${key}`
|
|
61
|
+
: `--${hyphenate(key)}`;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
@@ -67,21 +71,23 @@ function toArg(key: string) {
|
|
|
67
71
|
*/
|
|
68
72
|
function createWorker(options: WorkerOptions) {
|
|
69
73
|
// 将选项对象还原为 Node/Bun 可识别的 execArgv 数组
|
|
70
|
-
const execArgv = Object.entries(options).flatMap<string>(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const execArgv = Object.entries(options).flatMap<string>(
|
|
75
|
+
([key, value]) => {
|
|
76
|
+
if (key === "--") return [];
|
|
77
|
+
key = toArg(key);
|
|
78
|
+
if (value === true) {
|
|
79
|
+
return [key];
|
|
80
|
+
} else if (value === false) {
|
|
81
|
+
// 布尔假值转为 --no-xxx 形式
|
|
82
|
+
return [`--no-${key.slice(2)}`];
|
|
83
|
+
} else if (Array.isArray(value)) {
|
|
84
|
+
// 数组值展开为多组 "键 值" 对
|
|
85
|
+
return value.flatMap((value) => [key, value]);
|
|
86
|
+
} else {
|
|
87
|
+
return [key, String(value)];
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
85
91
|
execArgv.push(...(options["--"] ?? []));
|
|
86
92
|
|
|
87
93
|
// worker 入口为构建产物 index.mjs,而非本 TS 源文件
|
|
@@ -96,7 +102,9 @@ function createWorker(options: WorkerOptions) {
|
|
|
96
102
|
timer = config.heartbeatTimeout
|
|
97
103
|
? setTimeout(() => {
|
|
98
104
|
// eslint-disable-next-line no-console
|
|
99
|
-
console.log(
|
|
105
|
+
console.log(
|
|
106
|
+
pc.red("daemon: heartbeat timeout"),
|
|
107
|
+
);
|
|
100
108
|
child.kill("SIGKILL");
|
|
101
109
|
}, config.heartbeatTimeout)
|
|
102
110
|
: undefined;
|
|
@@ -110,25 +118,28 @@ function createWorker(options: WorkerOptions) {
|
|
|
110
118
|
clearTimeout(timer);
|
|
111
119
|
timer = setTimeout(() => {
|
|
112
120
|
// eslint-disable-next-line no-console
|
|
113
|
-
console.log(
|
|
121
|
+
console.log(pc.red("daemon: heartbeat timeout"));
|
|
114
122
|
child.kill("SIGKILL");
|
|
115
123
|
}, config.heartbeatTimeout);
|
|
116
124
|
}
|
|
117
125
|
};
|
|
118
126
|
|
|
119
|
-
child = Bun.spawn(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
child = Bun.spawn(
|
|
128
|
+
[process.execPath, worker, ...execArgv],
|
|
129
|
+
{
|
|
130
|
+
ipc: handleMessage,
|
|
131
|
+
// Bun.spawn 的 stdio 默认为 ignore,须显式继承输出通道,
|
|
132
|
+
// 否则 worker 的全部日志都会被丢弃
|
|
133
|
+
stdout: "inherit",
|
|
134
|
+
stderr: "inherit",
|
|
135
|
+
onExit: (_, code, signal) => {
|
|
136
|
+
if (shouldExit(code, signal)) {
|
|
137
|
+
process.exit(code ?? 1);
|
|
138
|
+
}
|
|
139
|
+
createWorker(options);
|
|
140
|
+
},
|
|
130
141
|
},
|
|
131
|
-
|
|
142
|
+
);
|
|
132
143
|
|
|
133
144
|
/**
|
|
134
145
|
* 判断子进程退出后父进程应跟随退出还是重新拉起。
|
|
@@ -136,7 +147,10 @@ function createWorker(options: WorkerOptions) {
|
|
|
136
147
|
* 退出码约定:0 表示正常退出;51 表示请求重启(如 loader 的整进程重载);
|
|
137
148
|
* 52 表示请求退出;收到信号一律视为外部终止,跟随退出。
|
|
138
149
|
*/
|
|
139
|
-
function shouldExit(
|
|
150
|
+
function shouldExit(
|
|
151
|
+
code: number | null,
|
|
152
|
+
signal: number | null,
|
|
153
|
+
) {
|
|
140
154
|
// 尚未收到 start 消息即退出,说明启动失败
|
|
141
155
|
if (!config) return true;
|
|
142
156
|
|
|
@@ -176,15 +190,24 @@ export default function (cli: CAC) {
|
|
|
176
190
|
.command("start [file]", "start a koishi bot")
|
|
177
191
|
.alias("run")
|
|
178
192
|
.allowUnknownOptions()
|
|
179
|
-
.option(
|
|
180
|
-
|
|
193
|
+
.option(
|
|
194
|
+
"--debug [namespace]",
|
|
195
|
+
"specify debug namespace",
|
|
196
|
+
)
|
|
197
|
+
.option(
|
|
198
|
+
"--log-level [level]",
|
|
199
|
+
"specify log level (default: 2)",
|
|
200
|
+
)
|
|
181
201
|
.option("--log-time [format]", "show timestamp in logs")
|
|
182
202
|
.action((file, options) => {
|
|
183
203
|
const { logLevel, debug, logTime, ...rest } = options;
|
|
184
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
logLevel !== undefined &&
|
|
206
|
+
(!isInteger(logLevel) || logLevel < 0)
|
|
207
|
+
) {
|
|
185
208
|
// eslint-disable-next-line no-console
|
|
186
209
|
console.warn(
|
|
187
|
-
`${
|
|
210
|
+
`${pc.red("error")} log level should be a positive integer.`,
|
|
188
211
|
);
|
|
189
212
|
process.exit(1);
|
|
190
213
|
}
|
package/src/worker/daemon.ts
CHANGED
|
@@ -25,8 +25,12 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
25
25
|
autoRestart: Schema.boolean()
|
|
26
26
|
.description("在运行时崩溃自动重启。")
|
|
27
27
|
.default(true),
|
|
28
|
-
heartbeatInterval: Schema.number()
|
|
29
|
-
|
|
28
|
+
heartbeatInterval: Schema.number()
|
|
29
|
+
.description("心跳发送间隔。")
|
|
30
|
+
.default(0),
|
|
31
|
+
heartbeatTimeout: Schema.number()
|
|
32
|
+
.description("心跳超时时间。")
|
|
33
|
+
.default(0),
|
|
30
34
|
})
|
|
31
35
|
.description("守护设置")
|
|
32
36
|
.hidden();
|
|
@@ -50,7 +54,9 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
50
54
|
process.send?.({ type: "exit" });
|
|
51
55
|
}
|
|
52
56
|
ctx.logger("app").info(`terminated by ${signal}`);
|
|
53
|
-
ctx
|
|
57
|
+
ctx
|
|
58
|
+
.parallel("exit", signal)
|
|
59
|
+
.finally(() => process.exit());
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
ctx.on("ready", () => {
|
package/src/worker/index.ts
CHANGED
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import net from "node:net";
|
|
16
16
|
import { dirname, resolve } from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Context,
|
|
19
|
+
type Dict,
|
|
20
|
+
Logger,
|
|
21
|
+
Schema,
|
|
22
|
+
Time,
|
|
23
|
+
} from "@koishi-ce/core";
|
|
18
24
|
import Loader, { resolvePlugin } from "@koishi-ce/loader";
|
|
25
|
+
import pc from "picocolors";
|
|
19
26
|
import * as daemon from "./daemon.ts";
|
|
20
27
|
import * as logger from "./logger.ts";
|
|
21
28
|
|
|
@@ -87,14 +94,19 @@ interface ServerPort {
|
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/** 从模块入口路径向上查找最近的 package.json,返回其 name 字段 */
|
|
90
|
-
function locatePackageName(
|
|
97
|
+
function locatePackageName(
|
|
98
|
+
filename: string,
|
|
99
|
+
): string | undefined {
|
|
91
100
|
let dir = dirname(filename);
|
|
92
101
|
while (true) {
|
|
93
102
|
const file = resolve(dir, "package.json");
|
|
94
103
|
if (existsSync(file)) {
|
|
95
104
|
try {
|
|
96
|
-
const manifest = JSON.parse(
|
|
97
|
-
|
|
105
|
+
const manifest = JSON.parse(
|
|
106
|
+
readFileSync(file, "utf8"),
|
|
107
|
+
);
|
|
108
|
+
if (typeof manifest?.name === "string")
|
|
109
|
+
return manifest.name;
|
|
98
110
|
} catch {}
|
|
99
111
|
}
|
|
100
112
|
const parent = dirname(dir);
|
|
@@ -107,9 +119,17 @@ function locatePackageName(filename: string): string | undefined {
|
|
|
107
119
|
* 遍历插件配置表(含 group 嵌套),收集服务器插件声明的端口区间。
|
|
108
120
|
* 引用键格式与 loader 一致:首个冒号前为插件名;`$` 开头为元属性。
|
|
109
121
|
*/
|
|
110
|
-
function collectServerPorts(
|
|
122
|
+
function collectServerPorts(
|
|
123
|
+
plugins: Dict,
|
|
124
|
+
baseDir: string,
|
|
125
|
+
out: ServerPort[],
|
|
126
|
+
) {
|
|
111
127
|
for (const [key, source] of Object.entries(plugins)) {
|
|
112
|
-
if (
|
|
128
|
+
if (
|
|
129
|
+
key.startsWith("$") ||
|
|
130
|
+
source === null ||
|
|
131
|
+
typeof source !== "object"
|
|
132
|
+
) {
|
|
113
133
|
continue;
|
|
114
134
|
}
|
|
115
135
|
const [name = ""] = key.split(":", 1);
|
|
@@ -118,14 +138,21 @@ function collectServerPorts(plugins: Dict, baseDir: string, out: ServerPort[]) {
|
|
|
118
138
|
continue;
|
|
119
139
|
}
|
|
120
140
|
try {
|
|
121
|
-
const pkgName = locatePackageName(
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
const pkgName = locatePackageName(
|
|
142
|
+
resolvePlugin(name, baseDir),
|
|
143
|
+
);
|
|
144
|
+
if (!pkgName || !serverPackages.has(pkgName))
|
|
145
|
+
continue;
|
|
146
|
+
const { host, port, maxPort } = source as Record<
|
|
147
|
+
string,
|
|
148
|
+
unknown
|
|
149
|
+
>;
|
|
124
150
|
if (typeof port !== "number") continue;
|
|
125
151
|
out.push({
|
|
126
152
|
host: typeof host === "string" ? host : "127.0.0.1",
|
|
127
153
|
port,
|
|
128
|
-
maxPort:
|
|
154
|
+
maxPort:
|
|
155
|
+
typeof maxPort === "number" ? maxPort : port,
|
|
129
156
|
});
|
|
130
157
|
} catch {}
|
|
131
158
|
}
|
|
@@ -136,7 +163,9 @@ function probePort(port: number, host: string) {
|
|
|
136
163
|
return new Promise<boolean>((promiseResolve) => {
|
|
137
164
|
const server = net.createServer();
|
|
138
165
|
server.once("error", () => promiseResolve(false));
|
|
139
|
-
server.once("listening", () =>
|
|
166
|
+
server.once("listening", () =>
|
|
167
|
+
server.close(() => promiseResolve(true)),
|
|
168
|
+
);
|
|
140
169
|
server.listen(port, host);
|
|
141
170
|
});
|
|
142
171
|
}
|
|
@@ -152,14 +181,19 @@ async function checkPorts(plugins: Dict, baseDir: string) {
|
|
|
152
181
|
collectServerPorts(plugins, baseDir, ports);
|
|
153
182
|
for (const { host, port, maxPort } of ports) {
|
|
154
183
|
let available = false;
|
|
155
|
-
for (
|
|
184
|
+
for (
|
|
185
|
+
let current = port;
|
|
186
|
+
current <= maxPort;
|
|
187
|
+
current++
|
|
188
|
+
) {
|
|
156
189
|
if (await probePort(current, host)) {
|
|
157
190
|
available = true;
|
|
158
191
|
break;
|
|
159
192
|
}
|
|
160
193
|
}
|
|
161
194
|
if (available) continue;
|
|
162
|
-
const range =
|
|
195
|
+
const range =
|
|
196
|
+
port === maxPort ? `${port}` : `${port}-${maxPort}`;
|
|
163
197
|
new Logger("app").error(
|
|
164
198
|
`端口 ${range} 已被占用(可能已有 Koishi 实例在运行),启动中止`,
|
|
165
199
|
);
|
|
@@ -167,12 +201,33 @@ async function checkPorts(plugins: Dict, baseDir: string) {
|
|
|
167
201
|
}
|
|
168
202
|
}
|
|
169
203
|
|
|
204
|
+
/** 启动字符画(ANSI Shadow 字体的 KOISHI CE) */
|
|
205
|
+
const banner = [
|
|
206
|
+
"██╗ ██╗ ██████╗ ██╗███████╗██╗ ██╗██╗ ██████╗███████╗",
|
|
207
|
+
"██║ ██╔╝██╔═══██╗██║██╔════╝██║ ██║██║ ██╔════╝██╔════╝",
|
|
208
|
+
"█████╔╝ ██║ ██║██║███████╗███████║██║█████╗██║ █████╗",
|
|
209
|
+
"██╔═██╗ ██║ ██║██║╚════██║██╔══██║██║╚════╝██║ ██╔══╝",
|
|
210
|
+
"██║ ██╗╚██████╔╝██║███████║██║ ██║██║ ╚██████╗███████╗",
|
|
211
|
+
"╚═╝ ╚═╝ ╚═════╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════╝",
|
|
212
|
+
].join("\n");
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 输出启动字符画。直走 stdout 而非 Logger 通道(多行消息会被日志器
|
|
216
|
+
* 统一缩进、首行混入时间戳前缀,破坏字符画对齐),因此以 isTTY 收敛:
|
|
217
|
+
* 测试与重定向/CI 等非交互环境自动跳过,不产生输出噪音。
|
|
218
|
+
*/
|
|
219
|
+
function printBanner() {
|
|
220
|
+
if (!process.stdout.isTTY) return;
|
|
221
|
+
console.log(pc.cyan(banner));
|
|
222
|
+
}
|
|
223
|
+
|
|
170
224
|
/** 应用启动主流程 */
|
|
171
225
|
async function start() {
|
|
172
226
|
const loader = new Loader();
|
|
173
227
|
await loader.init(process.env["KOISHI_CONFIG_FILE"]);
|
|
174
228
|
const config = await loader.readConfig(true);
|
|
175
229
|
logger.prepare(config.logger);
|
|
230
|
+
printBanner();
|
|
176
231
|
|
|
177
232
|
// 端口预检须在日志配置生效后、应用创建前执行
|
|
178
233
|
await checkPorts(config.plugins ?? {}, loader.baseDir);
|
package/src/worker/logger.ts
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* 完成全局 Logger 的等级、时间格式等设定。CLI 传入的环境变量优先级高于配置文件。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Context,
|
|
14
|
+
defineProperty,
|
|
15
|
+
Logger,
|
|
16
|
+
Schema,
|
|
17
|
+
} from "@koishi-ce/core";
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* 配置文件形态的日志等级表。
|
|
@@ -33,11 +38,15 @@ function normalizeLevels(
|
|
|
33
38
|
config: LogLevelConfig,
|
|
34
39
|
base: number,
|
|
35
40
|
): Logger.LevelConfig {
|
|
36
|
-
const result: Logger.LevelConfig = {
|
|
41
|
+
const result: Logger.LevelConfig = {
|
|
42
|
+
base: config.base ?? base,
|
|
43
|
+
};
|
|
37
44
|
for (const [name, level] of Object.entries(config)) {
|
|
38
45
|
if (name === "base") continue;
|
|
39
46
|
result[name] =
|
|
40
|
-
typeof level === "number"
|
|
47
|
+
typeof level === "number"
|
|
48
|
+
? level
|
|
49
|
+
: normalizeLevels(level, result.base);
|
|
41
50
|
}
|
|
42
51
|
return result;
|
|
43
52
|
}
|
|
@@ -54,7 +63,9 @@ export interface Config {
|
|
|
54
63
|
|
|
55
64
|
export const Config: Schema<Config> = Schema.object({
|
|
56
65
|
levels: Schema.any().description("默认的日志输出等级。"),
|
|
57
|
-
showDiff: Schema.boolean().description(
|
|
66
|
+
showDiff: Schema.boolean().description(
|
|
67
|
+
"标注相邻两次日志输出的时间差。",
|
|
68
|
+
),
|
|
58
69
|
showTime: Schema.union([Boolean, String])
|
|
59
70
|
.default(true)
|
|
60
71
|
.description("输出日志所使用的时间格式。"),
|
|
@@ -101,7 +112,10 @@ export function prepare(config: Config = {}) {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/** 递归为所有子命名空间补全 base 等级(未显式设置时继承父级) */
|
|
104
|
-
function ensureBaseLevel(
|
|
115
|
+
function ensureBaseLevel(
|
|
116
|
+
config: Logger.LevelConfig,
|
|
117
|
+
base: number,
|
|
118
|
+
) {
|
|
105
119
|
config.base ??= base;
|
|
106
120
|
Object.values(config).forEach((value) => {
|
|
107
121
|
if (typeof value !== "object") return;
|
|
@@ -113,7 +127,9 @@ export function prepare(config: Config = {}) {
|
|
|
113
127
|
|
|
114
128
|
// KOISHI_DEBUG 指定的各个命名空间一律开启 DEBUG 级输出
|
|
115
129
|
if (process.env["KOISHI_DEBUG"]) {
|
|
116
|
-
for (const name of process.env["KOISHI_DEBUG"].split(
|
|
130
|
+
for (const name of process.env["KOISHI_DEBUG"].split(
|
|
131
|
+
",",
|
|
132
|
+
)) {
|
|
117
133
|
new Logger(name).level = Logger.DEBUG;
|
|
118
134
|
}
|
|
119
135
|
}
|