@maiyunnet/kebab 9.15.5 → 9.16.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/doc/kebab-rag.md +194 -104
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +27 -3
- package/lib/core.js +32 -4
- package/lib/text.d.ts +6 -0
- package/lib/text.js +24 -1
- package/lib/undici/request.d.ts +6 -1
- package/lib/undici/request.js +8 -0
- package/lib/undici/response.d.ts +1 -1
- package/lib/undici/response.js +2 -1
- package/lib/undici.d.ts +7 -3
- package/lib/undici.js +48 -8
- package/package.json +1 -1
- package/sys/child.js +15 -12
- package/sys/ctr.js +3 -3
- package/sys/master.js +41 -2
- package/sys/monitor/watchdog.js +9 -5
- package/sys/monitor.d.ts +8 -1
- package/sys/monitor.js +198 -81
- package/sys/route.d.ts +1 -1
- package/sys/route.js +14 -14
- package/www/example/ctr/test.d.ts +2 -0
- package/www/example/ctr/test.js +44 -3
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.16.0';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/core.d.ts
CHANGED
|
@@ -181,6 +181,11 @@ export declare function passThroughAppend(passThrough: stream.PassThrough, data:
|
|
|
181
181
|
*/
|
|
182
182
|
export declare function exec(command: string, options?: {
|
|
183
183
|
'cwd'?: string;
|
|
184
|
+
/**
|
|
185
|
+
* --- 命令超时毫秒数,超时后终止命令并以失败返回 ---
|
|
186
|
+
* --- 默认无超时限制:不传时命令可一直运行直至完成,与 cp.exec 默认行为一致 ---
|
|
187
|
+
*/
|
|
188
|
+
'timeout'?: number;
|
|
184
189
|
}): Promise<string | false>;
|
|
185
190
|
/**
|
|
186
191
|
* --- 向主进程(或局域网同代码机子)发送广播将进行 reload 操作,等待回传 ---
|
|
@@ -347,12 +352,31 @@ export declare function debug(message?: any, ...optionalParams: any[]): void;
|
|
|
347
352
|
*/
|
|
348
353
|
export declare function display(message?: any, ...optionalParams: any[]): void;
|
|
349
354
|
/**
|
|
350
|
-
* ---
|
|
355
|
+
* --- 提交 HTTP 响应状态和头部,兼容 HTTP/1.1 与 HTTP/2 ---
|
|
356
|
+
*
|
|
357
|
+
* `setHeader()` 只暂存或修改单个头部,不会发送响应头,也不能提交状态码;在响应头提交前可反复调用。
|
|
358
|
+
* 本方法会立即提交状态码和此前设置的全部头部,必须在所有 `setHeader()` 调用之后、首次
|
|
359
|
+
* `write()`、`end()` 或 `pipe()` 之前调用。提交后再调用 `setHeader()` 会抛出 `ERR_HTTP_HEADERS_SENT`。
|
|
360
|
+
*
|
|
361
|
+
* 普通控制器应设置 `_httpCode`、调用 `_res.setHeader()` 并直接返回内容,由路由层统一提交响应头。
|
|
362
|
+
* 仅框架内部或手动接管响应(错误、重定向、代理、流式输出等)时才应直接调用本方法。
|
|
363
|
+
*
|
|
351
364
|
* @param res 响应对象
|
|
352
|
-
* @param statusCode 状态码
|
|
353
|
-
* @param headers
|
|
365
|
+
* @param statusCode HTTP 状态码
|
|
366
|
+
* @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
|
|
367
|
+
* @returns 无返回值
|
|
354
368
|
*/
|
|
355
369
|
export declare function writeHead(res: http2.Http2ServerResponse | http.ServerResponse, statusCode: number, headers?: http.OutgoingHttpHeaders): void;
|
|
370
|
+
/**
|
|
371
|
+
* --- 提交服务器发送事件(SSE)响应头 ---
|
|
372
|
+
*
|
|
373
|
+
* 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
|
|
374
|
+
* SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
|
|
375
|
+
* 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
|
|
376
|
+
*
|
|
377
|
+
* @param res 响应对象
|
|
378
|
+
* @returns 无返回值
|
|
379
|
+
*/
|
|
356
380
|
export declare function writeEventStreamHead(res: http2.Http2ServerResponse | http.ServerResponse): void;
|
|
357
381
|
/**
|
|
358
382
|
* --- 向 res 发送数据 ---
|
package/lib/core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-3 23:54
|
|
4
|
-
* Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00
|
|
4
|
+
* Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as cp from 'child_process';
|
|
7
7
|
import * as http2 from 'http2';
|
|
@@ -602,6 +602,7 @@ export function exec(command, options = {}) {
|
|
|
602
602
|
return new Promise(function (resolve) {
|
|
603
603
|
cp.exec(command, {
|
|
604
604
|
'cwd': options.cwd,
|
|
605
|
+
'timeout': options.timeout,
|
|
605
606
|
}, function (err, stdout) {
|
|
606
607
|
if (err) {
|
|
607
608
|
resolve(false);
|
|
@@ -630,6 +631,10 @@ export async function sendReload(hosts) {
|
|
|
630
631
|
if (hosts === 'config') {
|
|
631
632
|
hosts = globalConfig.hosts;
|
|
632
633
|
}
|
|
634
|
+
// --- 未传或 config 展开后为空数组,均回退到本机 ---
|
|
635
|
+
if (!hosts?.length) {
|
|
636
|
+
hosts = ['127.0.0.1'];
|
|
637
|
+
}
|
|
633
638
|
// --- 局域网模式 ---
|
|
634
639
|
const time = lTime.stamp();
|
|
635
640
|
/** --- 返回成功的 host --- */
|
|
@@ -675,6 +680,10 @@ export async function sendRestart(hosts) {
|
|
|
675
680
|
if (hosts === 'config') {
|
|
676
681
|
hosts = globalConfig.hosts;
|
|
677
682
|
}
|
|
683
|
+
// --- 未传或 config 展开后为空数组,均回退到本机 ---
|
|
684
|
+
if (!hosts?.length) {
|
|
685
|
+
hosts = ['127.0.0.1'];
|
|
686
|
+
}
|
|
678
687
|
// --- 局域网模式 ---
|
|
679
688
|
const time = lTime.stamp();
|
|
680
689
|
/** --- 返回成功的 host --- */
|
|
@@ -1212,10 +1221,19 @@ export function display(message, ...optionalParams) {
|
|
|
1212
1221
|
console.log(`KE-DISPLAY ${lTime.format(null, 'Y-m-d H:i:s')}`, message, ...optionalParams);
|
|
1213
1222
|
}
|
|
1214
1223
|
/**
|
|
1215
|
-
* ---
|
|
1224
|
+
* --- 提交 HTTP 响应状态和头部,兼容 HTTP/1.1 与 HTTP/2 ---
|
|
1225
|
+
*
|
|
1226
|
+
* `setHeader()` 只暂存或修改单个头部,不会发送响应头,也不能提交状态码;在响应头提交前可反复调用。
|
|
1227
|
+
* 本方法会立即提交状态码和此前设置的全部头部,必须在所有 `setHeader()` 调用之后、首次
|
|
1228
|
+
* `write()`、`end()` 或 `pipe()` 之前调用。提交后再调用 `setHeader()` 会抛出 `ERR_HTTP_HEADERS_SENT`。
|
|
1229
|
+
*
|
|
1230
|
+
* 普通控制器应设置 `_httpCode`、调用 `_res.setHeader()` 并直接返回内容,由路由层统一提交响应头。
|
|
1231
|
+
* 仅框架内部或手动接管响应(错误、重定向、代理、流式输出等)时才应直接调用本方法。
|
|
1232
|
+
*
|
|
1216
1233
|
* @param res 响应对象
|
|
1217
|
-
* @param statusCode 状态码
|
|
1218
|
-
* @param headers
|
|
1234
|
+
* @param statusCode HTTP 状态码
|
|
1235
|
+
* @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
|
|
1236
|
+
* @returns 无返回值
|
|
1219
1237
|
*/
|
|
1220
1238
|
export function writeHead(res, statusCode, headers) {
|
|
1221
1239
|
if (res instanceof http2.Http2ServerResponse) {
|
|
@@ -1225,6 +1243,16 @@ export function writeHead(res, statusCode, headers) {
|
|
|
1225
1243
|
res.writeHead(statusCode, headers);
|
|
1226
1244
|
}
|
|
1227
1245
|
}
|
|
1246
|
+
/**
|
|
1247
|
+
* --- 提交服务器发送事件(SSE)响应头 ---
|
|
1248
|
+
*
|
|
1249
|
+
* 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
|
|
1250
|
+
* SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
|
|
1251
|
+
* 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
|
|
1252
|
+
*
|
|
1253
|
+
* @param res 响应对象
|
|
1254
|
+
* @returns 无返回值
|
|
1255
|
+
*/
|
|
1228
1256
|
export function writeEventStreamHead(res) {
|
|
1229
1257
|
writeHead(res, 200, {
|
|
1230
1258
|
'content-type': 'text/event-stream; charset=utf-8',
|
package/lib/text.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export declare function parseJson<T>(str: string): T | false;
|
|
|
170
170
|
* @param space 美化方式
|
|
171
171
|
*/
|
|
172
172
|
export declare function stringifyJson(obj: kebab.Json, space?: string | number): string;
|
|
173
|
+
/**
|
|
174
|
+
* --- 将未知异常转换为适合单行日志的文本,Error 优先返回完整堆栈 ---
|
|
175
|
+
* @param error 异常对象
|
|
176
|
+
* @returns 已转义的单行错误文本
|
|
177
|
+
*/
|
|
178
|
+
export declare function stringifyError(error: unknown): string;
|
|
173
179
|
/**
|
|
174
180
|
* --- 输出文本格式的 buffer ---
|
|
175
181
|
* @param buf 原始 buffer
|
package/lib/text.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-15 16:49:39
|
|
4
|
-
* Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49
|
|
4
|
+
* Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as net from 'net';
|
|
7
7
|
import * as kebab from '#kebab/index.js';
|
|
@@ -624,6 +624,29 @@ export function stringifyJson(obj, space) {
|
|
|
624
624
|
return v;
|
|
625
625
|
}, space).replace(/"-mybigint-([-+0-9]+?)"/g, '$1');
|
|
626
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* --- 将未知异常转换为适合单行日志的文本,Error 优先返回完整堆栈 ---
|
|
629
|
+
* @param error 异常对象
|
|
630
|
+
* @returns 已转义的单行错误文本
|
|
631
|
+
*/
|
|
632
|
+
export function stringifyError(error) {
|
|
633
|
+
let message;
|
|
634
|
+
if (error instanceof Error) {
|
|
635
|
+
message = error.stack ?? error.message;
|
|
636
|
+
}
|
|
637
|
+
else if (typeof error === 'string') {
|
|
638
|
+
message = error;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
try {
|
|
642
|
+
message = stringifyJson(error);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
message = String(error);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return stringifyJson(message).slice(1, -1);
|
|
649
|
+
}
|
|
627
650
|
/**
|
|
628
651
|
* --- 输出文本格式的 buffer ---
|
|
629
652
|
* @param buf 原始 buffer
|
package/lib/undici/request.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2020-4-9 20:02:39
|
|
4
|
-
* Last: 2020-4-9 20:47:58, 2022-09-10 01:35:34, 2025-9-23 12:41:58
|
|
4
|
+
* Last: 2020-4-9 20:47:58, 2022-09-10 01:35:34, 2025-9-23 12:41:58, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as stream from 'stream';
|
|
7
7
|
import * as lCookie from '#kebab/lib/cookie.js';
|
|
@@ -47,6 +47,11 @@ export declare class Request {
|
|
|
47
47
|
* @param timeout 秒
|
|
48
48
|
*/
|
|
49
49
|
timeout(timeout: number): this;
|
|
50
|
+
/**
|
|
51
|
+
* --- 设置网络异常后的重试次数 ---
|
|
52
|
+
* @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
|
|
53
|
+
*/
|
|
54
|
+
retry(retry?: number): this;
|
|
50
55
|
/**
|
|
51
56
|
* --- 设置是否跟随请求方的 location,留空为跟随,不设置为不跟随 ---
|
|
52
57
|
* @param follow
|
package/lib/undici/request.js
CHANGED
|
@@ -59,6 +59,14 @@ export class Request {
|
|
|
59
59
|
this._opt['timeout'] = timeout;
|
|
60
60
|
return this;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* --- 设置网络异常后的重试次数 ---
|
|
64
|
+
* @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
|
|
65
|
+
*/
|
|
66
|
+
retry(retry = 1) {
|
|
67
|
+
this._opt['retry'] = retry;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
62
70
|
/**
|
|
63
71
|
* --- 设置是否跟随请求方的 location,留空为跟随,不设置为不跟随 ---
|
|
64
72
|
* @param follow
|
package/lib/undici/response.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2020-4-9 15:33:06
|
|
4
|
-
* Last: 2020-4-12 11:12:03, 2022-09-10 12:43:23, 2022-12-25 15:12:57, 2023-9-26 14:20:41
|
|
4
|
+
* Last: 2020-4-12 11:12:03, 2022-09-10 12:43:23, 2022-12-25 15:12:57, 2023-9-26 14:20:41, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as undici from 'undici';
|
|
7
7
|
import * as zlib from 'zlib';
|
package/lib/undici/response.js
CHANGED
|
@@ -28,7 +28,8 @@ export class Response {
|
|
|
28
28
|
return this._req ? await lBuffer.getFull(stream) : null;
|
|
29
29
|
}
|
|
30
30
|
catch (e) {
|
|
31
|
-
|
|
31
|
+
this.error = e instanceof Error ? e : new Error(String(e));
|
|
32
|
+
lCore.log({}, '[Undici][Response][getContent] ' + lText.stringifyError(e), '-error');
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
34
35
|
}
|
package/lib/undici.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare function postJson(u: string, data: kebab.Json[] | Record<string,
|
|
|
41
41
|
* @param data 数据
|
|
42
42
|
* @param opt 选项
|
|
43
43
|
*/
|
|
44
|
-
export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null>;
|
|
44
|
+
export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
|
|
45
45
|
/**
|
|
46
46
|
* --- 发起 POST 请求并解析 JSON 响应 ---
|
|
47
47
|
* @param u 网址
|
|
@@ -49,14 +49,14 @@ export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Rec
|
|
|
49
49
|
* @param opt 选项
|
|
50
50
|
* @returns JSON 数据,失败时返回 null
|
|
51
51
|
*/
|
|
52
|
-
export declare function postResponseJson(u: string, data: Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null>;
|
|
52
|
+
export declare function postResponseJson(u: string, data: Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
|
|
53
53
|
/**
|
|
54
54
|
* --- 发起 GET 请求并解析 JSON 响应 ---
|
|
55
55
|
* @param u 网址
|
|
56
56
|
* @param opt 选项
|
|
57
57
|
* @returns JSON 数据,失败时返回 null
|
|
58
58
|
*/
|
|
59
|
-
export declare function getResponseJson(u: string, opt?: IRequestOptions): Promise<kebab.Json | null>;
|
|
59
|
+
export declare function getResponseJson(u: string, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
|
|
60
60
|
/**
|
|
61
61
|
* --- 发起一个完全兼容 fetch 的请求 ---
|
|
62
62
|
* @param input 请求的 URL 或 Request 对象
|
|
@@ -71,6 +71,8 @@ export declare function fetch(input: string | URL | Request, init?: RequestInit
|
|
|
71
71
|
};
|
|
72
72
|
/** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
|
|
73
73
|
'hosts'?: Record<string, string> | string;
|
|
74
|
+
/** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
|
|
75
|
+
'retry'?: number;
|
|
74
76
|
}): Promise<Response>;
|
|
75
77
|
/**
|
|
76
78
|
* --- 发起一个请求 ---
|
|
@@ -126,6 +128,8 @@ export interface IRequestOptions {
|
|
|
126
128
|
'type'?: 'form' | 'json';
|
|
127
129
|
/** --- 秒数,默认 300 秒 --- */
|
|
128
130
|
'timeout'?: number;
|
|
131
|
+
/** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
|
|
132
|
+
'retry'?: number;
|
|
129
133
|
/** --- 追踪 location 次数,0 为不追踪,默认为 0 --- */
|
|
130
134
|
'follow'?: number;
|
|
131
135
|
/** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
|
package/lib/undici.js
CHANGED
|
@@ -26,6 +26,12 @@ function getMproxyUrl(u, mproxy) {
|
|
|
26
26
|
}
|
|
27
27
|
/** --- 复用的 undici.agent 对象列表 --- */
|
|
28
28
|
const agents = new Map();
|
|
29
|
+
/** --- 可重试的网络异常代码 --- */
|
|
30
|
+
const retryErrorCodes = [
|
|
31
|
+
'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ENETDOWN', 'ENETUNREACH',
|
|
32
|
+
'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_SOCKET',
|
|
33
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'
|
|
34
|
+
];
|
|
29
35
|
/** --- 获取或创建 undici.agent 对象 --- */
|
|
30
36
|
function getAgent(opt = {}) {
|
|
31
37
|
let k = opt.reuse ?? 'default';
|
|
@@ -59,6 +65,27 @@ function getAgent(opt = {}) {
|
|
|
59
65
|
}
|
|
60
66
|
return agents.get(k);
|
|
61
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* --- 获取请求使用的 dispatcher ---
|
|
70
|
+
* @param opt 请求选项
|
|
71
|
+
* @param method 请求方法
|
|
72
|
+
* @param data 请求数据
|
|
73
|
+
* @returns 普通或带网络异常重试能力的 dispatcher
|
|
74
|
+
*/
|
|
75
|
+
function getDispatcher(opt, method, data) {
|
|
76
|
+
const agent = getAgent(opt);
|
|
77
|
+
const retry = (typeof opt.retry === 'number') && Number.isFinite(opt.retry) ?
|
|
78
|
+
Math.max(0, Math.floor(opt.retry)) : 0;
|
|
79
|
+
if ((retry === 0) || (data instanceof stream.Readable)) {
|
|
80
|
+
return agent;
|
|
81
|
+
}
|
|
82
|
+
return new undici.RetryAgent(agent, {
|
|
83
|
+
'maxRetries': retry,
|
|
84
|
+
'methods': [method],
|
|
85
|
+
'statusCodes': [],
|
|
86
|
+
'errorCodes': retryErrorCodes,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
62
89
|
/**
|
|
63
90
|
* --- 创建一个请求对象 ---
|
|
64
91
|
* @param u
|
|
@@ -109,9 +136,13 @@ export async function postJsonResponseJson(u, data, opt = {}) {
|
|
|
109
136
|
if (!rtn) {
|
|
110
137
|
return null;
|
|
111
138
|
}
|
|
112
|
-
const
|
|
139
|
+
const rtnStr = rtn.toString();
|
|
140
|
+
const json = lText.parseJson(rtnStr);
|
|
113
141
|
if (!json) {
|
|
114
|
-
|
|
142
|
+
if (opt.log === undefined || opt.log) {
|
|
143
|
+
lCore.log({}, `[UNDICI][POSTJSONRESPONSEJSON] parse json failed, url: ${u}, data: ${lText.stringifyJson(data)}, content: ${rtnStr}`, '-neterror');
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
115
146
|
}
|
|
116
147
|
return json;
|
|
117
148
|
}
|
|
@@ -129,9 +160,13 @@ export async function postResponseJson(u, data, opt = {}) {
|
|
|
129
160
|
if (!rtn) {
|
|
130
161
|
return null;
|
|
131
162
|
}
|
|
132
|
-
const
|
|
163
|
+
const rtnStr = rtn.toString();
|
|
164
|
+
const json = lText.parseJson(rtnStr);
|
|
133
165
|
if (!json) {
|
|
134
|
-
|
|
166
|
+
if (opt.log === undefined || opt.log) {
|
|
167
|
+
lCore.log({}, `[UNDICI][POSTRESPONSEJSON] parse json failed, url: ${u}, data: ${lText.stringifyJson(data)}, content: ${rtnStr}`, '-neterror');
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
135
170
|
}
|
|
136
171
|
return json;
|
|
137
172
|
}
|
|
@@ -147,9 +182,13 @@ export async function getResponseJson(u, opt = {}) {
|
|
|
147
182
|
if (!rtn) {
|
|
148
183
|
return null;
|
|
149
184
|
}
|
|
150
|
-
const
|
|
185
|
+
const rtnStr = rtn.toString();
|
|
186
|
+
const json = lText.parseJson(rtnStr);
|
|
151
187
|
if (!json) {
|
|
152
|
-
|
|
188
|
+
if (opt.log === undefined || opt.log) {
|
|
189
|
+
lCore.log({}, `[UNDICI][GETRESPONSEJSON] parse json failed, url: ${u}, content: ${rtnStr}`, '-neterror');
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
153
192
|
}
|
|
154
193
|
return json;
|
|
155
194
|
}
|
|
@@ -244,6 +283,7 @@ export async function fetch(input, init = {}) {
|
|
|
244
283
|
'headers': headers,
|
|
245
284
|
'hosts': init.hosts,
|
|
246
285
|
'mproxy': init.mproxy,
|
|
286
|
+
'retry': init.retry,
|
|
247
287
|
'signal': init.signal ?? undefined,
|
|
248
288
|
'follow': init.redirect === 'follow' ? 10 : 0,
|
|
249
289
|
};
|
|
@@ -387,7 +427,7 @@ export async function request(u, data, opt = {}) {
|
|
|
387
427
|
}
|
|
388
428
|
return res;
|
|
389
429
|
}
|
|
390
|
-
const
|
|
430
|
+
const dispatcher = getDispatcher(opt, method, data);
|
|
391
431
|
req = await undici.request(opt.mproxy ? getMproxyUrl(u, opt.mproxy) : u, {
|
|
392
432
|
'method': method,
|
|
393
433
|
'body': data,
|
|
@@ -395,7 +435,7 @@ export async function request(u, data, opt = {}) {
|
|
|
395
435
|
'headersTimeout': timeout * 1_000,
|
|
396
436
|
'bodyTimeout': timeout * 1_000,
|
|
397
437
|
'signal': opt.signal,
|
|
398
|
-
'dispatcher':
|
|
438
|
+
'dispatcher': dispatcher,
|
|
399
439
|
});
|
|
400
440
|
}
|
|
401
441
|
catch (err) {
|
package/package.json
CHANGED
package/sys/child.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-3 23:54
|
|
4
|
-
* Last: 2020-3-31 15:01:07, 2020-4-9 22:28:50, 2022-07-22 14:19:46, 2022-9-29 22:11:07, 2023-5-1 18:26:57, 2024-1-12 13:32:00, 2024-3-4 16:49:19, 2026-3-8 16:21:40
|
|
4
|
+
* Last: 2020-3-31 15:01:07, 2020-4-9 22:28:50, 2022-07-22 14:19:46, 2022-9-29 22:11:07, 2023-5-1 18:26:57, 2024-1-12 13:32:00, 2024-3-4 16:49:19, 2026-3-8 16:21:40, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as http2 from 'http2';
|
|
7
7
|
import * as tls from 'tls';
|
|
@@ -57,15 +57,18 @@ const http2Sessions = new Set();
|
|
|
57
57
|
* @param method 请求方法
|
|
58
58
|
*/
|
|
59
59
|
function wrapWithLinkCount(key, handler, errorPrefix, method = 'GET') {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const queryIndex = key.search(/[?#]/u);
|
|
61
|
+
/** --- 查询参数可能含敏感信息,请求计数与诊断仅保留路径 --- */
|
|
62
|
+
const requestKey = queryIndex === -1 ? key : key.slice(0, queryIndex);
|
|
63
|
+
linkCount[requestKey] = (linkCount[requestKey] ?? 0) + 1;
|
|
64
|
+
const trackId = sMonitor.track(requestKey, method);
|
|
65
|
+
Promise.resolve().then(handler).catch((e) => {
|
|
66
|
+
lCore.log({}, `${errorPrefix} ${lText.stringifyError(e)}`, '-error');
|
|
64
67
|
}).finally(() => {
|
|
65
68
|
sMonitor.untrack(trackId);
|
|
66
|
-
--linkCount[
|
|
67
|
-
if (!linkCount[
|
|
68
|
-
delete linkCount[
|
|
69
|
+
--linkCount[requestKey];
|
|
70
|
+
if (!linkCount[requestKey]) {
|
|
71
|
+
delete linkCount[requestKey];
|
|
69
72
|
}
|
|
70
73
|
});
|
|
71
74
|
}
|
|
@@ -295,8 +298,8 @@ async function requestHandler(req, res, https) {
|
|
|
295
298
|
'req': req,
|
|
296
299
|
'get': uri.query ? lText.queryParse(uri.query) : {},
|
|
297
300
|
'cookie': {},
|
|
298
|
-
'headers': {}
|
|
299
|
-
}, '[CHILD][requestHandler][E0]' + lText.
|
|
301
|
+
'headers': {},
|
|
302
|
+
}, '[CHILD][requestHandler][E0]' + lText.stringifyError(e), '-error');
|
|
300
303
|
const content = '<h1>500 Server Error</h1><hr>Kebab';
|
|
301
304
|
if (!res.headersSent) {
|
|
302
305
|
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
@@ -339,7 +342,7 @@ async function requestHandler(req, res, https) {
|
|
|
339
342
|
}
|
|
340
343
|
}
|
|
341
344
|
catch (e) {
|
|
342
|
-
lCore.log({}, '[CHILD][requestHandler][E1]' + lText.
|
|
345
|
+
lCore.log({}, '[CHILD][requestHandler][E1]' + lText.stringifyError(e), '-error');
|
|
343
346
|
const content = '<h1>500 Server Error</h1><hr>Kebab';
|
|
344
347
|
if (!res.headersSent) {
|
|
345
348
|
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
@@ -618,7 +621,7 @@ process.on('message', function (msg) {
|
|
|
618
621
|
}
|
|
619
622
|
}
|
|
620
623
|
})().catch(function (e) {
|
|
621
|
-
lCore.log({}, '[CHILD][process][message] ' + lText.
|
|
624
|
+
lCore.log({}, '[CHILD][process][message] ' + lText.stringifyError(e), '-error');
|
|
622
625
|
});
|
|
623
626
|
});
|
|
624
627
|
/**
|
package/sys/ctr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2020-3-14 17:24:38
|
|
4
|
-
* Last: 2020-3-30 15:31:40, 2022-07-22 16:59:00, 2022-09-12 23:51:56, 2022-09-23 15:53:58, 2022-12-29 01:18:08, 2023-2-28 20:07:57, 2023-12-27 18:39:35, 2024-3-1 19:38:53, 2024-4-9 16:03:58, 2025-2-12 18:55:44, 2025-6-12 16:56:08
|
|
4
|
+
* Last: 2020-3-30 15:31:40, 2022-07-22 16:59:00, 2022-09-12 23:51:56, 2022-09-23 15:53:58, 2022-12-29 01:18:08, 2023-2-28 20:07:57, 2023-12-27 18:39:35, 2024-3-1 19:38:53, 2024-4-9 16:03:58, 2025-2-12 18:55:44, 2025-6-12 16:56:08, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import ejs from 'ejs';
|
|
7
7
|
import * as lCore from '#kebab/lib/core.js';
|
|
@@ -128,7 +128,7 @@ export class Ctr {
|
|
|
128
128
|
}
|
|
129
129
|
})().catch(e => {
|
|
130
130
|
lCore.display('[ERROR][CTR][ASYNCTASK]', e);
|
|
131
|
-
lCore.log(this, '[CTR][_asyncTask] ' + lText.
|
|
131
|
+
lCore.log(this, '[CTR][_asyncTask] ' + lText.stringifyError(e), '-error');
|
|
132
132
|
--this._waitInfo.asyncTask.count;
|
|
133
133
|
if (!this._waitInfo.asyncTask.count) {
|
|
134
134
|
this._waitInfo.asyncTask.resolve();
|
|
@@ -367,7 +367,7 @@ export class Ctr {
|
|
|
367
367
|
}
|
|
368
368
|
catch (e) {
|
|
369
369
|
lCore.debug(`[CTR][_loadReactPage] ${e.message ?? ''}`);
|
|
370
|
-
lCore.log(this, '[CTR][_loadReactPage] ' + lText.
|
|
370
|
+
lCore.log(this, '[CTR][_loadReactPage] ' + lText.stringifyError(e), '-error');
|
|
371
371
|
return '';
|
|
372
372
|
}
|
|
373
373
|
}
|
package/sys/master.js
CHANGED
|
@@ -83,6 +83,37 @@ function normalizeArchivePath(archivePath) {
|
|
|
83
83
|
}
|
|
84
84
|
return `/${normalized}`;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* --- 递归判断 JSON 内容中是否包含受保护的配置参数 ---
|
|
88
|
+
* @param value 待检查的 JSON 值
|
|
89
|
+
* @returns 是否包含 pwd 或 skey 参数
|
|
90
|
+
*/
|
|
91
|
+
function hasSensitiveConfigKey(value) {
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.some(item => hasSensitiveConfigKey(item));
|
|
94
|
+
}
|
|
95
|
+
if ((value === null) || (typeof value !== 'object')) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
for (const key of Object.keys(value)) {
|
|
99
|
+
if ((key === 'pwd') || (key === 'skey')) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (hasSensitiveConfigKey(value[key])) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* --- 判断 ZIP 内的 kebab.json 是否包含受保护的配置参数 ---
|
|
110
|
+
* @param content ZIP 文件内容
|
|
111
|
+
* @returns 是否包含 pwd 或 skey 参数
|
|
112
|
+
*/
|
|
113
|
+
function hasSensitiveConfig(content) {
|
|
114
|
+
const json = lText.parseJson(typeof content === 'string' ? content : content.toString('utf8'));
|
|
115
|
+
return (json !== false) && hasSensitiveConfigKey(json);
|
|
116
|
+
}
|
|
86
117
|
/**
|
|
87
118
|
* --- 等待指定 worker 开始监听端口就绪 ---
|
|
88
119
|
* @param worker 要等待的 worker 对象
|
|
@@ -265,13 +296,17 @@ function createRpcListener() {
|
|
|
265
296
|
res.end('Path not found');
|
|
266
297
|
return;
|
|
267
298
|
}
|
|
299
|
+
// --- npm install 上限与 sendNpm 客户端超时一致,超时后终止,避免无限期运行 ---
|
|
300
|
+
const npmTimeout = 300_000;
|
|
268
301
|
let rtn = await lCore.exec('npm i --omit=dev', {
|
|
269
302
|
'cwd': msg.path,
|
|
303
|
+
'timeout': npmTimeout,
|
|
270
304
|
});
|
|
271
305
|
if (rtn === false) {
|
|
272
306
|
// --- 部分项目可能存在 peerDependencies 冲突,失败后降级为 legacy-peer-deps 再试一次 ---
|
|
273
307
|
rtn = await lCore.exec('npm i --omit=dev --legacy-peer-deps', {
|
|
274
308
|
'cwd': msg.path,
|
|
309
|
+
'timeout': npmTimeout,
|
|
275
310
|
});
|
|
276
311
|
}
|
|
277
312
|
if (rtn === false) {
|
|
@@ -490,8 +525,12 @@ function createRpcListener() {
|
|
|
490
525
|
// --- 开发、依赖、缓存、构建产物目录不覆盖 ---
|
|
491
526
|
continue;
|
|
492
527
|
}
|
|
493
|
-
if (
|
|
494
|
-
// ---
|
|
528
|
+
if (fname === 'kebab.json' && hasSensitiveConfig(ls[archivePath])) {
|
|
529
|
+
// --- 包含密码或密钥的 kebab.json 不覆盖,避免部署包覆盖现有敏感配置 ---
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (pat === 'conf/' && fname === 'config.json') {
|
|
533
|
+
// --- 全局配置文件不能覆盖 ---
|
|
495
534
|
continue;
|
|
496
535
|
}
|
|
497
536
|
if (fname.endsWith('.js.map') || fname.endsWith('.ts') || fname.endsWith('.tsx') || fname.endsWith('.scss') || fname.endsWith('.gitignore') || fname.endsWith('.DS_Store')) {
|
package/sys/monitor/watchdog.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2026-02-08
|
|
4
|
+
* Last: 2026-08-22
|
|
4
5
|
* --- 看门狗 Worker 线程,独立事件循环监控主线程心跳 ---
|
|
5
6
|
* --- 阻塞时通过 inspector.connectToMainThread() 远程抓取主线程 JS 调用栈和 CPU Profile ---
|
|
6
7
|
* --- 看门狗必须最小依赖、最大自治,不引入项目库,确保主线程异常时仍能可靠运行 ---
|
|
@@ -10,7 +11,7 @@ import * as fs from 'fs';
|
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
import * as inspector from 'inspector';
|
|
12
13
|
const data = workerThreads.workerData;
|
|
13
|
-
const view = new
|
|
14
|
+
const view = new Uint32Array(data.buffer);
|
|
14
15
|
/** --- 上次告警时间(秒级时间戳) --- */
|
|
15
16
|
let lastAlertTime = 0;
|
|
16
17
|
/** --- 是否正在诊断采集 --- */
|
|
@@ -66,8 +67,8 @@ function captureDiag(blockSec) {
|
|
|
66
67
|
return;
|
|
67
68
|
}
|
|
68
69
|
capturing = true;
|
|
69
|
-
const diagDir = path.join(data.logDir, 'monitor', String(data.pid));
|
|
70
70
|
const ts = fmtTs();
|
|
71
|
+
const diagDir = path.join(data.logDir, 'monitor', ts.slice(0, 4), ts.slice(4, 6), ts.slice(6, 8), `${ts.slice(8)}-pid-${data.pid}`);
|
|
71
72
|
const session = new inspector.Session();
|
|
72
73
|
try {
|
|
73
74
|
session.connectToMainThread();
|
|
@@ -152,13 +153,15 @@ function captureDiag(blockSec) {
|
|
|
152
153
|
// --- 写入堆栈文件 ---
|
|
153
154
|
try {
|
|
154
155
|
fs.mkdirSync(diagDir, {
|
|
155
|
-
'recursive': true, 'mode':
|
|
156
|
+
'recursive': true, 'mode': 0o700,
|
|
156
157
|
});
|
|
158
|
+
fs.chmodSync(diagDir, 0o700);
|
|
157
159
|
const content = `Event Loop Blocked: ${blockSec}s\n` +
|
|
158
160
|
`Captured: ${new Date().toISOString()}\n` +
|
|
159
161
|
`PID: ${data.pid}\n\n` +
|
|
160
162
|
`Call Stack:\n${stackLines.join('\n')}\n`;
|
|
161
|
-
fs.writeFileSync(path.join(diagDir, `blocked-stack-${ts}.txt`), content, { 'mode':
|
|
163
|
+
fs.writeFileSync(path.join(diagDir, `blocked-stack-${ts}.txt`), content, { 'mode': 0o600 });
|
|
164
|
+
fs.chmodSync(path.join(diagDir, `blocked-stack-${ts}.txt`), 0o600);
|
|
162
165
|
}
|
|
163
166
|
catch {
|
|
164
167
|
// --- 忽略 ---
|
|
@@ -230,7 +233,8 @@ function collectProfile(session, diagDir, ts) {
|
|
|
230
233
|
session.post('Profiler.stop', (err3, r) => {
|
|
231
234
|
if (!err3 && r?.profile) {
|
|
232
235
|
try {
|
|
233
|
-
fs.writeFileSync(path.join(diagDir, `blocked-cpu-${ts}.cpuprofile`), JSON.stringify(r.profile), { 'mode':
|
|
236
|
+
fs.writeFileSync(path.join(diagDir, `blocked-cpu-${ts}.cpuprofile`), JSON.stringify(r.profile), { 'mode': 0o600 });
|
|
237
|
+
fs.chmodSync(path.join(diagDir, `blocked-cpu-${ts}.cpuprofile`), 0o600);
|
|
234
238
|
}
|
|
235
239
|
catch {
|
|
236
240
|
// --- 忽略 ---
|