@h-ai/scheduler 0.1.0-alpha.67 → 0.1.0-alpha.69

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/dist/index.d.ts CHANGED
@@ -16,25 +16,106 @@ import { PaginationOptionsInput, HaiResult, PaginatedResult } from '@h-ai/core';
16
16
  *
17
17
  * @example
18
18
  * ```ts
19
- * const config = SchedulerConfigSchema.parse({
20
- * enableDb: true,
21
- * tickInterval: 1000,
22
- * })
19
+ * import { SchedulerConfigSchema } from '@h-ai/scheduler'
20
+ *
21
+ * const parsed = SchedulerConfigSchema.safeParse({ enableDb: true, tickInterval: 1000, leaseMs: 300000 })
22
+ * if (parsed.success) {
23
+ * const renewalPeriod = Math.floor(parsed.data.leaseMs / 3)
24
+ * }
23
25
  * ```
24
26
  */
25
27
  declare const SchedulerConfigSchema: z.ZodObject<{
26
- enableDb: z.ZodDefault<z.ZodBoolean>;
28
+ enableDb: z.ZodDefault<z.ZodLiteral<true>>;
27
29
  tickInterval: z.ZodDefault<z.ZodNumber>;
28
- lockExpireMs: z.ZodDefault<z.ZodNumber>;
30
+ leaseMs: z.ZodDefault<z.ZodNumber>;
29
31
  nodeId: z.ZodOptional<z.ZodString>;
30
32
  maxLogs: z.ZodOptional<z.ZodNumber>;
31
33
  retentionDays: z.ZodOptional<z.ZodNumber>;
32
34
  }, z.core.$strip>;
33
- /** 调度器配置类型 */
35
+ /**
36
+ * 手动触发边界,重复使用同一幂等键会命中同一个执行记录。
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { TriggerTaskSchema } from '@h-ai/scheduler'
41
+ *
42
+ * const parsed = TriggerTaskSchema.safeParse({ source: 'billing-console', idempotencyKey: 'billing-2026-10' })
43
+ * if (parsed.success) {
44
+ * const sameOccurrenceKey = parsed.data.idempotencyKey
45
+ * }
46
+ * ```
47
+ */
48
+ declare const TriggerTaskSchema: z.ZodObject<{
49
+ source: z.ZodOptional<z.ZodString>;
50
+ idempotencyKey: z.ZodOptional<z.ZodString>;
51
+ }, z.core.$strip>;
52
+ /**
53
+ * 可信服务端人工核对结果;必须先停止旧 runner,不能将未知执行直接重排。
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { ExecutionResolutionSchema, scheduler } from '@h-ai/scheduler'
58
+ *
59
+ * // 可信运维流程已停止旧 runner 并核对实际外部结果。
60
+ * const pending = await scheduler.execution.listUnknown({ limit: 10 })
61
+ * if (!pending.success) throw new Error(pending.error.message)
62
+ * const entry = pending.data[0]
63
+ * if (entry) {
64
+ * const resolution = ExecutionResolutionSchema.parse({ status: 'discarded', expectedUpdatedAt: entry.updatedAt })
65
+ * const resolved = await scheduler.execution.resolveUnknown(entry.occurrenceId, resolution)
66
+ * if (!resolved.success) throw new Error(resolved.error.message)
67
+ * }
68
+ * ```
69
+ */
70
+ declare const ExecutionResolutionSchema: z.ZodObject<{
71
+ status: z.ZodEnum<{
72
+ success: "success";
73
+ discarded: "discarded";
74
+ }>;
75
+ expectedUpdatedAt: z.ZodNumber;
76
+ }, z.core.$strip>;
77
+ /**
78
+ * 调度器配置类型
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import type { SchedulerConfig } from '@h-ai/scheduler'
83
+ * import { SchedulerConfigSchema } from '@h-ai/scheduler'
84
+ *
85
+ * const config: SchedulerConfig = SchedulerConfigSchema.parse({ leaseMs: 60000 })
86
+ * const renewalInterval = config.leaseMs / 3
87
+ * ```
88
+ */
34
89
  type SchedulerConfig = z.infer<typeof SchedulerConfigSchema>;
35
- /** 调度器配置输入类型 */
90
+ /**
91
+ * 调度器配置输入类型
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import type { SchedulerConfigInput } from '@h-ai/scheduler'
96
+ * import { scheduler } from '@h-ai/scheduler'
97
+ *
98
+ * // 宿主已成功初始化共享 reldb。
99
+ * const config: SchedulerConfigInput = { leaseMs: 60000, retentionDays: 30 }
100
+ * const initialized = await scheduler.init(config)
101
+ * if (!initialized.success) throw new Error(initialized.error.message)
102
+ * ```
103
+ */
36
104
  type SchedulerConfigInput = z.input<typeof SchedulerConfigSchema>;
