@zhin.js/core 1.0.0 → 1.0.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +295 -74
  4. package/lib/adapter.d.ts +39 -0
  5. package/lib/adapter.d.ts.map +1 -0
  6. package/{dist → lib}/adapter.js +20 -2
  7. package/lib/adapter.js.map +1 -0
  8. package/lib/app.d.ts +115 -0
  9. package/lib/app.d.ts.map +1 -0
  10. package/{dist → lib}/app.js +148 -78
  11. package/lib/app.js.map +1 -0
  12. package/lib/bot.d.ts +31 -0
  13. package/lib/bot.d.ts.map +1 -0
  14. package/lib/command.d.ts +32 -0
  15. package/lib/command.d.ts.map +1 -0
  16. package/lib/command.js +46 -0
  17. package/lib/command.js.map +1 -0
  18. package/lib/component.d.ts +107 -0
  19. package/lib/component.d.ts.map +1 -0
  20. package/lib/component.js +273 -0
  21. package/lib/component.js.map +1 -0
  22. package/{dist → lib}/config.d.ts.map +1 -1
  23. package/{dist → lib}/config.js +6 -9
  24. package/lib/config.js.map +1 -0
  25. package/lib/cron.d.ts +81 -0
  26. package/lib/cron.d.ts.map +1 -0
  27. package/lib/cron.js +159 -0
  28. package/lib/cron.js.map +1 -0
  29. package/lib/errors.d.ts +165 -0
  30. package/lib/errors.d.ts.map +1 -0
  31. package/lib/errors.js +306 -0
  32. package/lib/errors.js.map +1 -0
  33. package/lib/index.d.ts +15 -0
  34. package/lib/index.d.ts.map +1 -0
  35. package/lib/index.js +17 -0
  36. package/lib/index.js.map +1 -0
  37. package/lib/message.d.ts +44 -0
  38. package/lib/message.d.ts.map +1 -0
  39. package/lib/message.js +11 -0
  40. package/lib/message.js.map +1 -0
  41. package/lib/plugin.d.ts +50 -0
  42. package/lib/plugin.d.ts.map +1 -0
  43. package/lib/plugin.js +170 -0
  44. package/lib/plugin.js.map +1 -0
  45. package/lib/prompt.d.ts +116 -0
  46. package/lib/prompt.d.ts.map +1 -0
  47. package/lib/prompt.js +240 -0
  48. package/lib/prompt.js.map +1 -0
  49. package/lib/schema.d.ts +83 -0
  50. package/lib/schema.d.ts.map +1 -0
  51. package/lib/schema.js +245 -0
  52. package/lib/schema.js.map +1 -0
  53. package/{dist → lib}/types-generator.d.ts.map +1 -1
  54. package/{dist → lib}/types-generator.js +6 -3
  55. package/lib/types-generator.js.map +1 -0
  56. package/lib/types.d.ts +119 -0
  57. package/lib/types.d.ts.map +1 -0
  58. package/lib/utils.d.ts +52 -0
  59. package/lib/utils.d.ts.map +1 -0
  60. package/lib/utils.js +338 -0
  61. package/lib/utils.js.map +1 -0
  62. package/package.json +15 -9
  63. package/src/adapter.ts +25 -9
  64. package/src/app.ts +363 -258
  65. package/src/bot.ts +29 -8
  66. package/src/command.ts +50 -0
  67. package/src/component.ts +318 -0
  68. package/src/config.ts +9 -12
  69. package/src/cron.ts +176 -0
  70. package/src/errors.ts +365 -0
  71. package/src/index.ts +16 -13
  72. package/src/message.ts +44 -0
  73. package/src/plugin.ts +148 -66
  74. package/src/prompt.ts +290 -0
  75. package/src/schema.ts +273 -0
  76. package/src/types-generator.ts +7 -3
  77. package/src/types.ts +77 -30
  78. package/src/utils.ts +312 -0
  79. package/tests/adapter.test.ts +36 -22
  80. package/tests/app.test.ts +30 -0
  81. package/tests/command.test.ts +545 -0
  82. package/tests/component.test.ts +656 -0
  83. package/tests/config.test.ts +1 -1
  84. package/tests/errors.test.ts +311 -0
  85. package/tests/message.test.ts +402 -0
  86. package/tests/plugin.test.ts +275 -143
  87. package/tests/utils.test.ts +80 -0
  88. package/tsconfig.json +3 -4
  89. package/dist/adapter.d.ts +0 -22
  90. package/dist/adapter.d.ts.map +0 -1
  91. package/dist/adapter.js.map +0 -1
  92. package/dist/app.d.ts +0 -69
  93. package/dist/app.d.ts.map +0 -1
  94. package/dist/app.js.map +0 -1
  95. package/dist/bot.d.ts +0 -9
  96. package/dist/bot.d.ts.map +0 -1
  97. package/dist/config.js.map +0 -1
  98. package/dist/index.d.ts +0 -9
  99. package/dist/index.d.ts.map +0 -1
  100. package/dist/index.js +0 -12
  101. package/dist/index.js.map +0 -1
  102. package/dist/logger.d.ts +0 -3
  103. package/dist/logger.d.ts.map +0 -1
  104. package/dist/logger.js +0 -3
  105. package/dist/logger.js.map +0 -1
  106. package/dist/plugin.d.ts +0 -41
  107. package/dist/plugin.d.ts.map +0 -1
  108. package/dist/plugin.js +0 -95
  109. package/dist/plugin.js.map +0 -1
  110. package/dist/types-generator.js.map +0 -1
  111. package/dist/types.d.ts +0 -69
  112. package/dist/types.d.ts.map +0 -1
  113. package/src/logger.ts +0 -3
  114. package/tests/logger.test.ts +0 -170
  115. package/tsconfig.tsbuildinfo +0 -1
  116. /package/{dist → lib}/bot.js +0 -0
  117. /package/{dist → lib}/bot.js.map +0 -0
  118. /package/{dist → lib}/config.d.ts +0 -0
  119. /package/{dist → lib}/types-generator.d.ts +0 -0
  120. /package/{dist → lib}/types.js +0 -0
  121. /package/{dist → lib}/types.js.map +0 -0
