@h-ai/cli 0.1.0-alpha.54 → 0.1.0-alpha.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +4 -0
  2. package/dist/{chunk-FVDXJYMO.js → chunk-O3ANVICQ.js} +31 -2
  3. package/dist/chunk-O3ANVICQ.js.map +1 -0
  4. package/dist/cli-config-templates-OWBEGBZQ.js +5 -0
  5. package/dist/{cli-config-templates-WGN2I23J.js.map → cli-config-templates-OWBEGBZQ.js.map} +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/{src-6ZBWDBUB.js → src-TBIET7YU.js} +908 -127
  8. package/dist/src-TBIET7YU.js.map +1 -0
  9. package/package.json +7 -7
  10. package/templates/base/.gitignore.hbs +3 -0
  11. package/templates/base/package.json.hbs +1 -1
  12. package/templates/skills/AGENTS.md +8 -22
  13. package/templates/skills/CLAUDE.md +3 -16
  14. package/templates/skills/bridges/admin/AGENTS.md +3 -16
  15. package/templates/skills/bridges/admin/CLAUDE.md +3 -11
  16. package/templates/skills/bridges/admin/copilot-instructions.md +2 -39
  17. package/templates/skills/bridges/api/AGENTS.md +5 -18
  18. package/templates/skills/bridges/api/CLAUDE.md +3 -11
  19. package/templates/skills/bridges/api/copilot-instructions.md +3 -38
  20. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +24 -59
  21. package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +1 -15
  22. package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +5 -45
  23. package/templates/skills/bridges/h5/AGENTS.md +4 -17
  24. package/templates/skills/bridges/h5/CLAUDE.md +3 -11
  25. package/templates/skills/bridges/h5/copilot-instructions.md +2 -37
  26. package/templates/skills/bridges/mobile-app/AGENTS.md +5 -18
  27. package/templates/skills/bridges/mobile-app/CLAUDE.md +3 -11
  28. package/templates/skills/bridges/mobile-app/copilot-instructions.md +2 -38
  29. package/templates/skills/bridges/website/AGENTS.md +4 -17
  30. package/templates/skills/bridges/website/CLAUDE.md +3 -11
  31. package/templates/skills/bridges/website/copilot-instructions.md +2 -37
  32. package/templates/skills/copilot-instructions.md +2 -36
  33. package/templates/skills/hai-ai/SKILL.md +35 -389
  34. package/templates/skills/hai-ai/guide.md +383 -0
  35. package/templates/skills/hai-ai/reference.md +1 -1
  36. package/templates/skills/hai-api-client/SKILL.md +6 -6
  37. package/templates/skills/hai-api-contract/SKILL.md +10 -25
  38. package/templates/skills/hai-app-create/SKILL.md +29 -388
  39. package/templates/skills/hai-app-create/reference.md +350 -0
  40. package/templates/skills/hai-app-review/SKILL.md +9 -23
  41. package/templates/skills/hai-app-tests/SKILL.md +29 -494
  42. package/templates/skills/hai-app-tests/reference.md +442 -0
  43. package/templates/skills/hai-audit/SKILL.md +6 -6
  44. package/templates/skills/hai-build/SKILL.md +19 -338
  45. package/templates/skills/hai-build/reference.md +326 -0
  46. package/templates/skills/hai-cache/SKILL.md +27 -32
  47. package/templates/skills/hai-capacitor/SKILL.md +19 -366
  48. package/templates/skills/hai-capacitor/reference.md +352 -0
  49. package/templates/skills/hai-ci/SKILL.md +6 -21
  50. package/templates/skills/hai-core/SKILL.md +25 -566
  51. package/templates/skills/hai-core/reference.md +533 -0
  52. package/templates/skills/hai-crypto/SKILL.md +6 -20
  53. package/templates/skills/hai-datapipe/SKILL.md +9 -23
  54. package/templates/skills/hai-deploy/SKILL.md +63 -25
  55. package/templates/skills/hai-framework-sync/SKILL.md +8 -20
  56. package/templates/skills/hai-iam/SKILL.md +27 -371
  57. package/templates/skills/hai-iam/reference.md +343 -0
  58. package/templates/skills/hai-kit/SKILL.md +6 -6
  59. package/templates/skills/hai-payment/SKILL.md +6 -26
  60. package/templates/skills/hai-pr-review/SKILL.md +6 -22
  61. package/templates/skills/hai-reach/SKILL.md +19 -317
  62. package/templates/skills/hai-reach/reference.md +303 -0
  63. package/templates/skills/hai-reldb/SKILL.md +19 -472
  64. package/templates/skills/hai-reldb/reference.md +452 -0
  65. package/templates/skills/hai-scheduler/SKILL.md +6 -6
  66. package/templates/skills/hai-serv/SKILL.md +27 -759
  67. package/templates/skills/hai-serv/reference.md +736 -0
  68. package/templates/skills/hai-storage/SKILL.md +7 -25
  69. package/templates/skills/hai-ui/SKILL.md +29 -625
  70. package/templates/skills/hai-ui/reference.md +598 -0
  71. package/templates/skills/hai-vecdb/SKILL.md +18 -303
  72. package/templates/skills/hai-vecdb/reference.md +291 -0
  73. package/dist/chunk-FVDXJYMO.js.map +0 -1
  74. package/dist/cli-config-templates-WGN2I23J.js +0 -5
  75. package/dist/src-6ZBWDBUB.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hai-reach