37
105
 
106
+ /**
107
+ * Scheduler 领域错误定义;业务调用先判断 success 再按错误码分支。
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { HaiSchedulerError, scheduler } from '@h-ai/scheduler'
112
+ *
113
+ * const result = await scheduler.trigger('missing-task')
114
+ * if (!result.success && result.error.code === HaiSchedulerError.TASK_NOT_FOUND.code) {
115
+ * const missingTask = true
116
+ * }
117
+ * ```
118
+ */
38
119
  declare const HaiSchedulerError: {
39
120
  readonly NOT_INITIALIZED: _h_ai_core.HaiErrorDef;
40
121
  readonly INIT_FAILED: _h_ai_core.HaiErrorDef;
@@ -48,27 +129,85 @@ declare const HaiSchedulerError: {
48
129
  readonly DB_SAVE_FAILED: _h_ai_core.HaiErrorDef;
49
130
  readonly ALREADY_RUNNING: _h_ai_core.HaiErrorDef;
50
131
  readonly NOT_RUNNING: _h_ai_core.HaiErrorDef;
51
- readonly LOCK_ACQUIRE_FAILED: _h_ai_core.HaiErrorDef;
52
132
  readonly JS_COMPILE_FAILED: _h_ai_core.HaiErrorDef;
53
133
  readonly HOOK_EXECUTION_FAILED: _h_ai_core.HaiErrorDef;
134
+ readonly LOG_PERSIST_FAILED: _h_ai_core.HaiErrorDef;
54
135
  };
55
- /** 任务参数字典(键值对) */
136
+ /**
137
+ * 任务参数字典(键值对)
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * import type { TaskParams } from '@h-ai/scheduler'
142
+ *
143
+ * const params: TaskParams = { month: '2026-10', dryRun: true }
144
+ * const month = params.month
145
+ * ```
146
+ */
56
147
  type TaskParams = Record<string, unknown>;
57
- /** 任务触发类型 */
148
+ /**
149
+ * 任务触发类型
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * import type { TaskTriggerType } from '@h-ai/scheduler'
154
+ *
155
+ * const triggerType: TaskTriggerType = 'manual'
156
+ * const isManual = triggerType === 'manual'
157
+ * ```
158
+ */
58
159
  type TaskTriggerType = 'scheduled' | 'manual';
59
- /** 任务处理器类型 */
160
+ /**
161
+ * 任务处理器类型
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * import type { TaskHandlerKind } from '@h-ai/scheduler'
166
+ *
167
+ * const kind: TaskHandlerKind = 'api'
168
+ * const needsNetwork = kind === 'api'
169
+ * ```
170
+ */
60
171
  type TaskHandlerKind = 'api' | 'js';
61
- /** 失败重试策略 */
172
+ /**
173
+ * 失败重试策略
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * import type { TaskRetryPolicy } from '@h-ai/scheduler'
178
+ *
179
+ * const retry: TaskRetryPolicy = { maxAttempts: 3, backoffMs: [1000, 5000] }
180
+ * // 只有外部副作用确实按 occurrenceId 持久去重时,任务才能声明 idempotent。
181
+ * ```
182
+ */
62
183
  interface TaskRetryPolicy {
63
- /** 最大尝试次数(包含首次执行,最小 1) */
184
+ /** 最大尝试次数(含首次及崩溃恢复,范围 1–100,默认 1)。 */
64
185
  maxAttempts: number;
65
186
  /** 每次重试前的退避时间(毫秒);按顺序对应第 2、3... 次尝试 */
66
187
  backoffMs?: number[];
67
188
  }
68
- /** 任务执行目标类型 */
189
+ /**
190
+ * 任务执行目标类型
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * import type { TaskExecutionTargetType } from '@h-ai/scheduler'
195
+ *
196
+ * const target: TaskExecutionTargetType = 'hook'
197
+ * const usesHostCallback = target === 'hook'
198
+ * ```
199
+ */
69
200
  type TaskExecutionTargetType = TaskHandlerKind | 'hook';
70
201
  /**
71
202
  * API 任务配置
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * import type { ApiTaskConfig } from '@h-ai/scheduler'
207
+ *
208
+ * const handler: ApiTaskConfig = { kind: 'api', url: 'https://example.com/health', method: 'GET', timeout: 5000, maxResponseBytes: 4096 }
209
+ * // API 接收端若支持重试,须持久处理 Idempotency-Key 与 fencing 元数据。
210
+ * ```
72
211
  */
73
212
  interface ApiTaskConfig {
74
213
  /** 处理器类型 */
@@ -95,6 +234,14 @@ interface ApiTaskConfig {
95
234
  *
96
235
  * `code` 为 JS 函数字符串,例如:
97
236
  * `(context) => ({ taskId: context.task.id, params: context.params })`
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * import type { JsTaskConfig } from '@h-ai/scheduler'
241
+ *
242
+ * const handler: JsTaskConfig = { kind: 'js', code: '(context) => ({ taskId: context.taskId })', timeout: 1000 }
243
+ * // 仅装载受信任的服务端源码;工作线程不是不可信代码沙箱。
244
+ * ```
98
245
  */
99
246
  interface JsTaskConfig {
100
247
  /** 处理器类型 */
@@ -104,34 +251,30 @@ interface JsTaskConfig {
104
251
  /** 独立线程执行期限,单位毫秒(默认 30000,含线程启动);超时终止线程后才允许重试 */
105
252
  timeout?: number;
106
253
  }
107
- /** 统一任务处理器配置 */
254
+ /**
255
+ * 统一任务处理器配置
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * import type { TaskHandlerConfig } from '@h-ai/scheduler'
260
+ *
261
+ * const handler: TaskHandlerConfig = { kind: 'api', url: 'https://example.com/health' }
262
+ * const timeout = handler.timeout ?? 30000
263
+ * ```
264
+ */
108
265
  type TaskHandlerConfig = ApiTaskConfig | JsTaskConfig;
109
266
  /**
110
267
  * 任务定义
111
268
  *
112
269
  * @example
113
270
  * ```ts
114
- * const jsTask: TaskDefinition = {
115
- * id: 'cleanup',
116
- * name: '清理过期数据',
117
- * cron: '0 2 * * *',
118
- * params: { channel: 'nightly' },
119
- * handler: {
120
- * kind: 'js',
121
- * code: '(context) => ({ taskId: context.task.id, params: context.params })',
122
- * },
123
- * }
271
+ * import type { TaskDefinition } from '@h-ai/scheduler'
272
+ * import { scheduler } from '@h-ai/scheduler'
124
273
  *
125
- * const apiTask: TaskDefinition = {
126
- * id: 'health-check',
127
- * name: '健康检查',
128
- * cron: '&#42;/5 * * * *',
129
- * handler: {
130
- * kind: 'api',
131
- * url: 'https://api.example.com/health',
132
- * method: 'GET',
133
- * },
134
- * }
274
+ * // 宿主已成功初始化共享 reldb 与 scheduler。
275
+ * const definition: TaskDefinition = { ...{ id: 'daily-report', name: '每日报表', cron: '0 2 * * *', timezone: 'UTC' }, handler: { kind: 'api', url: 'https://example.com/health', maxResponseBytes: 4096 } }
276
+ * const registered = await scheduler.register(definition)
277
+ * if (!registered.success) throw new Error(registered.error.message)
135
278
  * ```
136
279
  */
137
280
  interface TaskDefinition {
@@ -143,7 +286,7 @@ interface TaskDefinition {
143
286
  description?: string;
144
287
  /** cron 表达式(标准 5 字段:分 时 日 月 周) */
145
288
  cron: string;
146
- /** IANA 时区,用于解释 cron 墙上时间;省略时使用运行环境默认时区 */
289
+ /** IANA 时区,用于解释 cron 墙上时间;省略或清空时统一使用 UTC,不读取节点 TZ。 */
147
290
  timezone?: string;
148
291
  /** 是否启用(默认 true) */
149
292
  enabled?: boolean;
@@ -151,6 +294,8 @@ interface TaskDefinition {
151
294
  deleteAfterRun?: boolean;
152
295
  /** 失败重试策略 */
153
296
  retry?: TaskRetryPolicy;
297
+ /** 全部副作用按 context.occurrenceId 原子持久去重时才为 true;默认 false,未知结果禁止自动重跑。 */
298
+ idempotent?: boolean;
154
299
  /** 通用参数 */
155
300
  params?: TaskParams;
156
301
  /** 任务处理器;为空时可由全局事件回调统一处理 */
@@ -158,6 +303,14 @@ interface TaskDefinition {
158
303
  }
159
304
  /**
160
305
  * 任务触发信息
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * import type { TaskTriggerInfo } from '@h-ai/scheduler'
310
+ *
311
+ * const trigger: TaskTriggerInfo = { type: 'manual', source: 'billing-console' }
312
+ * const source = trigger.source ?? 'cron'
313
+ * ```
161
314
  */
162
315
  interface TaskTriggerInfo {
163
316
  /** 触发类型 */
@@ -167,10 +320,27 @@ interface TaskTriggerInfo {
167
320
  }
168
321
  /**
169
322
  * 任务执行上下文
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * import type { SchedulerTaskContext } from '@h-ai/scheduler'
327
+ *
328
+ * function safeReceipt(context: SchedulerTaskContext): string {
329
+ * if (!context.occurrenceId || !context.executionToken) throw new Error('Shared execution identity required')
330
+ * return context.occurrenceId
331
+ * }
332
+ * // 接收端将稳定 receipt 原子持久去重;executionToken 不写入用户可见日志。
333
+ * ```
170
334
  */
171
335
  interface SchedulerTaskContext {
172
336
  /** 一次调度发生的稳定标识;所有重试共享,供宿主构造业务幂等键。 */
173
337
  occurrenceId?: string;
338
+ /** 当前领取独有的凭据,用于宿主条件提交;续租失败后不能继续产生副作用。 */
339
+ executionToken?: string;
340
+ /** 数据库执行序号;与 attempt 组成接收端可比较的 fencing 版本。 */
341
+ sequence?: number;
342
+ /** 同一 occurrence 中持久化的尝试次数。 */
343
+ attempt?: number;
174
344
  /** 当前任务 */
175
345
  task: TaskDefinition;
176
346
  /** 任务 ID */
@@ -180,10 +350,35 @@ interface SchedulerTaskContext {
180
350
  /** 触发信息 */
181
351
  trigger: TaskTriggerInfo;
182
352
  }
183
- /** 编译后的 JS 任务函数 */
353
+ /**
354
+ * 编译后的 JS 任务函数
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * import type { JsTaskHandler } from '@h-ai/scheduler'
359
+ *
360
+ * const handler: JsTaskHandler = (context, signal) => {
361
+ * signal?.throwIfAborted()
362
+ * return { taskId: context.taskId, batch: context.params.batch }
363
+ * }
364
+ * ```
365
+ */
184
366
  type JsTaskHandler = (context: SchedulerTaskContext, signal?: AbortSignal) => unknown | Promise<unknown>;
185
- /** 任务开始事件 */
367
+ /**
368
+ * 任务开始事件
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * import type { SchedulerTaskStartEvent } from '@h-ai/scheduler'
373
+ *
374
+ * function summarizeStart(event: SchedulerTaskStartEvent) {
375
+ * return { taskId: event.task.id, occurrenceId: event.context?.occurrenceId, observedAt: event.startedAt }
376
+ * }
377
+ * ```
378
+ */
186
379
  interface SchedulerTaskStartEvent {
380
+ /** 已领取执行的持久身份,观测回调也可据此标记或幂等处理;竞争中断时不存在。 */
381
+ context?: SchedulerTaskContext;
187
382
  /** 关闭调度器时取消;回调必须停止后续副作用并释放资源 */
188
383
  signal: AbortSignal;
189
384
  /** 当前任务 */
@@ -193,19 +388,55 @@ interface SchedulerTaskStartEvent {
193
388
  /** 开始时间(Unix 时间戳,毫秒) */
194
389
  startedAt: number;
195
390
  }
196
- /** 任务执行事件 */
391
+ /**
392
+ * 任务执行事件
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * import type { SchedulerTaskExecuteEvent } from '@h-ai/scheduler'
397
+ *
398
+ * function execute(event: SchedulerTaskExecuteEvent) {
399
+ * event.signal.throwIfAborted()
400
+ * return { taskId: event.context.taskId, params: event.context.params }
401
+ * }
402
+ * ```
403
+ */
197
404
  interface SchedulerTaskExecuteEvent extends SchedulerTaskStartEvent {
198
405
  /** 执行上下文 */
199
406
  context: SchedulerTaskContext;
200
407
  }
201
- /** 任务中断事件 */
408
+ /**
409
+ * 任务中断事件
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * import type { SchedulerTaskInterruptedEvent } from '@h-ai/scheduler'
414
+ *
415
+ * function summarizeInterruption(event: SchedulerTaskInterruptedEvent) {
416
+ * return { taskId: event.task.id, reason: event.reason, observedAt: event.interruptedAt }
417
+ * }
418
+ * // 中断观测不能证明外部副作用不存在。
419
+ * ```
420
+ */
202
421
  interface SchedulerTaskInterruptedEvent extends SchedulerTaskStartEvent {
203
422
  /** 中断时间(Unix 时间戳,毫秒) */
204
423
  interruptedAt: number;
205
424
  /** 中断原因 */
206
425
  reason: string;
207
426
  }
208
- /** 任务结束事件 */
427
+ /**
428
+ * 任务结束事件
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * import type { SchedulerTaskFinishEvent } from '@h-ai/scheduler'
433
+ *
434
+ * function needsLogRepair(event: SchedulerTaskFinishEvent): boolean {
435
+ * return event.log.persistence.status === 'pending'
436
+ * }
437
+ * // 结束回调是观测;业务是否已提交以 journal 为准。
438
+ * ```
439
+ */
209
440
  interface SchedulerTaskFinishEvent extends SchedulerTaskStartEvent {
210
441
  /** 结束时间(Unix 时间戳,毫秒) */
211
442
  finishedAt: number;
@@ -217,6 +448,19 @@ interface SchedulerTaskFinishEvent extends SchedulerTaskStartEvent {
217
448
  *
218
449
  * - `onTaskExecute` 可用于统一处理无内置 handler 的任务
219
450
  * - `onTaskStart` / `onTaskInterrupted` / `onTaskFinish` 用于观测生命周期
451
+ *
452
+ * @example
453
+ * ```ts
454
+ * import type { SchedulerTaskHooks } from '@h-ai/scheduler'
455
+ *
456
+ * const hooks: SchedulerTaskHooks = {
457
+ * onTaskExecute: ({ context, signal }) => {
458
+ * signal.throwIfAborted()
459
+ * return { taskId: context.taskId }
460
+ * },
461
+ * }
462
+ * // 有副作用的宿主需使用 context.occurrenceId 和持久条件写入。
463
+ * ```
220
464
  */
221
465
  interface SchedulerTaskHooks {
222
466
  /** 任务开始前触发 */
@@ -228,14 +472,60 @@ interface SchedulerTaskHooks {
228
472
  /** 任务结束时触发 */
229
473
  onTaskFinish?: (event: SchedulerTaskFinishEvent) => void | Promise<void>;
230
474
  }
231
- /** 执行状态 */
232
- type ExecutionStatus = 'success' | 'failed' | 'interrupted';
475
+ /**
476
+ * 执行状态
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * import type { ExecutionStatus } from '@h-ai/scheduler'
481
+ *
482
+ * function needsReconciliation(status: ExecutionStatus): boolean {
483
+ * return status === 'unknown'
484
+ * }
485
+ * // unknown 先隔离旧执行并核对外部效果,不换键重试。
486
+ * ```
487
+ */
488
+ type ExecutionStatus = 'success' | 'failed' | 'interrupted' | 'unknown';
489
+ /**
490
+ * 日志投影状态,与业务 status 独立;unconfirmed 表示没有可补写的已确认终态。
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * import type { TaskLogPersistence } from '@h-ai/scheduler'
495
+ *
496
+ * const persistence: TaskLogPersistence = { status: 'pending', error: { code: 'hai:scheduler:032', message: 'Log projection pending' } }
497
+ * const needsRepair = persistence.status === 'pending'
498
+ * ```
499
+ */
500
+ interface TaskLogPersistence {
501
+ /** pending 可由已发布 journal 补写;persisted 已确认日志和回执;unconfirmed 不能推断业务结果。 */
502
+ status: 'pending' | 'persisted' | 'unconfirmed';
503
+ /** 安全的公开错误,不包含 SQL、驱动 cause 或连接参数。 */
504
+ error?: {
505
+ code: string;
506
+ message: string;
507
+ };
508
+ }
233
509
  /**
234
510
  * 任务执行日志
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * import type { TaskExecutionLog } from '@h-ai/scheduler'
515
+ *
516
+ * function summarize(log: TaskExecutionLog) {
517
+ * return { businessStatus: log.status, logStatus: log.persistence.status, occurrenceId: log.occurrenceId }
518
+ * }
519
+ * // pending 的成功业务只补写日志,不重跑处理器。
520
+ * ```
235
521
  */
236
522
  interface TaskExecutionLog {
237
- /** 日志 ID */
238
- id: number;
523
+ /** 持久执行回执;相同定时 occurrence 或手动幂等键共用。 */
524
+ occurrenceId?: string;
525
+ /** 每次尝试固定的 UUID 主键;未知写入回执或并发补写均复用。 */
526
+ id: string;
527
+ /** 独立持久化状态;业务成功不等于日志保存成功。 */
528
+ persistence: TaskLogPersistence;
239
529
  /** 任务 ID */
240
530
  taskId: string;
241
531
  /** 任务名称 */
@@ -248,7 +538,7 @@ interface TaskExecutionLog {
248
538
  triggerSource: string | null;
249
539
  /** 执行状态 */
250
540
  status: ExecutionStatus;
251
- /** 执行结果(JSON 字符串) */
541
+ /** 序列化 JS/Hook 结果或 API 响应正文;无结果为 null */
252
542
  result: string | null;
253
543
  /** 错误或中断信息 */
254
544
  error: string | null;
@@ -261,6 +551,17 @@ interface TaskExecutionLog {
261
551
  }
262
552
  /**
263
553
  * 执行日志查询选项
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * import type { LogQueryOptions } from '@h-ai/scheduler'
558
+ * import { scheduler } from '@h-ai/scheduler'
559
+ *
560
+ * const options: LogQueryOptions = { taskId: 'daily-report', status: 'success', pagination: { page: 1, pageSize: 20 } }
561
+ * const logs = await scheduler.getLogs(options)
562
+ * if (!logs.success) throw new Error(logs.error.message)
563
+ * const page = logs.data.items
564
+ * ```
264
565
  */
265
566
  interface LogQueryOptions {
266
567
  /** 按任务 ID 过滤 */
@@ -278,7 +579,17 @@ interface LogQueryOptions {
278
579
  /** 分页参数(page 从 1 开始,pageSize 默认 20) */
279
580
  pagination?: PaginationOptionsInput;
280
581
  }
281
- /** 日志清理策略 */
582
+ /**
583
+ * 日志清理策略
584
+ *
585
+ * @example
586
+ * ```ts
587
+ * import type { SchedulerLogCleanupPolicy } from '@h-ai/scheduler'
588
+ *
589
+ * const policy: SchedulerLogCleanupPolicy = { maxLogs: 1000, retentionDays: 30 }
590
+ * // 仅清理日志投影,不撤销或重排共享 execution journal。
591
+ * ```
592
+ */
282
593
  interface SchedulerLogCleanupPolicy {
283
594
  /** 最多保留日志条数 */
284
595
  maxLogs?: number;
@@ -290,15 +601,28 @@ interface SchedulerLogCleanupPolicy {
290
601
  *
291
602
  * 可更新的字段包括:名称、cron 表达式、启用状态、参数、处理器。
292
603
  * 任务 ID 不可更新。
604
+ *
605
+ * @example
606
+ * ```ts
607
+ * import type { TaskUpdateInput } from '@h-ai/scheduler'
608
+ * import { scheduler } from '@h-ai/scheduler'
609
+ *
610
+ * const updates: TaskUpdateInput = { enabled: false, timezone: null }
611
+ * const changed = await scheduler.updateTask('daily-report', updates)
612
+ * if (!changed.success) throw new Error(changed.error.message)
613
+ * // null 时区恢复 UTC,任务 ID 不能修改。
614
+ * ```
293
615
  */
294
616
  interface TaskUpdateInput {
617
+ /** 全部副作用的持久去重能力声明。 */
618
+ idempotent?: boolean;
295
619
  /** 任务名称 */
296
620
  name?: string;
297
621
  /** 任务描述 */
298
622
  description?: string;
299
623
  /** cron 表达式 */
300
624
  cron?: string;
301
- /** IANA 时区;传 null 表示清空任务时区 */
625
+ /** IANA 时区;传 null 表示清空并恢复 UTC,不读取节点 TZ。 */
302
626
  timezone?: string | null;
303
627
  /** 是否启用 */
304
628
  enabled?: boolean;
@@ -311,15 +635,122 @@ interface TaskUpdateInput {
311
635
  /** 任务处理器;传 null 表示清空内置处理器 */
312
636
  handler?: TaskHandlerConfig | null;
313
637
  }
314
- /** 手工触发输入 */
638
+ /**
639
+ * 手工触发输入
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * import type { TriggerTaskInput } from '@h-ai/scheduler'
644
+ * import { scheduler } from '@h-ai/scheduler'
645
+ *
646
+ * const options: TriggerTaskInput = { source: 'billing-console', idempotencyKey: 'billing-2026-10' }
647
+ * const result = await scheduler.trigger('daily-report', options)
648
+ * if (!result.success) throw new Error(result.error.message)
649
+ * const status = result.data.status
650
+ * ```
651
+ */
315
652
  interface TriggerTaskInput {
316
653
  /** 手工触发来源(例如 admin-console / api / cli) */
317
654
  source?: string;
655
+ /** 稳定业务触发键(1–128 字符);同任务重复使用会命中同一持久执行。 */
656
+ idempotencyKey?: string;
657
+ }
658
+ /**
659
+ * 可信运维可见的执行状态;unknown 继续占用任务串行槽。
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * import type { SchedulerExecutionState } from '@h-ai/scheduler'
664
+ *
665
+ * function occupiesUncertainSlot(state: SchedulerExecutionState): boolean {
666
+ * return state === 'unknown'
667
+ * }
668
+ * ```
669
+ */
670
+ type SchedulerExecutionState = 'claimed' | 'running' | ExecutionStatus | 'discarded';
671
+ /**
672
+ * 不包含代码、参数、URL 或 owner 凭据的共享执行记录。
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * import type { SchedulerExecutionEntry } from '@h-ai/scheduler'
677
+ *
678
+ * function publicAudit(entry: SchedulerExecutionEntry) {
679
+ * return { occurrenceId: entry.occurrenceId, status: entry.status, revision: entry.updatedAt }
680
+ * }
681
+ * // 安全投影不包含 owner token 和业务载荷。
682
+ * ```
683
+ */
684
+ interface SchedulerExecutionEntry {
685
+ /** 稳定执行回执。 */
686
+ occurrenceId: string;
687
+ /** 任务标识。 */
688
+ taskId: string;
689
+ /** 共享状态。 */
690
+ status: SchedulerExecutionState;
691
+ /** 数据库序号,配合 attempt 用于外部 fencing。 */
692
+ sequence: number;
693
+ /** 已开始的尝试次数。 */
694
+ attemptCount: number;
695
+ /** 最近状态版本(Unix 毫秒),用于人工裁定条件。 */
696
+ updatedAt: number;
697
+ /** 日志投影独立状态;pending 不会重新排队业务。 */
698
+ logPersistence: TaskLogPersistence['status'];
699
+ }
700
+ /**
701
+ * 可信服务端的执行查询与人工核对入口。
702
+ *
703
+ * @example
704
+ * ```ts
705
+ * import type { SchedulerExecutionOperations } from '@h-ai/scheduler'
706
+ *
707
+ * async function repairPending(execution: SchedulerExecutionOperations) {
708
+ * const pending = await execution.listLogFailures({ limit: 20 })
709
+ * if (!pending.success) return pending
710
+ * for (const entry of pending.data) {
711
+ * const repaired = await execution.repairLog(entry.occurrenceId)
712
+ * if (!repaired.success) return repaired
713
+ * }
714
+ * }
715
+ * // 此链路不会调用业务 handler。
716
+ * ```
717
+ */
718
+ interface SchedulerExecutionOperations {
719
+ /** 按回执查询安全元数据。@example await scheduler.execution.get(occurrenceId) */
720
+ get: (occurrenceId: string) => Promise<HaiResult<SchedulerExecutionEntry | null>>;
721
+ /** 有界查询待核对的 unknown;每页 1–100 条。@example await scheduler.execution.listUnknown({ limit: 50, offset: 0 }) */
722
+ listUnknown: (options?: {
723
+ limit?: number;
724
+ offset?: number;
725
+ }) => Promise<HaiResult<SchedulerExecutionEntry[]>>;
726
+ /** 分页查询已有终态但日志尚待补写的回执,不含业务载荷。@example await scheduler.execution.listLogFailures({ limit: 50 }) */
727
+ listLogFailures: (options?: {
728
+ limit?: number;
729
+ offset?: number;
730
+ }) => Promise<HaiResult<SchedulerExecutionEntry[]>>;
731
+ /** 只从已发布 journal 幂等补写日志,不执行业务;并发调用安全。@example await scheduler.execution.repairLog(occurrenceId) */
732
+ repairLog: (occurrenceId: string) => Promise<HaiResult<void>>;
733
+ /** 先终止旧 runner 并核对外部结果,再条件裁定;不能重新排队。@example await scheduler.execution.resolveUnknown(id, { status: 'discarded', expectedUpdatedAt: entry.updatedAt }) */
734
+ resolveUnknown: (occurrenceId: string, resolution: {
735
+ status: 'success' | 'discarded';
736
+ expectedUpdatedAt: number;
737
+ }) => Promise<HaiResult<void>>;
318
738
  }
319
739
  /**
320
740
  * 调度器初始化输入
321
741
  *
322
742
  * 包含调度器配置、可选的预定义任务列表,以及可选的全局生命周期回调。
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * import type { SchedulerInitInput } from '@h-ai/scheduler'
747
+ * import { scheduler } from '@h-ai/scheduler'
748
+ *
749
+ * // 宿主已成功初始化共享 reldb。
750
+ * const input: SchedulerInitInput = { leaseMs: 60000, tasks: [{ id: 'daily-report', name: '每日报表', cron: '0 2 * * *', timezone: 'UTC' }], hooks: { onTaskExecute: ({ context }) => ({ taskId: context.taskId }) } }
751
+ * const initialized = await scheduler.init(input)
752
+ * if (!initialized.success) throw new Error(initialized.error.message)
753
+ * ```
323
754
  */
324
755
  interface SchedulerInitInput extends SchedulerConfigInput {
325
756
  /** 预定义任务列表(从配置中加载,初始化时自动注册) */
@@ -329,8 +760,22 @@ interface SchedulerInitInput extends SchedulerConfigInput {
329
760
  }
330
761
  /**
331
762
  * 调度器函数接口
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * import type { SchedulerFunctions } from '@h-ai/scheduler'
767
+ *
768
+ * async function stopScheduler(instance: SchedulerFunctions) {
769
+ * const stopped = instance.stop()
770
+ * if (!stopped.success) return stopped
771
+ * await instance.close()
772
+ * }
773
+ * // close 返回 Promise<void>,宿主随后关闭自己拥有的共享数据库。
774
+ * ```
332
775
  */
333
776
  interface SchedulerFunctions {
777
+ /** 共享执行状态、未知结果查询与可信人工裁定。 */
778
+ readonly execution: SchedulerExecutionOperations;
334
779
  /** 刷新共享数据库中的任务定义,用于跨实例配置读取。 */
335
780
  refreshTasks: () => Promise<HaiResult<void>>;
336
781
  /** 初始化调度器 */
@@ -375,6 +820,38 @@ interface SchedulerFunctions {
375
820
  * @module scheduler-main
376
821
  */
377
822
 
823
+ /**
824
+ * 定时任务公共入口;先初始化共享 RelDB,再注册/触发/启动;关闭会排空任务和日志修复,宿主最后关闭数据库。
825
+ *
826
+ * @example
827
+ * ```ts
828
+ * import { core } from '@h-ai/core'
829
+ * import { reldb } from '@h-ai/reldb'
830
+ * import { scheduler } from '@h-ai/scheduler'
831
+ *
832
+ * // 本地开发示例;生产使用所有节点共同连接的 PostgreSQL/MySQL。
833
+ * const database = await reldb.init({ type: 'sqlite', database: ':memory:' })
834
+ * if (!database.success) throw new Error(database.error.message)
835
+ * try {
836
+ * const initialized = await scheduler.init({ hooks: { onTaskExecute: ({ context }) => ({ taskId: context.taskId }) } })
837
+ * if (!initialized.success) throw new Error(initialized.error.message)
838
+ * const registered = await scheduler.register({ id: 'daily-report', name: '每日报表', cron: '0 2 * * *', timezone: 'UTC' })
839
+ * if (!registered.success) throw new Error(registered.error.message)
840
+ * const executed = await scheduler.trigger('daily-report', { idempotencyKey: 'report-2026-10-23' })
841
+ * if (!executed.success) throw new Error(executed.error.message)
842
+ * // 业务 status 与 persistence 分开判断;pending 只补日志,不重跑业务。
843
+ * const status = executed.data.status
844
+ * const persistence = executed.data.persistence.status
845
+ * } finally {
846
+ * await scheduler.close()
847
+ * const closed = await reldb.close()
848
+ * if (!closed.success) {
849
+ * core.logger.error('Database cleanup failed', { errorCode: closed.error.code })
850
+ * process.exitCode = 1
851
+ * }
852
+ * }
853
+ * ```
854
+ */
378
855
  declare const scheduler: SchedulerFunctions;
379
856
 
380
- export { type ApiTaskConfig, type ExecutionStatus, HaiSchedulerError, type JsTaskConfig, type JsTaskHandler, type LogQueryOptions, type SchedulerConfig, type SchedulerConfigInput, SchedulerConfigSchema, type SchedulerFunctions, type SchedulerInitInput, type SchedulerLogCleanupPolicy, type SchedulerTaskContext, type SchedulerTaskExecuteEvent, type SchedulerTaskFinishEvent, type SchedulerTaskHooks, type SchedulerTaskInterruptedEvent, type SchedulerTaskStartEvent, type TaskDefinition, type TaskExecutionLog, type TaskExecutionTargetType, type TaskHandlerConfig, type TaskHandlerKind, type TaskParams, type TaskRetryPolicy, type TaskTriggerInfo, type TaskTriggerType, type TaskUpdateInput, type TriggerTaskInput, scheduler };
857
+ export { type ApiTaskConfig, ExecutionResolutionSchema, type ExecutionStatus, HaiSchedulerError, type JsTaskConfig, type JsTaskHandler, type LogQueryOptions, type SchedulerConfig, type SchedulerConfigInput, SchedulerConfigSchema, type SchedulerExecutionEntry, type SchedulerExecutionOperations, type SchedulerExecutionState, type SchedulerFunctions, type SchedulerInitInput, type SchedulerLogCleanupPolicy, type SchedulerTaskContext, type SchedulerTaskExecuteEvent, type SchedulerTaskFinishEvent, type SchedulerTaskHooks, type SchedulerTaskInterruptedEvent, type SchedulerTaskStartEvent, type TaskDefinition, type TaskExecutionLog, type TaskExecutionTargetType, type TaskHandlerConfig, type TaskHandlerKind, type TaskLogPersistence, type TaskParams, type TaskRetryPolicy, type TaskTriggerInfo, type TaskTriggerType, type TaskUpdateInput, type TriggerTaskInput, TriggerTaskSchema, scheduler };