@maiyunnet/kebab 8.6.5 → 9.0.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 = "8.6.5";
8
+ export declare const VER = "9.0.0";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
@@ -124,14 +124,6 @@ export interface IConfigDb {
124
124
  'user': string;
125
125
  'pwd': string;
126
126
  }
127
- /** --- Jwt 信息 --- */
128
- export interface IConfigJwt {
129
- 'name': string;
130
- 'ttl': number;
131
- 'ssl': boolean;
132
- 'secret': string;
133
- 'auth': boolean;
134
- }
135
127
  /** --- DNS --- */
136
128
  export interface IConfigDns {
137
129
  'sid': string;
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '8.6.5';
9
+ export const VER = '9.0.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
@@ -15,6 +15,8 @@ export declare const globalConfig: kebab.IConfig & {
15
15
  'max': number;
16
16
  'hosts': string[];
17
17
  'ind': string[];
18
+ /** --- 日志格式,csv 或 jsonl,默认 jsonl --- */
19
+ 'logFormat': 'csv' | 'jsonl';
18
20
  };
19
21
  /** --- Cookie 设置的选项 --- */
20
22
  export interface ICookieOptions {
@@ -294,3 +296,13 @@ export declare function writeEventStreamHead(res: http2.Http2ServerResponse | ht
294
296
  * @param data 数据
295
297
  */
296
298
  export declare function write(res: http2.Http2ServerResponse | http.ServerResponse | net.Socket, data: string | Buffer): void;
299
+ /**
300
+ * --- 加载 .env 文件到 process.env,若文件不存在则跳过 ---
301
+ * @param dir .env 文件所在目录路径(以 / 结尾)
302
+ */
303
+ export declare function loadEnv(dir: string): Promise<void>;
304
+ /**
305
+ * --- 将配置对象中的 ${ENV_VAR} 占位符替换为 process.env 的值 ---
306
+ * @param obj 配置对象
307
+ */
308
+ export declare function resolveEnvVars(obj: Record<string, any>): void;
package/lib/core.js CHANGED
@@ -738,31 +738,57 @@ export function log(opt, msg, fend = '') {
738
738
  if (!rtn) {
739
739
  return;
740
740
  }
741
- path += h + '.csv';
742
- if (!await lFs.isFile(path)) {
743
- if (!await lFs.putContent(path, 'TIME,UNIX,URL,COOKIE,SESSION,USER_AGENT,REALIP,CLIENTIP,OS,PROCESS,MESSAGE\n', {
741
+ const format = globalConfig.logFormat ?? 'jsonl';
742
+ if (format === 'jsonl') {
743
+ // --- JSON Lines 格式 ---
744
+ path += h + '.jsonl';
745
+ const entry = lText.stringifyJson({
746
+ 'time': lTime.format(null, 'H:i:s'),
747
+ 'unix': lTime.stamp(),
748
+ 'url': urlFull + wpath + (Object.keys(get).length ? '?' + lText.queryStringify(get) : ''),
749
+ 'cookie': cookie,
750
+ 'session': session,
751
+ 'userAgent': headers['user-agent'] ?? '',
752
+ 'realIp': realIp,
753
+ 'clientIp': clientIp,
754
+ 'osMem': lText.sizeFormat(os.totalmem() - os.freemem(), ''),
755
+ 'procMem': lText.sizeFormat(process.memoryUsage().rss, ''),
756
+ 'message': msg,
757
+ }) + '\n';
758
+ await lFs.putContent(path, entry, {
744
759
  'encoding': 'utf8',
745
- 'mode': 0o777
746
- })) {
747
- return;
760
+ 'mode': 0o777,
761
+ 'flag': 'a'
762
+ });
763
+ }
764
+ else {
765
+ // --- CSV 格式(默认) ---
766
+ path += h + '.csv';
767
+ if (!await lFs.isFile(path)) {
768
+ if (!await lFs.putContent(path, 'TIME,UNIX,URL,COOKIE,SESSION,USER_AGENT,REALIP,CLIENTIP,OS,PROCESS,MESSAGE\n', {
769
+ 'encoding': 'utf8',
770
+ 'mode': 0o777
771
+ })) {
772
+ return;
773
+ }
748
774
  }
775
+ await lFs.putContent(path, '"' +
776
+ lTime.format(null, 'H:i:s') + '","' +
777
+ lTime.stamp().toString() + '","' +
778
+ urlFull + wpath + (Object.keys(get).length ? '?' + lText.queryStringify(get).replace(/"/g, '""') : '') + '","' +
779
+ lText.queryStringify(cookie).replace(/"/g, '""') + '","' +
780
+ lText.stringifyJson(session).replace(/"/g, '""') + '","' +
781
+ (headers['user-agent']?.replace(/"/g, '""') ?? 'No HTTP_USER_AGENT') + '","' +
782
+ realIp.replace(/"/g, '""') + '","' +
783
+ clientIp.replace(/"/g, '""') + '","' +
784
+ lText.sizeFormat(os.totalmem() - os.freemem(), '') + '","' +
785
+ lText.sizeFormat(process.memoryUsage().rss, '') + '","' +
786
+ JSON.stringify(msg).slice(1, -1).replace(/"/g, '""') + '"\n', {
787
+ 'encoding': 'utf8',
788
+ 'mode': 0o777,
789
+ 'flag': 'a'
790
+ });
749
791
  }
750
- await lFs.putContent(path, '"' +
751
- lTime.format(null, 'H:i:s') + '","' +
752
- lTime.stamp().toString() + '","' +
753
- urlFull + wpath + (Object.keys(get).length ? '?' + lText.queryStringify(get).replace(/"/g, '""') : '') + '","' +
754
- lText.queryStringify(cookie).replace(/"/g, '""') + '","' +
755
- lText.stringifyJson(session).replace(/"/g, '""') + '","' +
756
- (headers['user-agent']?.replace(/"/g, '""') ?? 'No HTTP_USER_AGENT') + '","' +
757
- realIp.replace(/"/g, '""') + '","' +
758
- clientIp.replace(/"/g, '""') + '","' +
759
- lText.sizeFormat(os.totalmem() - os.freemem(), '') + '","' +
760
- lText.sizeFormat(process.memoryUsage().rss, '') + '","' +
761
- JSON.stringify(msg).slice(1, -1).replace(/"/g, '""') + '"\n', {
762
- 'encoding': 'utf8',
763
- 'mode': 0o777,
764
- 'flag': 'a'
765
- });
766
792
  })().catch((e) => {
767
793
  display('[CORE] [log]', e);
768
794
  });
@@ -919,3 +945,48 @@ export function write(res, data) {
919
945
  res.write(data);
920
946
  }
921
947
  }
948
+ /**
949
+ * --- 加载 .env 文件到 process.env,若文件不存在则跳过 ---
950
+ * @param dir .env 文件所在目录路径(以 / 结尾)
951
+ */
952
+ export async function loadEnv(dir) {
953
+ const content = await lFs.getContent(dir + '.env', 'utf8');
954
+ if (!content) {
955
+ return;
956
+ }
957
+ const lines = content.split('\n');
958
+ for (const line of lines) {
959
+ const trimmed = line.trim();
960
+ // --- 空行或注释行跳过 ---
961
+ if (!trimmed || trimmed.startsWith('#')) {
962
+ continue;
963
+ }
964
+ const eqIndex = trimmed.indexOf('=');
965
+ if (eqIndex === -1) {
966
+ continue;
967
+ }
968
+ const key = trimmed.slice(0, eqIndex).trim();
969
+ let val = trimmed.slice(eqIndex + 1).trim();
970
+ // --- 去掉首尾引号 ---
971
+ if ((val.startsWith('\'') && val.endsWith('\'')) || (val.startsWith('"') && val.endsWith('"'))) {
972
+ val = val.slice(1, -1);
973
+ }
974
+ process.env[key] = val;
975
+ }
976
+ }
977
+ /**
978
+ * --- 将配置对象中的 ${ENV_VAR} 占位符替换为 process.env 的值 ---
979
+ * @param obj 配置对象
980
+ */
981
+ export function resolveEnvVars(obj) {
982
+ for (const key in obj) {
983
+ if (typeof obj[key] === 'string') {
984
+ obj[key] = obj[key].replace(/\$\{([^}]+)\}/g, (_, envKey) => {
985
+ return process.env[envKey] ?? '';
986
+ });
987
+ }
988
+ else if (typeof obj[key] === 'object' && obj[key] !== null) {
989
+ resolveEnvVars(obj[key]);
990
+ }
991
+ }
992
+ }
package/lib/cron.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2025-9-20 19:46:30
4
- * Last: 2025-9-20 19:46:32
4
+ * Last: 2025-9-20 19:46:32, 2026-3-22 00:00:00
5
5
  */
6
6
  import * as kebab from '#kebab/index.js';
7
7
  import * as nodeCron from 'node-cron';
@@ -9,6 +9,7 @@ import * as lFs from '#kebab/lib/fs.js';
9
9
  import * as lCore from '#kebab/lib/core.js';
10
10
  import * as lText from '#kebab/lib/text.js';
11
11
  import * as lTime from '#kebab/lib/time.js';
12
+ import * as lKv from '#kebab/lib/kv.js';
12
13
  /** --- 定时任务列表 --- */
13
14
  const regulars = [];
14
15
  /** --- 获取定时任务列表 --- */
@@ -28,13 +29,14 @@ export async function regular(task, immediate = '') {
28
29
  // --- 当前进程已经注册过了,不能再注册了 ---
29
30
  return false;
30
31
  }
31
- // --- 检查是否存在相同名称的计划任务 ---
32
- if (!await lFs.isDir(kebab.LOG_CWD + `cron/`)) {
33
- await lFs.mkdir(kebab.LOG_CWD + `cron/`);
34
- await lFs.putContent(kebab.LOG_CWD + `cron/lock.txt`, 'Kebab');
32
+ // --- 检查 cron 数据目录是否存在 ---
33
+ const cronDataDir = kebab.LOG_CWD + 'cron/';
34
+ if (!await lFs.isDir(cronDataDir)) {
35
+ await lFs.mkdir(cronDataDir);
36
+ await lFs.putContent(cronDataDir + 'lock.txt', 'Kebab');
35
37
  }
36
- // --- 保存计划任务 ---
37
- const content = await lFs.getContent(kebab.LOG_CWD + `cron/${task.name}.json`, 'utf8');
38
+ // --- 加载已有的持久化数据 ---
39
+ const content = await lFs.getContent(cronDataDir + `${task.name}.json`, 'utf8');
38
40
  /** --- 任务对象 --- */
39
41
  const obj = {
40
42
  ...task,
@@ -43,41 +45,90 @@ export async function regular(task, immediate = '') {
43
45
  'rcount': 0,
44
46
  };
45
47
  if (content) {
46
- const json = JSON.parse(content);
47
- obj.last = json.last;
48
- obj.count = json.count;
48
+ const json = lText.parseJson(content);
49
+ if (json) {
50
+ obj.last = json.last ?? '';
51
+ obj.count = json.count ?? 0;
52
+ }
49
53
  }
50
- await lFs.putContent(kebab.LOG_CWD + `cron/${obj.name}.json`, JSON.stringify(obj));
51
- // --- 好,先注册 ---
54
+ await saveCronData(obj);
55
+ // --- 注册定时任务 ---
52
56
  nodeCron.schedule(task.rule, () => {
53
- /** --- 当前日期字符串 --- */
54
- const date = lTime.format(null, 'YmdHi');
55
- obj.callback(date, false);
56
- // --- 设置执行后的数据 ---
57
- obj.last = date;
58
- ++obj.count;
59
- ++obj.rcount;
60
- lFs.putContent(kebab.LOG_CWD + `cron/${obj.name}.json`, JSON.stringify(obj)).catch(() => { });
57
+ executeWithLock(obj, false).catch((e) => {
58
+ const msg = `[CRON][${obj.name}] ${lText.stringifyJson(e.message ?? '').slice(1, -1).replace(/"/g, '""')}`;
59
+ lCore.debug(msg);
60
+ lCore.log({}, msg, '-error');
61
+ });
61
62
  });
62
63
  // --- 检查是否需要立即执行 ---
63
64
  /** --- 当前日期字符串 --- */
64
65
  const date = lTime.format(null, 'YmdHi');
65
66
  if ((immediate && (immediate <= date) && (obj.last < immediate))) {
66
- // --- 理应执行的时间早于当前,或要执行的时间就是现在 ---
67
+ await executeWithLock(obj, true);
68
+ }
69
+ regulars.push(obj);
70
+ return true;
71
+ }
72
+ /**
73
+ * --- 带分布式锁执行定时任务(多进程/多机下只有一个 worker 会执行) ---
74
+ * @param obj 任务数据对象
75
+ * @param immediate 是否为立即执行模式
76
+ */
77
+ async function executeWithLock(obj, immediate) {
78
+ const date = lTime.format(null, 'YmdHi');
79
+ const lockKey = `cron_lock:${obj.name}:${date}`;
80
+ // --- 尝试通过 KV 获取分布式锁 ---
81
+ if (lCore.globalConfig.kv?.host) {
67
82
  try {
68
- obj.callback(date, true);
83
+ const kv = get();
84
+ // --- 锁的 TTL 为 120 秒,防止任务异常退出后锁永远不释放 ---
85
+ const acquired = await kv.add(lockKey, process.pid?.toString() ?? '1', 120);
86
+ if (!acquired) {
87
+ // --- 其他进程已经在执行此任务了 ---
88
+ return;
89
+ }
69
90
  }
70
- catch (e) {
71
- const msg = `[CRON][${obj.name}] ${lText.stringifyJson(e.message ?? '').slice(1, -1).replace(/"/g, '""')}`;
72
- lCore.debug(msg);
73
- lCore.log({}, msg, '-error');
91
+ catch {
92
+ // --- KV 不可用时降级为本地执行(不做锁控制) ---
93
+ lCore.debug(`[CRON][${obj.name}] KV lock unavailable, fallback to local execution`);
74
94
  }
75
- // --- 设置执行后的数据 ---
76
- obj.last = date;
77
- ++obj.count;
78
- ++obj.rcount;
79
- await lFs.putContent(kebab.LOG_CWD + `cron/${obj.name}.json`, JSON.stringify(obj));
80
95
  }
81
- regulars.push(obj);
82
- return true;
96
+ // --- 执行任务 ---
97
+ try {
98
+ await obj.callback(date, immediate);
99
+ }
100
+ catch (e) {
101
+ const msg = `[CRON][${obj.name}] ${lText.stringifyJson(e.message ?? '').slice(1, -1).replace(/"/g, '""')}`;
102
+ lCore.debug(msg);
103
+ lCore.log({}, msg, '-error');
104
+ }
105
+ // --- 更新执行数据 ---
106
+ obj.last = date;
107
+ ++obj.count;
108
+ ++obj.rcount;
109
+ await saveCronData(obj);
110
+ }
111
+ /**
112
+ * --- 保存定时任务持久化数据 ---
113
+ * @param obj 任务数据对象
114
+ */
115
+ async function saveCronData(obj) {
116
+ await lFs.putContent(kebab.LOG_CWD + `cron/${obj.name}.json`, lText.stringifyJson({
117
+ 'name': obj.name,
118
+ 'last': obj.last,
119
+ 'count': obj.count,
120
+ })).catch(() => { });
121
+ }
122
+ /**
123
+ * --- 获取 Kv 实例(内部使用,用于分布式锁) ---
124
+ */
125
+ function get() {
126
+ return lKv.get({
127
+ 'host': lCore.globalConfig.kv.host,
128
+ 'port': lCore.globalConfig.kv.port,
129
+ 'index': lCore.globalConfig.kv.index,
130
+ 'pre': lCore.globalConfig.kv.pre,
131
+ 'user': lCore.globalConfig.kv.user,
132
+ 'pwd': lCore.globalConfig.kv.pwd,
133
+ });
83
134
  }
package/lib/db.d.ts CHANGED
@@ -60,5 +60,7 @@ export interface IPacket {
60
60
  export declare function get(ctrEtc: sCtr.Ctr | kebab.IConfigDb, opt?: {
61
61
  /** --- 服务商,默认 PGSQL --- */
62
62
  'service'?: ESERVICE;
63
+ /** --- 是否使用只读库,默认 false --- */
64
+ 'read'?: boolean;
63
65
  }): Pool;
64
66
  export { Connection, Pool, IConnectionInfo, getConnectionList, Transaction };
package/lib/db.js CHANGED
@@ -24,7 +24,9 @@ export function get(ctrEtc, opt = {}) {
24
24
  // --- 从 ctr 中读取连接信息 ---
25
25
  const config = ctrEtc.getPrototype('_config');
26
26
  const service = opt.service ? ESERVICE[opt.service] : config.db.default;
27
- return new Pool(config.db[service].default, {
27
+ /** --- 读写分离:若 read 为 true 且存在 read 配置且 read 配置的 host 不为空,则使用 read 库 --- */
28
+ const dbKey = (opt.read && config.db[service].read?.host) ? 'read' : 'default';
29
+ return new Pool(config.db[service][dbKey], {
28
30
  'service': service === 'MYSQL' ? ESERVICE.MYSQL : ESERVICE.PGSQL,
29
31
  });
30
32
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Project: Kebab
3
+ * Date: 2026-3-22 00:00:00
4
+ * --- Rate Limiting 限速库,支持令牌桶和滑动窗口两种策略 ---
5
+ * --- 基于 KV(Redis)实现,天然支持多进程和多机部署 ---
6
+ */
7
+ import * as lKv from '#kebab/lib/kv.js';
8
+ /**
9
+ * --- 检查指定 key 是否超速,使用多段近似滑动窗口算法 ---
10
+ * @param kv KV 实例
11
+ * @param key 限速标识(如 IP、用户 UID 等)
12
+ * @param opt 限速选项
13
+ * @returns 返回结果对象
14
+ */
15
+ export declare function check(kv: lKv.Kv, key: string, opt?: {
16
+ /** --- 窗口时间(秒),默认 60 --- */
17
+ 'window'?: number;
18
+ /** --- 窗口内最大请求数,默认 60 --- */
19
+ 'max'?: number;
20
+ /** --- key 前缀,默认 rl: --- */
21
+ 'pre'?: string;
22
+ }): Promise<ICheckResult>;
23
+ /**
24
+ * --- 简易固定窗口限速检查(性能更高,精度较低) ---
25
+ * @param kv KV 实例
26
+ * @param key 限速标识
27
+ * @param opt 限速选项
28
+ */
29
+ export declare function checkFixed(kv: lKv.Kv, key: string, opt?: {
30
+ /** --- 窗口时间(秒),默认 60 --- */
31
+ 'window'?: number;
32
+ /** --- 窗口内最大请求数,默认 60 --- */
33
+ 'max'?: number;
34
+ /** --- key 前缀,默认 rl: --- */
35
+ 'pre'?: string;
36
+ }): Promise<ICheckResult>;
37
+ /** --- 限速检查结果 --- */
38
+ export interface ICheckResult {
39
+ /** --- 是否允许通过 --- */
40
+ 'allowed': boolean;
41
+ /** --- 剩余可用次数 --- */
42
+ 'remaining': number;
43
+ /** --- 总限额 --- */
44
+ 'limit': number;
45
+ /** --- 窗口重置时间(Unix 时间戳秒) --- */
46
+ 'reset': number;
47
+ }
@@ -0,0 +1,88 @@
1
+ import * as lTime from '#kebab/lib/time.js';
2
+ import * as lCore from '#kebab/lib/core.js';
3
+ /**
4
+ * --- 检查指定 key 是否超速,使用多段近似滑动窗口算法 ---
5
+ * @param kv KV 实例
6
+ * @param key 限速标识(如 IP、用户 UID 等)
7
+ * @param opt 限速选项
8
+ * @returns 返回结果对象
9
+ */
10
+ export async function check(kv, key, opt = {}) {
11
+ const window = opt.window ?? 60;
12
+ const max = opt.max ?? 60;
13
+ const pre = opt.pre ?? 'rl:';
14
+ const now = lTime.stamp();
15
+ /** --- 将窗口分为 6 个子段 --- */
16
+ const segSize = Math.max(Math.floor(window / 6), 1);
17
+ /** --- 当前段标识 --- */
18
+ const segId = Math.floor(now / segSize);
19
+ /** --- 需要统计的段数 --- */
20
+ const segCount = Math.ceil(window / segSize);
21
+ /** --- 统计各段请求数总和 --- */
22
+ let total = 0;
23
+ for (let i = 0; i < segCount; ++i) {
24
+ const segKey = `${pre}${key}:${segId - i}`;
25
+ const val = await kv.get(segKey);
26
+ if (val) {
27
+ total += parseInt(val) || 0;
28
+ }
29
+ }
30
+ // --- 当前段 +1 ---
31
+ const currentSegKey = `${pre}${key}:${segId}`;
32
+ const count = await kv.incr(currentSegKey);
33
+ if (count === false) {
34
+ // --- KV 连接失败,放行 ---
35
+ lCore.debug('[RATELIMIT] KV incr failed, allow by default');
36
+ return {
37
+ 'allowed': true,
38
+ 'remaining': max,
39
+ 'limit': max,
40
+ 'reset': now + window,
41
+ };
42
+ }
43
+ if (count === 1) {
44
+ await kv.expire(currentSegKey, window + segSize);
45
+ }
46
+ total += 1;
47
+ const allowed = total <= max;
48
+ return {
49
+ 'allowed': allowed,
50
+ 'remaining': allowed ? max - total : 0,
51
+ 'limit': max,
52
+ 'reset': (segId + 1) * segSize,
53
+ };
54
+ }
55
+ /**
56
+ * --- 简易固定窗口限速检查(性能更高,精度较低) ---
57
+ * @param kv KV 实例
58
+ * @param key 限速标识
59
+ * @param opt 限速选项
60
+ */
61
+ export async function checkFixed(kv, key, opt = {}) {
62
+ const window = opt.window ?? 60;
63
+ const max = opt.max ?? 60;
64
+ const pre = opt.pre ?? 'rl:';
65
+ const now = lTime.stamp();
66
+ const rkey = pre + key;
67
+ const count = await kv.incr(rkey);
68
+ if (count === false) {
69
+ // --- KV 连接失败,放行 ---
70
+ return {
71
+ 'allowed': true,
72
+ 'remaining': max,
73
+ 'limit': max,
74
+ 'reset': now + window,
75
+ };
76
+ }
77
+ if (count === 1) {
78
+ // --- 首次请求,设置过期时间 ---
79
+ await kv.expire(rkey, window);
80
+ }
81
+ const allowed = count <= max;
82
+ return {
83
+ 'allowed': allowed,
84
+ 'remaining': allowed ? max - count : 0,
85
+ 'limit': max,
86
+ 'reset': now + window,
87
+ };
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "8.6.5",
3
+ "version": "9.0.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": [
@@ -22,33 +22,40 @@
22
22
  "#kebab/*": "./*"
23
23
  },
24
24
  "dependencies": {
25
- "@aws-sdk/client-s3": "^3.993.0",
26
- "@aws-sdk/lib-storage": "^3.993.0",
25
+ "@aws-sdk/client-s3": "^3.1014.0",
26
+ "@aws-sdk/lib-storage": "^3.1014.0",
27
27
  "@litert/http-client": "^1.1.2",
28
28
  "@litert/mime": "^0.1.3",
29
- "@litert/redis": "^3.1.0",
29
+ "@litert/redis": "^3.2.0",
30
30
  "@litert/websocket": "^0.2.8",
31
31
  "@types/ssh2": "^1.15.5",
32
- "@zilliz/milvus2-sdk-node": "^2.6.10",
32
+ "@zilliz/milvus2-sdk-node": "^2.6.11",
33
33
  "ajv": "^8.18.0",
34
34
  "ajv-formats": "^3.0.1",
35
- "ejs": "^4.0.1",
35
+ "ejs": "^5.0.1",
36
+ "@tailwindcss/cli": "^4.2.2",
37
+ "esbuild": "^0.27.4",
36
38
  "jszip": "^3.10.1",
37
- "mysql2": "^3.17.2",
39
+ "mysql2": "^3.20.0",
38
40
  "node-cron": "^4.2.1",
39
- "openai": "^6.22.0",
40
- "pg": "^8.18.0",
41
+ "openai": "^6.32.0",
42
+ "pg": "^8.20.0",
43
+ "react": "^19.2.4",
44
+ "react-dom": "^19.2.4",
45
+ "react-router-dom": "^7.13.1",
41
46
  "ssh2": "^1.17.0",
42
47
  "svg-captcha": "^1.4.0",
43
- "tencentcloud-sdk-nodejs": "^4.1.186"
48
+ "tencentcloud-sdk-nodejs": "^4.1.199"
44
49
  },
45
50
  "devDependencies": {
46
51
  "@litert/eslint-plugin-rules": "^0.3.1",
47
52
  "@types/ejs": "^3.1.5",
48
- "@types/node": "^25.3.0",
49
- "@types/pg": "^8.16.0",
53
+ "@types/node": "^25.5.0",
54
+ "@types/pg": "^8.20.0",
55
+ "@types/react": "^19.2.14",
56
+ "@types/react-dom": "^19.2.3",
50
57
  "typedoc": "^0.28.17",
51
- "typedoc-plugin-markdown": "^4.10.0",
58
+ "typedoc-plugin-markdown": "^4.11.0",
52
59
  "typescript": "^5.9.3"
53
60
  }
54
61
  }
package/sys/child.js CHANGED
@@ -448,11 +448,13 @@ async function upgradeHandler(req, socket, https, head) {
448
448
  */
449
449
  async function reload() {
450
450
  // --- 清除全局 config,并重新加载全局信息 ---
451
+ await lCore.loadEnv(kebab.ROOT_CWD);
451
452
  const configContent = await lFs.getContent(kebab.CONF_CWD + 'config.json', 'utf8');
452
453
  if (!configContent) {
453
454
  throw `File '${kebab.CONF_CWD}config.json' not found.`;
454
455
  }
455
456
  const config = lText.parseJson(configContent);
457
+ lCore.resolveEnvVars(config);
456
458
  for (const key in lCore.globalConfig) {
457
459
  delete lCore.globalConfig[key];
458
460
  }