@zhin.js/core 1.0.39 → 1.0.41

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/lib/ai/index.d.ts +10 -5
  3. package/lib/ai/index.d.ts.map +1 -1
  4. package/lib/ai/index.js +7 -4
  5. package/lib/ai/index.js.map +1 -1
  6. package/lib/built/common-adapter-tools.d.ts.map +1 -1
  7. package/lib/built/common-adapter-tools.js +38 -32
  8. package/lib/built/common-adapter-tools.js.map +1 -1
  9. package/lib/cron.d.ts +2 -43
  10. package/lib/cron.d.ts.map +1 -1
  11. package/lib/cron.js +2 -126
  12. package/lib/cron.js.map +1 -1
  13. package/lib/errors.d.ts +3 -146
  14. package/lib/errors.d.ts.map +1 -1
  15. package/lib/errors.js +3 -279
  16. package/lib/errors.js.map +1 -1
  17. package/lib/feature.d.ts +5 -87
  18. package/lib/feature.d.ts.map +1 -1
  19. package/lib/feature.js +4 -105
  20. package/lib/feature.js.map +1 -1
  21. package/lib/index.d.ts +1 -0
  22. package/lib/index.d.ts.map +1 -1
  23. package/lib/scheduler/index.d.ts +3 -7
  24. package/lib/scheduler/index.d.ts.map +1 -1
  25. package/lib/scheduler/index.js +2 -9
  26. package/lib/scheduler/index.js.map +1 -1
  27. package/lib/types.d.ts +8 -1
  28. package/lib/types.d.ts.map +1 -1
  29. package/lib/utils.d.ts +7 -52
  30. package/lib/utils.d.ts.map +1 -1
  31. package/lib/utils.js +9 -325
  32. package/lib/utils.js.map +1 -1
  33. package/package.json +6 -4
  34. package/src/ai/index.ts +15 -9
  35. package/src/built/common-adapter-tools.ts +38 -32
  36. package/src/cron.ts +2 -140
  37. package/src/errors.ts +15 -334
  38. package/src/feature.ts +5 -154
  39. package/src/index.ts +3 -1
  40. package/src/scheduler/index.ts +8 -17
  41. package/src/types.ts +10 -2
  42. package/src/utils.ts +37 -334
  43. package/tests/cron.test.ts +4 -299
  44. package/tests/errors.test.ts +17 -307
  45. package/tests/utils.test.ts +11 -516
  46. package/tests/feature.test.ts +0 -145
package/src/feature.ts CHANGED
@@ -1,156 +1,7 @@
1
- import type { Plugin } from "./plugin.js";
2
1
  /**
3
- * Feature 抽象基类
4
- * 所有可追踪、可序列化的插件功能(命令、组件、定时任务、中间件等)的基类。
5
- * Feature 替换原来的 Context 接口,用于 provide/inject 机制。
2
+ * Re-export from @zhin.js/kernel for backward compatibility.
3
+ * Core's Plugin class implements PluginLike, so the Feature's
4
+ * mounted(plugin: PluginLike) signature is compatible.
6
5
  */