3
- description: 使用 @h-ai/reach 进行邮件、短信和 API 回调发送;当需求涉及用户触达、消息通知、验证码发送、多渠道模板管理时使用。
3
+ description: "使用 @h-ai/reach 配置邮件、短信、API 通知和多渠道消息模板。"
4
4
  ---
5
5
 
6
6
  # hai-reach
@@ -9,324 +9,26 @@ description: 使用 @h-ai/reach 进行邮件、短信和 API 回调发送;当
9
9
 
10
10
  | 项目 | 契约 |
11
11
  | --- | --- |
12
- | 能力 | 使用 @h-ai/reach 进行邮件、短信和 API 回调发送;当需求涉及用户触达、消息通知、验证码发送、多渠道模板管理时使用。 |
13
- | 适用场景 | 当任务与 `hai-reach` 的能力描述匹配,并且需要遵循本 Skill 的流程和边界时 |
14
- | 输入 | 模块配置、类型化业务参数、依赖初始化状态和目标运行环境 |
15
- | 输出 | 符合模块公共 API 的实现或示例;业务结果使用 HaiResult,并同步必要测试与文档 |
16
- | 限制 | 遵守 init → use → close 生命周期与运行环境边界;不绕过类型、授权、输入校验或敏感信息保护 |
12
+ | 能力 | 使用 @h-ai/reach 配置邮件、短信、API 通知和多渠道消息模板 |
13
+ | 适用场景 | 配置业务通知、验证码渠道或发送模板 |
14
+ | 输入 | ReachConfigInput、Provider、模板、收件人与模板参数 |
15
+ | 输出 | 发送和模板操作的 HaiResult |
16
+ | 限制 | 仅服务端;模板绑定 Provider,遵守 DND。区分测试与真实发送,不记录验证码或收件人敏感内容。 |
17
17
 
18
- > `@h-ai/reach` 提供统一的用户触达接口,支持同时注册多个 Provider(SMTP 邮件、短信、API 回调),内置模板引擎与免打扰(DND)机制。
18
+ ## 使用路径
19
19
 
