@h-ai/reach 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
@@ -10,27 +10,25 @@ import { HaiResult } from '@h-ai/core';
10
10
  */
11
11
 
12
12
  /**
13
- * Console Provider 配置(开发/测试用,将消息输出到日志)
13
+ * 开发/测试日志适配器配置;不证明外部投递,生产共享状态守卫拒绝console。
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { ConsoleProviderConfigSchema } from '@h-ai/reach'
18
+ * const config = ConsoleProviderConfigSchema.parse({ name: 'dev-log', type: 'console' })
19
+ * ```
14
20
  */
15
21
  declare const ConsoleProviderConfigSchema: z.ZodObject<{
16
22
  name: z.ZodString;
17
23
  type: z.ZodLiteral<"console">;
18
24
  }, z.core.$strip>;
19
25
  /**
20
- * SMTP 邮件 Provider 配置
26
+ * SMTP配置,默认TLS/465;凭据来自私有环境,SMTP没有接收端持久幂等能力声明。
21
27
  *
22
28
  * @example
23
29
  * ```ts
24
- * {
25
- * name: 'email',
26
- * type: 'smtp',
27
- * host: 'smtp.example.com',
28
- * port: 465,
29
- * secure: true,
30
- * user: 'noreply@example.com',
31
- * pass: 'password',
32
- * from: 'noreply@example.com'
33
- * }
30
+ * import { SmtpProviderConfigSchema } from '@h-ai/reach'
31
+ * const config = SmtpProviderConfigSchema.parse({ name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com', user: process.env.HAI_REACH_PROVIDERS_0_USER, pass: process.env.HAI_REACH_PROVIDERS_0_PASS })
34
32
  * ```
35
33
  */
36
34
  declare const SmtpProviderConfigSchema: z.ZodObject<{
@@ -44,18 +42,12 @@ declare const SmtpProviderConfigSchema: z.ZodObject<{
44
42
  from: z.ZodString;
45
43
  }, z.core.$strip>;
46
44
  /**
47
- * 阿里云短信 Provider 配置(通过 HTTP API 调用,无需 SDK)
45
+ * 阿里云短信POP API配置;Secret只在服务端私有注入,发送需Provider模板编码等扩展参数。
48
46
  *
49
47
  * @example
50
48
  * ```ts
51
- * {
52
- * name: 'sms',
53
- * type: 'aliyun-sms',
54
- * accessKeyId: 'LTAI...',
55
- * accessKeySecret: '...',
56
- * signName: '某某科技',
57
- * endpoint: 'dysmsapi.aliyuncs.com'
58
- * }
49
+ * import { AliyunSmsProviderConfigSchema } from '@h-ai/reach'
50
+ * const config = AliyunSmsProviderConfigSchema.parse({ name: 'sms', type: 'aliyun-sms', accessKeyId: process.env.HAI_REACH_PROVIDERS_0_ACCESSKEYID, accessKeySecret: process.env.HAI_REACH_PROVIDERS_0_ACCESSKEYSECRET, signName: process.env.HAI_REACH_PROVIDERS_0_SIGNNAME })
59
51
  * ```
60
52
  */
61
53
  declare const AliyunSmsProviderConfigSchema: z.ZodObject<{
@@ -67,17 +59,13 @@ declare const AliyunSmsProviderConfigSchema: z.ZodObject<{
67
59
  endpoint: z.ZodDefault<z.ZodString>;
68
60
  }, z.core.$strip>;
69
61
  /**
70
- * API 回调 Provider 配置(通用 HTTP 回调)
62
+ * HTTP回调配置;idempotent默认false,只有接收端按稳定Idempotency-Key原子持久去重并覆盖旧请求迟到窗口才允许true。
63
+ * 写入该布尔值或发送请求头本身不证明接收端具备此能力。
71
64
  *
72
65
  * @example
73
66
  * ```ts
74
- * {
75
- * name: 'webhook',
76
- * type: 'api',
77
- * url: 'https://api.example.com/notify',
78
- * method: 'POST',
79
- * headers: { 'Authorization': 'Bearer xxx' }
80
- * }
67
+ * import { ApiProviderConfigSchema } from '@h-ai/reach'
68
+ * const config = ApiProviderConfigSchema.parse({ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false })
81
69
  * ```
82
70
  */
83
71
  declare const ApiProviderConfigSchema: z.ZodObject<{
@@ -90,9 +78,16 @@ declare const ApiProviderConfigSchema: z.ZodObject<{
90
78
  }>>;
91
79
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
92
80
  timeout: z.ZodDefault<z.ZodNumber>;
81
+ idempotent: z.ZodDefault<z.ZodBoolean>;
93
82
  }, z.core.$strip>;
94
83
  /**
95
- * 单个 Provider 配置 Schema(判别联合体)
84
+ * 按type区分内置Provider配置,不把name当协议类型;name用于模板/消息路由。
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { ProviderConfigSchema } from '@h-ai/reach'
89
+ * const config = ProviderConfigSchema.parse({ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false })
90
+ * ```
96
91
  */
97
92
  declare const ProviderConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
98
93
  name: z.ZodString;
@@ -123,34 +118,145 @@ declare const ProviderConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
123
118
  }>>;
124
119
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
125
120
  timeout: z.ZodDefault<z.ZodNumber>;
121
+ idempotent: z.ZodDefault<z.ZodBoolean>;
126
122
  }, z.core.$strip>], "type">;
127
- /** 单个 Provider 配置类型(parse 后) */
123
+ /**
124
+ * 由ProviderConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { ProviderConfigSchema, type ProviderConfig } from '@h-ai/reach'
129
+ * const config: ProviderConfig = ProviderConfigSchema.parse({ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false })
130
+ * ```
131
+ */
128
132
  type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
129
- /** Console 配置类型 */
133
+ /**
134
+ * 由ConsoleProviderConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * import { ConsoleProviderConfigSchema, type ConsoleProviderConfig } from '@h-ai/reach'
139
+ * const config: ConsoleProviderConfig = ConsoleProviderConfigSchema.parse({ name: 'dev-log', type: 'console' })
140
+ * ```
141
+ */
130
142
  type ConsoleProviderConfig = z.infer<typeof ConsoleProviderConfigSchema>;
131
- /** SMTP 配置类型 */
143
+ /**
144
+ * 由SmtpProviderConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * import { SmtpProviderConfigSchema, type SmtpProviderConfig } from '@h-ai/reach'
149
+ * const config: SmtpProviderConfig = SmtpProviderConfigSchema.parse({ name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com', user: process.env.HAI_REACH_PROVIDERS_0_USER, pass: process.env.HAI_REACH_PROVIDERS_0_PASS })
150
+ * ```
151
+ */
132
152
  type SmtpProviderConfig = z.infer<typeof SmtpProviderConfigSchema>;
133
- /** 阿里云短信配置类型 */
153
+ /**
154
+ * 由AliyunSmsProviderConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * import { AliyunSmsProviderConfigSchema, type AliyunSmsProviderConfig } from '@h-ai/reach'
159
+ * const config: AliyunSmsProviderConfig = AliyunSmsProviderConfigSchema.parse({ name: 'sms', type: 'aliyun-sms', accessKeyId: process.env.HAI_REACH_PROVIDERS_0_ACCESSKEYID, accessKeySecret: process.env.HAI_REACH_PROVIDERS_0_ACCESSKEYSECRET, signName: process.env.HAI_REACH_PROVIDERS_0_SIGNNAME })
160
+ * ```
161
+ */
134
162
  type AliyunSmsProviderConfig = z.infer<typeof AliyunSmsProviderConfigSchema>;