7
-
8
- /**
9
- * Feature 序列化后的 JSON 格式,用于 HTTP API 返回给前端
10
- */
11
- export interface FeatureJSON {
12
- name: string;
13
- icon: string;
14
- desc: string;
15
- count: number;
16
- items: any[];
17
- }
18
-
19
- /**
20
- * Feature 变更事件监听器类型
21
- */
22
- export type FeatureListener<T> = (item: T, pluginName: string) => void;
23
-
24
- /**
25
- * Feature<T> 抽象类
26
- * - name / icon / desc: 自描述元数据
27
- * - items: 全局 item 列表
28
- * - pluginItems: 按插件名分组的 item 列表
29
- * - toJSON: 控制 HTTP API 返回的数据格式
30
- * - extensions: 提供给 Plugin.prototype 的扩展方法(如 addCommand)
31
- * - mounted / dispose: 可选生命周期钩子
32
- * - on('add' | 'remove'): 变更事件通知
33
- */
34
- export abstract class Feature<T = any> {
35
- abstract readonly name: string;
36
- abstract readonly icon: string;
37
- abstract readonly desc: string;
38
-
39
- /** 全局 item 列表 */
40
- readonly items: T[] = [];
41
-
42
- /** 按插件名分组的 item 列表 */
43
- protected pluginItems = new Map<string, T[]>();
44
-
45
- /** 事件监听器 */
46
- #listeners = new Map<string, Set<FeatureListener<T>>>();
47
-
48
- /**
49
- * 监听变更事件
50
- * @param event 'add' | 'remove'
51
- * @param listener 回调函数
52
- * @returns 取消监听的函数
53
- */
54
- on(event: 'add' | 'remove', listener: FeatureListener<T>): () => void {
55
- if (!this.#listeners.has(event)) {
56
- this.#listeners.set(event, new Set());
57
- }
58
- this.#listeners.get(event)!.add(listener);
59
- return () => { this.#listeners.get(event)?.delete(listener); };
60
- }
61
-
62
- /** 触发事件 */
63
- protected emit(event: 'add' | 'remove', item: T, pluginName: string): void {
64
- const listeners = this.#listeners.get(event);
65
- if (!listeners) return;
66
- for (const listener of listeners) {
67
- try { listener(item, pluginName); } catch { /* 防止监听器异常影响主流程 */ }
68
- }
69
- }
70
-
71
- /**
72
- * 添加 item,同时记录所属插件
73
- * @returns dispose 函数,用于移除该 item
74
- */
75
- add(item: T, pluginName: string): () => void {
76
- this.items.push(item);
77
- if (!this.pluginItems.has(pluginName)) {
78
- this.pluginItems.set(pluginName, []);
79
- }
80
- this.pluginItems.get(pluginName)!.push(item);
81
- this.emit('add', item, pluginName);
82
- return () => this.remove(item, pluginName);
83
- }
84
-
85
- /**
86
- * 移除 item
87
- */
88
- remove(item: T, pluginName?: string): boolean {
89
- const idx = this.items.indexOf(item);
90
- if (idx !== -1) {
91
- this.items.splice(idx, 1);
92
- const resolvedPluginName = pluginName ?? this.#findPluginName(item);
93
- for (const [, items] of this.pluginItems) {
94
- const i = items.indexOf(item);
95
- if (i !== -1) items.splice(i, 1);
96
- }
97
- this.emit('remove', item, resolvedPluginName ?? '');
98
- return true;
99
- }
100
- return false;
101
- }
102
-
103
- /** 反查 item 所属的 pluginName */
104
- #findPluginName(item: T): string | undefined {
105
- for (const [name, items] of this.pluginItems) {
106
- if (items.includes(item)) return name;
107
- }
108
- return undefined;
109
- }
110
-
111
- /**
112
- * 获取指定插件注册的 item 列表
113
- */
114
- getByPlugin(pluginName: string): T[] {
115
- return this.pluginItems.get(pluginName) || [];
116
- }
117
-
118
- /**
119
- * 全局 item 数量
120
- */
121
- get count(): number {
122
- return this.items.length;
123
- }
124
-
125
- /**
126
- * 指定插件的 item 数量
127
- */
128
- countByPlugin(pluginName: string): number {
129
- return this.getByPlugin(pluginName).length;
130
- }
131
-
132
- /**
133
- * 序列化为 JSON(用于 HTTP API)
134
- * @param pluginName 如果提供,则只返回该插件的 item;否则返回全部
135
- */
136
- abstract toJSON(pluginName?: string): FeatureJSON;
137
-
138
- /**
139
- * 提供给 Plugin.prototype 的扩展方法
140
- * 子类重写此 getter 以注册扩展(如 addCommand)
141
- */
142
- get extensions(): Record<string, Function> {
143
- return {};
144
- }
145
-
146
- /**
147
- * 生命周期: 服务挂载时调用
148
- * @param plugin 宿主插件(通常是 root plugin)
149
- */
150
- mounted?(plugin: Plugin): void | Promise<void>;
151
-
152
- /**
153
- * 生命周期: 服务销毁时调用
154
- */
155
- dispose?(): void;
156
- }
6
+ export { Feature } from '@zhin.js/kernel';
7
+ export type { FeatureJSON, FeatureListener } from '@zhin.js/kernel';
package/src/index.ts CHANGED
@@ -39,4 +39,6 @@ export * from './scheduler/index.js'
39
39
  export * from '@zhin.js/database'
