@koishi-ce/koishi 1.0.3 → 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/README.md +89 -0
- package/lib/cli/index.mjs +6 -7
- package/package.json +5 -6
- package/src/cli/index.ts +4 -0
- package/src/cli/start.ts +64 -37
- package/src/index.ts +4 -0
- package/src/worker/daemon.ts +13 -3
- package/src/worker/index.ts +50 -13
- package/src/worker/logger.ts +26 -6
- package/readme.md +0 -26
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @koishi-ce/koishi
|
|
2
|
+
|
|
3
|
+
**简体中文** | [English](#english)
|
|
4
|
+
|
|
5
|
+
Koishi 的社区再发行版(Community Edition)主包与命令行宿主,来自 [Koishi-CE/koishi](https://github.com/Koishi-CE/koishi)——将上游 [koishijs/koishi](https://github.com/koishijs/koishi) 与 [koishijs/webui](https://github.com/koishijs/webui) 合并重构的单一 Bun workspace 单仓库。本包入口与上游 `koishi` 主包同构:合并再导出 [`@koishi-ce/core`](https://github.com/Koishi-CE/koishi/tree/main/packages/node/core) 与 [`@koishi-ce/loader`](https://github.com/Koishi-CE/koishi/tree/main/packages/node/loader),并内置 `koishi` 命令与守护进程。
|
|
6
|
+
|
|
7
|
+
命令名保持为 `koishi`,与上游用法一致。本项目与 Koishijs 官方组织无从属关系,感谢原作者 Shigma 及所有上游贡献者,相关声明见 [NOTICE](https://github.com/Koishi-CE/koishi/blob/main/NOTICE) 与 [docs/process/upstream.md](https://github.com/Koishi-CE/koishi/blob/main/docs/process/upstream.md)。
|
|
8
|
+
|
|
9
|
+
## 安装与启动
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @koishi-ce/koishi
|
|
13
|
+
koishi start
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
更推荐用脚手架创建完整项目(自带控制台、市场与热重载配置):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx create-koishi-ce my-app
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 命令行
|
|
23
|
+
|
|
24
|
+
| 命令 | 说明 |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `koishi start [file]`(别名 `run`) | 启动应用,`file` 为配置文件路径 |
|
|
27
|
+
|
|
28
|
+
选项:
|
|
29
|
+
|
|
30
|
+
| 选项 | 默认值 | 说明 |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `--debug [namespace]` | - | 开启调试日志,可限定命名空间 |
|
|
33
|
+
| `--log-level [level]` | 2 | 日志等级 |
|
|
34
|
+
| `--log-time [format]` | - | 日志时间戳格式 |
|
|
35
|
+
|
|
36
|
+
不带子命令运行 `koishi` 时输出帮助。
|
|
37
|
+
|
|
38
|
+
## 守护进程机制
|
|
39
|
+
|
|
40
|
+
`koishi start` 是守护父进程:以 Bun.spawn 拉起工作进程并保持 IPC 心跳,心跳超时强制终止。退出码约定:51 = 工作进程请求整体重启(配置文件或框架依赖变更),52 = 请求退出。工作进程内由 loader 读取配置、预检服务器端口(被占则干净退出)、创建应用并挂载 daemon 插件。
|
|
41
|
+
|
|
42
|
+
## 编程式用法
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { App, defineConfig, NodeLoader } from "@koishi-ce/koishi";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
对 `koishi` / `@koishijs/core` / `@koishijs/loader` 三个上游名的兼容由 [`@koishi-ce/koishi-shim`](https://github.com/Koishi-CE/koishi/tree/main/packages/shim) 以 npm alias 承担,社区插件无需改动即可在 CE 项目中运行,详见 [packages/shim/README.md](https://github.com/Koishi-CE/koishi/blob/main/packages/shim/README.md)。
|
|
49
|
+
|
|
50
|
+
## 许可证
|
|
51
|
+
|
|
52
|
+
[MIT](https://github.com/Koishi-CE/koishi/blob/main/LICENSE)。本包是上游 koishijs/koishi 的社区再分发,版权归属见 [NOTICE](https://github.com/Koishi-CE/koishi/blob/main/NOTICE)。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## English
|
|
57
|
+
|
|
58
|
+
The Community Edition main package and CLI host of Koishi, from [Koishi-CE/koishi](https://github.com/Koishi-CE/koishi) — a single Bun-workspace monorepo merging the upstream [koishijs/koishi](https://github.com/koishijs/koishi) and [koishijs/webui](https://github.com/koishijs/webui). Its entry mirrors the upstream `koishi` package: a merged re-export of `@koishi-ce/core` and `@koishi-ce/loader`, plus the `koishi` command and a daemon. Not affiliated with the Koishijs organization; see [NOTICE](https://github.com/Koishi-CE/koishi/blob/main/NOTICE) and [docs/process/upstream.md](https://github.com/Koishi-CE/koishi/blob/main/docs/process/upstream.md).
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g @koishi-ce/koishi
|
|
64
|
+
koishi start
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or scaffold a full project (console, market and HMR preconfigured):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bunx create-koishi-ce my-app
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
`koishi start [file]` (alias `run`) starts the app, with options `--debug [namespace]`, `--log-level [level]` (default 2) and `--log-time [format]`. Running `koishi` without a subcommand prints help.
|
|
76
|
+
|
|
77
|
+
The `start` command is a daemon parent: it spawns the worker via Bun.spawn with IPC heartbeats. Exit code 51 asks for a full restart, 52 asks to quit. The worker reads the config through the loader, prechecks server ports, creates the app and mounts the daemon plugin.
|
|
78
|
+
|
|
79
|
+
## Programmatic usage
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { App, defineConfig, NodeLoader } from "@koishi-ce/koishi";
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Compatibility with the upstream names `koishi` / `@koishijs/core` / `@koishijs/loader` is provided by `@koishi-ce/koishi-shim` via npm aliases, so community plugins run unmodified in CE projects — see [packages/shim/README.md](https://github.com/Koishi-CE/koishi/blob/main/packages/shim/README.md).
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
[MIT](https://github.com/Koishi-CE/koishi/blob/main/LICENSE). Community redistribution of upstream koishijs/koishi; see [NOTICE](https://github.com/Koishi-CE/koishi/blob/main/NOTICE) for attribution.
|
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",
|
|
@@ -58,9 +58,8 @@ var package_default = {
|
|
|
58
58
|
"@koishi-ce/plugin-proxy-agent": "workspace:*",
|
|
59
59
|
"@koishi-ce/plugin-server": "workspace:*",
|
|
60
60
|
"@koishi-ce/utils": "workspace:*",
|
|
61
|
-
"@satorijs/core": "^4.6.0",
|
|
62
61
|
"cac": "^7.0.0",
|
|
63
|
-
"
|
|
62
|
+
"picocolors": "^1.1.1"
|
|
64
63
|
}
|
|
65
64
|
};
|
|
66
65
|
//#endregion
|
|
@@ -106,14 +105,14 @@ function createWorker(options) {
|
|
|
106
105
|
if (message.type === "start") {
|
|
107
106
|
config = message.body;
|
|
108
107
|
timer = config.heartbeatTimeout ? setTimeout(() => {
|
|
109
|
-
console.log(
|
|
108
|
+
console.log(pc.red("daemon: heartbeat timeout"));
|
|
110
109
|
child.kill("SIGKILL");
|
|
111
110
|
}, config.heartbeatTimeout) : void 0;
|
|
112
111
|
} else if (message.type === "shared") process.env["KOISHI_SHARED"] = message.body;
|
|
113
112
|
else if (message.type === "heartbeat" && timer && config.heartbeatTimeout) {
|
|
114
113
|
clearTimeout(timer);
|
|
115
114
|
timer = setTimeout(() => {
|
|
116
|
-
console.log(
|
|
115
|
+
console.log(pc.red("daemon: heartbeat timeout"));
|
|
117
116
|
child.kill("SIGKILL");
|
|
118
117
|
}, config.heartbeatTimeout);
|
|
119
118
|
}
|
|
@@ -164,7 +163,7 @@ function start_default(cli) {
|
|
|
164
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) => {
|
|
165
164
|
const { logLevel, debug, logTime, ...rest } = options;
|
|
166
165
|
if (logLevel !== void 0 && (!isInteger(logLevel) || logLevel < 0)) {
|
|
167
|
-
console.warn(`${
|
|
166
|
+
console.warn(`${pc.red("error")} log level should be a positive integer.`);
|
|
168
167
|
process.exit(1);
|
|
169
168
|
}
|
|
170
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,14 +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
|
-
"@satorijs/core": "^4.6.0",
|
|
71
70
|
"cac": "^7.0.0",
|
|
72
|
-
"
|
|
71
|
+
"picocolors": "^1.1.1"
|
|
73
72
|
}
|
|
74
73
|
}
|
package/src/cli/index.ts
CHANGED
package/src/cli/start.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* `koishi start` 子命令实现:守护进程(daemon)侧的父进程逻辑。
|
|
3
7
|
*
|
|
@@ -8,7 +12,7 @@
|
|
|
8
12
|
|
|
9
13
|
import { hyphenate, isInteger } from "@koishi-ce/utils";
|
|
10
14
|
import type { CAC } from "cac";
|
|
11
|
-
import
|
|
15
|
+
import pc from "picocolors";
|
|
12
16
|
import type { Config } from "../worker/daemon.ts";
|
|
13
17
|
|
|
14
18
|
/** 子进程通过 IPC 通道发来的消息类型(并集) */
|
|
@@ -16,7 +20,9 @@ type Event = Event.Start | Event.Env | Event.Heartbeat;
|
|
|
16
20
|
/** 单个命令行选项的取值形态 */
|
|
17
21
|
type WorkerOption = boolean | string | string[] | undefined;
|
|
18
22
|
/** 传递给 worker 的完整选项表;`--` 键对应 cac 收集的透传参数 */
|
|
19
|
-
type WorkerOptions = Record<string, WorkerOption> & {
|
|
23
|
+
type WorkerOptions = Record<string, WorkerOption> & {
|
|
24
|
+
"--"?: string[];
|
|
25
|
+
};
|
|
20
26
|
|
|
21
27
|
/** 子进程 IPC 消息的具体结构定义 */
|
|
22
28
|
namespace Event {
|
|
@@ -50,7 +56,9 @@ process.env["KOISHI_SHARED"] = JSON.stringify({
|
|
|
50
56
|
* 单字母键转为 `-x`,其余转为 `--kebab-case`。
|
|
51
57
|
*/
|
|
52
58
|
function toArg(key: string) {
|
|
53
|
-
return key.length === 1
|
|
59
|
+
return key.length === 1
|
|
60
|
+
? `-${key}`
|
|
61
|
+
: `--${hyphenate(key)}`;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
@@ -63,21 +71,23 @@ function toArg(key: string) {
|
|
|
63
71
|
*/
|
|
64
72
|
function createWorker(options: WorkerOptions) {
|
|
65
73
|
// 将选项对象还原为 Node/Bun 可识别的 execArgv 数组
|
|
66
|
-
const execArgv = Object.entries(options).flatMap<string>(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
);
|
|
81
91
|
execArgv.push(...(options["--"] ?? []));
|
|
82
92
|
|
|
83
93
|
// worker 入口为构建产物 index.mjs,而非本 TS 源文件
|
|
@@ -92,7 +102,9 @@ function createWorker(options: WorkerOptions) {
|
|
|
92
102
|
timer = config.heartbeatTimeout
|
|
93
103
|
? setTimeout(() => {
|
|
94
104
|
// eslint-disable-next-line no-console
|
|
95
|
-
console.log(
|
|
105
|
+
console.log(
|
|
106
|
+
pc.red("daemon: heartbeat timeout"),
|
|
107
|
+
);
|
|
96
108
|
child.kill("SIGKILL");
|
|
97
109
|
}, config.heartbeatTimeout)
|
|
98
110
|
: undefined;
|
|
@@ -106,25 +118,28 @@ function createWorker(options: WorkerOptions) {
|
|
|
106
118
|
clearTimeout(timer);
|
|
107
119
|
timer = setTimeout(() => {
|
|
108
120
|
// eslint-disable-next-line no-console
|
|
109
|
-
console.log(
|
|
121
|
+
console.log(pc.red("daemon: heartbeat timeout"));
|
|
110
122
|
child.kill("SIGKILL");
|
|
111
123
|
}, config.heartbeatTimeout);
|
|
112
124
|
}
|
|
113
125
|
};
|
|
114
126
|
|
|
115
|
-
child = Bun.spawn(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
},
|
|
126
141
|
},
|
|
127
|
-
|
|
142
|
+
);
|
|
128
143
|
|
|
129
144
|
/**
|
|
130
145
|
* 判断子进程退出后父进程应跟随退出还是重新拉起。
|
|
@@ -132,7 +147,10 @@ function createWorker(options: WorkerOptions) {
|
|
|
132
147
|
* 退出码约定:0 表示正常退出;51 表示请求重启(如 loader 的整进程重载);
|
|
133
148
|
* 52 表示请求退出;收到信号一律视为外部终止,跟随退出。
|
|
134
149
|
*/
|
|
135
|
-
function shouldExit(
|
|
150
|
+
function shouldExit(
|
|
151
|
+
code: number | null,
|
|
152
|
+
signal: number | null,
|
|
153
|
+
) {
|
|
136
154
|
// 尚未收到 start 消息即退出,说明启动失败
|
|
137
155
|
if (!config) return true;
|
|
138
156
|
|
|
@@ -172,15 +190,24 @@ export default function (cli: CAC) {
|
|
|
172
190
|
.command("start [file]", "start a koishi bot")
|
|
173
191
|
.alias("run")
|
|
174
192
|
.allowUnknownOptions()
|
|
175
|
-
.option(
|
|
176
|
-
|
|
193
|
+
.option(
|
|
194
|
+
"--debug [namespace]",
|
|
195
|
+
"specify debug namespace",
|
|
196
|
+
)
|
|
197
|
+
.option(
|
|
198
|
+
"--log-level [level]",
|
|
199
|
+
"specify log level (default: 2)",
|
|
200
|
+
)
|
|
177
201
|
.option("--log-time [format]", "show timestamp in logs")
|
|
178
202
|
.action((file, options) => {
|
|
179
203
|
const { logLevel, debug, logTime, ...rest } = options;
|
|
180
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
logLevel !== undefined &&
|
|
206
|
+
(!isInteger(logLevel) || logLevel < 0)
|
|
207
|
+
) {
|
|
181
208
|
// eslint-disable-next-line no-console
|
|
182
209
|
console.warn(
|
|
183
|
-
`${
|
|
210
|
+
`${pc.red("error")} log level should be a positive integer.`,
|
|
184
211
|
);
|
|
185
212
|
process.exit(1);
|
|
186
213
|
}
|
package/src/index.ts
CHANGED
package/src/worker/daemon.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* daemon 插件:worker 子进程侧的守护逻辑。
|
|
3
7
|
*
|
|
@@ -21,8 +25,12 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
21
25
|
autoRestart: Schema.boolean()
|
|
22
26
|
.description("在运行时崩溃自动重启。")
|
|
23
27
|
.default(true),
|
|
24
|
-
heartbeatInterval: Schema.number()
|
|
25
|
-
|
|
28
|
+
heartbeatInterval: Schema.number()
|
|
29
|
+
.description("心跳发送间隔。")
|
|
30
|
+
.default(0),
|
|
31
|
+
heartbeatTimeout: Schema.number()
|
|
32
|
+
.description("心跳超时时间。")
|
|
33
|
+
.default(0),
|
|
26
34
|
})
|
|
27
35
|
.description("守护设置")
|
|
28
36
|
.hidden();
|
|
@@ -46,7 +54,9 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
46
54
|
process.send?.({ type: "exit" });
|
|
47
55
|
}
|
|
48
56
|
ctx.logger("app").info(`terminated by ${signal}`);
|
|
49
|
-
ctx
|
|
57
|
+
ctx
|
|
58
|
+
.parallel("exit", signal)
|
|
59
|
+
.finally(() => process.exit());
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
ctx.on("ready", () => {
|
package/src/worker/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* worker 子进程入口:真正启动 Koishi 应用的地方。
|
|
3
7
|
*
|
|
@@ -10,7 +14,13 @@
|
|
|
10
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
15
|
import net from "node:net";
|
|
12
16
|
import { dirname, resolve } from "node:path";
|
|
13
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Context,
|
|
19
|
+
type Dict,
|
|
20
|
+
Logger,
|
|
21
|
+
Schema,
|
|
22
|
+
Time,
|
|
23
|
+
} from "@koishi-ce/core";
|
|
14
24
|
import Loader, { resolvePlugin } from "@koishi-ce/loader";
|
|
15
25
|
import * as daemon from "./daemon.ts";
|
|
16
26
|
import * as logger from "./logger.ts";
|
|
@@ -83,14 +93,19 @@ interface ServerPort {
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/** 从模块入口路径向上查找最近的 package.json,返回其 name 字段 */
|
|
86
|
-
function locatePackageName(
|
|
96
|
+
function locatePackageName(
|
|
97
|
+
filename: string,
|
|
98
|
+
): string | undefined {
|
|
87
99
|
let dir = dirname(filename);
|
|
88
100
|
while (true) {
|
|
89
101
|
const file = resolve(dir, "package.json");
|
|
90
102
|
if (existsSync(file)) {
|
|
91
103
|
try {
|
|
92
|
-
const manifest = JSON.parse(
|
|
93
|
-
|
|
104
|
+
const manifest = JSON.parse(
|
|
105
|
+
readFileSync(file, "utf8"),
|
|
106
|
+
);
|
|
107
|
+
if (typeof manifest?.name === "string")
|
|
108
|
+
return manifest.name;
|
|
94
109
|
} catch {}
|
|
95
110
|
}
|
|
96
111
|
const parent = dirname(dir);
|
|
@@ -103,9 +118,17 @@ function locatePackageName(filename: string): string | undefined {
|
|
|
103
118
|
* 遍历插件配置表(含 group 嵌套),收集服务器插件声明的端口区间。
|
|
104
119
|
* 引用键格式与 loader 一致:首个冒号前为插件名;`$` 开头为元属性。
|
|
105
120
|
*/
|
|
106
|
-
function collectServerPorts(
|
|
121
|
+
function collectServerPorts(
|
|
122
|
+
plugins: Dict,
|
|
123
|
+
baseDir: string,
|
|
124
|
+
out: ServerPort[],
|
|
125
|
+
) {
|
|
107
126
|
for (const [key, source] of Object.entries(plugins)) {
|
|
108
|
-
if (
|
|
127
|
+
if (
|
|
128
|
+
key.startsWith("$") ||
|
|
129
|
+
source === null ||
|
|
130
|
+
typeof source !== "object"
|
|
131
|
+
) {
|
|
109
132
|
continue;
|
|
110
133
|
}
|
|
111
134
|
const [name = ""] = key.split(":", 1);
|
|
@@ -114,14 +137,21 @@ function collectServerPorts(plugins: Dict, baseDir: string, out: ServerPort[]) {
|
|
|
114
137
|
continue;
|
|
115
138
|
}
|
|
116
139
|
try {
|
|
117
|
-
const pkgName = locatePackageName(
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
>;
|
|
120
149
|
if (typeof port !== "number") continue;
|
|
121
150
|
out.push({
|
|
122
151
|
host: typeof host === "string" ? host : "127.0.0.1",
|
|
123
152
|
port,
|
|
124
|
-
maxPort:
|
|
153
|
+
maxPort:
|
|
154
|
+
typeof maxPort === "number" ? maxPort : port,
|
|
125
155
|
});
|
|
126
156
|
} catch {}
|
|
127
157
|
}
|
|
@@ -132,7 +162,9 @@ function probePort(port: number, host: string) {
|
|
|
132
162
|
return new Promise<boolean>((promiseResolve) => {
|
|
133
163
|
const server = net.createServer();
|
|
134
164
|
server.once("error", () => promiseResolve(false));
|
|
135
|
-
server.once("listening", () =>
|
|
165
|
+
server.once("listening", () =>
|
|
166
|
+
server.close(() => promiseResolve(true)),
|
|
167
|
+
);
|
|
136
168
|
server.listen(port, host);
|
|
137
169
|
});
|
|
138
170
|
}
|
|
@@ -148,14 +180,19 @@ async function checkPorts(plugins: Dict, baseDir: string) {
|
|
|
148
180
|
collectServerPorts(plugins, baseDir, ports);
|
|
149
181
|
for (const { host, port, maxPort } of ports) {
|
|
150
182
|
let available = false;
|
|
151
|
-
for (
|
|
183
|
+
for (
|
|
184
|
+
let current = port;
|
|
185
|
+
current <= maxPort;
|
|
186
|
+
current++
|
|
187
|
+
) {
|
|
152
188
|
if (await probePort(current, host)) {
|
|
153
189
|
available = true;
|
|
154
190
|
break;
|
|
155
191
|
}
|
|
156
192
|
}
|
|
157
193
|
if (available) continue;
|
|
158
|
-
const range =
|
|
194
|
+
const range =
|
|
195
|
+
port === maxPort ? `${port}` : `${port}-${maxPort}`;
|
|
159
196
|
new Logger("app").error(
|
|
160
197
|
`端口 ${range} 已被占用(可能已有 Koishi 实例在运行),启动中止`,
|
|
161
198
|
);
|
package/src/worker/logger.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* 日志配置的解析与应用(worker 侧)。
|
|
3
7
|
*
|
|
@@ -5,7 +9,12 @@
|
|
|
5
9
|
* 完成全局 Logger 的等级、时间格式等设定。CLI 传入的环境变量优先级高于配置文件。
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Context,
|
|
14
|
+
defineProperty,
|
|
15
|
+
Logger,
|
|
16
|
+
Schema,
|
|
17
|
+
} from "@koishi-ce/core";
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* 配置文件形态的日志等级表。
|
|
@@ -29,11 +38,15 @@ function normalizeLevels(
|
|
|
29
38
|
config: LogLevelConfig,
|
|
30
39
|
base: number,
|
|
31
40
|
): Logger.LevelConfig {
|
|
32
|
-
const result: Logger.LevelConfig = {
|
|
41
|
+
const result: Logger.LevelConfig = {
|
|
42
|
+
base: config.base ?? base,
|
|
43
|
+
};
|
|
33
44
|
for (const [name, level] of Object.entries(config)) {
|
|
34
45
|
if (name === "base") continue;
|
|
35
46
|
result[name] =
|
|
36
|
-
typeof level === "number"
|
|
47
|
+
typeof level === "number"
|
|
48
|
+
? level
|
|
49
|
+
: normalizeLevels(level, result.base);
|
|
37
50
|
}
|
|
38
51
|
return result;
|
|
39
52
|
}
|
|
@@ -50,7 +63,9 @@ export interface Config {
|
|
|
50
63
|
|
|
51
64
|
export const Config: Schema<Config> = Schema.object({
|
|
52
65
|
levels: Schema.any().description("默认的日志输出等级。"),
|
|
53
|
-
showDiff: Schema.boolean().description(
|
|
66
|
+
showDiff: Schema.boolean().description(
|
|
67
|
+
"标注相邻两次日志输出的时间差。",
|
|
68
|
+
),
|
|
54
69
|
showTime: Schema.union([Boolean, String])
|
|
55
70
|
.default(true)
|
|
56
71
|
.description("输出日志所使用的时间格式。"),
|
|
@@ -97,7 +112,10 @@ export function prepare(config: Config = {}) {
|
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
/** 递归为所有子命名空间补全 base 等级(未显式设置时继承父级) */
|
|
100
|
-
function ensureBaseLevel(
|
|
115
|
+
function ensureBaseLevel(
|
|
116
|
+
config: Logger.LevelConfig,
|
|
117
|
+
base: number,
|
|
118
|
+
) {
|
|
101
119
|
config.base ??= base;
|
|
102
120
|
Object.values(config).forEach((value) => {
|
|
103
121
|
if (typeof value !== "object") return;
|
|
@@ -109,7 +127,9 @@ export function prepare(config: Config = {}) {
|
|
|
109
127
|
|
|
110
128
|
// KOISHI_DEBUG 指定的各个命名空间一律开启 DEBUG 级输出
|
|
111
129
|
if (process.env["KOISHI_DEBUG"]) {
|
|
112
|
-
for (const name of process.env["KOISHI_DEBUG"].split(
|
|
130
|
+
for (const name of process.env["KOISHI_DEBUG"].split(
|
|
131
|
+
",",
|
|
132
|
+
)) {
|
|
113
133
|
new Logger(name).level = Logger.DEBUG;
|
|
114
134
|
}
|
|
115
135
|
}
|
package/readme.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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
|