@maiyunnet/kebab 9.15.6 → 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/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.15.6";
8
+ export declare const VER = "9.16.0";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.15.6';
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
@@ -352,12 +352,31 @@ export declare function debug(message?: any, ...optionalParams: any[]): void;
352
352
  */
353
353
  export declare function display(message?: any, ...optionalParams: any[]): void;
354
354
  /**
355
- * --- res 发送头部(前提是头部没有被发送才能调用本方法 ---
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
+ *
356
364
  * @param res 响应对象
357
- * @param statusCode 状态码
358
- * @param headers 头部
365
+ * @param statusCode HTTP 状态码
366
+ * @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
367
+ * @returns 无返回值
359
368
  */
360
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
+ */
361
380
  export declare function writeEventStreamHead(res: http2.Http2ServerResponse | http.ServerResponse): void;
362
381
  /**
363
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';
@@ -1221,10 +1221,19 @@ export function display(message, ...optionalParams) {
1221
1221
  console.log(`KE-DISPLAY ${lTime.format(null, 'Y-m-d H:i:s')}`, message, ...optionalParams);
1222
1222
  }
1223
1223
  /**
1224
- * --- res 发送头部(前提是头部没有被发送才能调用本方法 ---
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
+ *
1225
1233
  * @param res 响应对象
1226
- * @param statusCode 状态码
1227
- * @param headers 头部
1234
+ * @param statusCode HTTP 状态码
1235
+ * @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
1236
+ * @returns 无返回值
1228
1237
  */
1229
1238
  export function writeHead(res, statusCode, headers) {
1230
1239
  if (res instanceof http2.Http2ServerResponse) {
@@ -1234,6 +1243,16 @@ export function writeHead(res, statusCode, headers) {
1234
1243
  res.writeHead(statusCode, headers);
1235
1244
  }
1236
1245
  }
1246
+ /**
1247
+ * --- 提交服务器发送事件(SSE)响应头 ---
1248
+ *
1249
+ * 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
1250
+ * SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
1251
+ * 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
1252
+ *
1253
+ * @param res 响应对象
1254
+ * @returns 无返回值
1255
+ */
1237
1256
  export function writeEventStreamHead(res) {
1238
1257
  writeHead(res, 200, {
1239
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
@@ -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
@@ -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
@@ -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';
@@ -28,7 +28,8 @@ export class Response {
28
28
  return this._req ? await lBuffer.getFull(stream) : null;
29
29
  }
30
30
  catch (e) {
31
- lCore.log({}, '[Undici][Response][getContent] ' + e.message, '-error');
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 json = lText.parseJson(rtn.toString());
139
+ const rtnStr = rtn.toString();
140
+ const json = lText.parseJson(rtnStr);
113
141
  if (!json) {
114
- return null;
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 json = lText.parseJson(rtn.toString());
163
+ const rtnStr = rtn.toString();
164
+ const json = lText.parseJson(rtnStr);
133
165
  if (!json) {
134
- return null;
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 json = lText.parseJson(rtn.toString());
185
+ const rtnStr = rtn.toString();
186
+ const json = lText.parseJson(rtnStr);
151
187
  if (!json) {
152
- return null;
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 agent = getAgent(opt);
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': agent,
438
+ 'dispatcher': dispatcher,
399
439
  });
400
440
  }
401
441
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.15.6",
3
+ "version": "9.16.0",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
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
- linkCount[key] = (linkCount[key] ?? 0) + 1;
61
- const trackId = sMonitor.track(key, method);
62
- handler().catch((e) => {
63
- lCore.log({}, `${errorPrefix} ${lText.stringifyJson(e.stack).slice(1, -1)}`, '-error');
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[key];
67
- if (!linkCount[key]) {
68
- delete linkCount[key];
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.stringifyJson(e.stack).slice(1, -1), '-error');
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.stringifyJson(e.stack).slice(1, -1), '-error');
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.stringifyJson(e.stack).slice(1, -1), '-error');
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.stringifyJson(e.stack).slice(1, -1), '-error');
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.stringifyJson(e.stack).slice(1, -1), '-error');
370
+ lCore.log(this, '[CTR][_loadReactPage] ' + lText.stringifyError(e), '-error');
371
371
  return '';
372
372
  }
373
373
  }
@@ -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 Int32Array(data.buffer);
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': 0o777,
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': 0o777 });
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': 0o777 });
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
  // --- 忽略 ---
package/sys/monitor.d.ts CHANGED
@@ -3,15 +3,18 @@ interface ISnapshotRequest {
3
3
  'url': string;
4
4
  'method': string;
5
5
  'duration': number;
6
+ /** --- 请求存续期间的进程整体用户态 CPU 增量(微秒),不代表请求独占 --- */
6
7
  'cpuUser': number;
8
+ /** --- 请求存续期间的进程整体系统态 CPU 增量(微秒),不代表请求独占 --- */
7
9
  'cpuSystem': number;
10
+ /** --- 请求存续期间的进程整体 RSS 增量(bytes),不代表请求独占 --- */
8
11
  'memDelta': number;
9
12
  }
10
13
  /** --- 整体资源快照 --- */
11
14
  export interface ISnapshot {
12
15
  'pid': number;
13
16
  'time': number;
14
- /** --- 本进程 CPU 占用(单核基准,0-100 --- */
17
+ /** --- 本进程 CPU 占用,100 代表占满一个逻辑核心,多线程时可超过 100 --- */
15
18
  'cpuProcess': number;
16
19
  /** --- 系统总 CPU 占用(所有核心合计,0-100,与任务管理器一致) --- */
17
20
  'cpuOs': number;
@@ -32,7 +35,9 @@ export interface ISnapshot {
32
35
  'free': number;
33
36
  };
34
37
  'eloopLag': number;
38
+ /** --- 最早开始的活跃请求,最多 100 条 --- */
35
39
  'activeRequests': ISnapshotRequest[];
40
+ /** --- 全部活跃请求数量,可能大于 activeRequests.length --- */
36
41
  'activeCount': number;
37
42
  }
38
43
  /**
@@ -46,6 +51,8 @@ export declare function start(opt?: {
46
51
  'mem'?: number;
47
52
  /** --- 事件循环延迟阈值 ms,默认 500 --- */
48
53
  'eloop'?: number;
54
+ /** --- 内存超阈值时是否采集堆快照,默认 false --- */
55
+ 'heapSnapshot'?: boolean;
49
56
  }): void;
50
57
  /**
51
58
  * --- 停止性能监控 ---