135
- /** API 回调配置类型 */
163
+ /**
164
+ * 由ApiProviderConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { ApiProviderConfigSchema, type ApiProviderConfig } from '@h-ai/reach'
169
+ * const config: ApiProviderConfig = ApiProviderConfigSchema.parse({ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false })
170
+ * ```
171
+ */
136
172
  type ApiProviderConfig = z.infer<typeof ApiProviderConfigSchema>;
137
173
  /**
138
- * 免打扰时间段配置
174
+ * 人工处置unknown的结构边界,仅sent/discarded,不提供重发;应用须先校验运维权限并隔离旧worker/核对外部效果。
139
175
  *
140
- * 在指定的时间段内,根据策略处理消息:
141
- * - `discard`:直接丢弃,返回 DND_BLOCKED 错误
142
- * - `delay`:暂存到数据库(状态为 pending),DND 结束后由定时任务集中发送
176
+ * @example
177
+ * ```ts
178
+ * import { ReachUnknownResolutionSchema } from '@h-ai/reach'
179
+ * const config = ReachUnknownResolutionSchema.parse({ status: 'discarded' })
180
+ * ```
181
+ */
182
+ declare const ReachUnknownResolutionSchema: z.ZodObject<{
183
+ status: z.ZodEnum<{
184
+ sent: "sent";
185
+ discarded: "discarded";
186
+ }>;
187
+ messageId: z.ZodOptional<z.ZodString>;
188
+ }, z.core.$strip>;
189
+ /**
190
+ * 持久入队回执UUID格式;合法UUID不代表存在,也不授予队列运维权限。
143
191
  *
144
- * 时间使用 HH:mm 格式(24 小时制),支持跨午夜(如 22:00-08:00)。
192
+ * @example
193
+ * ```ts
194
+ * import { ReachQueueIdSchema } from '@h-ai/reach'
195
+ * const config = ReachQueueIdSchema.parse('00000000-0000-4000-8000-000000000001')
196
+ * ```
197
+ */
198
+ declare const ReachQueueIdSchema: z.ZodString;
199
+ /**
200
+ * 可靠队列重试策略,入队时随消息持久保存;maxAttempts含首次与崩溃恢复,指数退避有上限。
145
201
  *
146
202
  * @example
147
203
  * ```ts
148
- * {
149
- * enabled: true,
150
- * strategy: 'delay',
151
- * start: '22:00',
152
- * end: '08:00'
153
- * }
204
+ * import { ReachQueueConfigSchema } from '@h-ai/reach'
205
+ * const config = ReachQueueConfigSchema.parse({ maxAttempts: 5, retryDelayMs: 60000, maxRetryDelayMs: 3600000 })
206
+ * ```
207
+ */
208
+ declare const ReachQueueConfigSchema: z.ZodObject<{
209
+ maxAttempts: z.ZodDefault<z.ZodNumber>;
210
+ retryDelayMs: z.ZodDefault<z.ZodNumber>;
211
+ maxRetryDelayMs: z.ZodDefault<z.ZodNumber>;
212
+ }, z.core.$strip>;
213
+ /**
214
+ * 由ReachQueueConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * import { ReachQueueConfigSchema, type ReachQueueConfig } from '@h-ai/reach'
219
+ * const config: ReachQueueConfig = ReachQueueConfigSchema.parse({ maxAttempts: 5, retryDelayMs: 60000, maxRetryDelayMs: 3600000 })
220
+ * ```
221
+ */
222
+ type ReachQueueConfig = z.infer<typeof ReachQueueConfigSchema>;
223
+ /**
224
+ * 仅分页列出dead/unknown元数据,限制limit/offset;不返回正文/收件人,不替代服务端运维授权。
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * import { ReachQueueListSchema } from '@h-ai/reach'
229
+ * const config = ReachQueueListSchema.parse({ status: 'dead', limit: 10, offset: 0 })
230
+ * ```
231
+ */
232
+ declare const ReachQueueListSchema: z.ZodObject<{
233
+ status: z.ZodEnum<{
234
+ unknown: "unknown";
235
+ dead: "dead";
236
+ }>;
237
+ limit: z.ZodDefault<z.ZodNumber>;
238
+ offset: z.ZodDefault<z.ZodNumber>;
239
+ }, z.core.$strip>;
240
+ /**
241
+ * 恢复dead所需的最近updatedAt条件输入,阻止过时/并发恢复;unknown禁止此操作。
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * import { ReachDeadRequeueSchema } from '@h-ai/reach'
246
+ * const config = ReachDeadRequeueSchema.parse({ expectedUpdatedAt: 1730000000000 })
247
+ * ```
248
+ */
249
+ declare const ReachDeadRequeueSchema: z.ZodObject<{
250
+ expectedUpdatedAt: z.ZodNumber;
251
+ }, z.core.$strip>;
252
+ /**
253
+ * 本机时区的HH:mm免打扰配置,支持跨午夜;start=end表示不拦截。
254
+ * discard返回DND_BLOCKED;delay要求数据库队列就绪且pending写入成功才返回deferred回执。
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * import { DndConfigSchema } from '@h-ai/reach'
259
+ * const config = DndConfigSchema.parse({ enabled: true, strategy: 'delay', start: '22:00', end: '08:00' })
154
260
  * ```
155
261
  */