20
- ---
21
-
22
- ## 运行环境
23
-
24
- > ⚠️ **服务端模块(Node.js only)。** 浏览器端不直接发送邮件/短信,而是通过 API 端点触发服务端 `reach.send()`。
25
-
26
- ---
27
-
28
- ## 依赖
29
-
30
- | 模块 | 用途 | 是否必需 | 初始化要求 |
31
- | --- | --- | --- | --- |
32
- | `@h-ai/reldb` | 数据库(发送日志与模板持久化) | 可选 | 已初始化时自动启用持久化 |
33
- | `@h-ai/cache` | 缓存(DND delay 策略分布式锁) | 可选 | 已初始化时自动启用分布式锁 |
34
-
35
- ---
36
-
37
- ## 适用场景
38
-
39
- - 同时使用邮件、短信、API 回调发送通知
40
- - 发送邮件通知(注册欢迎、密码重置、告警等)
41
- - 发送短信验证码或通知
42
- - 通过 HTTP API 回调触发第三方通知
43
- - 定义和管理消息模板(模板绑定到具体 Provider,支持配置文件定义)
44
- - 免打扰时段控制
45
- - 基于 `HaiReachError` 做错误分支处理
46
-
47
- ---
48
-
49
- ## 使用步骤
50
-
51
- ### 1. 配置
52
-
53
- ```yaml
54
- # config/_reach.yml
55
- providers:
56
- - name: email
57
- type: smtp
58
- host: ${HAI_REACH_SMTP_HOST:smtp.example.com}
59
- port: ${HAI_REACH_SMTP_PORT:465}
60
- secure: true
61
- user: ${HAI_REACH_SMTP_USER:}
62
- pass: ${HAI_REACH_SMTP_PASS:}
63
- from: ${HAI_REACH_SMTP_FROM:noreply@example.com}
64
- - name: sms
65
- type: aliyun-sms
66
- accessKeyId: ${HAI_REACH_SMS_ACCESS_KEY:}
67
- accessKeySecret: ${HAI_REACH_SMS_SECRET_KEY:}
68
- signName: ${HAI_REACH_SMS_SIGN_NAME:}
69
- - name: webhook
70
- type: api
71
- url: ${HAI_REACH_WEBHOOK_URL:}
72
-
73
- # 模板(可选,也可通过代码注册)
74
- templates:
75
- - name: verification_code
76
- provider: email
77
- subject: '验证码: {code}'
78
- body: '您的验证码是 {code},有效期 {minutes} 分钟。'
79
- - name: sms_code
80
- provider: sms
81
- body: '验证码: {code},{minutes} 分钟内有效。'
82
-
83
- # 免打扰(可选)
84
- dnd:
85
- enabled: true
86
- strategy: delay # discard(丢弃)或 delay(延时,DND 结束后集中发送)
87
- start: '22:00'
88
- end: '08:00'
89
- ```
90
-
91
- `providers` 是数组,因此示例保留稳定、可读的显式厂商变量名;若同时设置
92
- 按索引生成的约定变量(如 `HAI_REACH_PROVIDERS_0_HOST`),约定变量优先。
93
-
94
- API Provider 只接受 HTTP(S) URL。回调地址若来自不可信输入,业务层必须先做域名 allowlist,并由企业网络出口策略限制内网访问;不要在日志中记录完整 URL、收件人或第三方响应体。
95
-
96
- ### 2. 初始化与关闭
97
-
98
- ```typescript
99
- import { cache } from '@h-ai/cache'
100
- import { core } from '@h-ai/core'
101
- import { reach } from '@h-ai/reach'
102
- import { reldb } from '@h-ai/reldb'
103
-
104
- // 先初始化可选依赖(按需)
105
- await reldb.init(core.config.get('db')) // 可选,启用发送日志与模板持久化
106
- await cache.init(core.config.get('cache')) // 可选,启用 DND delay 策略分布式锁
107
-
108
- // 再初始化触达模块(自动检测已初始化的 reldb/cache 单例)
109
- await reach.init(core.config.get('reach'))
110
- // ... 使用触达服务
111
- await reach.close()
112
- ```
113
-
114
- `reach.config` 返回的是脱敏后的配置快照;`api.url` / `endpoint`、授权头、SMTP `pass`、短信密钥等敏感值会自动隐藏。
115
-
116
- ### 3. 保存模板(模板绑定 Provider)
117
-
118
- ```typescript
119
- // 通过代码保存(配置文件中的模板在 init 时自动注册)
120
- await reach.template.save({
121
- name: 'verification_code',
122
- provider: 'email',
123
- subject: '验证码: {code}',
124
- body: '您的验证码是 {code},有效期 {minutes} 分钟。',
125
- })
126
-
127
- await reach.template.saveBatch([
128
- { name: 'welcome', provider: 'email', subject: '欢迎 {userName}', body: '亲爱的 {userName},欢迎使用 {appName}!' },
129
- { name: 'sms_code', provider: 'sms', body: '验证码: {code},{minutes} 分钟内有效。' },
130
- ])
131
- ```
132
-
133
- ### 4. 发送消息
134
-
135
- ```typescript
136
- // 使用模板发送邮件(指定 provider)
137
- const result = await reach.send({
138
- provider: 'email',
139
- to: 'user@example.com',
140
- template: 'verification_code',
141
- vars: { code: '123456', minutes: '5' },
142
- })
143
-
144
- // 直接发送邮件(无模板)
145
- await reach.send({
146
- provider: 'email',
147
- to: 'user@example.com',
148
- subject: '通知',
149
- body: '<h1>Hello</h1>',
150
- })
151
-
152
- // 发送短信(通过 extra 传递 Provider 特有参数)
153
- await reach.send({
154
- provider: 'sms',
155
- to: '13800138000',
156
- extra: { templateCode: 'SMS_123456' },
157
- vars: { code: '654321' },
158
- })
20
+ - 先配置并注册 Provider,再保存绑定 Provider 的模板;发送结果须检查,DND/禁用与成功发送区分。
21
+ - 验证模板替换和错误分支时使用隔离测试收件人;真实通知须在用户授权范围内。
159
22
 
160
- // API 回调
161
- await reach.send({
162
- provider: 'webhook',
163
- to: 'user@example.com',
164
- body: '{"event":"signup"}',
165
- })
166
- ```
167
-
168
- ---
169
-
170
- ## 核心 API
171
-
172
- ### reach 对象
173
-
174
- | 方法 / 属性 | 签名 | 说明 |
175
- | --------------------- | -------------------------------------------------------- | ---------------------------------- |
176
- | `reach.init` | `(config: ReachConfigInput) => Promise<HaiResult<void>>` | 初始化(注册多个 Provider) |
177
- | `reach.send` | `(message: ReachMessage) => Promise<HaiResult<SendResult>>` | 发送消息(通过 provider 字段路由) |
178
- | `reach.template` | `ReachTemplateRegistry` | 模板注册表 |
179
- | `reach.config` | `ReachConfig \| null` | 当前脱敏配置快照 |
180
- | `reach.isInitialized` | `boolean` | 是否已初始化 |
181
- | `reach.close` | `() => Promise<void>` | 关闭所有连接 |
182
-
183
- ### ReachConfigInput
184
-
185
- ```typescript
186
- interface ReachConfigInput {
187
- providers: ProviderConfig[] // 多个 Provider 配置
188
- templates?: TemplateConfig[] // 通过配置文件定义的模板
189
- dnd?: {
190
- enabled: boolean // 是否启用
191
- strategy: 'discard' | 'delay' // discard 丢弃 / delay 延时发送
192
- start: string // 开始时间 HH:mm
193
- end: string // 结束时间 HH:mm
194
- }
195
- }
196
- ```
197
-
198
- ### ReachMessage
199
-
200
- | 字段 | 类型 | 必填 | 说明 |
201
- | ---------- | ------------------------- | ---- | ----------------------------------- |
202
- | `provider` | `string` | ✅ | 目标 Provider 名称 |
203
- | `to` | `string` | ✅ | 接收方(邮箱或手机号) |
204
- | `subject` | `string` | — | 邮件主题(直接发送时) |
205
- | `body` | `string` | — | 消息正文(直接发送时) |
206
- | `template` | `string` | — | 模板名称(模板发送时) |
207
- | `vars` | `Record<string, string>` | — | 模板变量 |
208
- | `extra` | `Record<string, unknown>` | — | Provider 扩展参数(如短信模板编码) |
209
-
210
- ### ReachTemplate(模板绑定 Provider)
211
-
212
- | 字段 | 类型 | 必填 | 说明 |
213
- | ---------- | -------- | ---- | -------------------- |
214
- | `name` | `string` | ✅ | 模板名称 |
215
- | `provider` | `string` | ✅ | 绑定的 Provider 名称 |
216
- | `subject` | `string` | — | 邮件主题模板 |
217
- | `body` | `string` | ✅ | 正文模板 |
218
-
219
- ---
220
-
221
- ## 错误码
222
-
223
- | 错误码 | code | 说明 |
224
- |--------|------|------|
225
- | `HaiReachError.SEND_FAILED` | `hai:reach:001` | 发送失败 |
226
- | `HaiReachError.TEMPLATE_NOT_FOUND` | `hai:reach:002` | 模板未找到 |
227
- | `HaiReachError.TEMPLATE_RENDER_FAILED` | `hai:reach:003` | 模板渲染失败 |
228
- | `HaiReachError.INVALID_RECIPIENT` | `hai:reach:004` | 无效接收方 |
229
- | `HaiReachError.PROVIDER_NOT_FOUND` | `hai:reach:005` | Provider 未找到 |
230
- | `HaiReachError.DND_BLOCKED` | `hai:reach:006` | 免打扰丢弃 |
231
- | `HaiReachError.DND_DEFERRED` | `hai:reach:007` | 免打扰延时暂存 |
232
- | `HaiReachError.NOT_INITIALIZED` | `hai:reach:010` | 模块未初始化 |
233
- | `HaiReachError.UNSUPPORTED_TYPE` | `hai:reach:011` | 不支持的类型 |
234
- | `HaiReachError.CONFIG_ERROR` | `hai:reach:012` | 配置错误 |
235
-
236
- ---
237
-
238
- ## 常见模式
239
-
240
- ### 多渠道验证码发送
241
-
242
- ```typescript
243
- reach.template.save({
244
- name: 'email_code',
245
- provider: 'email',
246
- subject: '验证码: {code}',
247
- body: '您的验证码是 {code},有效期 {minutes} 分钟。',
248
- })
249
-
250
- reach.template.save({
251
- name: 'sms_code',
252
- provider: 'sms',
253
- body: '验证码: {code},{minutes} 分钟内有效。',
254
- })
255
-
256
- // 根据用户选择的渠道发送
257
- async function sendCode(channel: 'email' | 'sms', target: string, code: string) {
258
- const template = channel === 'email' ? 'email_code' : 'sms_code'
259
- return reach.send({
260
- provider: channel,
261
- to: target,
262
- template,
263
- vars: { code, minutes: '5' },
264
- })
265
- }
266
- ```
267
-
268
- ### 与 IAM 集成(密码重置 / OTP 验证码)
269
-
270
- ```typescript
271
- import { iam } from '@h-ai/iam'
272
- import { reach } from '@h-ai/reach'
273
-
274
- // 初始化 reach(注册 email 和 sms Provider)
275
- await reach.init({
276
- providers: [
277
- { name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
278
- { name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '...' },
279
- ],
280
- templates: [
281
- { name: 'password_reset', provider: 'email', subject: 'Password Reset', body: 'Token: {token}, expires: {expiresAt}' },
282
- { name: 'otp_email', provider: 'email', subject: 'Code: {code}', body: 'Your code is {code}' },
283
- { name: 'otp_sms', provider: 'sms', body: 'Your code is {code}' },
284
- ],
285
- })
286
-
287
- // 初始化 IAM,使用 reach 发送通知
288
- await iam.init({
289
- db,
290
- cache,
291
- onPasswordResetRequest: async (user, token, expiresAt) => {
292
- await reach.send({ provider: 'email', to: user.email ?? '', template: 'password_reset', vars: { token, expiresAt: expiresAt.toISOString() } })
293
- },
294
- onOtpSendEmail: async (email, code) => {
295
- await reach.send({ provider: 'email', to: email, template: 'otp_email', vars: { code } })
296
- },
297
- onOtpSendSms: async (phone, code) => {
298
- await reach.send({ provider: 'sms', to: phone, template: 'otp_sms', vars: { code } })
299
- },
300
- })
301
- ```
302
-
303
- ### 错误处理
304
-
305
- ```typescript
306
- import { HaiReachError } from '@h-ai/reach'
307
-
308
- const result = await reach.send(message)
309
- if (!result.success) {
310
- switch (result.error.code) {
311
- case HaiReachError.NOT_INITIALIZED.code:
312
- break
313
- case HaiReachError.PROVIDER_NOT_FOUND.code:
314
- break
315
- case HaiReachError.DND_BLOCKED.code:
316
- // 免打扰时段(discard 策略),消息已丢弃
317
- break
318
- case HaiReachError.TEMPLATE_NOT_FOUND.code:
319
- break
320
- case HaiReachError.SEND_FAILED.code:
321
- break
322
- }
323
- }
324
- ```
325
-
326
- ---
23
+ ## 按需参考
327
24
 
