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