@koishi-ce/koishi 1.0.0
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.d.ts +1 -0
- package/lib/cli/index.mjs +191 -0
- package/lib/index-CGzU40S-.d.ts +2 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.mjs +4 -0
- package/lib/rolldown-runtime-BBjsoOtd.mjs +27 -0
- package/lib/src-1HtO5Rx4.mjs +11 -0
- package/lib/worker/index.d.ts +59 -0
- package/lib/worker/index.mjs +149 -0
- package/package.json +74 -0
- package/readme.md +26 -0
- package/src/cli/index.ts +26 -0
- package/src/cli/start.ts +193 -0
- package/src/index.ts +12 -0
- package/src/worker/daemon.ts +64 -0
- package/src/worker/index.ts +92 -0
- package/src/worker/logger.ts +119 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { cac } from "cac";
|
|
3
|
+
import { hyphenate, isInteger } from "@koishi-ce/utils";
|
|
4
|
+
import kleur from "kleur";
|
|
5
|
+
//#region package.json
|
|
6
|
+
var package_default = {
|
|
7
|
+
name: "@koishi-ce/koishi",
|
|
8
|
+
type: "module",
|
|
9
|
+
description: "Cross-Platform Chatbot Framework Made with Love",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
main: "lib/index.mjs",
|
|
12
|
+
module: "lib/index.mjs",
|
|
13
|
+
types: "lib/index.d.ts",
|
|
14
|
+
bin: "lib/cli/index.mjs",
|
|
15
|
+
exports: {
|
|
16
|
+
".": {
|
|
17
|
+
"source": "./src/index.ts",
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"import": "./lib/index.mjs",
|
|
20
|
+
"default": "./lib/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./lib/cli": "./lib/cli/index.mjs",
|
|
23
|
+
"./lib/worker": "./lib/worker/index.mjs",
|
|
24
|
+
"./src/*": "./src/*",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
files: ["lib", "src"],
|
|
28
|
+
contributors: ["Shigma <shigma10826@gmail.com>", "Oppenheymu <oppenheymu@gmail.com>"],
|
|
29
|
+
license: "MIT",
|
|
30
|
+
repository: {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/Koishi-CE/koishi.git",
|
|
33
|
+
"directory": "packages/node/cli"
|
|
34
|
+
},
|
|
35
|
+
bugs: { "url": "https://github.com/Koishi-CE/koishi/issues" },
|
|
36
|
+
homepage: "https://koishi.chat",
|
|
37
|
+
keywords: [
|
|
38
|
+
"bot",
|
|
39
|
+
"chatbot",
|
|
40
|
+
"discord",
|
|
41
|
+
"telegram",
|
|
42
|
+
"cordis",
|
|
43
|
+
"framework"
|
|
44
|
+
],
|
|
45
|
+
cordis: {
|
|
46
|
+
"property": "koishi",
|
|
47
|
+
"ecosystem": { "pattern": [
|
|
48
|
+
"@koishi-ce/plugin-*",
|
|
49
|
+
"@koishijs/plugin-*",
|
|
50
|
+
"koishi-plugin-*"
|
|
51
|
+
] },
|
|
52
|
+
"service": { "implements": ["koishi"] }
|
|
53
|
+
},
|
|
54
|
+
dependencies: {
|
|
55
|
+
"@koishi-ce/core": "workspace:*",
|
|
56
|
+
"@koishi-ce/loader": "workspace:*",
|
|
57
|
+
"@koishi-ce/plugin-http": "workspace:*",
|
|
58
|
+
"@koishi-ce/plugin-proxy-agent": "workspace:*",
|
|
59
|
+
"@koishi-ce/plugin-server": "workspace:*",
|
|
60
|
+
"@koishi-ce/utils": "workspace:*",
|
|
61
|
+
"@satorijs/core": "^4.6.0",
|
|
62
|
+
"cac": "^7.0.0",
|
|
63
|
+
"kleur": "^4.1.5"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/cli/start.ts
|
|
68
|
+
/**
|
|
69
|
+
* `koishi start` 子命令实现:守护进程(daemon)侧的父进程逻辑。
|
|
70
|
+
*
|
|
71
|
+
* 通过 Bun.spawn 以 IPC 通道拉起 worker 子进程,并依据子进程发来的消息
|
|
72
|
+
* (启动配置、共享环境数据、心跳)决定是否重启。子进程以约定的退出码
|
|
73
|
+
* 表达意图:51 表示请求重启,52 表示请求退出,其余交由 autoRestart 判断。
|
|
74
|
+
*/
|
|
75
|
+
let child;
|
|
76
|
+
process.env["KOISHI_SHARED"] = JSON.stringify({ startTime: Date.now() });
|
|
77
|
+
/**
|
|
78
|
+
* 将 cac 解析出的选项键转换为命令行参数形式。
|
|
79
|
+
* 单字母键转为 `-x`,其余转为 `--kebab-case`。
|
|
80
|
+
*/
|
|
81
|
+
function toArg(key) {
|
|
82
|
+
return key.length === 1 ? `-${key}` : `--${hyphenate(key)}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 拉起 worker 子进程并接管其生命周期。
|
|
86
|
+
*
|
|
87
|
+
* @param options 命令行选项,会被还原成 execArgv 传给子进程
|
|
88
|
+
*
|
|
89
|
+
* 子进程退出时依据退出码/信号判断是重启还是跟随退出;
|
|
90
|
+
* 启动后若配置了心跳超时,则心跳超时视为进程假死,直接 SIGKILL。
|
|
91
|
+
*/
|
|
92
|
+
function createWorker(options) {
|
|
93
|
+
const execArgv = Object.entries(options).flatMap(([key, value]) => {
|
|
94
|
+
if (key === "--") return [];
|
|
95
|
+
key = toArg(key);
|
|
96
|
+
if (value === true) return [key];
|
|
97
|
+
else if (value === false) return [`--no-${key.slice(2)}`];
|
|
98
|
+
else if (Array.isArray(value)) return value.flatMap((value) => [key, value]);
|
|
99
|
+
else return [key, String(value)];
|
|
100
|
+
});
|
|
101
|
+
execArgv.push(...options["--"] ?? []);
|
|
102
|
+
const worker = `${import.meta.dir}/../worker/index.mjs`;
|
|
103
|
+
let config;
|
|
104
|
+
let timer;
|
|
105
|
+
const handleMessage = (message) => {
|
|
106
|
+
if (message.type === "start") {
|
|
107
|
+
config = message.body;
|
|
108
|
+
timer = config.heartbeatTimeout ? setTimeout(() => {
|
|
109
|
+
console.log(kleur.red("daemon: heartbeat timeout"));
|
|
110
|
+
child.kill("SIGKILL");
|
|
111
|
+
}, config.heartbeatTimeout) : void 0;
|
|
112
|
+
} else if (message.type === "shared") process.env["KOISHI_SHARED"] = message.body;
|
|
113
|
+
else if (message.type === "heartbeat" && timer && config.heartbeatTimeout) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
console.log(kleur.red("daemon: heartbeat timeout"));
|
|
117
|
+
child.kill("SIGKILL");
|
|
118
|
+
}, config.heartbeatTimeout);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
child = Bun.spawn([
|
|
122
|
+
process.execPath,
|
|
123
|
+
worker,
|
|
124
|
+
...execArgv
|
|
125
|
+
], {
|
|
126
|
+
ipc: handleMessage,
|
|
127
|
+
stdout: "inherit",
|
|
128
|
+
stderr: "inherit",
|
|
129
|
+
onExit: (_, code, signal) => {
|
|
130
|
+
if (shouldExit(code, signal)) process.exit(code ?? 1);
|
|
131
|
+
createWorker(options);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* 判断子进程退出后父进程应跟随退出还是重新拉起。
|
|
136
|
+
*
|
|
137
|
+
* 退出码约定:0 表示正常退出;51 表示请求重启(如 loader 的整进程重载);
|
|
138
|
+
* 52 表示请求退出;收到信号一律视为外部终止,跟随退出。
|
|
139
|
+
*/
|
|
140
|
+
function shouldExit(code, signal) {
|
|
141
|
+
if (!config) return true;
|
|
142
|
+
if (code === 0) return true;
|
|
143
|
+
if (signal !== null) return true;
|
|
144
|
+
if (code === 51) return false;
|
|
145
|
+
if (code === 52) return true;
|
|
146
|
+
return !config.autoRestart;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 将命令行选项写入环境变量。
|
|
151
|
+
* 值为 `true` 时写入空字符串(仅表示开关打开),其余写入字符串值。
|
|
152
|
+
*/
|
|
153
|
+
function setEnvArg(name, value) {
|
|
154
|
+
if (value === true) process.env[name] = "";
|
|
155
|
+
else if (value) process.env[name] = value;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 向 cac 实例注册 `koishi start`(别名 `koishi run`)子命令。
|
|
159
|
+
*
|
|
160
|
+
* 解析 --debug / --log-level / --log-time 等选项后,把它们写入对应的环境变量
|
|
161
|
+
* (由 worker 侧的 logger 读取),最后交给 createWorker 拉起守护子进程。
|
|
162
|
+
*/
|
|
163
|
+
function start_default(cli) {
|
|
164
|
+
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) => {
|
|
165
|
+
const { logLevel, debug, logTime, ...rest } = options;
|
|
166
|
+
if (logLevel !== void 0 && (!isInteger(logLevel) || logLevel < 0)) {
|
|
167
|
+
console.warn(`${kleur.red("error")} log level should be a positive integer.`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
setEnvArg("KOISHI_LOG_TIME", logTime);
|
|
171
|
+
process.env["KOISHI_LOG_LEVEL"] = logLevel || "";
|
|
172
|
+
process.env["KOISHI_DEBUG"] = debug || "";
|
|
173
|
+
process.env["KOISHI_CONFIG_FILE"] = file || "";
|
|
174
|
+
createWorker(rest);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/cli/index.ts
|
|
179
|
+
/**
|
|
180
|
+
* Koishi 命令行入口。
|
|
181
|
+
*
|
|
182
|
+
* 基于 cac 构建名为 `koishi` 的命令行程序,注册 `start`(别名 `run`)子命令后解析
|
|
183
|
+
* 进程参数。若用户未输入任何子命令(且未请求帮助),则自动打印帮助信息退出。
|
|
184
|
+
*/
|
|
185
|
+
const { version } = package_default;
|
|
186
|
+
const cli = cac("koishi").help().version(version);
|
|
187
|
+
start_default(cli);
|
|
188
|
+
const argv = cli.parse();
|
|
189
|
+
if (!cli.matchedCommand && !argv.options["help"]) cli.outputHelp();
|
|
190
|
+
//#endregion
|
|
191
|
+
export {};
|
package/lib/index.d.ts
ADDED
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
17
|
+
key = keys[i];
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
19
|
+
get: ((k) => from[k]).bind(null, key),
|
|
20
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
26
|
+
//#endregion
|
|
27
|
+
export { __reExport as n, __exportAll as t };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./rolldown-runtime-BBjsoOtd.mjs";
|
|
2
|
+
import Loader$1 from "@koishi-ce/loader";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@koishi-ce/koishi` 包入口。
|
|
6
|
+
*
|
|
7
|
+
* 本文件专为不使用 CLI 的用户准备:将 core 与 loader 两包的导出合并再分发,
|
|
8
|
+
* 并默认导出 NodeLoader 实现,使开发者可以直接以编程方式启动 Koishi。
|
|
9
|
+
*/
|
|
10
|
+
//#endregion
|
|
11
|
+
export { Loader$1 as t };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { t as Loader } from "../index-CGzU40S-.js";
|
|
2
|
+
import { Context, Dict, Schema } from "@koishi-ce/core";
|
|
3
|
+
export * from "@koishi-ce/loader";
|
|
4
|
+
export * from "@koishi-ce/core";
|
|
5
|
+
//#region src/worker/daemon.d.ts
|
|
6
|
+
/** 守护设置 */
|
|
7
|
+
interface Config$1 {
|
|
8
|
+
/** 运行时崩溃后是否自动重启 */
|
|
9
|
+
autoRestart?: boolean;
|
|
10
|
+
/** 心跳发送间隔(毫秒),0 表示不发送 */
|
|
11
|
+
heartbeatInterval?: number;
|
|
12
|
+
/** 心跳超时时间(毫秒),超时后父进程会强杀子进程,0 表示不检测 */
|
|
13
|
+
heartbeatTimeout?: number;
|
|
14
|
+
}
|
|
15
|
+
declare const Config$1: Schema<Config$1>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/worker/logger.d.ts
|
|
18
|
+
/**
|
|
19
|
+
* 配置文件形态的日志等级表。
|
|
20
|
+
* 与 @koishi-ce/utils 中的实现略有差异:此处不强制用户提供 base 等级,
|
|
21
|
+
* 缺省时回退到继承的默认值。
|
|
22
|
+
*/
|
|
23
|
+
interface LogLevelConfig {
|
|
24
|
+
/** 基础日志等级,未提供时沿用上级默认值 */
|
|
25
|
+
base?: number;
|
|
26
|
+
[k: string]: LogLevel;
|
|
27
|
+
}
|
|
28
|
+
/** 日志等级:既可以是单一数值,也可以是按名称分级的嵌套配置表 */
|
|
29
|
+
type LogLevel = number | LogLevelConfig;
|
|
30
|
+
/** 日志设置 */
|
|
31
|
+
interface Config {
|
|
32
|
+
/** 默认的日志输出等级 */
|
|
33
|
+
levels?: LogLevel;
|
|
34
|
+
/** 是否标注相邻两次日志输出的时间差 */
|
|
35
|
+
showDiff?: boolean;
|
|
36
|
+
/** 日志时间戳的输出格式,true 表示使用默认格式 */
|
|
37
|
+
showTime?: string | boolean;
|
|
38
|
+
}
|
|
39
|
+
declare const Config: Schema<Config>;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/worker/index.d.ts
|
|
42
|
+
declare module "@koishi-ce/core" {
|
|
43
|
+
namespace Context {
|
|
44
|
+
interface Config {
|
|
45
|
+
/** 插件配置表,键为插件引用(可带 `group:` 前缀),值为插件配置 */
|
|
46
|
+
plugins?: Dict;
|
|
47
|
+
/** 时区偏移量(分钟) */
|
|
48
|
+
timezoneOffset?: number;
|
|
49
|
+
/** 报错时的调用堆栈深度上限 */
|
|
50
|
+
stackTraceLimit?: number;
|
|
51
|
+
/** 日志设置,见 logger.Config */
|
|
52
|
+
logger?: Config;
|
|
53
|
+
/** 守护设置,见 daemon.Config */
|
|
54
|
+
daemon?: Config$1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { Loader };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { t as __exportAll } from "../rolldown-runtime-BBjsoOtd.mjs";
|
|
2
|
+
import { t as Loader } from "../src-1HtO5Rx4.mjs";
|
|
3
|
+
import Loader$1 from "@koishi-ce/loader";
|
|
4
|
+
import { Context, Logger, Schema, Time, defineProperty } from "@koishi-ce/core";
|
|
5
|
+
export * from "@koishi-ce/loader";
|
|
6
|
+
export * from "@koishi-ce/core";
|
|
7
|
+
//#region src/worker/daemon.ts
|
|
8
|
+
/**
|
|
9
|
+
* daemon 插件:worker 子进程侧的守护逻辑。
|
|
10
|
+
*
|
|
11
|
+
* 与父守护进程(src/cli/start.ts)配合:启动后上报自身配置,周期发送心跳,
|
|
12
|
+
* 并在收到 SIGINT / SIGTERM 时广播 exit 事件、清理现场后退出进程。
|
|
13
|
+
*/
|
|
14
|
+
var daemon_exports = /* @__PURE__ */ __exportAll({
|
|
15
|
+
Config: () => Config$1,
|
|
16
|
+
apply: () => apply,
|
|
17
|
+
name: () => name
|
|
18
|
+
});
|
|
19
|
+
const Config$1 = Schema.object({
|
|
20
|
+
autoRestart: Schema.boolean().description("在运行时崩溃自动重启。").default(true),
|
|
21
|
+
heartbeatInterval: Schema.number().description("心跳发送间隔。").default(0),
|
|
22
|
+
heartbeatTimeout: Schema.number().description("心跳超时时间。").default(0)
|
|
23
|
+
}).description("守护设置").hidden();
|
|
24
|
+
Context.Config.list.push(Schema.object({ daemon: Config$1 }));
|
|
25
|
+
const name = "daemon";
|
|
26
|
+
function apply(ctx, config = {}) {
|
|
27
|
+
/**
|
|
28
|
+
* 信号处理:先通知父进程"本次是主动退出"(防止被当作崩溃重启),
|
|
29
|
+
* 再广播 exit 事件等待各插件清理完毕后结束进程。
|
|
30
|
+
*/
|
|
31
|
+
function handleSignal(signal) {
|
|
32
|
+
if (config.autoRestart) process.send?.({ type: "exit" });
|
|
33
|
+
ctx.logger("app").info(`terminated by ${signal}`);
|
|
34
|
+
ctx.parallel("exit", signal).finally(() => process.exit());
|
|
35
|
+
}
|
|
36
|
+
ctx.on("ready", () => {
|
|
37
|
+
process.send?.({
|
|
38
|
+
type: "start",
|
|
39
|
+
body: config
|
|
40
|
+
});
|
|
41
|
+
process.on("SIGINT", handleSignal);
|
|
42
|
+
process.on("SIGTERM", handleSignal);
|
|
43
|
+
config.heartbeatInterval && setInterval(() => {
|
|
44
|
+
process.send?.({ type: "heartbeat" });
|
|
45
|
+
}, config.heartbeatInterval);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/worker/logger.ts
|
|
50
|
+
/**
|
|
51
|
+
* 日志配置的解析与应用(worker 侧)。
|
|
52
|
+
*
|
|
53
|
+
* 在应用启动前根据配置文件与 CLI 环境变量(KOISHI_LOG_LEVEL / KOISHI_DEBUG 等)
|
|
54
|
+
* 完成全局 Logger 的等级、时间格式等设定。CLI 传入的环境变量优先级高于配置文件。
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* 将配置文件形态的等级表归一化为 Logger 内部的 LevelConfig 结构:
|
|
58
|
+
* 逐层填充 base 并递归处理嵌套对象。
|
|
59
|
+
*/
|
|
60
|
+
function normalizeLevels(config, base) {
|
|
61
|
+
const result = { base: config.base ?? base };
|
|
62
|
+
for (const [name, level] of Object.entries(config)) {
|
|
63
|
+
if (name === "base") continue;
|
|
64
|
+
result[name] = typeof level === "number" ? level : normalizeLevels(level, result.base);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
const Config = Schema.object({
|
|
69
|
+
levels: Schema.any().description("默认的日志输出等级。"),
|
|
70
|
+
showDiff: Schema.boolean().description("标注相邻两次日志输出的时间差。"),
|
|
71
|
+
showTime: Schema.union([Boolean, String]).default(true).description("输出日志所使用的时间格式。")
|
|
72
|
+
}).description("日志设置").hidden();
|
|
73
|
+
defineProperty(Context.Config, "logger", Config);
|
|
74
|
+
Context.Config.list.push(Schema.object({ logger: Config }));
|
|
75
|
+
/**
|
|
76
|
+
* 在应用启动前应用日志配置。
|
|
77
|
+
*
|
|
78
|
+
* 处理顺序:配置文件中的等级表 → 时间格式与时间差显示 → CLI 环境变量覆盖 →
|
|
79
|
+
* 补全所有分组的 base 等级 → KOISHI_DEBUG 指定的命名空间设为 DEBUG 级。
|
|
80
|
+
*
|
|
81
|
+
* @param config 来自配置文件 logger 节点的设置
|
|
82
|
+
*/
|
|
83
|
+
function prepare(config = {}) {
|
|
84
|
+
const { levels } = config;
|
|
85
|
+
if (typeof levels === "object") Logger.levels = normalizeLevels(levels, 2);
|
|
86
|
+
else if (typeof levels === "number") Logger.levels.base = levels;
|
|
87
|
+
let showTime = config.showTime;
|
|
88
|
+
if (showTime === true) showTime = "yyyy-MM-dd hh:mm:ss";
|
|
89
|
+
const target = Logger.targets[0];
|
|
90
|
+
if (target) {
|
|
91
|
+
if (showTime) target.showTime = showTime;
|
|
92
|
+
target.showDiff = config.showDiff ?? false;
|
|
93
|
+
}
|
|
94
|
+
if (process.env["KOISHI_LOG_LEVEL"]) Logger.levels.base = +process.env["KOISHI_LOG_LEVEL"];
|
|
95
|
+
/** 递归为所有子命名空间补全 base 等级(未显式设置时继承父级) */
|
|
96
|
+
function ensureBaseLevel(config, base) {
|
|
97
|
+
config.base ??= base;
|
|
98
|
+
Object.values(config).forEach((value) => {
|
|
99
|
+
if (typeof value !== "object") return;
|
|
100
|
+
ensureBaseLevel(value, config.base);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
ensureBaseLevel(Logger.levels, 2);
|
|
104
|
+
if (process.env["KOISHI_DEBUG"]) for (const name of process.env["KOISHI_DEBUG"].split(",")) new Logger(name).level = Logger.DEBUG;
|
|
105
|
+
if (target) target.timestamp = Date.now();
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/worker/index.ts
|
|
109
|
+
/**
|
|
110
|
+
* worker 子进程入口:真正启动 Koishi 应用的地方。
|
|
111
|
+
*
|
|
112
|
+
* 由 CLI 守护进程(src/cli/start.ts)通过 Bun.spawn 拉起。流程为:
|
|
113
|
+
* 初始化 Loader 并读取配置文件 → 应用日志配置 → 应用时区与堆栈深度设置 →
|
|
114
|
+
* 创建应用上下文 → 挂载 daemon 插件 → 启动。未捕获异常将以错误码 1 退出,
|
|
115
|
+
* 由父进程依据退出码决定是否重启。
|
|
116
|
+
*/
|
|
117
|
+
const advancedDict = Context.Config.Advanced.dict;
|
|
118
|
+
if (advancedDict) Object.assign(advancedDict, {
|
|
119
|
+
timezoneOffset: Schema.number().description("时区偏移量 (分钟)。").default((/* @__PURE__ */ new Date()).getTimezoneOffset()),
|
|
120
|
+
stackTraceLimit: Schema.natural().description("报错的调用堆栈深度。").default(10),
|
|
121
|
+
plugins: Schema.any().hidden()
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* 未捕获异常的兜底处理:记录日志后以退出码 1 结束进程,
|
|
125
|
+
* 交由父守护进程决定是否重启。
|
|
126
|
+
*/
|
|
127
|
+
function handleException(error) {
|
|
128
|
+
new Logger("app").error(error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
process.on("uncaughtException", handleException);
|
|
132
|
+
process.on("unhandledRejection", (error) => {
|
|
133
|
+
new Logger("app").warn(error);
|
|
134
|
+
});
|
|
135
|
+
/** 应用启动主流程 */
|
|
136
|
+
async function start() {
|
|
137
|
+
const loader = new Loader$1();
|
|
138
|
+
await loader.init(process.env["KOISHI_CONFIG_FILE"]);
|
|
139
|
+
const config = await loader.readConfig(true);
|
|
140
|
+
prepare(config.logger);
|
|
141
|
+
if (config.timezoneOffset !== void 0) Time.setTimezoneOffset(config.timezoneOffset);
|
|
142
|
+
if (config.stackTraceLimit !== void 0) Error.stackTraceLimit = config.stackTraceLimit;
|
|
143
|
+
const app = await loader.createApp();
|
|
144
|
+
app.plugin(daemon_exports, config.daemon);
|
|
145
|
+
await app.start();
|
|
146
|
+
}
|
|
147
|
+
await start().catch(handleException);
|
|
148
|
+
//#endregion
|
|
149
|
+
export { Loader };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@koishi-ce/koishi",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"description": "Cross-Platform Chatbot Framework Made with Love",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"module": "lib/index.mjs",
|
|
8
|
+
"types": "lib/index.d.ts",
|
|
9
|
+
"bin": "lib/cli/index.mjs",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"source": "./src/index.ts",
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"import": "./lib/index.mjs",
|
|
15
|
+
"default": "./lib/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./lib/cli": "./lib/cli/index.mjs",
|
|
18
|
+
"./lib/worker": "./lib/worker/index.mjs",
|
|
19
|
+
"./src/*": "./src/*",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"contributors": [
|
|
27
|
+
"Shigma <shigma10826@gmail.com>",
|
|
28
|
+
"Oppenheymu <oppenheymu@gmail.com>"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Koishi-CE/koishi.git",
|
|
34
|
+
"directory": "packages/node/cli"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Koishi-CE/koishi/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://koishi.chat",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"bot",
|
|
42
|
+
"chatbot",
|
|
43
|
+
"discord",
|
|
44
|
+
"telegram",
|
|
45
|
+
"cordis",
|
|
46
|
+
"framework"
|
|
47
|
+
],
|
|
48
|
+
"cordis": {
|
|
49
|
+
"property": "koishi",
|
|
50
|
+
"ecosystem": {
|
|
51
|
+
"pattern": [
|
|
52
|
+
"@koishi-ce/plugin-*",
|
|
53
|
+
"@koishijs/plugin-*",
|
|
54
|
+
"koishi-plugin-*"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"service": {
|
|
58
|
+
"implements": [
|
|
59
|
+
"koishi"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@koishi-ce/core": "^1.0.0",
|
|
65
|
+
"@koishi-ce/loader": "^1.0.0",
|
|
66
|
+
"@koishi-ce/plugin-http": "^1.0.0",
|
|
67
|
+
"@koishi-ce/plugin-proxy-agent": "^1.0.0",
|
|
68
|
+
"@koishi-ce/plugin-server": "^1.0.0",
|
|
69
|
+
"@koishi-ce/utils": "^1.0.0",
|
|
70
|
+
"@satorijs/core": "^4.6.0",
|
|
71
|
+
"cac": "^7.0.0",
|
|
72
|
+
"kleur": "^4.1.5"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://koishi.chat/" target="_blank">
|
|
3
|
+
<img width="160" src="https://koishi.chat/logo.png" alt="logo">
|
|
4
|
+
</a>
|
|
5
|
+
<h1 id="koishi"><a href="https://github.com/Koishi-CE/koishi" target="_blank">Koishi CE</a></h1>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@koishi-ce/koishi)
|
|
8
|
+
[](https://github.com/Koishi-CE/koishi/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
`@koishi-ce/koishi` 是 [Koishi](https://koishi.chat) 的社区再发行版 (Community Edition) 命令行工具,来自 [Koishi-CE/koishi](https://github.com/Koishi-CE/koishi) —— 将 Koishi 核心与 webui 合并重构的单一 Bun workspace 单仓库。
|
|
13
|
+
|
|
14
|
+
命令名保持为 `koishi`,与上游用法一致:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @koishi-ce/koishi
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> 本项目与 Koishijs 官方组织无从属关系。感谢原作者 Shigma 及所有上游贡献者,相关声明见仓库根目录的 NOTICE 与 UPSTREAM 文件。
|
|
21
|
+
|
|
22
|
+
## 许可证
|
|
23
|
+
|
|
24
|
+
本包基于 [MIT](./LICENSE) 协议开源。
|
|
25
|
+
|
|
26
|
+
Copyright © 2019-2023, Shigma; Koishi-CE contributors
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Koishi 命令行入口。
|
|
5
|
+
*
|
|
6
|
+
* 基于 cac 构建名为 `koishi` 的命令行程序,注册 `start`(别名 `run`)子命令后解析
|
|
7
|
+
* 进程参数。若用户未输入任何子命令(且未请求帮助),则自动打印帮助信息退出。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cac } from "cac";
|
|
11
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
12
|
+
|
|
13
|
+
const { version } = pkg;
|
|
14
|
+
|
|
15
|
+
import registerStartCommand from "./start.ts";
|
|
16
|
+
|
|
17
|
+
const cli = cac("koishi").help().version(version);
|
|
18
|
+
|
|
19
|
+
registerStartCommand(cli);
|
|
20
|
+
|
|
21
|
+
const argv = cli.parse();
|
|
22
|
+
|
|
23
|
+
// 未匹配到子命令且未请求 --help 时,主动输出帮助信息,避免静默退出
|
|
24
|
+
if (!cli.matchedCommand && !argv.options["help"]) {
|
|
25
|
+
cli.outputHelp();
|
|
26
|
+
}
|
package/src/cli/start.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `koishi start` 子命令实现:守护进程(daemon)侧的父进程逻辑。
|
|
3
|
+
*
|
|
4
|
+
* 通过 Bun.spawn 以 IPC 通道拉起 worker 子进程,并依据子进程发来的消息
|
|
5
|
+
* (启动配置、共享环境数据、心跳)决定是否重启。子进程以约定的退出码
|
|
6
|
+
* 表达意图:51 表示请求重启,52 表示请求退出,其余交由 autoRestart 判断。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { hyphenate, isInteger } from "@koishi-ce/utils";
|
|
10
|
+
import type { CAC } from "cac";
|
|
11
|
+
import kleur from "kleur";
|
|
12
|
+
import type { Config } from "../worker/daemon.ts";
|
|
13
|
+
|
|
14
|
+
/** 子进程通过 IPC 通道发来的消息类型(并集) */
|
|
15
|
+
type Event = Event.Start | Event.Env | Event.Heartbeat;
|
|
16
|
+
/** 单个命令行选项的取值形态 */
|
|
17
|
+
type WorkerOption = boolean | string | string[] | undefined;
|
|
18
|
+
/** 传递给 worker 的完整选项表;`--` 键对应 cac 收集的透传参数 */
|
|
19
|
+
type WorkerOptions = Record<string, WorkerOption> & { "--"?: string[] };
|
|
20
|
+
|
|
21
|
+
/** 子进程 IPC 消息的具体结构定义 */
|
|
22
|
+
namespace Event {
|
|
23
|
+
/** worker 启动完成,附带解析后的守护配置 */
|
|
24
|
+
export interface Start {
|
|
25
|
+
type: "start";
|
|
26
|
+
body: Config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** loader 触发整进程重载时回传的共享环境数据(KOISHI_SHARED) */
|
|
30
|
+
export interface Env {
|
|
31
|
+
type: "shared";
|
|
32
|
+
body: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 周期性心跳,用于父进程检测子进程是否假死 */
|
|
36
|
+
export interface Heartbeat {
|
|
37
|
+
type: "heartbeat";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let child: Bun.Subprocess;
|
|
42
|
+
|
|
43
|
+
// 跨重启保留的共享数据:首次启动仅记录启动时间,后续由子进程回传覆盖
|
|
44
|
+
process.env["KOISHI_SHARED"] = JSON.stringify({
|
|
45
|
+
startTime: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 将 cac 解析出的选项键转换为命令行参数形式。
|
|
50
|
+
* 单字母键转为 `-x`,其余转为 `--kebab-case`。
|
|
51
|
+
*/
|
|
52
|
+
function toArg(key: string) {
|
|
53
|
+
return key.length === 1 ? `-${key}` : `--${hyphenate(key)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 拉起 worker 子进程并接管其生命周期。
|
|
58
|
+
*
|
|
59
|
+
* @param options 命令行选项,会被还原成 execArgv 传给子进程
|
|
60
|
+
*
|
|
61
|
+
* 子进程退出时依据退出码/信号判断是重启还是跟随退出;
|
|
62
|
+
* 启动后若配置了心跳超时,则心跳超时视为进程假死,直接 SIGKILL。
|
|
63
|
+
*/
|
|
64
|
+
function createWorker(options: WorkerOptions) {
|
|
65
|
+
// 将选项对象还原为 Node/Bun 可识别的 execArgv 数组
|
|
66
|
+
const execArgv = Object.entries(options).flatMap<string>(([key, value]) => {
|
|
67
|
+
if (key === "--") return [];
|
|
68
|
+
key = toArg(key);
|
|
69
|
+
if (value === true) {
|
|
70
|
+
return [key];
|
|
71
|
+
} else if (value === false) {
|
|
72
|
+
// 布尔假值转为 --no-xxx 形式
|
|
73
|
+
return [`--no-${key.slice(2)}`];
|
|
74
|
+
} else if (Array.isArray(value)) {
|
|
75
|
+
// 数组值展开为多组 "键 值" 对
|
|
76
|
+
return value.flatMap((value) => [key, value]);
|
|
77
|
+
} else {
|
|
78
|
+
return [key, String(value)];
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
execArgv.push(...(options["--"] ?? []));
|
|
82
|
+
|
|
83
|
+
// worker 入口为构建产物 index.mjs,而非本 TS 源文件
|
|
84
|
+
const worker = `${import.meta.dir}/../worker/index.mjs`;
|
|
85
|
+
|
|
86
|
+
let config: Config;
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
88
|
+
// 处理子进程 IPC 消息:start / shared / heartbeat
|
|
89
|
+
const handleMessage = (message: Event) => {
|
|
90
|
+
if (message.type === "start") {
|
|
91
|
+
config = message.body;
|
|
92
|
+
timer = config.heartbeatTimeout
|
|
93
|
+
? setTimeout(() => {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.log(kleur.red("daemon: heartbeat timeout"));
|
|
96
|
+
child.kill("SIGKILL");
|
|
97
|
+
}, config.heartbeatTimeout)
|
|
98
|
+
: undefined;
|
|
99
|
+
} else if (message.type === "shared") {
|
|
100
|
+
process.env["KOISHI_SHARED"] = message.body;
|
|
101
|
+
} else if (
|
|
102
|
+
message.type === "heartbeat" &&
|
|
103
|
+
timer &&
|
|
104
|
+
config.heartbeatTimeout
|
|
105
|
+
) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
timer = setTimeout(() => {
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.log(kleur.red("daemon: heartbeat timeout"));
|
|
110
|
+
child.kill("SIGKILL");
|
|
111
|
+
}, config.heartbeatTimeout);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
child = Bun.spawn([process.execPath, worker, ...execArgv], {
|
|
116
|
+
ipc: handleMessage,
|
|
117
|
+
// Bun.spawn 的 stdio 默认为 ignore,须显式继承输出通道,
|
|
118
|
+
// 否则 worker 的全部日志都会被丢弃
|
|
119
|
+
stdout: "inherit",
|
|
120
|
+
stderr: "inherit",
|
|
121
|
+
onExit: (_, code, signal) => {
|
|
122
|
+
if (shouldExit(code, signal)) {
|
|
123
|
+
process.exit(code ?? 1);
|
|
124
|
+
}
|
|
125
|
+
createWorker(options);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 判断子进程退出后父进程应跟随退出还是重新拉起。
|
|
131
|
+
*
|
|
132
|
+
* 退出码约定:0 表示正常退出;51 表示请求重启(如 loader 的整进程重载);
|
|
133
|
+
* 52 表示请求退出;收到信号一律视为外部终止,跟随退出。
|
|
134
|
+
*/
|
|
135
|
+
function shouldExit(code: number | null, signal: number | null) {
|
|
136
|
+
// 尚未收到 start 消息即退出,说明启动失败
|
|
137
|
+
if (!config) return true;
|
|
138
|
+
|
|
139
|
+
// 手动退出(正常退出码或被信号终止)
|
|
140
|
+
if (code === 0) return true;
|
|
141
|
+
if (signal !== null) return true;
|
|
142
|
+
|
|
143
|
+
// 手动重启 / 手动停止
|
|
144
|
+
if (code === 51) return false;
|
|
145
|
+
if (code === 52) return true;
|
|
146
|
+
|
|
147
|
+
// 其余情况交由 autoRestart 配置决定
|
|
148
|
+
return !config.autoRestart;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 将命令行选项写入环境变量。
|
|
154
|
+
* 值为 `true` 时写入空字符串(仅表示开关打开),其余写入字符串值。
|
|
155
|
+
*/
|
|
156
|
+
function setEnvArg(name: string, value: string | boolean) {
|
|
157
|
+
if (value === true) {
|
|
158
|
+
process.env[name] = "";
|
|
159
|
+
} else if (value) {
|
|
160
|
+
process.env[name] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 向 cac 实例注册 `koishi start`(别名 `koishi run`)子命令。
|
|
166
|
+
*
|
|
167
|
+
* 解析 --debug / --log-level / --log-time 等选项后,把它们写入对应的环境变量
|
|
168
|
+
* (由 worker 侧的 logger 读取),最后交给 createWorker 拉起守护子进程。
|
|
169
|
+
*/
|
|
170
|
+
export default function (cli: CAC) {
|
|
171
|
+
cli
|
|
172
|
+
.command("start [file]", "start a koishi bot")
|
|
173
|
+
.alias("run")
|
|
174
|
+
.allowUnknownOptions()
|
|
175
|
+
.option("--debug [namespace]", "specify debug namespace")
|
|
176
|
+
.option("--log-level [level]", "specify log level (default: 2)")
|
|
177
|
+
.option("--log-time [format]", "show timestamp in logs")
|
|
178
|
+
.action((file, options) => {
|
|
179
|
+
const { logLevel, debug, logTime, ...rest } = options;
|
|
180
|
+
if (logLevel !== undefined && (!isInteger(logLevel) || logLevel < 0)) {
|
|
181
|
+
// eslint-disable-next-line no-console
|
|
182
|
+
console.warn(
|
|
183
|
+
`${kleur.red("error")} log level should be a positive integer.`,
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
setEnvArg("KOISHI_LOG_TIME", logTime);
|
|
188
|
+
process.env["KOISHI_LOG_LEVEL"] = logLevel || "";
|
|
189
|
+
process.env["KOISHI_DEBUG"] = debug || "";
|
|
190
|
+
process.env["KOISHI_CONFIG_FILE"] = file || "";
|
|
191
|
+
createWorker(rest);
|
|
192
|
+
});
|
|
193
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@koishi-ce/koishi` 包入口。
|
|
3
|
+
*
|
|
4
|
+
* 本文件专为不使用 CLI 的用户准备:将 core 与 loader 两包的导出合并再分发,
|
|
5
|
+
* 并默认导出 NodeLoader 实现,使开发者可以直接以编程方式启动 Koishi。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Loader from "@koishi-ce/loader";
|
|
9
|
+
|
|
10
|
+
export * from "@koishi-ce/core";
|
|
11
|
+
export * from "@koishi-ce/loader";
|
|
12
|
+
export { Loader };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon 插件:worker 子进程侧的守护逻辑。
|
|
3
|
+
*
|
|
4
|
+
* 与父守护进程(src/cli/start.ts)配合:启动后上报自身配置,周期发送心跳,
|
|
5
|
+
* 并在收到 SIGINT / SIGTERM 时广播 exit 事件、清理现场后退出进程。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Context, Schema } from "@koishi-ce/core";
|
|
9
|
+
|
|
10
|
+
/** 守护设置 */
|
|
11
|
+
export interface Config {
|
|
12
|
+
/** 运行时崩溃后是否自动重启 */
|
|
13
|
+
autoRestart?: boolean;
|
|
14
|
+
/** 心跳发送间隔(毫秒),0 表示不发送 */
|
|
15
|
+
heartbeatInterval?: number;
|
|
16
|
+
/** 心跳超时时间(毫秒),超时后父进程会强杀子进程,0 表示不检测 */
|
|
17
|
+
heartbeatTimeout?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Config: Schema<Config> = Schema.object({
|
|
21
|
+
autoRestart: Schema.boolean()
|
|
22
|
+
.description("在运行时崩溃自动重启。")
|
|
23
|
+
.default(true),
|
|
24
|
+
heartbeatInterval: Schema.number().description("心跳发送间隔。").default(0),
|
|
25
|
+
heartbeatTimeout: Schema.number().description("心跳超时时间。").default(0),
|
|
26
|
+
})
|
|
27
|
+
.description("守护设置")
|
|
28
|
+
.hidden();
|
|
29
|
+
|
|
30
|
+
Context.Config.list.push(
|
|
31
|
+
Schema.object({
|
|
32
|
+
daemon: Config,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const name = "daemon";
|
|
37
|
+
|
|
38
|
+
export function apply(ctx: Context, config: Config = {}) {
|
|
39
|
+
/**
|
|
40
|
+
* 信号处理:先通知父进程"本次是主动退出"(防止被当作崩溃重启),
|
|
41
|
+
* 再广播 exit 事件等待各插件清理完毕后结束进程。
|
|
42
|
+
*/
|
|
43
|
+
function handleSignal(signal: NodeJS.Signals) {
|
|
44
|
+
// 子进程主动退出时须防止父进程按 autoRestart 策略重启
|
|
45
|
+
if (config.autoRestart) {
|
|
46
|
+
process.send?.({ type: "exit" });
|
|
47
|
+
}
|
|
48
|
+
ctx.logger("app").info(`terminated by ${signal}`);
|
|
49
|
+
ctx.parallel("exit", signal).finally(() => process.exit());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ctx.on("ready", () => {
|
|
53
|
+
// 向父进程上报守护配置,供其心跳超时与重启判定使用
|
|
54
|
+
process.send?.({ type: "start", body: config });
|
|
55
|
+
process.on("SIGINT", handleSignal);
|
|
56
|
+
process.on("SIGTERM", handleSignal);
|
|
57
|
+
|
|
58
|
+
// 按配置间隔向父进程发送心跳
|
|
59
|
+
config.heartbeatInterval &&
|
|
60
|
+
setInterval(() => {
|
|
61
|
+
process.send?.({ type: "heartbeat" });
|
|
62
|
+
}, config.heartbeatInterval);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worker 子进程入口:真正启动 Koishi 应用的地方。
|
|
3
|
+
*
|
|
4
|
+
* 由 CLI 守护进程(src/cli/start.ts)通过 Bun.spawn 拉起。流程为:
|
|
5
|
+
* 初始化 Loader 并读取配置文件 → 应用日志配置 → 应用时区与堆栈深度设置 →
|
|
6
|
+
* 创建应用上下文 → 挂载 daemon 插件 → 启动。未捕获异常将以错误码 1 退出,
|
|
7
|
+
* 由父进程依据退出码决定是否重启。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Context, type Dict, Logger, Schema, Time } from "@koishi-ce/core";
|
|
11
|
+
import Loader from "@koishi-ce/loader";
|
|
12
|
+
import * as daemon from "./daemon.ts";
|
|
13
|
+
import * as logger from "./logger.ts";
|
|
14
|
+
|
|
15
|
+
// 以相对导入 re-export 包主入口(上游同款写法):worker 产物本身属于本包,
|
|
16
|
+
// 若写包名自引用,作为 main entry 直接执行时会触发 Bun 的自引用解析问题
|
|
17
|
+
// (进程静默退出);相对导入在构建期内联,运行时无自引用。
|
|
18
|
+
export * from "../index.ts";
|
|
19
|
+
|
|
20
|
+
// 通过模块合并向全局 Context.Config 追加本入口支持的配置项
|
|
21
|
+
declare module "@koishi-ce/core" {
|
|
22
|
+
namespace Context {
|
|
23
|
+
interface Config {
|
|
24
|
+
/** 插件配置表,键为插件引用(可带 `group:` 前缀),值为插件配置 */
|
|
25
|
+
plugins?: Dict;
|
|
26
|
+
/** 时区偏移量(分钟) */
|
|
27
|
+
timezoneOffset?: number;
|
|
28
|
+
/** 报错时的调用堆栈深度上限 */
|
|
29
|
+
stackTraceLimit?: number;
|
|
30
|
+
/** 日志设置,见 logger.Config */
|
|
31
|
+
logger?: logger.Config;
|
|
32
|
+
/** 守护设置,见 daemon.Config */
|
|
33
|
+
daemon?: daemon.Config;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 向控制台"高级设置"分区注册本入口新增的配置项 schema
|
|
39
|
+
const advancedDict = Context.Config.Advanced.dict;
|
|
40
|
+
if (advancedDict) {
|
|
41
|
+
Object.assign(advancedDict, {
|
|
42
|
+
timezoneOffset: Schema.number()
|
|
43
|
+
.description("时区偏移量 (分钟)。")
|
|
44
|
+
.default(new Date().getTimezoneOffset()),
|
|
45
|
+
stackTraceLimit: Schema.natural()
|
|
46
|
+
.description("报错的调用堆栈深度。")
|
|
47
|
+
.default(10),
|
|
48
|
+
plugins: Schema.any().hidden(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 未捕获异常的兜底处理:记录日志后以退出码 1 结束进程,
|
|
54
|
+
* 交由父守护进程决定是否重启。
|
|
55
|
+
*/
|
|
56
|
+
function handleException(error: unknown) {
|
|
57
|
+
new Logger("app").error(error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.on("uncaughtException", handleException);
|
|
62
|
+
|
|
63
|
+
// Promise 拒绝不致命,仅告警,避免应用因单个异步错误退出
|
|
64
|
+
process.on("unhandledRejection", (error) => {
|
|
65
|
+
new Logger("app").warn(error);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** 应用启动主流程 */
|
|
69
|
+
async function start() {
|
|
70
|
+
const loader = new Loader();
|
|
71
|
+
await loader.init(process.env["KOISHI_CONFIG_FILE"]);
|
|
72
|
+
const config = await loader.readConfig(true);
|
|
73
|
+
logger.prepare(config.logger);
|
|
74
|
+
|
|
75
|
+
if (config.timezoneOffset !== undefined) {
|
|
76
|
+
Time.setTimezoneOffset(config.timezoneOffset);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (config.stackTraceLimit !== undefined) {
|
|
80
|
+
Error.stackTraceLimit = config.stackTraceLimit;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const app = await loader.createApp();
|
|
84
|
+
// 挂载 daemon 插件以接管信号处理与心跳上报
|
|
85
|
+
app.plugin(daemon, config.daemon);
|
|
86
|
+
await app.start();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 顶层 await 启动:main entry 的模块求值须挂起至启动链推进完成——
|
|
90
|
+
// 若以 start().catch(...) 形式让出,Bun 事件循环在无其他句柄时会随首个
|
|
91
|
+
// await 立即退出(进程静默 exit 0,插件与 daemon 心跳均来不及注册保活)
|
|
92
|
+
await start().catch(handleException);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志配置的解析与应用(worker 侧)。
|
|
3
|
+
*
|
|
4
|
+
* 在应用启动前根据配置文件与 CLI 环境变量(KOISHI_LOG_LEVEL / KOISHI_DEBUG 等)
|
|
5
|
+
* 完成全局 Logger 的等级、时间格式等设定。CLI 传入的环境变量优先级高于配置文件。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Context, defineProperty, Logger, Schema } from "@koishi-ce/core";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 配置文件形态的日志等级表。
|
|
12
|
+
* 与 @koishi-ce/utils 中的实现略有差异:此处不强制用户提供 base 等级,
|
|
13
|
+
* 缺省时回退到继承的默认值。
|
|
14
|
+
*/
|
|
15
|
+
interface LogLevelConfig {
|
|
16
|
+
/** 基础日志等级,未提供时沿用上级默认值 */
|
|
17
|
+
base?: number;
|
|
18
|
+
[k: string]: LogLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 日志等级:既可以是单一数值,也可以是按名称分级的嵌套配置表 */
|
|
22
|
+
type LogLevel = number | LogLevelConfig;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 将配置文件形态的等级表归一化为 Logger 内部的 LevelConfig 结构:
|
|
26
|
+
* 逐层填充 base 并递归处理嵌套对象。
|
|
27
|
+
*/
|
|
28
|
+
function normalizeLevels(
|
|
29
|
+
config: LogLevelConfig,
|
|
30
|
+
base: number,
|
|
31
|
+
): Logger.LevelConfig {
|
|
32
|
+
const result: Logger.LevelConfig = { base: config.base ?? base };
|
|
33
|
+
for (const [name, level] of Object.entries(config)) {
|
|
34
|
+
if (name === "base") continue;
|
|
35
|
+
result[name] =
|
|
36
|
+
typeof level === "number" ? level : normalizeLevels(level, result.base);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 日志设置 */
|
|
42
|
+
export interface Config {
|
|
43
|
+
/** 默认的日志输出等级 */
|
|
44
|
+
levels?: LogLevel;
|
|
45
|
+
/** 是否标注相邻两次日志输出的时间差 */
|
|
46
|
+
showDiff?: boolean;
|
|
47
|
+
/** 日志时间戳的输出格式,true 表示使用默认格式 */
|
|
48
|
+
showTime?: string | boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Config: Schema<Config> = Schema.object({
|
|
52
|
+
levels: Schema.any().description("默认的日志输出等级。"),
|
|
53
|
+
showDiff: Schema.boolean().description("标注相邻两次日志输出的时间差。"),
|
|
54
|
+
showTime: Schema.union([Boolean, String])
|
|
55
|
+
.default(true)
|
|
56
|
+
.description("输出日志所使用的时间格式。"),
|
|
57
|
+
})
|
|
58
|
+
.description("日志设置")
|
|
59
|
+
.hidden();
|
|
60
|
+
|
|
61
|
+
defineProperty(Context.Config, "logger", Config);
|
|
62
|
+
|
|
63
|
+
Context.Config.list.push(
|
|
64
|
+
Schema.object({
|
|
65
|
+
logger: Config,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 在应用启动前应用日志配置。
|
|
71
|
+
*
|
|
72
|
+
* 处理顺序:配置文件中的等级表 → 时间格式与时间差显示 → CLI 环境变量覆盖 →
|
|
73
|
+
* 补全所有分组的 base 等级 → KOISHI_DEBUG 指定的命名空间设为 DEBUG 级。
|
|
74
|
+
*
|
|
75
|
+
* @param config 来自配置文件 logger 节点的设置
|
|
76
|
+
*/
|
|
77
|
+
export function prepare(config: Config = {}) {
|
|
78
|
+
const { levels } = config;
|
|
79
|
+
// 应用配置文件中的日志等级设置
|
|
80
|
+
if (typeof levels === "object") {
|
|
81
|
+
Logger.levels = normalizeLevels(levels, 2);
|
|
82
|
+
} else if (typeof levels === "number") {
|
|
83
|
+
Logger.levels.base = levels;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let showTime = config.showTime;
|
|
87
|
+
if (showTime === true) showTime = "yyyy-MM-dd hh:mm:ss";
|
|
88
|
+
const target = Logger.targets[0];
|
|
89
|
+
if (target) {
|
|
90
|
+
if (showTime) target.showTime = showTime;
|
|
91
|
+
target.showDiff = config.showDiff ?? false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// CLI 传入的选项优先级高于配置文件
|
|
95
|
+
if (process.env["KOISHI_LOG_LEVEL"]) {
|
|
96
|
+
Logger.levels.base = +process.env["KOISHI_LOG_LEVEL"];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 递归为所有子命名空间补全 base 等级(未显式设置时继承父级) */
|
|
100
|
+
function ensureBaseLevel(config: Logger.LevelConfig, base: number) {
|
|
101
|
+
config.base ??= base;
|
|
102
|
+
Object.values(config).forEach((value) => {
|
|
103
|
+
if (typeof value !== "object") return;
|
|
104
|
+
ensureBaseLevel(value, config.base);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ensureBaseLevel(Logger.levels, 2);
|
|
109
|
+
|
|
110
|
+
// KOISHI_DEBUG 指定的各个命名空间一律开启 DEBUG 级输出
|
|
111
|
+
if (process.env["KOISHI_DEBUG"]) {
|
|
112
|
+
for (const name of process.env["KOISHI_DEBUG"].split(",")) {
|
|
113
|
+
new Logger(name).level = Logger.DEBUG;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 重置时间差计算的基准时间戳,避免把启动前的耗时计入首条日志
|
|
118
|
+
if (target) target.timestamp = Date.now();
|
|
119
|
+
}
|