328
- ## 相关 Skills
25
+ 详细配置、API 签名、错误码和范本在 [reference.md](reference.md)。按下列标题定位所需小节,不默认通读;用例中的业务变量需结合当前应用补齐。
329
26
 
330
- - `hai-core` — 配置加载、日志、HaiResult 模式
331
- - `hai-reldb` — 数据库操作(存储发送记录等)
332
- - `hai-cache` — 缓存操作、分布式锁(DND flush 互斥)
27
+ - 运行环境
28
+ - 依赖
29
+ - 适用场景
30
+ - 使用步骤
31
+ - 核心 API
32
+ - 错误码
33
+ - 常见模式
34
+ - 相关 Skills
@@ -0,0 +1,303 @@
1
+ # hai-reach — 详细用法
2
+
3
+ 从 [SKILL.md](SKILL.md) 选择主题后读取对应小节。代码片段展示 API 用法;省略的业务变量、schema、依赖与 return 上下文需由应用补齐。
4
+
5
+ > `@h-ai/reach` 提供统一的用户触达接口,支持同时注册多个 Provider(SMTP 邮件、短信、API 回调),内置模板引擎与免打扰(DND)机制。
6
+
7
+ ## 运行环境
8
+
9
+ > ⚠️ **服务端模块(Node.js only)。** 浏览器端不直接发送邮件/短信,而是通过 API 端点触发服务端 `reach.send()`。
10
+
11
+ ## 依赖
12
+
13
+ | 模块 | 用途 | 是否必需 | 初始化要求 |
14
+ | --- | --- | --- | --- |
15
+ | `@h-ai/reldb` | 数据库(发送日志与模板持久化) | 可选 | 已初始化时自动启用持久化 |
16
+ | `@h-ai/cache` | 缓存(DND delay 策略分布式锁) | 可选 | 已初始化时自动启用分布式锁 |
17
+
18
+ ## 适用场景
19
+
20
+ - 同时使用邮件、短信、API 回调发送通知
21
+ - 发送邮件通知(注册欢迎、密码重置、告警等)
22
+ - 发送短信验证码或通知
23
+ - 通过 HTTP API 回调触发第三方通知
24
+ - 定义和管理消息模板(模板绑定到具体 Provider,支持配置文件定义)
25
+ - 免打扰时段控制
26
+ - 基于 `HaiReachError` 做错误分支处理
27
+
28
+ ## 使用步骤
29
+
30
+ ### 1. 配置
31
+
32
+ ```yaml
33
+ # config/_reach.yml
34
+ providers:
35
+ - name: email
36
+ type: smtp
37
+ host: ${HAI_REACH_SMTP_HOST:smtp.example.com}
38
+ port: ${HAI_REACH_SMTP_PORT:465}
39
+ secure: true
40
+ user: ${HAI_REACH_SMTP_USER:}
41
+ pass: ${HAI_REACH_SMTP_PASS:}
42
+ from: ${HAI_REACH_SMTP_FROM:noreply@example.com}
43
+ - name: sms
44
+ type: aliyun-sms
45
+ accessKeyId: ${HAI_REACH_SMS_ACCESS_KEY:}
46
+ accessKeySecret: ${HAI_REACH_SMS_SECRET_KEY:}
47
+ signName: ${HAI_REACH_SMS_SIGN_NAME:}
48
+ - name: webhook
49
+ type: api
50
+ url: ${HAI_REACH_WEBHOOK_URL:}
51
+
52
+ # 模板(可选,也可通过代码注册)
53
+ templates:
54
+ - name: verification_code
55
+ provider: email
56
+ subject: '验证码: {code}'
57
+ body: '您的验证码是 {code},有效期 {minutes} 分钟。'
58
+ - name: sms_code
59
+ provider: sms
60
+ body: '验证码: {code},{minutes} 分钟内有效。'
61
+
62
+ # 免打扰(可选)
63
+ dnd:
64
+ enabled: true
65
+ strategy: delay # discard(丢弃)或 delay(延时,DND 结束后集中发送)
66
+ start: '22:00'
67
+ end: '08:00'
68
+ ```
69
+
70
+ `providers` 是数组,因此示例保留稳定、可读的显式厂商变量名;若同时设置
71
+ 按索引生成的约定变量(如 `HAI_REACH_PROVIDERS_0_HOST`),约定变量优先。
72
+
73
+ API Provider 只接受 HTTP(S) URL。回调地址若来自不可信输入,业务层必须先做域名 allowlist,并由企业网络出口策略限制内网访问;不要在日志中记录完整 URL、收件人或第三方响应体。
74
+
75
+ ### 2. 初始化与关闭
76
+
77
+ ```typescript
78
+ import { cache } from '@h-ai/cache'
79
+ import { core } from '@h-ai/core'
80
+ import { reach } from '@h-ai/reach'
81
+ import { reldb } from '@h-ai/reldb'
82
+
83
+ // 先初始化可选依赖(按需)
84
+ await reldb.init(core.config.get('db')) // 可选,启用发送日志与模板持久化
85
+ await cache.init(core.config.get('cache')) // 可选,启用 DND delay 策略分布式锁
86
+
87
+ // 再初始化触达模块(自动检测已初始化的 reldb/cache 单例)
88
+ await reach.init(core.config.get('reach'))
89
+ // ... 使用触达服务
90
+ await reach.close()
91
+ ```
92
+
93
+ `reach.config` 返回的是脱敏后的配置快照;`api.url` / `endpoint`、授权头、SMTP `pass`、短信密钥等敏感值会自动隐藏。
94
+
95
+ ### 3. 保存模板(模板绑定 Provider)
96
+
97
+ ```typescript
98
+ // 通过代码保存(配置文件中的模板在 init 时自动注册)
99
+ await reach.template.save({
100
+ name: 'verification_code',
101
+ provider: 'email',
102
+ subject: '验证码: {code}',
103
+ body: '您的验证码是 {code},有效期 {minutes} 分钟。',
104
+ })
105
+
106
+ await reach.template.saveBatch([
107
+ { name: 'welcome', provider: 'email', subject: '欢迎 {userName}', body: '亲爱的 {userName},欢迎使用 {appName}!' },
108
+ { name: 'sms_code', provider: 'sms', body: '验证码: {code},{minutes} 分钟内有效。' },
109
+ ])
110
+ ```
111
+
112
+ ### 4. 发送消息
113
+
114
+ ```typescript
115
+ // 使用模板发送邮件(指定 provider)
116
+ const result = await reach.send({
117
+ provider: 'email',
118
+ to: 'user@example.com',
119
+ template: 'verification_code',
120
+ vars: { code: '123456', minutes: '5' },
121
+ })
122
+
123
+ // 直接发送邮件(无模板)
124
+ await reach.send({
125
+ provider: 'email',
126
+ to: 'user@example.com',
127
+ subject: '通知',
128
+ body: '<h1>Hello</h1>',
129
+ })
130
+
131
+ // 发送短信(通过 extra 传递 Provider 特有参数)
132
+ await reach.send({
133
+ provider: 'sms',
134
+ to: '13800138000',
135
+ extra: { templateCode: 'SMS_123456' },
136
+ vars: { code: '654321' },
137
+ })
138
+
139
+ // API 回调
140
+ await reach.send({
141
+ provider: 'webhook',
142
+ to: 'user@example.com',
143
+ body: '{"event":"signup"}',
144
+ })
145
+ ```
146
+
147
+ ## 核心 API
148
+
149
+ ### reach 对象
150
+
151
+ | 方法 / 属性 | 签名 | 说明 |
152
+ | --------------------- | -------------------------------------------------------- | ---------------------------------- |
153
+ | `reach.init` | `(config: ReachConfigInput) => Promise<HaiResult<void>>` | 初始化(注册多个 Provider) |
154
+ | `reach.send` | `(message: ReachMessage) => Promise<HaiResult<SendResult>>` | 发送消息(通过 provider 字段路由) |
155
+ | `reach.template` | `ReachTemplateRegistry` | 模板注册表 |
156
+ | `reach.config` | `ReachConfig \| null` | 当前脱敏配置快照 |
157
+ | `reach.isInitialized` | `boolean` | 是否已初始化 |
158
+ | `reach.close` | `() => Promise<void>` | 关闭所有连接 |
159
+
160
+ ### ReachConfigInput
161
+
162
+ ```typescript
163
+ interface ReachConfigInput {
164
+ providers: ProviderConfig[] // 多个 Provider 配置
165
+ templates?: TemplateConfig[] // 通过配置文件定义的模板
166
+ dnd?: {
167
+ enabled: boolean // 是否启用
168
+ strategy: 'discard' | 'delay' // discard 丢弃 / delay 延时发送
169
+ start: string // 开始时间 HH:mm
170
+ end: string // 结束时间 HH:mm
171
+ }
172
+ }
173
+ ```
174
+
175
+ ### ReachMessage
176
+
177
+ | 字段 | 类型 | 必填 | 说明 |
178
+ | ---------- | ------------------------- | ---- | ----------------------------------- |
179
+ | `provider` | `string` | ✅ | 目标 Provider 名称 |
180
+ | `to` | `string` | ✅ | 接收方(邮箱或手机号) |
181
+ | `subject` | `string` | — | 邮件主题(直接发送时) |
182
+ | `body` | `string` | — | 消息正文(直接发送时) |
183
+ | `template` | `string` | — | 模板名称(模板发送时) |
184
+ | `vars` | `Record<string, string>` | — | 模板变量 |
185
+ | `extra` | `Record<string, unknown>` | — | Provider 扩展参数(如短信模板编码) |
186
+
187
+ ### ReachTemplate(模板绑定 Provider)
188
+
189
+ | 字段 | 类型 | 必填 | 说明 |
190
+ | ---------- | -------- | ---- | -------------------- |
191
+ | `name` | `string` | ✅ | 模板名称 |
192
+ | `provider` | `string` | ✅ | 绑定的 Provider 名称 |
193
+ | `subject` | `string` | — | 邮件主题模板 |
194
+ | `body` | `string` | ✅ | 正文模板 |
195
+
196
+ ## 错误码
197
+
198
+ | 错误码 | code | 说明 |
199
+ |--------|------|------|
200
+ | `HaiReachError.SEND_FAILED` | `hai:reach:001` | 发送失败 |
201
+ | `HaiReachError.TEMPLATE_NOT_FOUND` | `hai:reach:002` | 模板未找到 |
202
+ | `HaiReachError.TEMPLATE_RENDER_FAILED` | `hai:reach:003` | 模板渲染失败 |
203
+ | `HaiReachError.INVALID_RECIPIENT` | `hai:reach:004` | 无效接收方 |
204
+ | `HaiReachError.PROVIDER_NOT_FOUND` | `hai:reach:005` | Provider 未找到 |
205
+ | `HaiReachError.DND_BLOCKED` | `hai:reach:006` | 免打扰丢弃 |
206
+ | `HaiReachError.DND_DEFERRED` | `hai:reach:007` | 免打扰延时暂存 |
207
+ | `HaiReachError.NOT_INITIALIZED` | `hai:reach:010` | 模块未初始化 |
208
+ | `HaiReachError.UNSUPPORTED_TYPE` | `hai:reach:011` | 不支持的类型 |
209
+ | `HaiReachError.CONFIG_ERROR` | `hai:reach:012` | 配置错误 |
210
+
211
+ ## 常见模式
212
+
213
+ ### 多渠道验证码发送
214
+
215
+ ```typescript
216
+ reach.template.save({
217
+ name: 'email_code',
218
+ provider: 'email',
219
+ subject: '验证码: {code}',
220
+ body: '您的验证码是 {code},有效期 {minutes} 分钟。',
221
+ })
222
+
223
+ reach.template.save({
224
+ name: 'sms_code',
225
+ provider: 'sms',
226
+ body: '验证码: {code},{minutes} 分钟内有效。',
227
+ })
228
+
229
+ // 根据用户选择的渠道发送
230
+ async function sendCode(channel: 'email' | 'sms', target: string, code: string) {
231
+ const template = channel === 'email' ? 'email_code' : 'sms_code'
232
+ return reach.send({
233
+ provider: channel,
234
+ to: target,
235
+ template,
236
+ vars: { code, minutes: '5' },
237
+ })
238
+ }
239
+ ```
240
+
241
+ ### 与 IAM 集成(密码重置 / OTP 验证码)
242
+
243
+ ```typescript
244
+ import { iam } from '@h-ai/iam'
245
+ import { reach } from '@h-ai/reach'
246
+
247
+ // 初始化 reach(注册 email 和 sms Provider)
248
+ await reach.init({
249
+ providers: [
250
+ { name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
251
+ { name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '...' },
252
+ ],
253
+ templates: [
254
+ { name: 'password_reset', provider: 'email', subject: 'Password Reset', body: 'Token: {token}, expires: {expiresAt}' },
255
+ { name: 'otp_email', provider: 'email', subject: 'Code: {code}', body: 'Your code is {code}' },
256
+ { name: 'otp_sms', provider: 'sms', body: 'Your code is {code}' },
257
+ ],
258
+ })
259
+
260
+ // 初始化 IAM,使用 reach 发送通知
261
+ await iam.init({
262
+ db,
263
+ cache,
264
+ onPasswordResetRequest: async (user, token, expiresAt) => {
265
+ await reach.send({ provider: 'email', to: user.email ?? '', template: 'password_reset', vars: { token, expiresAt: expiresAt.toISOString() } })
266
+ },
267
+ onOtpSendEmail: async (email, code) => {
268
+ await reach.send({ provider: 'email', to: email, template: 'otp_email', vars: { code } })
269
+ },
270
+ onOtpSendSms: async (phone, code) => {
271
+ await reach.send({ provider: 'sms', to: phone, template: 'otp_sms', vars: { code } })
272
+ },
273
+ })
274
+ ```
275
+
276
+ ### 错误处理
277
+
278
+ ```typescript
279
+ import { HaiReachError } from '@h-ai/reach'
280
+
281
+ const result = await reach.send(message)
282
+ if (!result.success) {
283
+ switch (result.error.code) {
284
+ case HaiReachError.NOT_INITIALIZED.code:
285
+ break
286
+ case HaiReachError.PROVIDER_NOT_FOUND.code:
287
+ break
288
+ case HaiReachError.DND_BLOCKED.code:
289
+ // 免打扰时段(discard 策略),消息已丢弃
290
+ break
291
+ case HaiReachError.TEMPLATE_NOT_FOUND.code:
292
+ break
293
+ case HaiReachError.SEND_FAILED.code:
294
+ break
295
+ }
296
+ }
297
+ ```
298
+
299
+ ## 相关 Skills
300
+
301
+ - `hai-core` — 配置加载、日志、HaiResult 模式
302
+ - `hai-reldb` — 数据库操作(存储发送记录等)
303
+ - `hai-cache` — 缓存操作、分布式锁(DND flush 互斥)