@maiyunnet/kebab 9.13.8 → 9.14.1

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.13.8";
8
+ export declare const VER = "9.14.1";
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.13.8';
9
+ export const VER = '9.14.1';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/lib/core.js CHANGED
@@ -307,10 +307,11 @@ export function muid(ctr, opt = {}) {
307
307
  export function ip(ctr, req) {
308
308
  const headers = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
309
309
  if (typeof headers['cf-connecting-ip'] === 'string') {
310
- return headers['cf-connecting-ip'];
310
+ return normalizeIP(headers['cf-connecting-ip']);
311
311
  }
312
312
  else if (typeof headers['x-forwarded-for'] === 'string') {
313
- return headers['x-forwarded-for'];
313
+ // --- X-Forwarded-For 可能为逗号分隔的多个 IP,取第一个即客户端 IP ---
314
+ return normalizeIP(headers['x-forwarded-for'].split(',')[0].trim());
314
315
  }
315
316
  else {
316
317
  if (!req) {
@@ -321,21 +322,37 @@ export function ip(ctr, req) {
321
322
  return '';
322
323
  }
323
324
  }
324
- return req.socket.remoteAddress ?? '';
325
+ return normalizeIP(req.socket.remoteAddress ?? '');
325
326
  }
326
327
  }
327
328
  /** --- 获取 CF 和 X 的 IP --- */
328
329
  export function ips(ctr) {
329
330
  const headers = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
330
331
  return {
331
- 'cf': typeof headers['cf-connecting-ip'] === 'string' ? headers['cf-connecting-ip'] : '',
332
- 'x': typeof headers['x-forwarded-for'] === 'string' ? headers['x-forwarded-for'] : ''
332
+ 'cf': typeof headers['cf-connecting-ip'] === 'string' ? normalizeIP(headers['cf-connecting-ip']) : '',
333
+ 'x': typeof headers['x-forwarded-for'] === 'string' ? normalizeIP(headers['x-forwarded-for'].split(',')[0].trim()) : ''
333
334
  };
334
335
  }
335
336
  /** --- 使用 X-Forwarded-For 的 CDN 厂商 --- */
336
337
  export const REAL_IP_X = 'x-forwarded-for';
337
338
  /** --- 使用的是 Cloudflare --- */
338
339
  export const REAL_IP_CF = 'cf-connecting-ip';
340
+ /**
341
+ * --- 规范化 IP 地址 ---
342
+ * --- 将 IPv4-mapped IPv6(如 ::ffff:127.0.0.1)转换为纯 IPv4,原生 IPv6 保持不变 ---
343
+ * @param ip 原始 IP 地址
344
+ */
345
+ function normalizeIP(ip) {
346
+ if (!ip) {
347
+ return '';
348
+ }
349
+ const value = ip.trim();
350
+ const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(value);
351
+ if (mapped) {
352
+ return mapped[1];
353
+ }
354
+ return value;
355
+ }
339
356
  /**
340
357
  * --- 获取直连 IP(安全 IP) ---
341
358
  * @param ctr
@@ -346,11 +363,11 @@ export function realIP(ctr, name = '') {
346
363
  if (name !== '') {
347
364
  const value = headers[name];
348
365
  if (typeof value === 'string') {
349
- return value;
366
+ return normalizeIP(value);
350
367
  }
351
368
  }
352
369
  const req = ctr.getPrototype('_req');
353
- return req.socket.remoteAddress ?? '';
370
+ return normalizeIP(req.socket.remoteAddress ?? '');
354
371
  }
355
372
  /**
356
373
  * --- 间隔一段时间 ---
package/lib/crypto.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2019-4-2 14:01:06
4
- * Last: 2020-3-12 14:05:24, 2022-09-12 11:52:35, 2024-9-8 17:09:39, 2024-11-11 00:21:58, 2025-6-18 20:27:47
4
+ * Last: 2020-3-12 14:05:24, 2022-09-12 11:52:35, 2024-9-8 17:09:39, 2024-11-11 00:21:58, 2025-6-18 20:27:47, 2026-07-29 11:51
5
5
  */
6
6
  import * as crypto from 'crypto';
7
7
  /**
@@ -31,8 +31,8 @@ export declare function generateKeyPair(type: string, options?: {
31
31
  * @param format 输出格式
32
32
  * @param algorithm 哈希方式
33
33
  */
34
- export declare function sign(data: crypto.BinaryLike, privateKey: crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput | crypto.SignJsonWebKeyInput, format: 'hex' | 'base64' | 'binary', algorithm?: string): string;
35
- export declare function sign(data: crypto.BinaryLike, privateKey: crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput | crypto.SignJsonWebKeyInput, format?: 'buffer', algorithm?: string): Buffer;
34
+ export declare function sign(data: string | NodeJS.ArrayBufferView, privateKey: crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput | crypto.SignJsonWebKeyInput, format: 'hex' | 'base64' | 'binary', algorithm?: string): string;
35
+ export declare function sign(data: string | NodeJS.ArrayBufferView, privateKey: crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput | crypto.SignJsonWebKeyInput, format?: 'buffer', algorithm?: string): Buffer;
36
36
  /**
37
37
  * --- 非对称验签 ---
38
38
  * @param data 数据
@@ -40,31 +40,31 @@ export declare function sign(data: crypto.BinaryLike, privateKey: crypto.KeyLike
40
40
  * @param signature 签名
41
41
  * @param algorithm 哈希方式
42
42
  */
43
- export declare function verify(data: crypto.BinaryLike, object: crypto.KeyLike | crypto.VerifyKeyObjectInput | crypto.VerifyPublicKeyInput | crypto.VerifyJsonWebKeyInput, signature: NodeJS.ArrayBufferView, algorithm?: string): boolean;
43
+ export declare function verify(data: string | NodeJS.ArrayBufferView, object: crypto.KeyLike | crypto.VerifyKeyObjectInput | crypto.VerifyPublicKeyInput | crypto.VerifyJsonWebKeyInput, signature: NodeJS.ArrayBufferView, algorithm?: string): boolean;
44
44
  /**
45
45
  * --- 非对称公钥加密 ---
46
46
  * @param key 公钥
47
47
  * @param buffer 数据
48
48
  */
49
- export declare function publicEncrypt(key: crypto.RsaPublicKey | crypto.RsaPrivateKey | crypto.KeyLike, buffer: NodeJS.ArrayBufferView | string): Buffer;
49
+ export declare function publicEncrypt(key: crypto.KeyLike | crypto.PublicKeyInput | crypto.PrivateKeyInput, buffer: NodeJS.ArrayBufferView | string): Buffer;
50
50
  /**
51
51
  * --- 非对称私钥加密 ---
52
52
  * @param key 私钥
53
53
  * @param buffer 数据
54
54
  */
55
- export declare function privateEncrypt(key: crypto.RsaPrivateKey | crypto.KeyLike, buffer: NodeJS.ArrayBufferView | string): Buffer;
55
+ export declare function privateEncrypt(key: crypto.KeyLike | crypto.PrivateKeyInput, buffer: NodeJS.ArrayBufferView | string): Buffer;
56
56
  /**
57
57
  * --- 非对称公钥解密 ---
58
58
  * @param key 公钥
59
59
  * @param buffer 数据
60
60
  */
61
- export declare function publicDecrypt(key: crypto.RsaPublicKey | crypto.RsaPrivateKey | crypto.KeyLike, buffer: NodeJS.ArrayBufferView | string): Buffer;
61
+ export declare function publicDecrypt(key: crypto.KeyLike | crypto.PublicKeyInput | crypto.PrivateKeyInput, buffer: NodeJS.ArrayBufferView | string): Buffer;
62
62
  /**
63
63
  * --- 非对称私钥解密 ---
64
64
  * @param key 私钥
65
65
  * @param buffer 数据
66
66
  */
67
- export declare function privateDecrypt(key: crypto.RsaPrivateKey | crypto.KeyLike, buffer: NodeJS.ArrayBufferView | string): Buffer;
67
+ export declare function privateDecrypt(key: crypto.KeyLike | crypto.PrivateKeyInput, buffer: NodeJS.ArrayBufferView | string): Buffer;
68
68
  /** --- 勿使用,无 iv 默认,但勿使用 --- */
69
69
  export declare const AES_256_ECB = "aes-256-ecb";
70
70
  /** --- 一般不用,兼容性场景下用 --- */
@@ -86,7 +86,7 @@ export declare const SM4_CFB = "sm4-cfb";
86
86
  * @param method 加密方法
87
87
  * @param output 输出类型
88
88
  */
89
- export declare function cipherEncrypt(original: string | Buffer, key: crypto.CipherKey, iv?: string, method?: string, output?: 'base64' | 'buffer'): string | Buffer | false;
89
+ export declare function cipherEncrypt(original: string | Buffer, key: crypto.KeyLike, iv?: string, method?: string, output?: 'base64' | 'buffer'): string | Buffer | false;
90
90
  /**
91
91
  * --- AES 加密 ---
92
92
  * @param original 原始字符串
@@ -95,16 +95,16 @@ export declare function cipherEncrypt(original: string | Buffer, key: crypto.Cip
95
95
  * @param method 加密方法
96
96
  * @param output 输出类型
97
97
  */
98
- export declare function aesEncrypt(original: string | Buffer, key: crypto.CipherKey, iv: string, method: string, output: 'buffer'): Buffer | false;
99
- export declare function aesEncrypt(original: string | Buffer, key: crypto.CipherKey, iv?: string, method?: string, output?: 'base64'): string | false;
98
+ export declare function aesEncrypt(original: string | Buffer, key: crypto.KeyLike, iv: string, method: string, output: 'buffer'): Buffer | false;
99
+ export declare function aesEncrypt(original: string | Buffer, key: crypto.KeyLike, iv?: string, method?: string, output?: 'base64'): string | false;
100
100
  /**
101
101
  * --- AES GCM 托管加密 ---
102
102
  * @param original 原始字符串
103
103
  * @param key 密钥尽量 32 个英文字母和数字,不是 32 个系统会自动处理
104
104
  * @param output 输出类型
105
105
  */
106
- export declare function gcmEncrypt(original: string | Buffer, key: crypto.CipherKey, output: 'buffer'): Buffer | false;
107
- export declare function gcmEncrypt(original: string | Buffer, key: crypto.CipherKey, output?: 'base64'): string | false;
106
+ export declare function gcmEncrypt(original: string | Buffer, key: crypto.KeyLike, output: 'buffer'): Buffer | false;
107
+ export declare function gcmEncrypt(original: string | Buffer, key: crypto.KeyLike, output?: 'base64'): string | false;
108
108
  /**
109
109
  * --- SM4 加密 ---
110
110
  * @param original 原始字符串
@@ -112,8 +112,8 @@ export declare function gcmEncrypt(original: string | Buffer, key: crypto.Cipher
112
112
  * @param iv 向量 16 个英文字母和数字
113
113
  * @param method 加密方法
114
114
  */
115
- export declare function sm4Encrypt(original: string | Buffer, key: crypto.CipherKey, iv: string, method: string, output: 'buffer'): Buffer | false;
116
- export declare function sm4Encrypt(original: string | Buffer, key: crypto.CipherKey, iv?: string, method?: string, output?: 'base64'): string | false;
115
+ export declare function sm4Encrypt(original: string | Buffer, key: crypto.KeyLike, iv: string, method: string, output: 'buffer'): Buffer | false;
116
+ export declare function sm4Encrypt(original: string | Buffer, key: crypto.KeyLike, iv?: string, method?: string, output?: 'base64'): string | false;
117
117
  /**
118
118
  * --- cipher 解密 ---
119
119
  * @param encrypt 需解密的字符串
@@ -122,7 +122,7 @@ export declare function sm4Encrypt(original: string | Buffer, key: crypto.Cipher
122
122
  * @param method 加密方法
123
123
  * @param output 输出类型
124
124
  */
125
- export declare function cipherDecrypt(encrypt: string | Buffer, key: crypto.CipherKey, iv?: string, method?: string, output?: 'binary' | 'buffer'): string | Buffer | false;
125
+ export declare function cipherDecrypt(encrypt: string | Buffer, key: crypto.KeyLike, iv?: string, method?: string, output?: 'binary' | 'buffer'): string | Buffer | false;
126
126
  /**
127
127
  * --- AES 解密 ---
128
128
  * @param encrypt 需解密的字符串
@@ -130,16 +130,16 @@ export declare function cipherDecrypt(encrypt: string | Buffer, key: crypto.Ciph
130
130
  * @param iv 向量 16(CTR) 或 12(GCM) 个英文字母和数字
131
131
  * @param method 加密方法
132
132
  */
133
- export declare function aesDecrypt(encrypt: string | Buffer, key: crypto.CipherKey, iv: string, method: string, output: 'buffer'): Buffer | false;
134
- export declare function aesDecrypt(encrypt: string | Buffer, key: crypto.CipherKey, iv?: string, method?: string, output?: 'binary'): string | false;
133
+ export declare function aesDecrypt(encrypt: string | Buffer, key: crypto.KeyLike, iv: string, method: string, output: 'buffer'): Buffer | false;
134
+ export declare function aesDecrypt(encrypt: string | Buffer, key: crypto.KeyLike, iv?: string, method?: string, output?: 'binary'): string | false;
135
135
  /**
136
136
  * --- AES 解密 ---
137
137
  * @param encrypt 需解密的字符串
138
138
  * @param key 密钥 32 个英文字母和数字
139
139
  * @param output 输出类型
140
140
  */
141
- export declare function gcmDecrypt(encrypt: string | Buffer, key: crypto.CipherKey, output: 'buffer'): Buffer | false;
142
- export declare function gcmDecrypt(encrypt: string | Buffer, key: crypto.CipherKey, output?: 'binary'): string | false;
141
+ export declare function gcmDecrypt(encrypt: string | Buffer, key: crypto.KeyLike, output: 'buffer'): Buffer | false;
142
+ export declare function gcmDecrypt(encrypt: string | Buffer, key: crypto.KeyLike, output?: 'binary'): string | false;
143
143
  /**
144
144
  * --- SM4 解密 ---
145
145
  * @param encrypt 需解密的字符串
@@ -147,24 +147,24 @@ export declare function gcmDecrypt(encrypt: string | Buffer, key: crypto.CipherK
147
147
  * @param iv 向量 16 个英文字母和数字
148
148
  * @param method 加密方法
149
149
  */
150
- export declare function sm4Decrypt(encrypt: string | Buffer, key: crypto.CipherKey, iv: string, method: string, output: 'buffer'): Buffer | false;
151
- export declare function sm4Decrypt(encrypt: string | Buffer, key: crypto.CipherKey, iv?: string, method?: string, output?: 'binary'): string | false;
150
+ export declare function sm4Decrypt(encrypt: string | Buffer, key: crypto.KeyLike, iv: string, method: string, output: 'buffer'): Buffer | false;
151
+ export declare function sm4Decrypt(encrypt: string | Buffer, key: crypto.KeyLike, iv?: string, method?: string, output?: 'binary'): string | false;
152
152
  /**
153
153
  * --- hash 或 hmac 加密 ---
154
154
  * @param algorithm 哈希方式
155
155
  * @param data 源数据
156
156
  * @param key 设置则采用 hmac 加密
157
157
  */
158
- export declare function hashHmac(algorithm: string, data: Buffer | string, key?: crypto.CipherKey, format?: 'hex' | 'base64'): string;
159
- export declare function hashHmac(algorithm: string, data: Buffer | string, key: crypto.CipherKey | undefined, format: 'buffer'): Buffer;
158
+ export declare function hashHmac(algorithm: string, data: Buffer | string, key?: crypto.KeyLike, format?: 'hex' | 'base64'): string;
159
+ export declare function hashHmac(algorithm: string, data: Buffer | string, key: crypto.KeyLike | undefined, format: 'buffer'): Buffer;
160
160
  /**
161
161
  * --- hash 或 hmac 加密文件 ---
162
162
  * @param algorithm 加密方式,如 md5、sha256、sm3 等
163
163
  * @param path 文件路径
164
164
  * @param key 设置则采用 hmac 加密
165
165
  */
166
- export declare function hashHmacFile(algorithm: string, path: string, key?: crypto.CipherKey, encoding?: 'hex' | 'base64' | 'base64url'): Promise<string | false>;
167
- export declare function hashHmacFile(algorithm: string, path: string, key: crypto.CipherKey, encoding: 'buffer'): Promise<Buffer | false>;
166
+ export declare function hashHmacFile(algorithm: string, path: string, key?: crypto.KeyLike, encoding?: 'hex' | 'base64' | 'base64url'): Promise<string | false>;
167
+ export declare function hashHmacFile(algorithm: string, path: string, key: crypto.KeyLike, encoding: 'buffer'): Promise<Buffer | false>;
168
168
  /**
169
169
  * --- base64 编码 ---
170
170
  * @param data 字符串或 Buffer
package/lib/crypto.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2019-4-2 14:01:06
4
- * Last: 2020-3-12 14:05:24, 2022-09-12 11:52:35, 2024-9-8 17:09:39, 2024-11-11 00:21:58, 2025-6-18 20:27:47
4
+ * Last: 2020-3-12 14:05:24, 2022-09-12 11:52:35, 2024-9-8 17:09:39, 2024-11-11 00:21:58, 2025-6-18 20:27:47, 2026-07-29 11:51
5
5
  */
6
6
  import * as crypto from 'crypto';
7
7
  // --- 库和定义 ---
package/lib/sql.d.ts CHANGED
@@ -42,6 +42,8 @@ export declare class Sql {
42
42
  private _placeholderCounter;
43
43
  /** --- 是否忽略错误 --- */
44
44
  private _ignore;
45
+ /** --- MySQL 优化器 Hint,如 INDEX(table idx) --- */
46
+ private _hint;
45
47
  constructor(opt: {
46
48
  'service': ESERVICE;
47
49
  'ctr'?: ctr.Ctr;
@@ -51,6 +53,30 @@ export declare class Sql {
51
53
  'sql'?: string[];
52
54
  'alias'?: string[];
53
55
  });
56
+ /**
57
+ * --- 设置 MySQL 优化器 Hint ---
58
+ * --- 会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
59
+ * --- 传入 hint 内容即可,无需包裹注释符号 ---
60
+ * @param h Hint 内容
61
+ * @example
62
+ * // --- 强制走指定索引(最常用) ---
63
+ * INDEX(`supply_date_0_0` `idx_supply_date_query`)
64
+ *
65
+ * // --- 指定某表不用某索引 ---
66
+ * NO_INDEX(`supply_date_0_0` `idx_old`)
67
+ *
68
+ * // --- 指定 JOIN 顺序和索引 ---
69
+ * JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)
70
+ *
71
+ * // --- 指定 JOIN 中某表使用的索引 ---
72
+ * JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)
73
+ *
74
+ * // --- 多表多索引组合 ---
75
+ * INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)
76
+ *
77
+ * // --- 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html ---
78
+ */
79
+ hint(h: string): this;
54
80
  /**
55
81
  * --- 插入数据前导 ---
56
82
  * @param table 表名
@@ -303,4 +329,4 @@ export declare function value(val: kebab.DbValue): {
303
329
  * --- 将对象转换为 JSON 字符串并避开类型检查,用于适配 PostgreSQL 的 jsonb 字段 ---
304
330
  * @param obj 要转换的 JSON 对象
305
331
  */
306
- export declare function json(obj: kebab.Json): any;
332
+ export declare function json<T>(obj: T): T;
package/lib/sql.js CHANGED
@@ -45,6 +45,8 @@ export class Sql {
45
45
  _placeholderCounter = 1;
46
46
  /** --- 是否忽略错误 --- */
47
47
  _ignore = false;
48
+ /** --- MySQL 优化器 Hint,如 INDEX(table idx) --- */
49
+ _hint = '';
48
50
  // --- 实例化 ---
49
51
  constructor(opt) {
50
52
  this._ctr = opt.ctr;
@@ -60,6 +62,33 @@ export class Sql {
60
62
  this._alias = opt.alias;
61
63
  }
62
64
  }
65
+ /**
66
+ * --- 设置 MySQL 优化器 Hint ---
67
+ * --- 会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
68
+ * --- 传入 hint 内容即可,无需包裹注释符号 ---
69
+ * @param h Hint 内容
70
+ * @example
71
+ * // --- 强制走指定索引(最常用) ---
72
+ * INDEX(`supply_date_0_0` `idx_supply_date_query`)
73
+ *
74
+ * // --- 指定某表不用某索引 ---
75
+ * NO_INDEX(`supply_date_0_0` `idx_old`)
76
+ *
77
+ * // --- 指定 JOIN 顺序和索引 ---
78
+ * JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)
79
+ *
80
+ * // --- 指定 JOIN 中某表使用的索引 ---
81
+ * JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)
82
+ *
83
+ * // --- 多表多索引组合 ---
84
+ * INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)
85
+ *
86
+ * // --- 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html ---
87
+ */
88
+ hint(h) {
89
+ this._hint = h;
90
+ return this;
91
+ }
63
92
  // --- 前导 ---
64
93
  /**
65
94
  * --- 插入数据前导 ---
@@ -880,7 +909,7 @@ export class Sql {
880
909
  'data': data,
881
910
  'sql': sql,
882
911
  'alias': lCore.clone(this._alias),
883
- });
912
+ }).hint(this._hint);
884
913
  }
885
914
  // --- 操作 ---
886
915
  /**
@@ -888,6 +917,10 @@ export class Sql {
888
917
  */
889
918
  getSql() {
890
919
  let sql = this._sql.join('');
920
+ // --- 注入优化器 Hint 到 SELECT 关键字后 ---
921
+ if (this._hint && !sql.includes('/*+')) {
922
+ sql = sql.replace(/^SELECT /i, `SELECT /*+ ${this._hint} */ `);
923
+ }
891
924
  if (this._pre) {
892
925
  return this._alias.reduce((result, item) => {
893
926
  if (this._service === ESERVICE.MYSQL) {
@@ -31,7 +31,7 @@ export declare class Response {
31
31
  /**
32
32
  * --- 获取响应读取流对象 ---
33
33
  */
34
- getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
34
+ getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress | null;
35
35
  /**
36
36
  * --- 获取原生响应读取流对象 ---
37
37
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.13.8",
3
+ "version": "9.14.1",
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": [
@@ -22,47 +22,47 @@
22
22
  "#kebab/*": "./*"
23
23
  },
24
24
  "dependencies": {
25
- "@aws-sdk/client-s3": "^3.1047.0",
26
- "@aws-sdk/lib-storage": "^3.1047.0",
25
+ "@aws-sdk/client-s3": "^3.1096.0",
26
+ "@aws-sdk/lib-storage": "^3.1096.0",
27
27
  "@litert/http-client": "^1.1.2",
28
28
  "@litert/mime": "^0.1.3",
29
29
  "@litert/redis": "^3.2.1",
30
30
  "@litert/websocket": "^0.2.8",
31
- "@radix-ui/react-checkbox": "^1.3.3",
32
- "@radix-ui/react-label": "^2.1.8",
33
- "@radix-ui/react-slot": "^1.2.4",
34
- "@radix-ui/react-switch": "^1.2.6",
35
- "@tailwindcss/cli": "^4.3.0",
31
+ "@radix-ui/react-checkbox": "^1.3.11",
32
+ "@radix-ui/react-label": "^2.1.15",
33
+ "@radix-ui/react-slot": "^1.3.3",
34
+ "@radix-ui/react-switch": "^1.3.7",
35
+ "@tailwindcss/cli": "^4.3.3",
36
36
  "@types/ssh2": "^1.15.5",
37
37
  "ajv": "^8.20.0",
38
38
  "ajv-formats": "^3.0.1",
39
39
  "class-variance-authority": "^0.7.1",
40
40
  "clsx": "^2.1.1",
41
- "ejs": "^5.0.2",
42
- "esbuild": "^0.28.0",
41
+ "ejs": "^6.0.1",
42
+ "esbuild": "^0.28.1",
43
43
  "jszip": "^3.10.1",
44
- "mysql2": "^3.22.3",
45
- "node-cron": "^4.2.1",
46
- "openai": "^6.37.0",
47
- "pg": "^8.20.0",
48
- "react": "^19.2.6",
49
- "react-dom": "^19.2.6",
50
- "react-router-dom": "^7.15.1",
44
+ "mysql2": "^3.23.2",
45
+ "node-cron": "^4.6.0",
46
+ "openai": "^7.0.0",
47
+ "pg": "^8.22.0",
48
+ "react": "^19.2.8",
49
+ "react-dom": "^19.2.8",
50
+ "react-router-dom": "^7.18.1",
51
51
  "ssh2": "^1.17.0",
52
52
  "svg-captcha": "^1.4.0",
53
53
  "tailwind-merge": "^3.6.0",
54
- "tencentcloud-sdk-nodejs": "^4.1.232",
55
- "undici": "^8.3.0"
54
+ "tencentcloud-sdk-nodejs": "^4.1.277",
55
+ "undici": "^8.9.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@litert/eslint-plugin-rules": "^0.3.1",
59
59
  "@types/ejs": "^3.1.5",
60
- "@types/node": "^25.8.0",
60
+ "@types/node": "^26.1.2",
61
61
  "@types/pg": "^8.20.0",
62
- "@types/react": "^19.2.14",
62
+ "@types/react": "^19.2.17",
63
63
  "@types/react-dom": "^19.2.3",
64
- "typedoc": "^0.28.19",
65
- "typedoc-plugin-markdown": "^4.11.0",
64
+ "typedoc": "^0.28.20",
65
+ "typedoc-plugin-markdown": "^4.12.0",
66
66
  "typescript": "^6.0.3"
67
67
  }
68
68
  }
package/sys/child.js CHANGED
@@ -47,6 +47,8 @@ let http2Server;
47
47
  const linkCount = {};
48
48
  /** --- 是否正在停止,停止时对 HTTP/1.1 响应追加 Connection: close,避免请求完成后连接回到保活池 --- */
49
49
  let stopping = false;
50
+ /** --- 当前活跃的 HTTP/2 会话集合,用于 graceful shutdown 时发送 GOAWAY 帧 --- */
51
+ const http2Sessions = new Set();
50
52
  /**
51
53
  * --- 包装请求处理函数,统一管理 linkCount 计数和错误处理 ---
52
54
  * @param key 连接标识
@@ -144,6 +146,12 @@ async function run() {
144
146
  }
145
147
  wrapWithLinkCount(host + (req.url ?? ''), () => upgradeHandler(req, socket, true, head), '[CHILD][http2][upgrade]', 'UPGRADE');
146
148
  }).listen(lCore.globalConfig.httpsPort);
149
+ // --- 追踪 HTTP/2 会话,用于 graceful shutdown 时发送 GOAWAY 帧通知 CDN 切换连接 ---
150
+ http2Server.on('session', (session) => {
151
+ http2Sessions.add(session);
152
+ session.on('close', () => { http2Sessions.delete(session); });
153
+ session.on('error', () => { http2Sessions.delete(session); });
154
+ });
147
155
  // --- 增大 HTTP/2 超时,防止 CDN 预建连接被 Node.js 过早关闭 ---
148
156
  http2Server.setTimeout(120_000);
149
157
  httpServer = http.createServer(function (req, res) {
@@ -562,6 +570,13 @@ process.on('message', function (msg) {
562
570
  case 'stop': {
563
571
  // --- 需要停止监听,等待已有连接全部断开,然后关闭线程 ---
564
572
  stopping = true;
573
+ // --- 向所有 HTTP/2 会话发送 GOAWAY 帧,通知 CDN 客户端停止在该连接上发送新请求并切换到新连接 ---
574
+ for (const session of http2Sessions) {
575
+ try {
576
+ session.goaway(http2.constants.NGHTTP2_NO_ERROR);
577
+ }
578
+ catch { }
579
+ }
565
580
  httpServer?.close();
566
581
  http2Server?.close();
567
582
  // --- 立即关闭空闲保活连接(无活跃请求的 keep-alive socket),避免进程长时间等待 ---
package/sys/ctr.d.ts CHANGED
@@ -342,5 +342,9 @@ export declare class Ctr {
342
342
  'maxFileSize'?: number;
343
343
  /** --- 允许的文件扩展名(含点号),如 ['.jpg', '.png', '.pdf'] --- */
344
344
  'allowedExts'?: string[];
345
+ /** --- 单个字段(非文件)最大字节数,默认 1 MB --- */
346
+ 'maxFieldSize'?: number;
347
+ /** --- 整体请求超时时间(毫秒),默认 5 分钟,设为 0 禁用超时 --- */
348
+ 'timeout'?: number;
345
349
  }): Promise<boolean>;
346
350
  }
package/sys/master.js CHANGED
@@ -18,6 +18,29 @@ import * as lTime from '#kebab/lib/time.js';
18
18
  import * as lZip from '#kebab/lib/zip.js';
19
19
  /** --- 当前运行中的子进程列表 --- */
20
20
  const workerList = {};
21
+ /**
22
+ * --- 等待指定 worker 开始监听端口就绪 ---
23
+ * @param worker 要等待的 worker 对象
24
+ * @param timeout 超时时间(毫秒),默认 30 秒
25
+ */
26
+ function waitForWorkerListening(worker, timeout = 30_000) {
27
+ return new Promise((resolve) => {
28
+ let timer;
29
+ const onListening = (w) => {
30
+ if (w.id === worker.id) {
31
+ clearTimeout(timer);
32
+ cluster.removeListener('listening', onListening);
33
+ resolve();
34
+ }
35
+ };
36
+ timer = setTimeout(() => {
37
+ lCore.display(`[master] Worker ${worker.process.pid ?? 'undefined'} timed out waiting for listening, proceeding anyway.`);
38
+ cluster.removeListener('listening', onListening);
39
+ resolve();
40
+ }, timeout);
41
+ cluster.on('listening', onListening);
42
+ });
43
+ }
21
44
  /**
22
45
  * --- 最终调用执行的函数块 ---
23
46
  */
@@ -124,16 +147,7 @@ function createRpcListener() {
124
147
  break;
125
148
  }
126
149
  case 'restart': {
127
- // --- 为所有子线程发送 stop 信息 ---
128
- for (const pid in workerList) {
129
- workerList[pid].worker.send({
130
- 'action': 'stop'
131
- });
132
- // --- 开启新线程 ---
133
- await createChildProcess(workerList[pid].cpu);
134
- // --- 删除记录 ---
135
- delete workerList[pid];
136
- }
150
+ await restartWorkers();
137
151
  break;
138
152
  }
139
153
  case 'global': {
@@ -768,6 +782,34 @@ function createRpcListener() {
768
782
  });
769
783
  }).listen(lCore.globalConfig.rpcPort);
770
784
  }
785
+ /**
786
+ * --- 零宕机重启所有 worker 进程:先创建新进程并等待监听就绪,再停止旧进程 ---
787
+ */
788
+ async function restartWorkers() {
789
+ // --- 收集旧进程信息(先快照,避免迭代过程中修改 workerList) ---
790
+ const oldEntries = [];
791
+ for (const pid in workerList) {
792
+ oldEntries.push({
793
+ 'pid': pid,
794
+ 'cpu': workerList[pid].cpu,
795
+ 'worker': workerList[pid].worker
796
+ });
797
+ }
798
+ // --- 阶段 1:创建新进程并等待其监听端口就绪 ---
799
+ for (const entry of oldEntries) {
800
+ const newWorker = await createChildProcess(entry.cpu);
801
+ if (newWorker) {
802
+ await waitForWorkerListening(newWorker);
803
+ }
804
+ }
805
+ // --- 阶段 2:停止旧进程 ---
806
+ for (const entry of oldEntries) {
807
+ entry.worker.send({
808
+ 'action': 'stop'
809
+ });
810
+ delete workerList[entry.pid];
811
+ }
812
+ }
771
813
  /**
772
814
  * --- 检测是否有死掉丢失的子进程,复活之 ---
773
815
  */
@@ -795,7 +837,7 @@ async function checkWorkerLost() {
795
837
  async function createChildProcess(cpu) {
796
838
  const worker = cluster.fork();
797
839
  if (!worker.process.pid) {
798
- return;
840
+ return null;
799
841
  }
800
842
  workerList[worker.process.pid] = {
801
843
  'worker': worker,
@@ -823,16 +865,7 @@ async function createChildProcess(cpu) {
823
865
  break;
824
866
  }
825
867
  case 'restart': {
826
- // --- 为所有子进程发送 restart 信息 ---
827
- for (const pid in workerList) {
828
- workerList[pid].worker.send({
829
- 'action': 'stop'
830
- });
831
- // --- 开启新线程 ---
832
- await createChildProcess(workerList[pid].cpu);
833
- // --- 删除记录 ---
834
- delete workerList[pid];
835
- }
868
+ await restartWorkers();
836
869
  break;
837
870
  }
838
871
  case 'global': {
@@ -889,6 +922,7 @@ async function createChildProcess(cpu) {
889
922
  'data': lCore.global
890
923
  });
891
924
  }
925
+ return worker;
892
926
  }
893
927
  /**
894
928
  * --- 开发模式下的文件监听自动重载(HMR),仅在 debug: true 时启用 ---
@@ -919,18 +953,10 @@ function startFileWatcher() {
919
953
  }
920
954
  restarting = true;
921
955
  lCore.display(`[HMR] File changed: ${formatName}, reloading workers...`);
922
- (async () => {
923
- // --- 为所有子进程发送 stop 信息并重启 ---
924
- for (const pid in workerList) {
925
- workerList[pid].worker.send({
926
- 'action': 'stop'
927
- });
928
- await createChildProcess(workerList[pid].cpu);
929
- delete workerList[pid];
930
- }
956
+ restartWorkers().then(() => {
931
957
  restarting = false;
932
958
  lCore.display('[HMR] All workers reloaded.');
933
- })().catch((e) => {
959
+ }).catch((e) => {
934
960
  restarting = false;
935
961
  lCore.display('[HMR] Reload error:', e);
936
962
  });