package/src/bot.ts CHANGED
@@ -1,9 +1,30 @@
1
- import type {BotConfig, SendOptions} from "./types";
2
-
3
- export interface Bot<T extends BotConfig=BotConfig> {
4
- config: T;
5
- connected?: boolean;
6
- connect():Promise<void>
7
- disconnect():Promise<void>
8
- sendMessage(options: SendOptions): Promise<void>
1
+ import type {RegisteredAdapters, SendOptions,AdapterConfig} from "./types.js";
2
+ import {Message} from "./message.js";
3
+ /**
4
+ * Bot接口:所有平台机器人需实现的统一接口。
5
+ * 负责消息格式化、连接、断开、消息发送等。
6
+ * @template M 消息类型
7
+ * @template T 配置类型
8
+ */
9
+ export interface Bot<M extends object={},T extends BotConfig=BotConfig> {
10
+ /** 机器人配置 */
11
+ $config: T;
12
+ /** 是否已连接 */
13
+ $connected?: boolean;
14
+ /** 格式化平台消息为标准Message结构 */
15
+ $formatMessage(message:M):Message<M>
16
+ /** 连接机器人 */
17
+ $connect():Promise<void>
18
+ /** 断开机器人 */
19
+ $disconnect():Promise<void>
20
+ /** 发送消息 */
21
+ $sendMessage(options: SendOptions): Promise<void>
22
+ }
23
+ /**
24
+ * Bot配置类型,所有平台机器人通用
25
+ */
26
+ export interface BotConfig{
27
+ context:string
28
+ name:string
29
+ [key:string]:any
9
30
  }
package/src/command.ts ADDED
@@ -0,0 +1,50 @@
1
+ import {MatchResult, SegmentMatcher} from "segment-matcher";
2
+ import {AdapterMessage, RegisteredAdapters, SendContent} from "./types.js";
3
+ import type {Message} from "./message.js";
4
+ import {MaybePromise} from "@zhin.js/types";
5
+
6
+ /**
7
+ * MessageCommand类:命令系统核心,基于segment-matcher实现。
8
+ * 支持多平台命令注册、作用域限制、参数解析、异步处理等。
9
+ */
10
+ export class MessageCommand<T extends keyof RegisteredAdapters=keyof RegisteredAdapters> extends SegmentMatcher{
11
+ #callbacks:MessageCommand.Callback<T>[]=[];
12
+ #checkers:MessageCommand.Checker<T>[]=[]
13
+ /**
14
+ * 限定命令作用域(适配器名)
15
+ * @param scopes 适配器名列表
16
+ */
17
+ scope<R extends T>(...scopes:R[]):MessageCommand<R>{
18
+ this.#checkers.push((m)=>(scopes as string[]).includes(m.$adapter))
19
+ return this as MessageCommand<R>
20
+ }
21
+ /**
22
+ * 注册命令回调
23
+ * @param callback 命令处理函数
24
+ */
25
+ action(callback:MessageCommand.Callback<T>){
26
+ this.#callbacks.push(callback)
27
+ return this as MessageCommand<T>;
28
+ }
29
+ /**
30
+ * 处理消息,自动匹配命令并执行回调
31
+ * @param message 消息对象
32
+ * @returns 命令返回内容或undefined
33
+ */
34
+ async handle(message:Message<AdapterMessage<T>>):Promise<SendContent|undefined>{
35
+ for(const check of this.#checkers){
36
+ const result=await check(message)
37
+ if(!result) return;
38
+ }
39
+ const matched=this.match(message.$content);
40
+ if(!matched) return
41
+ for(const handler of this.#callbacks){
42
+ const result=await handler(message,matched)
43
+ if(result) return result
44
+ }
45
+ }
46
+ }
47
+ export namespace MessageCommand{
48
+ export type Callback<T extends keyof RegisteredAdapters>=(message:Message<AdapterMessage<T>>,result:MatchResult)=>MaybePromise<SendContent|void>;
49
+ export type Checker<T extends keyof RegisteredAdapters>=(message:Message<AdapterMessage<T>>)=>MaybePromise<boolean>
50
+ }
@@ -0,0 +1,318 @@
1
+ import {getValueWithRuntime, compiler, segment} from './utils.js';
2
+ import {Dict, SendContent, SendOptions} from './types.js';
3
+ import {Message} from "./message.js";
4
+ import {MaybePromise} from "@zhin.js/types";
5
+ export const CapWithChild = Symbol('CapWithChild');
6
+ export const CapWithClose = Symbol('CapWithClose');
7
+ /**
8
+ * Component类:消息组件系统核心,支持模板渲染、属性解析、循环等。
9
+ * 用于自定义消息结构和复用UI片段。
10
+ * @template T 组件props类型
11
+ * @template D 组件data类型
12
+ * @template P 组件props配置类型
13
+ */
14
+ export class Component<T = {}, D = {}, P = Component.Props<T>> {
15
+ [CapWithClose]: RegExp;
16
+ [CapWithChild]: RegExp;
17
+ $props: Component.PropConfig[] = [];
18
+ get name(){
19
+ return this.$options.name;
20
+ }
21
+ set name(value:string){
22
+ this.$options.name=value;
23
+ }
24
+ /**
25
+ * 构造函数:初始化组件,生成属性正则
26
+ * @param $options 组件配置项
27
+ */
28
+ constructor(private $options: Component.Options<T, D, P>) {
29
+ this.formatProps();
30
+ this[CapWithChild] = new RegExp(`<${$options.name}([^>]*)?>([^<])*?</${$options.name}>`);
31
+ this[CapWithClose] = new RegExp(`<${$options.name}([^>]*)?/>`);
32
+ }
33
+ /**
34
+ * 判断模板是否为自闭合标签
35
+ * @param template 模板字符串
36
+ */
37
+ isClosing(template: string) {
38
+ return this[CapWithClose].test(template);
39
+ }
40
+ /**
41
+ * 匹配组件标签
42
+ * @param template 模板字符串
43
+ * @returns 匹配到的标签内容
44
+ */
45
+ match(template: string) {
46
+ let [match] = this[CapWithChild].exec(template) || [];
47
+ if (match) return match;
48
+ [match] = this[CapWithClose].exec(template) || [];
49
+ return match;
50
+ }
51
+ /**
52
+ * 格式化props配置,生成props数组
53
+ */
54
+ private formatProps() {
55
+ for (const [key, value] of Object.entries(this.$options.props || {})) {
56
+ this.formatProp(key, value as any);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 格式化单个prop配置
62
+ * @param name 属性名
63
+ * @param value 类型或配置
64
+ */
65
+ private formatProp(name: string, value: Exclude<Component.PropConfig, 'name'> | Component.TypeConstruct) {
66
+ if (typeof value === 'function') {
67
+ return this.$props.push({
68
+ name,
69
+ type: value,
70
+ default: undefined,
71
+ });
72
+ }
73
+ return this.$props.push({
74
+ name,
75
+ type: value.type,
76
+ default: value.default,
77
+ });
78
+ }
79
+
80
+ parseProps(template: string) {
81
+ const result = Object.fromEntries(
82
+ this.$props.map(prop => {
83
+ const generateDefault = typeof prop.default === 'function' ? prop.default : () => prop.default;
84
+ return [prop.name, generateDefault()];
85
+ }),
86
+ );
87
+ const matchedArr = [...template.matchAll(/([a-zA-Z\-:]+)\s*=\s*(['"])(.*?)\2/g)].filter(Boolean);
88
+ if (!matchedArr.length) return result;
89
+ for (const [_, key, __, value] of matchedArr) {
90
+ Object.defineProperty(result, key, {
91
+ enumerable: true,
92
+ writable: false,
93
+ value,
94
+ });
95
+ }
96
+ return result;
97
+ }
98
+ parseChildren(template: string): string {
99
+ if (this.isClosing(template)) return '';
100
+ const matched = template.match(/<[^>]+>([^<]*?)<\/[^?]+>/);
101
+ if (!matched) return '';
102
+ return matched[1];
103
+ }
104
+ async render(template: string, context: Component.Context): Promise<SendContent> {
105
+ const props = this.parseProps(template);
106
+ const assignValue = () => {
107
+ for (const key of keys) {
108
+ if (!key.startsWith(':')) continue;
109
+ Object.defineProperty(props, key.slice(1), {
110
+ value: getValueWithRuntime(Reflect.get(props, key), context.parent),
111
+ });
112
+ Reflect.deleteProperty(props, key);
113
+ }
114
+ };
115
+ const keys = Object.keys(props).map(key => {
116
+ const newKey = key.replace(/(\w)+-(\w)/g, function (_, char, later) {
117
+ return `${char}${later.toUpperCase()}`;
118
+ });
119
+ if (key !== newKey) {
120
+ Object.defineProperty(props, newKey, {
121
+ value: Reflect.get(props, key),
122
+ enumerable: true,
123
+ });
124
+ Reflect.deleteProperty(props, key);
125
+ }
126
+ return newKey;
127
+ });
128
+ assignValue();
129
+ const data = this.$options.data ? this.$options.data.apply(props as P) : ({} as D);
130
+ for (const key of keys) {
131
+ if (key === 'vFor') {
132
+ const { 'vFor': expression, 'v-for': _, ...rest } = props as any;
133
+ const { name, value, ...other } = Component.fixLoop(expression);
134
+ const list = value === '__loop__' ? other[value] : getValueWithRuntime(value, context);
135
+ const fnStr = `
136
+ const result=[];\n
137
+ for(const ${name} of list){\n
138
+ result.push(render(props,{\n
139
+ ...context,\n
140
+ children:'',\n
141
+ $origin:'${template.replace(/'/g, "'")}',
142
+ parent:{\n
143
+ ...context.parent,\n
144
+ ${name}:list[${name}]
145
+ }\n
146
+ }))\n
147
+ }
148
+ return result;`;
149
+ const fn = new Function('render,list,props,context', fnStr);
150
+ const newTpl = template
151
+ .replace(`v-for="${expression}"`, '')
152
+ .replace(`v-for='${expression}'`, '')
153
+ .replace(`vFor="${expression}"`, '')
154
+ .replace(`vFor='${expression}'`, '');
155
+ return (await Promise.all(fn(this.render.bind(this), list, newTpl, context))).join('');
156
+ }
157
+ if (key === 'vIf') {
158
+ const needRender = getValueWithRuntime(Reflect.get(props as object, 'vIf'), context);
159
+ if (!needRender) return '';
160
+ }
161
+ }
162
+ context.children = this.parseChildren(template) || context.children;
163
+ const ctx = {
164
+ $slots: context.$slots || {},
165
+ ...props,
166
+ ...data,
167
+ $message: context.$message,
168
+ render: context.render,
169
+ parent: context,
170
+ children: context.children,
171
+ } as Component.Context<D & P>;
172
+ const result = segment.toString(await this.$options.render(props as P, ctx));
173
+ context.$root = context.$root.replace(context.$origin || template,result.includes('<')?segment.escape(result):result);
174
+ return context.render(context.$root, context);
175
+ }
176
+ }
177
+ export function defineComponent<P>(render: Component.Render<P, {}>, name?: string): Component<{}, {}, P>;
178
+ export function defineComponent<T, D = {}, P = Component.Props<T>>(options: Component.Options<T, D>): Component<T, D, P>;
179
+ export function defineComponent<T = {}, D = {}, P = Component.Props<T>>(options: Component.Options<T, D, P> | Component.Render<P, D>, name = options.name) {
180
+ if (typeof options === 'function')
181
+ options = {
182
+ name,
183
+ render: options,
184
+ };
185
+ return new Component(options);
186
+ }
187
+
188
+ export namespace Component {
189
+ export type TypeConstruct<T = any> = {
190
+ new (): T;
191
+ readonly prototype: T;
192
+ };
193
+ export type PropConfig<T extends TypeConstruct = TypeConstruct> = {
194
+ name: string;
195
+ type: T;
196
+ default: Prop<T>;
197
+ };
198
+
199
+ export interface Options<T, D, P = Props<T>> {
200
+ name: string;
201
+ props?: T;
202
+ data?: (this: P) => D;
203
+ render: Render<P, D>;
204
+ }
205
+
206
+ export type Context<T = {}> = {
207
+ $slots: Dict<Render<any, any>>;
208
+ $message: Message;
209
+ $origin?: string;
210
+ $root: string;
211
+ parent: Context;
212
+ render(template: string, context: Context): Promise<SendContent>;
213
+ children?: string;
214
+ } & T;
215
+ export type Render<P = {}, D = {}> = (props: P, context: Context<P & D>) => MaybePromise<SendContent>;
216
+ export type Props<T> = {
217
+ [P in keyof T]: Prop<T[P]>;
218
+ };
219
+ export type PropWithDefault<T> = {
220
+ type: T;
221
+ default?: DefaultValue<T>;
222
+ };
223
+ type DefaultValue<T> = T extends ObjectConstructor | ArrayConstructor ? () => Prop<T> : Prop<T>;
224
+ export type Prop<T> = T extends BooleanConstructor
225
+ ? boolean
226
+ : T extends StringConstructor
227
+ ? string
228
+ : T extends NumberConstructor
229
+ ? number
230
+ : T extends ArrayConstructor
231
+ ? any[]
232
+ : T extends ObjectConstructor
233
+ ? Dict
234
+ : T extends PropWithDefault<infer R>
235
+ ? Prop<R>
236
+ : unknown;
237
+ export const fixLoop = (loop: string) => {
238
+ let [_, name, value] = /(\S+)\sin\s(\S+)/.exec(loop) || [];
239
+ if (/\d+/.test(value))
240
+ value = `[${new Array(+value)
241
+ .fill(0)
242
+ .map((_, i) => i)
243
+ .join(',')}]`;
244
+ if (/^\[.+]$/.test(value)) {
245
+ return { name, value: '__loop__', __loop__: JSON.parse(value) };
246
+ }
247
+ return { name, value };
248
+ };
249
+
250
+ export async function render(componentMap:Map<string,Component>,options:SendOptions):Promise<SendOptions>{
251
+ if(!componentMap.size) return options;
252
+ const components=[
253
+ ...componentMap.values(),
254
+ Template,
255
+ Slot,
256
+ ]
257
+ const createContext = (runtime: Dict = {}, parent: Component.Context, $root: string): Component.Context => {
258
+ return {
259
+ $slots: {},
260
+ ...runtime,
261
+ $message: parent.$message,
262
+ $root,
263
+ parent,
264
+ render: (template: string, context) => {
265
+ return renderWithRuntime(template, context, context.$root);
266
+ },
267
+ };
268
+ };
269
+ const renderWithRuntime = async (template: string, runtime: Dict, $root: string):Promise<SendContent> => {
270
+ const ctx = createContext(runtime, runtime as Component.Context, $root);
271
+ template = compiler(template, runtime);
272
+ for (const comp of components) {
273
+ const match = comp.match(template);
274
+ if (!match) continue;
275
+ return await comp.render(match, ctx);
276
+ }
277
+ return template;
278
+ };
279
+ const template=segment.toString(options.content);
280
+ const output=await renderWithRuntime(template, options, template)
281
+ const content=segment.from(output)
282
+ return {
283
+ ...options,
284
+ content
285
+ };
286
+ }
287
+
288
+ export const Template=defineComponent({
289
+ name: 'template',
290
+ render(props, context) {
291
+ const keys = Object.keys(props);
292
+ if (!keys.length) keys.push('#default');
293
+ for (const key of Object.keys(props)) {
294
+ if (key.startsWith('#')) {
295
+ context.parent.$slots[key.slice(1)] = (async p => {
296
+ return await context.render(context.children || '', { ...context, ...p });
297
+ }) as Render;
298
+ }
299
+ }
300
+ return '';
301
+ },
302
+ })
303
+ export const Slot=defineComponent({
304
+ name: 'slot',
305
+ props: {
306
+ name: String,
307
+ },
308
+ render({ name, ...props }, context) {
309
+ name = name || 'default';
310
+ if (!context.parent) return '';
311
+ if (context.parent.$slots[name]) return context.parent.$slots[name](props, context) as string;
312
+ return context.children || '';
313
+ },
314
+ })
315
+ }
316
+ process.on('unhandledRejection',e=>{
317
+ // console.error 已替换为注释
318
+ })
package/src/config.ts CHANGED
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
5
5
  import { parse as parseToml } from 'toml';
6
6
  import { config as loadDotenv } from 'dotenv';
7
- import type { AppConfig,DefineConfig } from './types.js';
7
+ import type {AppConfig, DefineConfig} from './types.js';
8
8
 
9
9
  export interface ConfigOptions {
10
10
  configPath?: string;
@@ -49,7 +49,7 @@ function replaceEnvVars(str: string): string {
49
49
  } else if (defaultValue !== undefined) {
50
50
  return defaultValue;
51
51
  } else {
52
- console.warn(`环境变量 ${envName} 未定义,保持原值`);
52
+ // console.warn 已替换为注释
53
53
  return match;
54
54
  }
55
55
  });
@@ -131,7 +131,7 @@ function findConfigFile(cwd: string = process.cwd()): string | null {
131
131
  'zhin.config.yml',
132
132
  'zhin.config.json',
133
133
  'zhin.config.toml',
134
- 'zhin.config.js',
134
+ 'zhin.config.ts',
135
135
  'zhin.config.ts',
136
136
  // 然后查找 config.* 格式
137
137
  'config.yaml',
@@ -262,15 +262,12 @@ export function saveConfig(config: AppConfig, filePath: string): void {
262
262
  */
263
263
  export function createDefaultConfig(format: ConfigFormat = 'yaml'): AppConfig {
264
264
  return {
265
- bots: [
266
- {
267
- name: 'onebot11',
268
- context: 'onebot11',
269
- url: '${ONEBOT_URL:-ws://localhost:8080}',
270
- access_token: '${ONEBOT_ACCESS_TOKEN:-}'
271
- }
272
- ],
265
+ bots: [{
266
+ name: 'onebot11',
267
+ context: 'onebot11',
268
+ url: '${ONEBOT_URL:-ws://localhost:8080}'
269
+ }],
273
270
  plugin_dirs: ['./src/plugins', 'node_modules'],
274
- plugins: []
271
+ plugins: [],
275
272
  };
276
273
  }
package/src/cron.ts ADDED
@@ -0,0 +1,176 @@
1
+ import { CronExpressionParser, CronExpression } from 'cron-parser';
2
+
3
+ /**
4
+ * Cron 定时任务类
5
+ * 基于 cron-parser 实现的定时任务调度器
6
+ */
7
+ export class Cron {
8
+ private expression: CronExpression;
9
+ private callback: () => void | Promise<void>;
10
+ private timeoutId?: NodeJS.Timeout;
11
+ private isRunning = false;
12
+ private isDisposed = false;
13
+
14
+ /**
15
+ * 创建一个新的 Cron 实例
16
+ * @param cronExpression - Cron 表达式 (例如: '0 0 * * *' 表示每天午夜执行)
17
+ * @param callback - 要执行的回调函数
18
+ */
19
+ constructor(cronExpression: string, callback: () => void | Promise<void>) {
20
+ try {
21
+ this.expression = CronExpressionParser.parse(cronExpression);
22
+ this.callback = callback;
23
+ } catch (error) {
24
+ throw new Error(`Invalid cron expression "${cronExpression}": ${(error as Error).message}`);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 启动定时任务
30
+ */
31
+ run(): void {
32
+ if (this.isDisposed) {
33
+ throw new Error('Cannot run a disposed cron job');
34
+ }
35
+
36
+ if (this.isRunning) {
37
+ return; // 已经在运行中
38
+ }
39
+
40
+ this.isRunning = true;
41
+ this.scheduleNext();
42
+ }
43
+
44
+ /**
45
+ * 停止定时任务
46
+ */
47
+ stop(): void {
48
+ if (this.timeoutId) {
49
+ clearTimeout(this.timeoutId);
50
+ this.timeoutId = undefined;
51
+ }
52
+ this.isRunning = false;
53
+ }
54
+
55
+ /**
56
+ * 销毁定时任务,释放资源
57
+ */
58
+ dispose(): void {
59
+ this.stop();
60
+ this.isDisposed = true;
61
+ }
62
+
63
+ /**
64
+ * 获取下一次执行时间
65
+ */
66
+ getNextExecutionTime(): Date {
67
+ if (this.isDisposed) {
68
+ throw new Error('Cannot get next execution time for a disposed cron job');
69
+ }
70
+
71
+ // 重置表达式到当前时间
72
+ this.expression.reset();
73
+ return this.expression.next().toDate();
74
+ }
75
+
76
+ /**
77
+ * 检查任务是否正在运行
78
+ */
79
+ get running(): boolean {
80
+ return this.isRunning;
81
+ }
82
+
83
+ /**
84
+ * 检查任务是否已被销毁
85
+ */
86
+ get disposed(): boolean {
87
+ return this.isDisposed;
88
+ }
89
+
90
+ /**
91
+ * 获取原始的 cron 表达式字符串
92
+ */
93
+ get cronExpression(): string {
94
+ return this.expression.stringify();
95
+ }
96
+
97
+ /**
98
+ * 调度下一次执行
99
+ */
100
+ private scheduleNext(): void {
101
+ if (!this.isRunning || this.isDisposed) {
102
+ return;
103
+ }
104
+
105
+ try {
106
+ // 重置到当前时间并获取下一次执行时间
107
+ this.expression.reset();
108
+ const nextDate = this.expression.next().toDate();
109
+ const now = new Date();
110
+ const delay = nextDate.getTime() - now.getTime();
111
+
112
+ // 如果延迟时间为负数或0,说明应该立即执行
113
+ if (delay <= 0) {
114
+ this.executeCallback();
115
+ return;
116
+ }
117
+
118
+ // 设置定时器
119
+ this.timeoutId = setTimeout(() => {
120
+ this.executeCallback();
121
+ }, delay);
122
+
123
+ } catch (error) {
124
+ console.error(`Error scheduling next cron execution: ${(error as Error).message}`);
125
+ // 如果出错,停止任务
126
+ this.stop();
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 执行回调函数并调度下一次执行
132
+ */
133
+ private async executeCallback(): Promise<void> {
134
+ if (!this.isRunning || this.isDisposed) {
135
+ return;
136
+ }
137
+
138
+ try {
139
+ // 执行回调函数
140
+ await this.callback();
141
+ } catch (error) {
142
+ console.error(`Error executing cron callback: ${(error as Error).message}`);
143
+ }
144
+
145
+ // 调度下一次执行
146
+ this.scheduleNext();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Cron 表达式格式说明:
152
+ *
153
+ * 标准格式: "秒 分 时 日 月 周"
154
+ *
155
+ * 字段说明:
156
+ * - 秒: 0-59
157
+ * - 分: 0-59
158
+ * - 时: 0-23
159
+ * - 日: 1-31
160
+ * - 月: 1-12 (或 JAN-DEC)
161
+ * - 周: 0-7 (0和7都表示周日,或 SUN-SAT)
162
+ *
163
+ * 特殊字符:
164
+ * - 星号: 匹配任意值
165
+ * - 问号: 用于日和周字段,表示不指定值
166
+ * - 横线: 表示范围,如 1-5
167
+ * - 逗号: 表示列表,如 1,3,5
168
+ * - 斜杠: 表示步长,如 0/15 表示每15分钟
169
+ *
170
+ * 常用示例:
171
+ * - "0 0 0 * * *": 每天午夜执行
172
+ * - "0 0/15 * * * *": 每15分钟执行
173
+ * - "0 0 12 * * *": 每天中午12点执行
174
+ * - "0 0 0 1 * *": 每月1号午夜执行
175
+ * - "0 0 0 * * 0": 每周日午夜执行
176
+ */