40
40
  export * from '@zhin.js/logger'
41
41
  // 只导出 Schema 类,避免与 utils.js 的 isEmpty 冲突
42
- export { Schema } from '@zhin.js/schema'
42
+ export { Schema } from '@zhin.js/schema'
43
+ // Re-export PluginLike from kernel (generic plugin interface)
44
+ export type { PluginLike } from '@zhin.js/kernel'
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Scheduler module — at / every / cron + heartbeat
2
+ * Re-export from @zhin.js/kernel for backward compatibility.
3
3
  */
4
-
4
+ export {
5
+ Scheduler,
6
+ getScheduler,
7
+ setScheduler,
8
+ } from '@zhin.js/kernel';
5
9
  export type {
6
10
  Schedule,
7
11
  JobPayload,
@@ -11,18 +15,5 @@ export type {
11
15
  JobCallback,
12
16
  AddJobOptions,
13
17
  IScheduler,
14
- } from './types.js';
15
- export { Scheduler } from './scheduler.js';
16
- export type { SchedulerOptions } from './scheduler.js';
17
-
18
- import type { Scheduler } from './scheduler.js';
19
-
20
- let schedulerInstance: Scheduler | null = null;
21
-
22
- export function getScheduler(): Scheduler | null {
23
- return schedulerInstance;
24
- }
25
-
26
- export function setScheduler(s: Scheduler | null): void {
27
- schedulerInstance = s;
28
- }
18
+ SchedulerOptions,
19
+ } from '@zhin.js/kernel';
package/src/types.ts CHANGED
@@ -221,7 +221,8 @@ export interface ToolParametersSchema<TArgs extends Record<string, any> = Record
221
221
 
222
222
  /**
223
223
  * 工具执行上下文
224
- * 包含消息来源、发送者等信息
224
+ * 包含消息来源、发送者等 IM 信息。
225
+ * 通用(IM 无关)版本请使用 @zhin.js/ai 的 ToolContext。
225
226
  */
