@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.
- package/README.md +4 -0
- package/dist/{chunk-FVDXJYMO.js → chunk-O3ANVICQ.js} +31 -2
- package/dist/chunk-O3ANVICQ.js.map +1 -0
- package/dist/cli-config-templates-OWBEGBZQ.js +5 -0
- package/dist/{cli-config-templates-WGN2I23J.js.map → cli-config-templates-OWBEGBZQ.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/{src-6ZBWDBUB.js → src-TBIET7YU.js} +908 -127
- package/dist/src-TBIET7YU.js.map +1 -0
- package/package.json +7 -7
- package/templates/base/.gitignore.hbs +3 -0
- package/templates/base/package.json.hbs +1 -1
- package/templates/skills/AGENTS.md +8 -22
- package/templates/skills/CLAUDE.md +3 -16
- package/templates/skills/bridges/admin/AGENTS.md +3 -16
- package/templates/skills/bridges/admin/CLAUDE.md +3 -11
- package/templates/skills/bridges/admin/copilot-instructions.md +2 -39
- package/templates/skills/bridges/api/AGENTS.md +5 -18
- package/templates/skills/bridges/api/CLAUDE.md +3 -11
- package/templates/skills/bridges/api/copilot-instructions.md +3 -38
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +24 -59
- package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +1 -15
- package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +5 -45
- package/templates/skills/bridges/h5/AGENTS.md +4 -17
- package/templates/skills/bridges/h5/CLAUDE.md +3 -11
- package/templates/skills/bridges/h5/copilot-instructions.md +2 -37
- package/templates/skills/bridges/mobile-app/AGENTS.md +5 -18
- package/templates/skills/bridges/mobile-app/CLAUDE.md +3 -11
- package/templates/skills/bridges/mobile-app/copilot-instructions.md +2 -38
- package/templates/skills/bridges/website/AGENTS.md +4 -17
- package/templates/skills/bridges/website/CLAUDE.md +3 -11
- package/templates/skills/bridges/website/copilot-instructions.md +2 -37
- package/templates/skills/copilot-instructions.md +2 -36
- package/templates/skills/hai-ai/SKILL.md +35 -389
- package/templates/skills/hai-ai/guide.md +383 -0
- package/templates/skills/hai-ai/reference.md +1 -1
- package/templates/skills/hai-api-client/SKILL.md +6 -6
- package/templates/skills/hai-api-contract/SKILL.md +10 -25
- package/templates/skills/hai-app-create/SKILL.md +29 -388
- package/templates/skills/hai-app-create/reference.md +350 -0
- package/templates/skills/hai-app-review/SKILL.md +9 -23
- package/templates/skills/hai-app-tests/SKILL.md +29 -494
- package/templates/skills/hai-app-tests/reference.md +442 -0
- package/templates/skills/hai-audit/SKILL.md +6 -6
- package/templates/skills/hai-build/SKILL.md +19 -338
- package/templates/skills/hai-build/reference.md +326 -0
- package/templates/skills/hai-cache/SKILL.md +27 -32
- package/templates/skills/hai-capacitor/SKILL.md +19 -366
- package/templates/skills/hai-capacitor/reference.md +352 -0
- package/templates/skills/hai-ci/SKILL.md +6 -21
- package/templates/skills/hai-core/SKILL.md +25 -566
- package/templates/skills/hai-core/reference.md +533 -0
- package/templates/skills/hai-crypto/SKILL.md +6 -20
- package/templates/skills/hai-datapipe/SKILL.md +9 -23
- package/templates/skills/hai-deploy/SKILL.md +63 -25
- package/templates/skills/hai-framework-sync/SKILL.md +8 -20
- package/templates/skills/hai-iam/SKILL.md +27 -371
- package/templates/skills/hai-iam/reference.md +343 -0
- package/templates/skills/hai-kit/SKILL.md +6 -6
- package/templates/skills/hai-payment/SKILL.md +6 -26
- package/templates/skills/hai-pr-review/SKILL.md +6 -22
- package/templates/skills/hai-reach/SKILL.md +19 -317
- package/templates/skills/hai-reach/reference.md +303 -0
- package/templates/skills/hai-reldb/SKILL.md +19 -472
- package/templates/skills/hai-reldb/reference.md +452 -0
- package/templates/skills/hai-scheduler/SKILL.md +6 -6
- package/templates/skills/hai-serv/SKILL.md +27 -759
- package/templates/skills/hai-serv/reference.md +736 -0
- package/templates/skills/hai-storage/SKILL.md +7 -25
- package/templates/skills/hai-ui/SKILL.md +29 -625
- package/templates/skills/hai-ui/reference.md +598 -0
- package/templates/skills/hai-vecdb/SKILL.md +18 -303
- package/templates/skills/hai-vecdb/reference.md +291 -0
- package/dist/chunk-FVDXJYMO.js.map +0 -1
- package/dist/cli-config-templates-WGN2I23J.js +0 -5
- package/dist/src-6ZBWDBUB.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-reach
|
|
3
|
-
description: 使用 @h-ai/reach
|
|
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
|
|
13
|
-
| 适用场景 |
|
|
14
|
-
| 输入 |
|
|
15
|
-
| 输出 |
|
|
16
|
-
| 限制 |
|
|
12
|
+
| 能力 | 使用 @h-ai/reach 配置邮件、短信、API 通知和多渠道消息模板 |
|
|
13
|
+
| 适用场景 | 配置业务通知、验证码渠道或发送模板 |
|
|
14
|
+
| 输入 | ReachConfigInput、Provider、模板、收件人与模板参数 |
|
|
15
|
+
| 输出 | 发送和模板操作的 HaiResult |
|
|
16
|
+
| 限制 | 仅服务端;模板绑定 Provider,遵守 DND。区分测试与真实发送,不记录验证码或收件人敏感内容。 |
|
|
17
17
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
详细配置、API 签名、错误码和范本在 [reference.md](reference.md)。按下列标题定位所需小节,不默认通读;用例中的业务变量需结合当前应用补齐。
|
|
329
26
|
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
-
|
|
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 互斥)
|