@koishi-ce/koishi 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/lib/cli/index.mjs +6 -6
- package/package.json +5 -5
- package/src/cli/start.ts +60 -37
- package/src/worker/daemon.ts +9 -3
- package/src/worker/index.ts +46 -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.6",
|
|
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/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.6",
|
|
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.5",
|
|
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,7 +14,13 @@
|
|
|
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";
|
|
19
25
|
import * as daemon from "./daemon.ts";
|
|
20
26
|
import * as logger from "./logger.ts";
|
|
@@ -87,14 +93,19 @@ interface ServerPort {
|
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
/** 从模块入口路径向上查找最近的 package.json,返回其 name 字段 */
|
|
90
|
-
function locatePackageName(
|
|
96
|
+
function locatePackageName(
|
|
97
|
+
filename: string,
|
|
98
|
+
): string | undefined {
|
|
91
99
|
let dir = dirname(filename);
|
|
92
100
|
while (true) {
|
|
93
101
|
const file = resolve(dir, "package.json");
|
|
94
102
|
if (existsSync(file)) {
|
|
95
103
|
try {
|
|
96
|
-
const manifest = JSON.parse(
|
|
97
|
-
|
|
104
|
+
const manifest = JSON.parse(
|
|
105
|
+
readFileSync(file, "utf8"),
|
|
106
|
+
);
|
|
107
|
+
if (typeof manifest?.name === "string")
|
|
108
|
+
return manifest.name;
|
|
98
109
|
} catch {}
|
|
99
110
|
}
|
|
100
111
|
const parent = dirname(dir);
|
|
@@ -107,9 +118,17 @@ function locatePackageName(filename: string): string | undefined {
|
|
|
107
118
|
* 遍历插件配置表(含 group 嵌套),收集服务器插件声明的端口区间。
|
|
108
119
|
* 引用键格式与 loader 一致:首个冒号前为插件名;`$` 开头为元属性。
|
|
109
120
|
*/
|
|
110
|
-
function collectServerPorts(
|
|
121
|
+
function collectServerPorts(
|
|
122
|
+
plugins: Dict,
|
|
123
|
+
baseDir: string,
|
|
124
|
+
out: ServerPort[],
|
|
125
|
+
) {
|
|
111
126
|
for (const [key, source] of Object.entries(plugins)) {
|
|
112
|
-
if (
|
|
127
|
+
if (
|
|
128
|
+
key.startsWith("$") ||
|
|
129
|
+
source === null ||
|
|
130
|
+
typeof source !== "object"
|
|
131
|
+
) {
|
|
113
132
|
continue;
|
|
114
133
|
}
|
|
115
134
|
const [name = ""] = key.split(":", 1);
|
|
@@ -118,14 +137,21 @@ function collectServerPorts(plugins: Dict, baseDir: string, out: ServerPort[]) {
|
|
|
118
137
|
continue;
|
|
119
138
|
}
|
|
120
139
|
try {
|
|
121
|
-
const pkgName = locatePackageName(
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
const pkgName = locatePackageName(
|
|
141
|
+
resolvePlugin(name, baseDir),
|
|
142
|
+
);
|
|
143
|
+
if (!pkgName || !serverPackages.has(pkgName))
|
|
144
|
+
continue;
|
|
145
|
+
const { host, port, maxPort } = source as Record<
|
|
146
|
+
string,
|
|
147
|
+
unknown
|
|
148
|
+
>;
|
|
124
149
|
if (typeof port !== "number") continue;
|
|
125
150
|
out.push({
|
|
126
151
|
host: typeof host === "string" ? host : "127.0.0.1",
|
|
127
152
|
port,
|
|
128
|
-
maxPort:
|
|
153
|
+
maxPort:
|
|
154
|
+
typeof maxPort === "number" ? maxPort : port,
|
|
129
155
|
});
|
|
130
156
|
} catch {}
|
|
131
157
|
}
|
|
@@ -136,7 +162,9 @@ function probePort(port: number, host: string) {
|
|
|
136
162
|
return new Promise<boolean>((promiseResolve) => {
|
|
137
163
|
const server = net.createServer();
|
|
138
164
|
server.once("error", () => promiseResolve(false));
|
|
139
|
-
server.once("listening", () =>
|
|
165
|
+
server.once("listening", () =>
|
|
166
|
+
server.close(() => promiseResolve(true)),
|
|
167
|
+
);
|
|
140
168
|
server.listen(port, host);
|
|
141
169
|
});
|
|
142
170
|
}
|
|
@@ -152,14 +180,19 @@ async function checkPorts(plugins: Dict, baseDir: string) {
|
|
|
152
180
|
collectServerPorts(plugins, baseDir, ports);
|
|
153
181
|
for (const { host, port, maxPort } of ports) {
|
|
154
182
|
let available = false;
|
|
155
|
-
for (
|
|
183
|
+
for (
|
|
184
|
+
let current = port;
|
|
185
|
+
current <= maxPort;
|
|
186
|
+
current++
|
|
187
|
+
) {
|
|
156
188
|
if (await probePort(current, host)) {
|
|
157
189
|
available = true;
|
|
158
190
|
break;
|
|
159
191
|
}
|
|
160
192
|
}
|
|
161
193
|
if (available) continue;
|
|
162
|
-
const range =
|
|
194
|
+
const range =
|
|
195
|
+
port === maxPort ? `${port}` : `${port}-${maxPort}`;
|
|
163
196
|
new Logger("app").error(
|
|
164
197
|
`端口 ${range} 已被占用(可能已有 Koishi 实例在运行),启动中止`,
|
|
165
198
|
);
|
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
|
}
|