226
227
  export interface ToolContext {
227
228
  /** 来源平台 */
@@ -462,4 +463,11 @@ export namespace Tool {
462
463
  // ============================================================================
463
464
 
464
465
  /** @deprecated 使用 Tool 替代 */
465
- export type AITool = Tool;
466
+ export type AITool = Tool;
467
+
468
+ /**
469
+ * IMToolContext — ToolContext 的显式 IM 别名。
470
+ * 当同时使用 @zhin.js/ai (通用 ToolContext) 和 @zhin.js/core (IM ToolContext) 时,
471
+ * 用此类型消除歧义。
472
+ */
473
+ export type IMToolContext = ToolContext;
package/src/utils.ts CHANGED
@@ -1,55 +1,46 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import * as vm from "vm";
1
+ /**
2
+ * @zhin.js/core utilities.
3
+ *
4
+ * Generic utilities are re-exported from @zhin.js/kernel.
5
+ * IM-specific utilities (compose, segment) remain here.
6
+ */
7
+
8
+ // ── Re-export generic utils from kernel ──
9
+ export {
10
+ evaluate,
11
+ execute,
12
+ clearEvalCache,
13
+ getEvalCacheStats,
14
+ getValueWithRuntime,
15
+ compiler,
16
+ remove,
17
+ isEmpty,
18
+ Time,
19
+ supportedPluginExtensions,
20
+ resolveEntry,
21
+ sleep,
22
+ } from '@zhin.js/kernel';
23
+
24
+ // ── IM-specific utilities ──
4
25
  import {
5
26
  AdapterMessage,
6
- Dict,
7
27
  MessageElement,
8
28
  MessageMiddleware,
9
29
  RegisteredAdapter,
10
30
  SendContent,
11
- } from "./types";
31
+ } from "./types.js";
12
32
  import { Message } from "./message.js";
13
33
 
14
- export function getValueWithRuntime(template: string, ctx: Dict) {
15
- return evaluate(template, ctx);
16
- }
17
- /**
18
- * Evaluate a single expression in a sandboxed vm context.
19
- * Unlike `execute`, does NOT wrap in IIFE — the expression value is returned directly.
20
- */
21
- export const evaluate = <S extends Record<string, unknown>, T = unknown>(exp: string, context: S): T | undefined => {
22
- const script = getOrCompileScript(exp);
23
- if (!script) return undefined;
24
-
25
- try {
26
- return script.runInNewContext(buildSandbox(context), { timeout: 200 }) as T;
27
- } catch {
28
- return undefined;
29
- }
30
- };
31
34
  /**
32
35
  * 组合中间件,洋葱模型
33
- * 灵感来源于 zhinjs/next 的 Hooks.compose
34
- *
35
- * @param middlewares 中间件列表
36
- * @returns 中间件处理函数
37
- *
38
- * @example
39
- * ```typescript
40
- * const composed = compose([middleware1, middleware2]);
41
- * await composed(message);
42
- * ```
43
36
  */
44
37
  export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
45
38
  middlewares: MessageMiddleware<P>[]
46
39
  ) {
47
- // 性能优化:空数组直接返回空函数
48
40
  if (middlewares.length === 0) {
49
41
  return () => Promise.resolve();
50
42
  }
51
43
 
52
- // 性能优化:单个中间件直接返回
53
44
  if (middlewares.length === 1) {
54
45
  return (message: Message<AdapterMessage<P>>, next: () => Promise<void> = () => Promise.resolve()) => {
55
46
  return middlewares[0](message, next);
@@ -62,7 +53,6 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
62
53
  ) {
63
54
  let index = -1;
64
55
  const dispatch = async (i: number = 0): Promise<void> => {
65
- // 防止 next() 被多次调用
66
56
  if (i <= index) {
67
57
  return Promise.reject(new Error("next() called multiple times"));
68
58
  }
@@ -73,7 +63,6 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
73
63
  try {
74
64
  return await fn(message, () => dispatch(i + 1));
75
65
  } catch (error) {
76
- // 中间件异常应该被记录但不中断整个流程
77
66
  console.error("Middleware error:", error);
78
67
  throw error;
79
68
  }
@@ -81,87 +70,7 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
81
70
  return dispatch(0);
82
71
  };
83
72
  }
84
- // LRU cache for compiled vm.Script instances
85
- const MAX_EVAL_CACHE_SIZE = 1000;
86
- const scriptCache = new Map<string, vm.Script>();
87
-
88
- function getOrCompileScript(code: string): vm.Script | null {
89
- let script = scriptCache.get(code);
90
- if (script) return script;
91
- try {
92
- script = new vm.Script(code);
93
- } catch {
94
- return null;
95
- }
96
- if (scriptCache.size >= MAX_EVAL_CACHE_SIZE) {
97
- const oldest = scriptCache.keys().next().value;
98
- if (oldest !== undefined) scriptCache.delete(oldest);
99
- }
100
- scriptCache.set(code, script);
101
- return script;
102
- }
103
-
104
- function buildSandbox<S extends Record<string, unknown>>(context: S): Record<string, unknown> {
105
- return {
106
- ...context,
107
- process: {
108
- version: process.version,
109
- versions: process.versions,
110
- platform: process.platform,
111
- arch: process.arch,
112
- release: process.release,
113
- uptime: process.uptime(),
114
- memoryUsage: process.memoryUsage(),
115
- cpuUsage: process.cpuUsage(),
116
- pid: process.pid,
117
- ppid: process.ppid,
118
- },
119
- global: undefined,
120
- globalThis: undefined,
121
- Buffer: undefined,
122
- crypto: undefined,
123
- require: undefined,
124
- import: undefined,
125
- __dirname: undefined,
126
- __filename: undefined,
127
- Bun: undefined,
128
- Deno: undefined,
129
- };
130
- }
131
-
132
- /**
133
- * Execute a code block in a sandboxed vm context.
134
- * Supports `return` statements by wrapping in an IIFE.
135
- * Throws on compilation or runtime errors.
136
- */
137
- export const execute = <S extends Record<string, unknown>, T = unknown>(code: string, context: S): T => {
138
- const wrapped = `(function(){${code}})()`;
139
- const script = getOrCompileScript(wrapped);
140
- if (!script) throw new SyntaxError(`Failed to compile: ${code.slice(0, 80)}`);
141
-
142
- return script.runInNewContext(buildSandbox(context), { timeout: 200 }) as T;
143
- };
144
-
145
- export function clearEvalCache(): void {
146
- scriptCache.clear();
147
- }
148
73
 
149
- export function getEvalCacheStats(): { size: number; maxSize: number } {
150
- return {
151
- size: scriptCache.size,
152
- maxSize: MAX_EVAL_CACHE_SIZE,
153
- };
154
- }
155
- export function compiler(template: string, ctx: Dict) {
156
- const matched = [...template.matchAll(/\${([^}]*?)}/g)];
157
- for (const item of matched) {
158
- const tpl = item[1];
159
- const raw = getValueWithRuntime(tpl, ctx);
160
- const value = typeof raw === 'string' ? raw : (raw == null ? 'undefined' : JSON.stringify(raw, null, 2));
161
- template = template.replace(`\${${item[1]}}`, value);
162
- }
163
- return template;
164
- }
165
74
  export function segment<T extends object>(type: string, data: T) {
166
75
  return {
167
76
  type,
@@ -197,33 +106,27 @@ export namespace segment {
197
106
  if (!Array.isArray(content)) content = [content];
198
107
  const toString = (template: string | MessageElement) => {
199
108
  if (typeof template !== "string") return [template];
200
-
201
- // 安全检查:限制输入长度,防止 ReDoS 攻击
202
- const MAX_TEMPLATE_LENGTH = 100000; // 100KB
109
+
110
+ const MAX_TEMPLATE_LENGTH = 100000;
203
111
  if (template.length > MAX_TEMPLATE_LENGTH) {
204
112
  throw new Error(`Template too large: ${template.length} > ${MAX_TEMPLATE_LENGTH}`);
205
113
  }
206
-
114
+
207
115
  template = unescape(template);
208
116
  const result: MessageElement[] = [];
209
- // 修复 ReDoS 漏洞:使用更安全的正则表达式
210
- // 注意:需要使用捕获组来获取属性字符串,否则无法正确重建原始标签
211
- // closingReg: 自闭合标签 <type attr="val"/>
212
117
  const closingReg = /<(\w+)(\s+[^>]*?)?\/>/;
213
- // twinningReg: 成对标签 <type attr="val">child</type>
214
118
  const twinningReg = /<(\w+)(\s+[^>]*?)?>([^]*?)<\/\1>/;
215
-
119
+
216
120
  let iterations = 0;
217
- const MAX_ITERATIONS = 1000; // 防止无限循环
218
-
121
+ const MAX_ITERATIONS = 1000;
122
+
219
123
  while (template.length && iterations++ < MAX_ITERATIONS) {
220
124
  const twinMatch = template.match(twinningReg);
221
125
  const closeMatch = template.match(closingReg);
222
-
223
- // 选择位置更靠前的匹配
126
+
224
127
  let match: RegExpMatchArray | null = null;
225
128
  let isClosing = false;
226
-
129
+
227
130
  if (twinMatch && closeMatch) {
228
131
  const twinIndex = template.indexOf(twinMatch[0]);
229
132
  const closeIndex = template.indexOf(closeMatch[0]);
@@ -239,14 +142,14 @@ export namespace segment {
239
142
  } else if (twinMatch) {
240
143
  match = twinMatch;
241
144
  }
242
-
145
+
243
146
  if (!match) break;
244
-
245
- const [fullMatch, type, attrStr = "", child = ""] = isClosing
147
+
148
+ const [fullMatch, type, attrStr = "", child = ""] = isClosing
246
149
  ? [match[0], match[1], match[2] || ""]
247
150
  : [match[0], match[1], match[2] || "", match[3] || ""];
248
151
  const index = template.indexOf(fullMatch);
249
- if (index === -1) break; // 安全检查
152
+ if (index === -1) break;
250
153
  const prevText = template.slice(0, index);
251
154
  if (prevText)
252
155
  result.push({
@@ -256,8 +159,6 @@ export namespace segment {
256
159
  },
257
160
  });
258
161
  template = template.slice(index + fullMatch.length);
259
- // 修复 ReDoS 漏洞:使用更简单的正则表达式
260
- // 原: /\s([^=]+)(?=(?=="([^"]+)")|(?=='([^']+)'))/g 嵌套前瞻断言
261
162
  const attrArr = [
262
163
  ...attrStr.matchAll(/\s+([^=\s]+)=(?:"([^"]*)"|'([^']*)')/g),
263
164
  ];
@@ -324,201 +225,3 @@ export namespace segment {
324
225
  .join("");
325
226
  }
326
227
  }
327
-
328
- export function remove<T>(list: T[], fn: (item: T) => boolean): void;
329
- export function remove<T>(list: T[], item: T): void;
330
- export function remove<T>(list: T[], arg: T | ((item: T) => boolean)) {
331
- const index =
332
- typeof arg === "function" &&
333
- !list.every((item) => typeof item === "function")
334
- ? list.findIndex(arg as (item: T) => boolean)
335
- : list.indexOf(arg as T);
336
- if (index !== -1) list.splice(index, 1);
337
- }
338
- export function isEmpty<T>(item: T) {
339
- if (Array.isArray(item)) return item.length === 0;
340
- if (typeof item === "object") {
341
- if (!item) return true;
342
- return Reflect.ownKeys(item).length === 0;
343
- }
344
- return false;
345
- }
346
-
347
- export namespace Time {
348
- export const millisecond = 1;
349
- export const second = 1000;
350
- export const minute = second * 60;
351
- export const hour = minute * 60;
352
- export const day = hour * 24;
353
- export const week = day * 7;
354
-
355
- let timezoneOffset = new Date().getTimezoneOffset();
356
-
357
- export function setTimezoneOffset(offset: number) {
358
- timezoneOffset = offset;
359
- }
360
-
361
- export function getTimezoneOffset() {
362
- return timezoneOffset;
363
- }
364
-
365
- export function getDateNumber(
366
- date: number | Date = new Date(),
367
- offset?: number
368
- ) {
369
- if (typeof date === "number") date = new Date(date);
370
- if (offset === undefined) offset = timezoneOffset;
371
- return Math.floor((date.valueOf() / minute - offset) / 1440);
372
- }
373
-
374
- export function fromDateNumber(value: number, offset?: number) {
375
- const date = new Date(value * day);
376
- if (offset === undefined) offset = timezoneOffset;
377
- return new Date(+date + offset * minute);
378
- }
379
-
380
- const numeric = /\d+(?:\.\d+)?/.source;
381
- const timeRegExp = new RegExp(
382
- `^${[
383
- "w(?:eek(?:s)?)?",
384
- "d(?:ay(?:s)?)?",
385
- "h(?:our(?:s)?)?",
386
- "m(?:in(?:ute)?(?:s)?)?",
387
- "s(?:ec(?:ond)?(?:s)?)?",
388
- ]
389
- .map((unit) => `(${numeric}${unit})?`)
390
- .join("")}$`
391
- );
392
-
393
- export function parseTime(source: string) {
394
- const capture = timeRegExp.exec(source);
395
- if (!capture) return 0;
396
- return (
397
- (parseFloat(capture[1]) * week || 0) +
398
- (parseFloat(capture[2]) * day || 0) +
399
- (parseFloat(capture[3]) * hour || 0) +
400
- (parseFloat(capture[4]) * minute || 0) +
401
- (parseFloat(capture[5]) * second || 0)
402
- );
403
- }
404
-
405
- export function parseDate(date: string) {
406
- const parsed = parseTime(date);
407
- let dateInput: string | number = date;
408
- if (parsed) {
409
- dateInput = Date.now() + parsed;
410
- } else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
411
- dateInput = `${new Date().toLocaleDateString()}-${date}`;
412
- } else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
413
- dateInput = `${new Date().getFullYear()}-${date}`;
414
- }
415
- return dateInput ? new Date(dateInput) : new Date();
416
- }
417
-
418
- export function formatTimeShort(ms: number) {
419
- const abs = Math.abs(ms);
420
- if (abs >= day - hour / 2) {
421
- return Math.round(ms / day) + "d";
422
- } else if (abs >= hour - minute / 2) {
423
- return Math.round(ms / hour) + "h";
424
- } else if (abs >= minute - second / 2) {
425
- return Math.round(ms / minute) + "m";
426
- } else if (abs >= second) {
427
- return Math.round(ms / second) + "s";
428
- }
429
- return ms + "ms";
430
- }
431
-
432
- export function formatTime(ms: number) {
433
- let result: string;
434
- if (ms >= day - hour / 2) {
435
- ms += hour / 2;
436
- result = Math.floor(ms / day) + " 天";
437
- if (ms % day > hour) {
438
- result += ` ${Math.floor((ms % day) / hour)} 小时`;
439
- }
440
- } else if (ms >= hour - minute / 2) {
441
- ms += minute / 2;
442
- result = Math.floor(ms / hour) + " 小时";
443
- if (ms % hour > minute) {
444
- result += ` ${Math.floor((ms % hour) / minute)} 分钟`;
445
- }
446
- } else if (ms >= minute - second / 2) {
447
- ms += second / 2;
448
- result = Math.floor(ms / minute) + " 分钟";
449
- if (ms % minute > second) {
450
- result += ` ${Math.floor((ms % minute) / second)} 秒`;
451
- }
452
- } else {
453
- result = Math.round(ms / second) + " 秒";
454
- }
455
- return result;
456
- }
457
-
458
- const dayMap = ["日", "一", "二", "三", "四", "五", "六"];
459
-
460
- function toDigits(source: number, length = 2) {
461
- return source.toString().padStart(length, "0");
462
- }
463
-
464
- export function template(template: string, time = new Date()) {
465
- return template
466
- .replace("yyyy", time.getFullYear().toString())
467
- .replace("yy", time.getFullYear().toString().slice(2))
468
- .replace("MM", toDigits(time.getMonth() + 1))
469
- .replace("dd", toDigits(time.getDate()))
470
- .replace("hh", toDigits(time.getHours()))
471
- .replace("mm", toDigits(time.getMinutes()))
472
- .replace("ss", toDigits(time.getSeconds()))
473
- .replace("SSS", toDigits(time.getMilliseconds(), 3));
474
- }
475
-
476
- function toHourMinute(time: Date) {
477
- return `${toDigits(time.getHours())}:${toDigits(time.getMinutes())}`;
478
- }
479
-
480
- export function formatTimeInterval(time: Date, interval?: number) {
481
- if (!interval) {
482
- return template("yyyy-MM-dd hh:mm:ss", time);
483
- } else if (interval === day) {
484
- return `每天 ${toHourMinute(time)}`;
485
- } else if (interval === week) {
486
- return `每周${dayMap[time.getDay()]} ${toHourMinute(time)}`;
487
- } else {
488
- return `${template("yyyy-MM-dd hh:mm:ss", time)} 起每隔 ${formatTime(
489
- interval
490
- )}`;
491
- }
492
- }
493
- }
494
- export const supportedPluginExtensions = [
495
- ".js",
496
- ".ts",
497
- ".mjs",
498
- ".cjs",
499
- ".jsx",
500
- ".tsx",
501
- "",
502
- ];
503
-
504
- export function resolveEntry(entry: string) {
505
- if (fs.existsSync(entry)) {
506
- const stat = fs.statSync(entry);
507
- if (stat.isFile()) return entry;
508
- if (stat.isSymbolicLink()) return resolveEntry(fs.realpathSync(entry));
509
- if (stat.isDirectory()) {
510
- const packageJsonPath = path.resolve(entry, 'package.json');
511
- if (!fs.existsSync(packageJsonPath)) return resolveEntry(path.join(entry, 'index'));
512
- const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
513
- return resolveEntry(path.resolve(entry, pkgJson.main || 'index.js'));
514
- }
515
- } else {
516
- for (const ext of supportedPluginExtensions) {
517
- const fullPath = path.resolve(entry + ext);
518
- if (fs.existsSync(fullPath)) return resolveEntry(fullPath);
519
- }
520
- }
521
- }
522
- export function sleep(ms: number) {
523
- return new Promise((resolve) => setTimeout(resolve, ms));
524
- }