@koishi-ce/koishi 1.0.0 → 1.0.2
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 +1 -1
- package/lib/worker/index.mjs +80 -1
- package/package.json +2 -2
- package/src/worker/index.ts +102 -1
package/lib/cli/index.mjs
CHANGED
package/lib/worker/index.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { t as __exportAll } from "../rolldown-runtime-BBjsoOtd.mjs";
|
|
2
2
|
import { t as Loader } from "../src-1HtO5Rx4.mjs";
|
|
3
|
-
import Loader$1 from "@koishi-ce/loader";
|
|
3
|
+
import Loader$1, { resolvePlugin } from "@koishi-ce/loader";
|
|
4
4
|
import { Context, Logger, Schema, Time, defineProperty } from "@koishi-ce/core";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
5
8
|
export * from "@koishi-ce/loader";
|
|
6
9
|
export * from "@koishi-ce/core";
|
|
7
10
|
//#region src/worker/daemon.ts
|
|
@@ -132,12 +135,88 @@ process.on("uncaughtException", handleException);
|
|
|
132
135
|
process.on("unhandledRejection", (error) => {
|
|
133
136
|
new Logger("app").warn(error);
|
|
134
137
|
});
|
|
138
|
+
/** HTTP 服务器插件的包名集合(端口预检的识别目标,覆盖本仓 vendored 包与上下游命名) */
|
|
139
|
+
const serverPackages = /* @__PURE__ */ new Set([
|
|
140
|
+
"@koishi-ce/plugin-server",
|
|
141
|
+
"@koishijs/plugin-server",
|
|
142
|
+
"@cordisjs/plugin-server"
|
|
143
|
+
]);
|
|
144
|
+
/** 从模块入口路径向上查找最近的 package.json,返回其 name 字段 */
|
|
145
|
+
function locatePackageName(filename) {
|
|
146
|
+
let dir = dirname(filename);
|
|
147
|
+
while (true) {
|
|
148
|
+
const file = resolve(dir, "package.json");
|
|
149
|
+
if (existsSync(file)) try {
|
|
150
|
+
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
151
|
+
if (typeof manifest?.name === "string") return manifest.name;
|
|
152
|
+
} catch {}
|
|
153
|
+
const parent = dirname(dir);
|
|
154
|
+
if (parent === dir) return void 0;
|
|
155
|
+
dir = parent;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 遍历插件配置表(含 group 嵌套),收集服务器插件声明的端口区间。
|
|
160
|
+
* 引用键格式与 loader 一致:首个冒号前为插件名;`$` 开头为元属性。
|
|
161
|
+
*/
|
|
162
|
+
function collectServerPorts(plugins, baseDir, out) {
|
|
163
|
+
for (const [key, source] of Object.entries(plugins)) {
|
|
164
|
+
if (key.startsWith("$") || source === null || typeof source !== "object") continue;
|
|
165
|
+
const [name = ""] = key.split(":", 1);
|
|
166
|
+
if (name === "group") {
|
|
167
|
+
collectServerPorts(source, baseDir, out);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const pkgName = locatePackageName(resolvePlugin(name, baseDir));
|
|
172
|
+
if (!pkgName || !serverPackages.has(pkgName)) continue;
|
|
173
|
+
const { host, port, maxPort } = source;
|
|
174
|
+
if (typeof port !== "number") continue;
|
|
175
|
+
out.push({
|
|
176
|
+
host: typeof host === "string" ? host : "127.0.0.1",
|
|
177
|
+
port,
|
|
178
|
+
maxPort: typeof maxPort === "number" ? maxPort : port
|
|
179
|
+
});
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** 探测端口当前是否可绑定(未被占用返回 true) */
|
|
184
|
+
function probePort(port, host) {
|
|
185
|
+
return new Promise((promiseResolve) => {
|
|
186
|
+
const server = net.createServer();
|
|
187
|
+
server.once("error", () => promiseResolve(false));
|
|
188
|
+
server.once("listening", () => server.close(() => promiseResolve(true)));
|
|
189
|
+
server.listen(port, host);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 端口预检:server 插件在应用启动期绑定失败会以 cordis 错误事件暴露,
|
|
194
|
+
* 连锁触发依赖 server 服务的全部插件 dispose,堆栈噪音远超问题本身;
|
|
195
|
+
* 这里在创建应用前先行探测,端口区间全部被占时只输出一行提示并以
|
|
196
|
+
* 退出码 1 结束(父守护进程未收到 start 消息,随之干净退出)。
|
|
197
|
+
*/
|
|
198
|
+
async function checkPorts(plugins, baseDir) {
|
|
199
|
+
const ports = [];
|
|
200
|
+
collectServerPorts(plugins, baseDir, ports);
|
|
201
|
+
for (const { host, port, maxPort } of ports) {
|
|
202
|
+
let available = false;
|
|
203
|
+
for (let current = port; current <= maxPort; current++) if (await probePort(current, host)) {
|
|
204
|
+
available = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (available) continue;
|
|
208
|
+
const range = port === maxPort ? `${port}` : `${port}-${maxPort}`;
|
|
209
|
+
new Logger("app").error(`端口 ${range} 已被占用(可能已有 Koishi 实例在运行),启动中止`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
135
213
|
/** 应用启动主流程 */
|
|
136
214
|
async function start() {
|
|
137
215
|
const loader = new Loader$1();
|
|
138
216
|
await loader.init(process.env["KOISHI_CONFIG_FILE"]);
|
|
139
217
|
const config = await loader.readConfig(true);
|
|
140
218
|
prepare(config.logger);
|
|
219
|
+
await checkPorts(config.plugins ?? {}, loader.baseDir);
|
|
141
220
|
if (config.timezoneOffset !== void 0) Time.setTimezoneOffset(config.timezoneOffset);
|
|
142
221
|
if (config.stackTraceLimit !== void 0) Error.stackTraceLimit = config.stackTraceLimit;
|
|
143
222
|
const app = await loader.createApp();
|
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.2",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"module": "lib/index.mjs",
|
|
8
8
|
"types": "lib/index.d.ts",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@koishi-ce/core": "^1.0.0",
|
|
65
|
-
"@koishi-ce/loader": "^1.0.
|
|
65
|
+
"@koishi-ce/loader": "^1.0.2",
|
|
66
66
|
"@koishi-ce/plugin-http": "^1.0.0",
|
|
67
67
|
"@koishi-ce/plugin-proxy-agent": "^1.0.0",
|
|
68
68
|
"@koishi-ce/plugin-server": "^1.0.0",
|
package/src/worker/index.ts
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* 由父进程依据退出码决定是否重启。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
import { dirname, resolve } from "node:path";
|
|
10
13
|
import { Context, type Dict, Logger, Schema, Time } from "@koishi-ce/core";
|
|
11
|
-
import Loader from "@koishi-ce/loader";
|
|
14
|
+
import Loader, { resolvePlugin } from "@koishi-ce/loader";
|
|
12
15
|
import * as daemon from "./daemon.ts";
|
|
13
16
|
import * as logger from "./logger.ts";
|
|
14
17
|
|
|
@@ -65,6 +68,101 @@ process.on("unhandledRejection", (error) => {
|
|
|
65
68
|
new Logger("app").warn(error);
|
|
66
69
|
});
|
|
67
70
|
|
|
71
|
+
/** HTTP 服务器插件的包名集合(端口预检的识别目标,覆盖本仓 vendored 包与上下游命名) */
|
|
72
|
+
const serverPackages = new Set([
|
|
73
|
+
"@koishi-ce/plugin-server",
|
|
74
|
+
"@koishijs/plugin-server",
|
|
75
|
+
"@cordisjs/plugin-server",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** 服务器插件声明的待检端口区间 */
|
|
79
|
+
interface ServerPort {
|
|
80
|
+
host: string;
|
|
81
|
+
port: number;
|
|
82
|
+
maxPort: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 从模块入口路径向上查找最近的 package.json,返回其 name 字段 */
|
|
86
|
+
function locatePackageName(filename: string): string | undefined {
|
|
87
|
+
let dir = dirname(filename);
|
|
88
|
+
while (true) {
|
|
89
|
+
const file = resolve(dir, "package.json");
|
|
90
|
+
if (existsSync(file)) {
|
|
91
|
+
try {
|
|
92
|
+
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
93
|
+
if (typeof manifest?.name === "string") return manifest.name;
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
const parent = dirname(dir);
|
|
97
|
+
if (parent === dir) return undefined;
|
|
98
|
+
dir = parent;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 遍历插件配置表(含 group 嵌套),收集服务器插件声明的端口区间。
|
|
104
|
+
* 引用键格式与 loader 一致:首个冒号前为插件名;`$` 开头为元属性。
|
|
105
|
+
*/
|
|
106
|
+
function collectServerPorts(plugins: Dict, baseDir: string, out: ServerPort[]) {
|
|
107
|
+
for (const [key, source] of Object.entries(plugins)) {
|
|
108
|
+
if (key.startsWith("$") || source === null || typeof source !== "object") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const [name = ""] = key.split(":", 1);
|
|
112
|
+
if (name === "group") {
|
|
113
|
+
collectServerPorts(source as Dict, baseDir, out);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const pkgName = locatePackageName(resolvePlugin(name, baseDir));
|
|
118
|
+
if (!pkgName || !serverPackages.has(pkgName)) continue;
|
|
119
|
+
const { host, port, maxPort } = source as Record<string, unknown>;
|
|
120
|
+
if (typeof port !== "number") continue;
|
|
121
|
+
out.push({
|
|
122
|
+
host: typeof host === "string" ? host : "127.0.0.1",
|
|
123
|
+
port,
|
|
124
|
+
maxPort: typeof maxPort === "number" ? maxPort : port,
|
|
125
|
+
});
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 探测端口当前是否可绑定(未被占用返回 true) */
|
|
131
|
+
function probePort(port: number, host: string) {
|
|
132
|
+
return new Promise<boolean>((promiseResolve) => {
|
|
133
|
+
const server = net.createServer();
|
|
134
|
+
server.once("error", () => promiseResolve(false));
|
|
135
|
+
server.once("listening", () => server.close(() => promiseResolve(true)));
|
|
136
|
+
server.listen(port, host);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 端口预检:server 插件在应用启动期绑定失败会以 cordis 错误事件暴露,
|
|
142
|
+
* 连锁触发依赖 server 服务的全部插件 dispose,堆栈噪音远超问题本身;
|
|
143
|
+
* 这里在创建应用前先行探测,端口区间全部被占时只输出一行提示并以
|
|
144
|
+
* 退出码 1 结束(父守护进程未收到 start 消息,随之干净退出)。
|
|
145
|
+
*/
|
|
146
|
+
async function checkPorts(plugins: Dict, baseDir: string) {
|
|
147
|
+
const ports: ServerPort[] = [];
|
|
148
|
+
collectServerPorts(plugins, baseDir, ports);
|
|
149
|
+
for (const { host, port, maxPort } of ports) {
|
|
150
|
+
let available = false;
|
|
151
|
+
for (let current = port; current <= maxPort; current++) {
|
|
152
|
+
if (await probePort(current, host)) {
|
|
153
|
+
available = true;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (available) continue;
|
|
158
|
+
const range = port === maxPort ? `${port}` : `${port}-${maxPort}`;
|
|
159
|
+
new Logger("app").error(
|
|
160
|
+
`端口 ${range} 已被占用(可能已有 Koishi 实例在运行),启动中止`,
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
68
166
|
/** 应用启动主流程 */
|
|
69
167
|
async function start() {
|
|
70
168
|
const loader = new Loader();
|
|
@@ -72,6 +170,9 @@ async function start() {
|
|
|
72
170
|
const config = await loader.readConfig(true);
|
|
73
171
|
logger.prepare(config.logger);
|
|
74
172
|
|
|
173
|
+
// 端口预检须在日志配置生效后、应用创建前执行
|
|
174
|
+
await checkPorts(config.plugins ?? {}, loader.baseDir);
|
|
175
|
+
|
|
75
176
|
if (config.timezoneOffset !== undefined) {
|
|
76
177
|
Time.setTimezoneOffset(config.timezoneOffset);
|
|
77
178
|
}
|