@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/README.en.md +76 -15
- package/README.ja.md +76 -15
- package/README.md +87 -26
- package/dist/index.d.ts +990 -155
- package/dist/index.js +568 -160
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -10,27 +10,25 @@ import { HaiResult } from '@h-ai/core';
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
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
|
|
26
|
+
* SMTP配置,默认TLS/465;凭据来自私有环境,SMTP没有接收端持久幂等能力声明。
|
|
21
27
|
*
|
|
22
28
|
* @example
|
|
23
29
|
* ```ts
|
|
24
|
-
* {
|
|
25
|
-
*
|
|
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
|
|
45
|
+
* 阿里云短信POP API配置;Secret只在服务端私有注入,发送需Provider模板编码等扩展参数。
|
|
48
46
|
*
|
|
49
47
|
* @example
|
|
50
48
|
* ```ts
|
|
51
|
-
* {
|
|
52
|
-
*
|
|
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
|
-
*
|
|
62
|
+
* HTTP回调配置;idempotent默认false,只有接收端按稳定Idempotency-Key原子持久去重并覆盖旧请求迟到窗口才允许true。
|
|
63
|
+
* 写入该布尔值或发送请求头本身不证明接收端具备此能力。
|
|
71
64
|
*
|
|
72
65
|
* @example
|
|
73
66
|
* ```ts
|
|
74
|
-
* {
|
|
75
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
142
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
282
|
+
* 绑定Provider名称的持久模板配置,{key}仅做文本替换;需要先初始化数据库,不提供HTML消毒。
|
|
169
283
|
*
|
|
170
284
|
* @example
|
|
171
|
-
* ```
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
-
*
|
|
307
|
+
* 模块配置Schema,解析Provider/模板/DND及持久重试默认值;Schema校验不证明外部服务可用或接收端幂等。
|
|
189
308
|
*
|
|
190
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
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
|
|
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
|
-
|
|
309
|
-
/**
|
|
482
|
+
type SendResult = {
|
|
483
|
+
/** 是否已完成 Provider 发送或可靠入队;具体状态同时检查 deferred。 */
|
|
310
484
|
success: boolean;
|
|
311
485
|
/** Provider 返回的消息 ID(如有) */
|
|
312
486
|
messageId?: string;
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
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
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* })
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
*
|
|
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
|
|
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 };
|