156
262
  declare const DndConfigSchema: z.ZodObject<{
@@ -162,18 +268,23 @@ declare const DndConfigSchema: z.ZodObject<{
162
268
  start: z.ZodDefault<z.ZodString>;
163
269
  end: z.ZodDefault<z.ZodString>;
164
270
  }, z.core.$strip>;
165
- /** DND 配置类型 */
271
+ /**
272
+ * 由DndConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { DndConfigSchema, type DndConfig } from '@h-ai/reach'
277
+ * const config: DndConfig = DndConfigSchema.parse({ enabled: true, strategy: 'delay', start: '22:00', end: '08:00' })
278
+ * ```
279
+ */
166
280
  type DndConfig = z.infer<typeof DndConfigSchema>;
167
281
  /**
168
- * 模板配置 Schema(通过配置文件定义模板)
282
+ * 绑定Provider名称的持久模板配置,{key}仅做文本替换;需要先初始化数据库,不提供HTML消毒。
169
283
  *
170
284
  * @example
171
- * ```yaml
172
- * templates:
173
- * - name: verification_code
174
- * provider: email
175
- * subject: "验证码: {code}"
176
- * body: "您的验证码是 {code},有效期 {minutes} 分钟。"
285
+ * ```ts
286
+ * import { TemplateConfigSchema } from '@h-ai/reach'
287
+ * const config = TemplateConfigSchema.parse({ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' })
177
288
  * ```
178
289
  */
179
290
  declare const TemplateConfigSchema: z.ZodObject<{
@@ -182,13 +293,24 @@ declare const TemplateConfigSchema: z.ZodObject<{
182
293
  subject: z.ZodOptional<z.ZodString>;
183
294
  body: z.ZodString;
184
295
  }, z.core.$strip>;
185
- /** 模板配置类型 */
296
+ /**
297
+ * 由TemplateConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * import { TemplateConfigSchema, type TemplateConfig } from '@h-ai/reach'
302
+ * const config: TemplateConfig = TemplateConfigSchema.parse({ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' })
303
+ * ```
304
+ */
186
305
  type TemplateConfig = z.infer<typeof TemplateConfigSchema>;
187
306
  /**
188
- * 触达模块配置 Schema
307
+ * 模块配置Schema,解析Provider/模板/DND及持久重试默认值;Schema校验不证明外部服务可用或接收端幂等。
189
308
  *
190
- * 接受一个 Provider 配置数组,支持同时注册多个 Provider。
191
- * 支持通过配置文件定义模板和免打扰时间段。
309
+ * @example
310
+ * ```ts
311
+ * import { ReachConfigSchema } from '@h-ai/reach'
312
+ * const config = ReachConfigSchema.parse({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
313
+ * ```
192
314
  */
193
315
  declare const ReachConfigSchema: z.ZodObject<{
194
316
  providers: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -220,6 +342,7 @@ declare const ReachConfigSchema: z.ZodObject<{
220
342
  }>>;
221
343
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
222
344
  timeout: z.ZodDefault<z.ZodNumber>;
345
+ idempotent: z.ZodDefault<z.ZodBoolean>;
223
346
  }, z.core.$strip>], "type">>;
224
347
  templates: z.ZodOptional<z.ZodArray<z.ZodObject<{
225
348
  name: z.ZodString;
@@ -236,14 +359,47 @@ declare const ReachConfigSchema: z.ZodObject<{
236
359
  start: z.ZodDefault<z.ZodString>;
237
360
  end: z.ZodDefault<z.ZodString>;
238
361
  }, z.core.$strip>>;
362
+ queue: z.ZodPrefault<z.ZodObject<{
363
+ maxAttempts: z.ZodDefault<z.ZodNumber>;
364
+ retryDelayMs: z.ZodDefault<z.ZodNumber>;
365
+ maxRetryDelayMs: z.ZodDefault<z.ZodNumber>;
366
+ }, z.core.$strip>>;
239
367
  }, z.core.$strip>;
240
- /** 触达模块配置类型(parse 后) */
368
+ /**
369
+ * 由ReachConfigSchema解析后的配置类型,默认值已经填充;与输入类型区分,秘密字段不写日志。
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * import { ReachConfigSchema, type ReachConfig } from '@h-ai/reach'
374
+ * const config: ReachConfig = ReachConfigSchema.parse({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
375
+ * ```
376
+ */
241
377
  type ReachConfig = z.infer<typeof ReachConfigSchema>;
242
378
  /**
243
- * 触达模块配置输入类型
379
+ * reach.init接收的配置输入,queue默认值可省略;生产选真实Provider、共享数据库,不能用console假定通知完成。
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * import { ReachConfigSchema, type ReachConfigInput } from '@h-ai/reach'
384
+ * const input: ReachConfigInput = { providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } }
385
+ * const parsed = ReachConfigSchema.parse(input)
386
+ * ```
244
387
  */
245
388
  type ReachConfigInput = z.input<typeof ReachConfigSchema>;
246
389
 
390
+ /**
391
+ * Reach标准错误集合;DND拒绝、发送失败与配置错误均通过HaiResult判断,不把失败当已入队。
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * import { reach, HaiReachError } from '@h-ai/reach'
396
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', body: 'hello' })
397
+ * if (!result.success && result.error.code === HaiReachError.NOT_INITIALIZED.code) {
398
+ * // 先执行依赖初始化,不能把未初始化失败当作触达成功。
399
+ * const message = result.error.message
400
+ * }
401
+ * ```
402
+ */
247
403
  declare const HaiReachError: {
248
404
  readonly SEND_FAILED: _h_ai_core.HaiErrorDef;
249
405
  readonly TEMPLATE_NOT_FOUND: _h_ai_core.HaiErrorDef;
@@ -251,39 +407,29 @@ declare const HaiReachError: {
251
407
  readonly INVALID_RECIPIENT: _h_ai_core.HaiErrorDef;
252
408
  readonly PROVIDER_NOT_FOUND: _h_ai_core.HaiErrorDef;
253
409
  readonly DND_BLOCKED: _h_ai_core.HaiErrorDef;
254
- readonly DND_DEFERRED: _h_ai_core.HaiErrorDef;
255
410
  readonly NOT_INITIALIZED: _h_ai_core.HaiErrorDef;
256
411
  readonly UNSUPPORTED_TYPE: _h_ai_core.HaiErrorDef;
257
412
  readonly CONFIG_ERROR: _h_ai_core.HaiErrorDef;
413
+ readonly DELIVERY_REJECTED: _h_ai_core.HaiErrorDef;
258
414
  };
259
415
  /**
260
- * 触达渠道类型
416
+ * 渠道语义标签email/sms/api;消息路由实际使用配置中的Provider名称。
261
417
  *
262
- * - `email` — 邮件
263
- * - `sms` — 短信
264
- * - `api` — API 回调
418
+ * @example
419
+ * ```ts
420
+ * import type { ReachChannel } from '@h-ai/reach'
421
+ * const channel: ReachChannel = 'api'
422
+ * ```
265
423
  */
266
424
  type ReachChannel = 'email' | 'sms' | 'api';
267
425
  /**
268
- * 触达消息(发送请求)
269
- *
270
- * 通过 `provider` 字段指定目标 Provider,通过 `template` 字段指定模板。
426
+ * 直接发送必须指定Provider名和内容;模板发送可由持久模板推导路由,vars做文本替换。
427
+ * 队列幂等键固定为持久queueId;直接调用的业务重试由应用提供稳定idempotencyKey并核验接收端契约。
271
428
  *
272
429
  * @example
273
430
  * ```ts
274
- * // 使用模板发送邮件
275
- * const msg: ReachMessage = {
276
- * to: 'user@example.com',
277
- * template: 'welcome',
278
- * vars: { userName: '张三' },
279
- * }
280
- *
281
- * // 使用模板发送短信
282
- * const msg: ReachMessage = {
283
- * to: '13800138000',
284
- * template: 'verification_code',
285
- * vars: { code: '123456' },
286
- * }
431
+ * import type { ReachMessage } from '@h-ai/reach'
432
+ * const message: ReachMessage = { provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } }
287
433
  * ```
288
434
  */
289
435
  interface ReachMessage {
@@ -301,30 +447,402 @@ interface ReachMessage {
301
447
  vars?: Record<string, string>;
302
448
  /** Provider 扩展参数(如短信 Provider 的模板编码、签名等) */
303
449
  extra?: Record<string, unknown>;
450
+ /** API 接收端幂等键;直接发送可指定,队列投递固定使用持久化 queueId。 */
451
+ idempotencyKey?: string;
304
452
  }
305
453
  /**
306
- * 发送结果
454
+ * 发送或可靠入队结果;deferred=true只证明持久pending写入成功,queueId用于后续查询,不代表已送达。
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * import { reach, type SendResult } from '@h-ai/reach'
459
+ * const { reldb } = await import('@h-ai/reldb')
460
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
461
+ * if (!connected.success) throw new Error(connected.error.message)
462
+ * try {
463
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
464
+ * if (!initialized.success) throw new Error(initialized.error.message)
465
+ * try {
466
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', body: 'hello' })
467
+ * if (!result.success) throw new Error(result.error.message)
468
+ * const outcome: SendResult = result.data
469
+ * if (outcome.deferred) {
470
+ * const queued = await reach.queue.get(outcome.queueId)
471
+ * if (!queued.success) throw new Error(queued.error.message)
472
+ * }
473
+ * }
474
+ * finally { await reach.close() }
475
+ * }
476
+ * finally {
477
+ * const closed = await reldb.close()
478
+ * if (!closed.success) throw new Error(closed.error.message)
479
+ * }
480
+ * ```
307
481
  */
308
- interface SendResult {
309
- /** 是否发送成功 */
482
+ type SendResult = {
483
+ /** 是否已完成 Provider 发送或可靠入队;具体状态同时检查 deferred。 */
310
484
  success: boolean;
311
485
  /** Provider 返回的消息 ID(如有) */
312
486
  messageId?: string;
313
- /** 是否被延时(DND delay 策略暂存,待 DND 结束后发送) */
314
- deferred?: boolean;
487
+ } & ({
488
+ /** 已可靠入队,尚未投递。 */
489
+ deferred: true;
490
+ /** 持久化回执 UUID,对应 hai_reach_send_log.queue_id。 */
491
+ queueId: string;
492
+ } | {
493
+ /** Provider 即时发送结果,不含入队回执。 */
494
+ deferred?: false;
495
+ queueId?: never;
496
+ });
497
+ /**
498
+ * 持久队列状态;sending已开始外部效果,非幂等失败/失租约进unknown禁止自动重发;dead停止自动处理但可条件恢复。
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * import type { ReachQueueStatus } from '@h-ai/reach'
503
+ * const status: ReachQueueStatus = 'unknown'
504
+ * ```
505
+ */
506
+ type ReachQueueStatus = 'pending' | 'processing' | 'sending' | 'unknown' | 'sent' | 'discarded' | 'dead';
507
+ /**
508
+ * 有界失败分类,不保存Provider响应正文、凭据或收件人;用于修复根因和处置死信。
509
+ *
510
+ * @example
511
+ * ```ts
512
+ * import type { ReachQueueFailure } from '@h-ai/reach'
513
+ * const failure: ReachQueueFailure = 'provider_unavailable'
514
+ * ```
515
+ */
516
+ type ReachQueueFailure = 'provider_unavailable' | 'invalid_message' | 'permanent_rejection' | 'transient_failure' | 'attempts_exhausted';
517
+ /**
518
+ * 不含消息正文/收件人的队列运维快照;时间为安全整数Unix毫秒,计数/策略来自持久消息。
519
+ *
520
+ * @example
521
+ * ```ts
522
+ * import { reach, type ReachQueueEntry } from '@h-ai/reach'
523
+ * const { reldb } = await import('@h-ai/reldb')
524
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
525
+ * if (!connected.success) throw new Error(connected.error.message)
526
+ * try {
527
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
528
+ * if (!initialized.success) throw new Error(initialized.error.message)
529
+ * try {
530
+ * const listed = await reach.queue.list({ status: 'dead', limit: 10 })
531
+ * if (!listed.success) throw new Error(listed.error.message)
532
+ * const entry: ReachQueueEntry | undefined = listed.data[0]
533
+ * if (entry) { const attempts = entry.attemptCount }
534
+ * }
535
+ * finally { await reach.close() }
536
+ * }
537
+ * finally {
538
+ * const closed = await reldb.close()
539
+ * if (!closed.success) throw new Error(closed.error.message)
540
+ * }
541
+ * ```
542
+ */
543
+ interface ReachQueueEntry {
544
+ /** 持久化入队回执。 */
545
+ queueId: string;
546
+ /** Provider 配置名称。 */
547
+ provider: string;
548
+ /** 当前处理状态。 */
549
+ status: ReachQueueStatus;
550
+ /** 最近更新时间(Unix 毫秒)。 */
551
+ updatedAt?: number;
552
+ /** 外部 Provider 确认的消息编号。 */
553
+ messageId?: string;
554
+ /** 已领取次数;节点崩溃后的重新领取也计数。 */
555
+ attemptCount: number;
556
+ /** 此条消息持久保存的自动领取上限。 */
557
+ maxAttempts: number;
558
+ /** 下次允许自动领取的 Unix 毫秒时间;终态为 0。 */
559
+ nextAttemptAt: number;
560
+ /** 最近失败分类,便于可信运维流程排查与修复。 */
561
+ lastFailure?: ReachQueueFailure;
562
+ }
563
+ /**
564
+ * 仅列出需人工处理的dead/unknown;分页有界,不提供凭据或替代运维权限。
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * import { reach, type ReachQueueListOptions } from '@h-ai/reach'
569
+ * const { reldb } = await import('@h-ai/reldb')
570
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
571
+ * if (!connected.success) throw new Error(connected.error.message)
572
+ * try {
573
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
574
+ * if (!initialized.success) throw new Error(initialized.error.message)
575
+ * try {
576
+ * const options: ReachQueueListOptions = { status: 'unknown', limit: 10, offset: 0 }
577
+ * const listed = await reach.queue.list(options)
578
+ * if (!listed.success) throw new Error(listed.error.message)
579
+ * }
580
+ * finally { await reach.close() }
581
+ * }
582
+ * finally {
583
+ * const closed = await reldb.close()
584
+ * if (!closed.success) throw new Error(closed.error.message)
585
+ * }
586
+ * ```
587
+ */
588
+ interface ReachQueueListOptions {
589
+ /** 需要人工处理的状态。 */
590
+ status: 'dead' | 'unknown';
591
+ /** 每页 1–100 条,默认 50。 */
592
+ limit?: number;
593
+ /** 跳过条数,默认 0,上限 1000000。 */
594
+ offset?: number;
595
+ }
596
+ /**
597
+ * dead条件恢复输入;expectedUpdatedAt必须来自最近快照,不能用本机当前时间冒充版本。
598
+ * 先修根因并确认旧执行与接收端幂等边界,unknown不能重排。
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * import { reach, type ReachDeadRequeue } from '@h-ai/reach'
603
+ * const { reldb } = await import('@h-ai/reldb')
604
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
605
+ * if (!connected.success) throw new Error(connected.error.message)
606
+ * try {
607
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
608
+ * if (!initialized.success) throw new Error(initialized.error.message)
609
+ * try {
610
+ * const listed = await reach.queue.list({ status: 'dead', limit: 10 })
611
+ * if (!listed.success) throw new Error(listed.error.message)
612
+ * const entry = listed.data[0]
613
+ * if (entry && entry.updatedAt !== undefined) {
614
+ * const options: ReachDeadRequeue = { expectedUpdatedAt: entry.updatedAt }
615
+ * // 此调用仅在受信运维流程核验完成后执行。
616
+ * const restored = await reach.queue.requeueDead(entry.queueId, options)
617
+ * if (!restored.success) throw new Error(restored.error.message)
618
+ * }
619
+ * }
620
+ * finally { await reach.close() }
621
+ * }
622
+ * finally {
623
+ * const closed = await reldb.close()
624
+ * if (!closed.success) throw new Error(closed.error.message)
625
+ * }
626
+ * ```
627
+ */
628
+ interface ReachDeadRequeue {
629
+ /** 必须等于最近查询的更新时间,阻止并发及过时请求。 */
630
+ expectedUpdatedAt: number;
315
631
  }
316
632
  /**
317
- * 触达模板定义
633
+ * unknown人工裁定,不提供自动重发;先校验运维权限、停止/隔离旧worker并核对Provider实际记录。
634
+ * discarded只终止后续处理,不能撤销已开始的外部请求或已发送消息。
318
635
  *
319
- * 每个模板绑定到一个 Provider,发送时按模板中的 `provider` 路由。
636
+ * @example
637
+ * ```ts
638
+ * import { reach, type ReachUnknownResolution } from '@h-ai/reach'
639
+ * const { reldb } = await import('@h-ai/reldb')
640
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
641
+ * if (!connected.success) throw new Error(connected.error.message)
642
+ * try {
643
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
644
+ * if (!initialized.success) throw new Error(initialized.error.message)
645
+ * try {
646
+ * const listed = await reach.queue.list({ status: 'unknown', limit: 10 })
647
+ * if (!listed.success) throw new Error(listed.error.message)
648
+ * const entry = listed.data[0]
649
+ * if (entry) {
650
+ * // 可信运维前置:旧worker已隔离且外部投递记录已核对。
651
+ * const resolution: ReachUnknownResolution = { status: 'discarded' }
652
+ * const resolved = await reach.queue.resolveUnknown(entry.queueId, resolution)
653
+ * if (!resolved.success) throw new Error(resolved.error.message)
654
+ * }
655
+ * }
656
+ * finally { await reach.close() }
657
+ * }
658
+ * finally {
659
+ * const closed = await reldb.close()
660
+ * if (!closed.success) throw new Error(closed.error.message)
661
+ * }
662
+ * ```
663
+ */
664
+ interface ReachUnknownResolution {
665
+ /** 经外部核对确认已投递,或终止后续处理。discarded 不撤销已经开始的外部请求。 */
666
+ status: 'sent' | 'discarded';
667
+ /** 确认已投递时,可记录 Provider 消息编号,最多 255 个字符。 */
668
+ messageId?: string;
669
+ }
670
+ /**
671
+ * 受信服务端队列运维入口,需应用先授权;只查询安全元数据,恢复/裁定均由数据库条件提交。
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * import { reach } from '@h-ai/reach'
676
+ * const { reldb } = await import('@h-ai/reldb')
677
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
678
+ * if (!connected.success) throw new Error(connected.error.message)
679
+ * try {
680
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
681
+ * if (!initialized.success) throw new Error(initialized.error.message)
682
+ * try {
683
+ * const listed = await reach.queue.list({ status: 'unknown', limit: 10 })
684
+ * if (!listed.success) throw new Error(listed.error.message)
685
+ * const count = listed.data.length
686
+ * }
687
+ * finally { await reach.close() }
688
+ * }
689
+ * finally {
690
+ * const closed = await reldb.close()
691
+ * if (!closed.success) throw new Error(closed.error.message)
692
+ * }
693
+ * ```
694
+ */
695
+ interface ReachQueueOperations {
696
+ /**
697
+ * 按回执查询队列状态。
698
+ * @param queueId - 持久UUID回执,不代替运维权限。
699
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
700
+ *
701
+ * @example
702
+ * ```ts
703
+ * import { reach } from '@h-ai/reach'
704
+ * const { reldb } = await import('@h-ai/reldb')
705
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
706
+ * if (!connected.success) throw new Error(connected.error.message)
707
+ * try {
708
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
709
+ * if (!initialized.success) throw new Error(initialized.error.message)
710
+ * try {
711
+ * const result = await reach.queue.get('00000000-0000-4000-8000-000000000001')
712
+ * if (!result.success) throw new Error(result.error.message)
713
+ * if (result.data) { const status = result.data.status }
714
+ * }
715
+ * finally { await reach.close() }
716
+ * }
717
+ * finally {
718
+ * const closed = await reldb.close()
719
+ * if (!closed.success) throw new Error(closed.error.message)
720
+ * }
721
+ * ```
722
+ */
723
+ get: (queueId: string) => Promise<HaiResult<ReachQueueEntry | null>>;
724
+ /**
725
+ * 分页查询死信或未知结果,仅返回运维元数据。
726
+ * @param options - 实际操作输入,按本方法约束校验。
727
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
728
+ *
729
+ * @example
730
+ * ```ts
731
+ * import { reach } from '@h-ai/reach'
732
+ * const { reldb } = await import('@h-ai/reldb')
733
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
734
+ * if (!connected.success) throw new Error(connected.error.message)
735
+ * try {
736
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
737
+ * if (!initialized.success) throw new Error(initialized.error.message)
738
+ * try {
739
+ * const listed = await reach.queue.list({ status: 'unknown', limit: 10 })
740
+ * if (!listed.success) throw new Error(listed.error.message)
741
+ * const count = listed.data.length
742
+ * }
743
+ * finally { await reach.close() }
744
+ * }
745
+ * finally {
746
+ * const closed = await reldb.close()
747
+ * if (!closed.success) throw new Error(closed.error.message)
748
+ * }
749
+ * ```
750
+ */
751
+ list: (options: ReachQueueListOptions) => Promise<HaiResult<ReachQueueEntry[]>>;
752
+ /**
753
+ * 修复根因并确认接收端幂等契约后恢复 dead;重置计数,保留 queueId/策略。unknown 不能重排。
754
+ * @param queueId - 持久UUID回执,不代替运维权限。
755
+ * @param options - 实际操作输入,按本方法约束校验。
756
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * import { reach, type ReachDeadRequeue } from '@h-ai/reach'
761
+ * const { reldb } = await import('@h-ai/reldb')
762
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
763
+ * if (!connected.success) throw new Error(connected.error.message)
764
+ * try {
765
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
766
+ * if (!initialized.success) throw new Error(initialized.error.message)
767
+ * try {
768
+ * const listed = await reach.queue.list({ status: 'dead', limit: 10 })
769
+ * if (!listed.success) throw new Error(listed.error.message)
770
+ * const entry = listed.data[0]
771
+ * if (entry && entry.updatedAt !== undefined) {
772
+ * const options: ReachDeadRequeue = { expectedUpdatedAt: entry.updatedAt }
773
+ * // 此调用仅在受信运维流程核验完成后执行。
774
+ * const restored = await reach.queue.requeueDead(entry.queueId, options)
775
+ * if (!restored.success) throw new Error(restored.error.message)
776
+ * }
777
+ * }
778
+ * finally { await reach.close() }
779
+ * }
780
+ * finally {
781
+ * const closed = await reldb.close()
782
+ * if (!closed.success) throw new Error(closed.error.message)
783
+ * }
784
+ * ```
785
+ */
786
+ requeueDead: (queueId: string, options: ReachDeadRequeue) => Promise<HaiResult<void>>;
787
+ /**
788
+ * 停止/隔离旧 worker 并核对外部投递记录后,裁定 unknown;不重新发送。
789
+ * @param queueId - 持久UUID回执,不代替运维权限。
790
+ * @param resolution - 实际操作输入,按本方法约束校验。
791
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
792
+ *
793
+ * @example
794
+ * ```ts
795
+ * import { reach, type ReachUnknownResolution } from '@h-ai/reach'
796
+ * const { reldb } = await import('@h-ai/reldb')
797
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
798
+ * if (!connected.success) throw new Error(connected.error.message)
799
+ * try {
800
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
801
+ * if (!initialized.success) throw new Error(initialized.error.message)
802
+ * try {
803
+ * const listed = await reach.queue.list({ status: 'unknown', limit: 10 })
804
+ * if (!listed.success) throw new Error(listed.error.message)
805
+ * const entry = listed.data[0]
806
+ * if (entry) {
807
+ * // 可信运维前置:旧worker已隔离且外部投递记录已核对。
808
+ * const resolution: ReachUnknownResolution = { status: 'discarded' }
809
+ * const resolved = await reach.queue.resolveUnknown(entry.queueId, resolution)
810
+ * if (!resolved.success) throw new Error(resolved.error.message)
811
+ * }
812
+ * }
813
+ * finally { await reach.close() }
814
+ * }
815
+ * finally {
816
+ * const closed = await reldb.close()
817
+ * if (!closed.success) throw new Error(closed.error.message)
818
+ * }
819
+ * ```
820
+ */
821
+ resolveUnknown: (queueId: string, resolution: ReachUnknownResolution) => Promise<HaiResult<void>>;
822
+ }
823
+ /**
824
+ * 持久文本模板,provider是配置名称,subject/body支持{key}替换;未提供的占位符保留原文。
825
+ * 不提供HTML转义或消毒,渲染结果的最终安全上下文由使用方处理。
320
826
  *
321
827
  * @example
322
828
  * ```ts
323
- * const template: ReachTemplate = {
324
- * name: 'verification_code',
325
- * provider: 'email',
326
- * subject: '验证码: {code}',
327
- * body: '您的验证码是 {code},有效期 {minutes} 分钟。',
829
+ * import { reach, type ReachTemplate } from '@h-ai/reach'
830
+ * const { reldb } = await import('@h-ai/reldb')
831
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
832
+ * if (!connected.success) throw new Error(connected.error.message)
833
+ * try {
834
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
835
+ * if (!initialized.success) throw new Error(initialized.error.message)
836
+ * try {
837
+ * const template: ReachTemplate = { name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }
838
+ * const saved = await reach.template.save(template)
839
+ * if (!saved.success) throw new Error(saved.error.message)
840
+ * }
841
+ * finally { await reach.close() }
842
+ * }
843
+ * finally {
844
+ * const closed = await reldb.close()
845
+ * if (!closed.success) throw new Error(closed.error.message)
328
846
  * }
329
847
  * ```
330
848
  */
@@ -339,7 +857,30 @@ interface ReachTemplate {
339
857
  body: string;
340
858
  }
341
859
  /**
342
- * 渲染后的模板
860
+ * 渲染后的主题/正文;文本替换不等于HTML安全,可选subject按原模板保留。
861
+ *
862
+ * @example
863
+ * ```ts
864
+ * import { reach, type RenderedTemplate } from '@h-ai/reach'
865
+ * const { reldb } = await import('@h-ai/reldb')
866
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
867
+ * if (!connected.success) throw new Error(connected.error.message)
868
+ * try {
869
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
870
+ * if (!initialized.success) throw new Error(initialized.error.message)
871
+ * try {
872
+ * const rendered = await reach.template.render('welcome', { userName: 'Alice' })
873
+ * if (!rendered.success) throw new Error(rendered.error.message)
874
+ * const value: RenderedTemplate = rendered.data
875
+ * const body = value.body
876
+ * }
877
+ * finally { await reach.close() }
878
+ * }
879
+ * finally {
880
+ * const closed = await reldb.close()
881
+ * if (!closed.success) throw new Error(closed.error.message)
882
+ * }
883
+ * ```
343
884
  */
344
885
  interface RenderedTemplate {
345
886
  /** 渲染后的主题 */
@@ -348,7 +889,35 @@ interface RenderedTemplate {
348
889
  body: string;
349
890
  }
350
891
  /**
351
- * 发送操作接口
892
+ * 统一发送操作;失败HaiResult不表示已发送,成功结果仍检查内部success与deferred。
893
+ * 即时外部发送成功不因可选日志失败而转失败,防调用方重复发送;延迟入队必须确认持久写入。
894
+ *
895
+ * @example
896
+ * ```ts
897
+ * import { reach } from '@h-ai/reach'
898
+ * const { reldb } = await import('@h-ai/reldb')
899
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
900
+ * if (!connected.success) throw new Error(connected.error.message)
901
+ * try {
902
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
903
+ * if (!initialized.success) throw new Error(initialized.error.message)
904
+ * try {
905
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } })
906
+ * if (!result.success) throw new Error(result.error.message)
907
+ * if (!result.data.success) throw new Error('Delivery was not accepted')
908
+ * if (result.data.deferred) {
909
+ * const queueId = result.data.queueId
910
+ * // 持久入队成功,尚未证明接收端已完成外部投递。
911
+ * }
912
+ * else { const providerMessageId = result.data.messageId }
913
+ * }
914
+ * finally { await reach.close() }
915
+ * }
916
+ * finally {
917
+ * const closed = await reldb.close()
918
+ * if (!closed.success) throw new Error(closed.error.message)
919
+ * }
920
+ * ```
352
921
  */
353
922
  interface SendOperations {
354
923
  /**
@@ -359,68 +928,296 @@ interface SendOperations {
359
928
  *
360
929
  * @example
361
930
  * ```ts
362
- * const result = await reach.send({
363
- * provider: 'email',
364
- * to: 'user@example.com',
365
- * template: 'welcome',
366
- * vars: { userName: '张三' },
367
- * })
931
+ * import { reach } from '@h-ai/reach'
932
+ * const { reldb } = await import('@h-ai/reldb')
933
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
934
+ * if (!connected.success) throw new Error(connected.error.message)
935
+ * try {
936
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
937
+ * if (!initialized.success) throw new Error(initialized.error.message)
938
+ * try {
939
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } })
940
+ * if (!result.success) throw new Error(result.error.message)
941
+ * if (!result.data.success) throw new Error('Delivery was not accepted')
942
+ * if (result.data.deferred) {
943
+ * const queueId = result.data.queueId
944
+ * // 持久入队成功,尚未证明接收端已完成外部投递。
945
+ * }
946
+ * else { const providerMessageId = result.data.messageId }
947
+ * }
948
+ * finally { await reach.close() }
949
+ * }
950
+ * finally {
951
+ * const closed = await reldb.close()
952
+ * if (!closed.success) throw new Error(closed.error.message)
953
+ * }
368
954
  * ```
369
955
  */
370
956
  send: (message: ReachMessage) => Promise<HaiResult<SendResult>>;
371
957
  }
372
958
  /**
373
- * 模板注册表接口
959
+ * 数据库模板操作;无数据库时不回退进程模板缓存,resolve失败、list为空,保存/删除返回失败。
960
+ * saveBatch并发逐条upsert,失败时不承诺其它项回滚;需要原子业务批次时由受控事务流程实现。
374
961
  *
375
- * 所有模板通过数据库持久化存储,避免节点间状态不一致。
376
- * 配置文件中的模板在 init 时写入数据库。
962
+ * @example
963
+ * ```ts
964
+ * import { reach } from '@h-ai/reach'
965
+ * const { reldb } = await import('@h-ai/reldb')
966
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
967
+ * if (!connected.success) throw new Error(connected.error.message)
968
+ * try {
969
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
970
+ * if (!initialized.success) throw new Error(initialized.error.message)
971
+ * try {
972
+ * const saved = await reach.template.save({ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' })
973
+ * if (!saved.success) throw new Error(saved.error.message)
974
+ * const rendered = await reach.template.render('welcome', { userName: 'Alice' })
975
+ * if (!rendered.success) throw new Error(rendered.error.message)
976
+ * }
977
+ * finally { await reach.close() }
978
+ * }
979
+ * finally {
980
+ * const closed = await reldb.close()
981
+ * if (!closed.success) throw new Error(closed.error.message)
982
+ * }
983
+ * ```
377
984
  */
378
985
  interface ReachTemplateRegistry {
379
- /** 按名称解析模板 */
986
+ /**
987
+ * 按名称解析模板
988
+ * @param name - 实际操作输入,按本方法约束校验。
989
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
990
+ *
991
+ * @example
992
+ * ```ts
993
+ * import { reach } from '@h-ai/reach'
994
+ * const { reldb } = await import('@h-ai/reldb')
995
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
996
+ * if (!connected.success) throw new Error(connected.error.message)
997
+ * try {
998
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
999
+ * if (!initialized.success) throw new Error(initialized.error.message)
1000
+ * try {
1001
+ * const result = await reach.template.resolve('welcome')
1002
+ * if (!result.success) throw new Error(result.error.message)
1003
+ * const providerName = result.data.provider
1004
+ * }
1005
+ * finally { await reach.close() }
1006
+ * }
1007
+ * finally {
1008
+ * const closed = await reldb.close()
1009
+ * if (!closed.success) throw new Error(closed.error.message)
1010
+ * }
1011
+ * ```
1012
+ */
380
1013
  resolve: (name: string) => Promise<HaiResult<ReachTemplate>>;
381
- /** 保存模板到数据库(upsert) */
1014
+ /**
1015
+ * 保存模板到数据库(upsert)
1016
+ * @param template - 实际操作输入,按本方法约束校验。
1017
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
1018
+ *
1019
+ * @example
1020
+ * ```ts
1021
+ * import { reach, type ReachTemplate } from '@h-ai/reach'
1022
+ * const { reldb } = await import('@h-ai/reldb')
1023
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1024
+ * if (!connected.success) throw new Error(connected.error.message)
1025
+ * try {
1026
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1027
+ * if (!initialized.success) throw new Error(initialized.error.message)
1028
+ * try {
1029
+ * const template: ReachTemplate = { name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }
1030
+ * const saved = await reach.template.save(template)
1031
+ * if (!saved.success) throw new Error(saved.error.message)
1032
+ * }
1033
+ * finally { await reach.close() }
1034
+ * }
1035
+ * finally {
1036
+ * const closed = await reldb.close()
1037
+ * if (!closed.success) throw new Error(closed.error.message)
1038
+ * }
1039
+ * ```
1040
+ */
382
1041
  save: (template: ReachTemplate) => Promise<HaiResult<void>>;
383
- /** 批量保存模板到数据库 */
1042
+ /**
1043
+ * 批量保存模板到数据库
1044
+ * @param templates - 实际操作输入,按本方法约束校验。
1045
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
1046
+ *
1047
+ * @example
1048
+ * ```ts
1049
+ * import { reach } from '@h-ai/reach'
1050
+ * const { reldb } = await import('@h-ai/reldb')
1051
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1052
+ * if (!connected.success) throw new Error(connected.error.message)
1053
+ * try {
1054
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1055
+ * if (!initialized.success) throw new Error(initialized.error.message)
1056
+ * try {
1057
+ * const result = await reach.template.saveBatch([{ name: 'receipt', provider: 'webhook', body: 'Receipt {orderId}' }, { name: 'reminder', provider: 'webhook', body: 'Reminder {orderId}' }])
1058
+ * if (!result.success) throw new Error(result.error.message)
1059
+ * // 非原子批次;失败时应核对已保存条目,不假定整体回滚。
1060
+ * }
1061
+ * finally { await reach.close() }
1062
+ * }
1063
+ * finally {
1064
+ * const closed = await reldb.close()
1065
+ * if (!closed.success) throw new Error(closed.error.message)
1066
+ * }
1067
+ * ```
1068
+ */
384
1069
  saveBatch: (templates: ReachTemplate[]) => Promise<HaiResult<void>>;
385
- /** 从数据库删除模板 */
1070
+ /**
1071
+ * 从数据库删除模板
1072
+ * @param name - 实际操作输入,按本方法约束校验。
1073
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
1074
+ *
1075
+ * @example
1076
+ * ```ts
1077
+ * import { reach } from '@h-ai/reach'
1078
+ * const { reldb } = await import('@h-ai/reldb')
1079
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1080
+ * if (!connected.success) throw new Error(connected.error.message)
1081
+ * try {
1082
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1083
+ * if (!initialized.success) throw new Error(initialized.error.message)
1084
+ * try {
1085
+ * const result = await reach.template.remove('welcome')
1086
+ * if (!result.success) throw new Error(result.error.message)
1087
+ * }
1088
+ * finally { await reach.close() }
1089
+ * }
1090
+ * finally {
1091
+ * const closed = await reldb.close()
1092
+ * if (!closed.success) throw new Error(closed.error.message)
1093
+ * }
1094
+ * ```
1095
+ */
386
1096
  remove: (name: string) => Promise<HaiResult<void>>;
387
- /** 列出所有模板 */
1097
+ /**
1098
+ * 列出所有模板
1099
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
1100
+ *
1101
+ * @example
1102
+ * ```ts
1103
+ * import { reach } from '@h-ai/reach'
1104
+ * const { reldb } = await import('@h-ai/reldb')
1105
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1106
+ * if (!connected.success) throw new Error(connected.error.message)
1107
+ * try {
1108
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1109
+ * if (!initialized.success) throw new Error(initialized.error.message)
1110
+ * try {
1111
+ * const result = await reach.template.list()
1112
+ * if (!result.success) throw new Error(result.error.message)
1113
+ * const count = result.data.length
1114
+ * }
1115
+ * finally { await reach.close() }
1116
+ * }
1117
+ * finally {
1118
+ * const closed = await reldb.close()
1119
+ * if (!closed.success) throw new Error(closed.error.message)
1120
+ * }
1121
+ * ```
1122
+ */
388
1123
  list: () => Promise<HaiResult<ReachTemplate[]>>;
389
- /** 渲染模板(从数据库解析后渲染) */
1124
+ /**
1125
+ * 渲染模板(从数据库解析后渲染)
1126
+ * @param name - 实际操作输入,按本方法约束校验。
1127
+ * @param vars - 实际操作输入,按本方法约束校验。
1128
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
1129
+ *
1130
+ * @example
1131
+ * ```ts
1132
+ * import { reach, type RenderedTemplate } from '@h-ai/reach'
1133
+ * const { reldb } = await import('@h-ai/reldb')
1134
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1135
+ * if (!connected.success) throw new Error(connected.error.message)
1136
+ * try {
1137
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1138
+ * if (!initialized.success) throw new Error(initialized.error.message)
1139
+ * try {
1140
+ * const rendered = await reach.template.render('welcome', { userName: 'Alice' })
1141
+ * if (!rendered.success) throw new Error(rendered.error.message)
1142
+ * const value: RenderedTemplate = rendered.data
1143
+ * const body = value.body
1144
+ * }
1145
+ * finally { await reach.close() }
1146
+ * }
1147
+ * finally {
1148
+ * const closed = await reldb.close()
1149
+ * if (!closed.success) throw new Error(closed.error.message)
1150
+ * }
1151
+ * ```
1152
+ */
390
1153
  render: (name: string, vars: Record<string, string>) => Promise<HaiResult<RenderedTemplate>>;
391
1154
  }
392
1155
  /**
393
- * 触达模块函数接口
1156
+ * Reach生命周期、模板、发送及受信队列运维能力;先初始化数据库,再检查reach.init结果,反序close。
1157
+ * API Provider init只建立适配器配置,不探测远端可用性;实际send结果才能判断本次投递。
394
1158
  *
395
1159
  * @example
396
1160
  * ```ts
397
1161
  * import { reach } from '@h-ai/reach'
398
- *
399
- * // 初始化(同时注册多个 Provider)
400
- * await reach.init({
401
- * providers: [
402
- * { name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'no-reply@example.com' },
403
- * { name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '...' },
404
- * ],
405
- * })
406
- *
407
- * // 保存模板(绑定到 Provider)
408
- * await reach.template.save({
409
- * name: 'welcome',
410
- * provider: 'email',
411
- * subject: '欢迎 {userName}',
412
- * body: '亲爱的 {userName},欢迎使用我们的服务!',
413
- * })
414
- *
415
- * // 发送(指定 Provider)
416
- * await reach.send({ provider: 'email', to: 'user@example.com', template: 'welcome', vars: { userName: '张三' } })
417
- *
418
- * // 关闭
419
- * await reach.close()
1162
+ * const { reldb } = await import('@h-ai/reldb')
1163
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1164
+ * if (!connected.success) throw new Error(connected.error.message)
1165
+ * try {
1166
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1167
+ * if (!initialized.success) throw new Error(initialized.error.message)
1168
+ * try {
1169
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } })
1170
+ * if (!result.success) throw new Error(result.error.message)
1171
+ * if (!result.data.success) throw new Error('Delivery was not accepted')
1172
+ * if (result.data.deferred) {
1173
+ * const queueId = result.data.queueId
1174
+ * // 持久入队成功,尚未证明接收端已完成外部投递。
1175
+ * }
1176
+ * else { const providerMessageId = result.data.messageId }
1177
+ * }
1178
+ * finally { await reach.close() }
1179
+ * }
1180
+ * finally {
1181
+ * const closed = await reldb.close()
1182
+ * if (!closed.success) throw new Error(closed.error.message)
1183
+ * }
420
1184
  * ```
421
1185
  */
422
1186
  interface ReachFunctions extends SendOperations {
423
- /** 初始化触达模块(注册多个 Provider) */
1187
+ /** 持久化队列查询、死信恢复与未知结果人工裁定。 */
1188
+ readonly queue: ReachQueueOperations;
1189
+ /**
1190
+ * 初始化触达模块(注册多个 Provider)
1191
+ * @param config - 实际操作输入,按本方法约束校验。
1192
+ * @returns HaiResult;先检查success,条件提交失败不能伪装成功。
1193
+ *
1194
+ * @example
1195
+ * ```ts
1196
+ * import { reach } from '@h-ai/reach'
1197
+ * const { reldb } = await import('@h-ai/reldb')
1198
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1199
+ * if (!connected.success) throw new Error(connected.error.message)
1200
+ * try {
1201
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1202
+ * if (!initialized.success) throw new Error(initialized.error.message)
1203
+ * try {
1204
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } })
1205
+ * if (!result.success) throw new Error(result.error.message)
1206
+ * if (!result.data.success) throw new Error('Delivery was not accepted')
1207
+ * if (result.data.deferred) {
1208
+ * const queueId = result.data.queueId
1209
+ * // 持久入队成功,尚未证明接收端已完成外部投递。
1210
+ * }
1211
+ * else { const providerMessageId = result.data.messageId }
1212
+ * }
1213
+ * finally { await reach.close() }
1214
+ * }
1215
+ * finally {
1216
+ * const closed = await reldb.close()
1217
+ * if (!closed.success) throw new Error(closed.error.message)
1218
+ * }
1219
+ * ```
1220
+ */
424
1221
  init: (config: ReachConfigInput) => Promise<HaiResult<void>>;
425
1222
  /** 当前脱敏后的配置快照(未初始化时为 null) */
426
1223
  readonly config: ReachConfig | null;
@@ -428,25 +1225,37 @@ interface ReachFunctions extends SendOperations {
428
1225
  readonly isInitialized: boolean;
429
1226
  /** 模板注册表 */
430
1227
  readonly template: ReachTemplateRegistry;
431
- /** 关闭连接并释放资源 */
432
- close: () => Promise<void>;
433
- }
434
- /**
435
- * 触达 Provider 接口
436
- *
437
- * 由具体实现(console / smtp / aliyun-sms / api)提供。
438
- */
439
- interface ReachProvider {
440
- /** Provider 名称 */
441
- readonly name: string;
442
- /** 连接/初始化 Provider */
443
- connect: (config: ProviderConfig) => Promise<HaiResult<void>>;
444
- /** 发送消息 */
445
- send: (message: ReachMessage) => Promise<HaiResult<SendResult>>;
446
- /** 关闭连接 */
1228
+ /**
1229
+ * 关闭连接并释放资源
1230
+ *
1231
+ * @example
1232
+ * ```ts
1233
+ * import { reach } from '@h-ai/reach'
1234
+ * const { reldb } = await import('@h-ai/reldb')
1235
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1236
+ * if (!connected.success) throw new Error(connected.error.message)
1237
+ * try {
1238
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1239
+ * if (!initialized.success) throw new Error(initialized.error.message)
1240
+ * try {
1241
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } })
1242
+ * if (!result.success) throw new Error(result.error.message)
1243
+ * if (!result.data.success) throw new Error('Delivery was not accepted')
1244
+ * if (result.data.deferred) {
1245
+ * const queueId = result.data.queueId
1246
+ * // 持久入队成功,尚未证明接收端已完成外部投递。
1247
+ * }
1248
+ * else { const providerMessageId = result.data.messageId }
1249
+ * }
1250
+ * finally { await reach.close() }
1251
+ * }
1252
+ * finally {
1253
+ * const closed = await reldb.close()
1254
+ * if (!closed.success) throw new Error(closed.error.message)
1255
+ * }
1256
+ * ```
1257
+ */
447
1258
  close: () => Promise<void>;
448
- /** 是否已连接 */
449
- isConnected: () => boolean;
450
1259
  }
451
1260
 
452
1261
  /**
@@ -457,10 +1266,36 @@ interface ReachProvider {
457
1266
  */
458
1267
 
459
1268
  /**
460
- * 触达服务对象
1269
+ * 邮件/短信/API触达统一入口;先检查init与send的HaiResult,成功后还区分deferred和即时Provider结果。
1270
+ * 模板与delay队列使用已初始化数据库,生产共享后端;close停止本地调度/关闭Provider,不撤销已发外部请求。
461
1271
  *
462
- * 统一的用户触达入口,支持同时使用邮件、短信、API 回调等多种 Provider。
1272
+ * @example
1273
+ * ```ts
1274
+ * import { reach } from '@h-ai/reach'
1275
+ * const { reldb } = await import('@h-ai/reldb')
1276
+ * const connected = await reldb.init({ type: 'postgresql', host: 'db', port: 5432, database: 'hai', user: 'postgres', password: process.env.HAI_DB_PASSWORD })
1277
+ * if (!connected.success) throw new Error(connected.error.message)
1278
+ * try {
1279
+ * const initialized = await reach.init({ providers: [{ name: 'webhook', type: 'api', url: 'https://notify.example.com/events', idempotent: false }], templates: [{ name: 'welcome', provider: 'webhook', subject: 'Welcome {userName}', body: 'Hello {userName}' }], dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' } })
1280
+ * if (!initialized.success) throw new Error(initialized.error.message)
1281
+ * try {
1282
+ * const result = await reach.send({ provider: 'webhook', to: 'user@example.com', template: 'welcome', vars: { userName: 'Alice' } })
1283
+ * if (!result.success) throw new Error(result.error.message)
1284
+ * if (!result.data.success) throw new Error('Delivery was not accepted')
1285
+ * if (result.data.deferred) {
1286
+ * const queueId = result.data.queueId
1287
+ * // 持久入队成功,尚未证明接收端已完成外部投递。
1288
+ * }
1289
+ * else { const providerMessageId = result.data.messageId }
1290
+ * }
1291
+ * finally { await reach.close() }
1292
+ * }
1293
+ * finally {
1294
+ * const closed = await reldb.close()
1295
+ * if (!closed.success) throw new Error(closed.error.message)
1296
+ * }
1297
+ * ```
463
1298
  */
464
1299
  declare const reach: ReachFunctions;
465
1300
 
466
- export { type AliyunSmsProviderConfig, AliyunSmsProviderConfigSchema, type ApiProviderConfig, ApiProviderConfigSchema, type ConsoleProviderConfig, ConsoleProviderConfigSchema, type DndConfig, DndConfigSchema, HaiReachError, type ProviderConfig, ProviderConfigSchema, type ReachChannel, type ReachConfig, type ReachConfigInput, ReachConfigSchema, type ReachFunctions, type ReachMessage, type ReachProvider, type ReachTemplate, type ReachTemplateRegistry, type RenderedTemplate, type SendOperations, type SendResult, type SmtpProviderConfig, SmtpProviderConfigSchema, type TemplateConfig, TemplateConfigSchema, reach };
1301
+ export { type AliyunSmsProviderConfig, AliyunSmsProviderConfigSchema, type ApiProviderConfig, ApiProviderConfigSchema, type ConsoleProviderConfig, ConsoleProviderConfigSchema, type DndConfig, DndConfigSchema, HaiReachError, type ProviderConfig, ProviderConfigSchema, type ReachChannel, type ReachConfig, type ReachConfigInput, ReachConfigSchema, type ReachDeadRequeue, ReachDeadRequeueSchema, type ReachFunctions, type ReachMessage, type ReachQueueConfig, ReachQueueConfigSchema, type ReachQueueEntry, type ReachQueueFailure, ReachQueueIdSchema, type ReachQueueListOptions, ReachQueueListSchema, type ReachQueueOperations, type ReachQueueStatus, type ReachTemplate, type ReachTemplateRegistry, type ReachUnknownResolution, ReachUnknownResolutionSchema, type RenderedTemplate, type SendOperations, type SendResult, type SmtpProviderConfig, SmtpProviderConfigSchema, type TemplateConfig, TemplateConfigSchema, reach };