| name | hai-reach |
| description | 使用 @h-ai/reach 进行邮件、短信和 API 回调发送;当需求涉及用户触达、消息通知、验证码发送、多渠道模板管理时使用。 |
hai-reach
能力契约
| 项目 | 契约 |
|---|
| 能力 | 使用 @h-ai/reach 进行邮件、短信和 API 回调发送;当需求涉及用户触达、消息通知、验证码发送、多渠道模板管理时使用。 |
| 适用场景 | 当任务与 hai-reach 的能力描述匹配,并且需要遵循本 Skill 的流程和边界时 |
| 输入 | 模块配置、类型化业务参数、依赖初始化状态和目标运行环境 |
| 输出 | 符合模块公共 API 的实现或示例;业务结果使用 HaiResult,并同步必要测试与文档 |
| 限制 | 遵守 init → use → close 生命周期与运行环境边界;不绕过类型、授权、输入校验或敏感信息保护 |
@h-ai/reach 提供统一的用户触达接口,支持同时注册多个 Provider(SMTP 邮件、短信、API 回调),内置模板引擎与免打扰(DND)机制。
运行环境
⚠️ 服务端模块(Node.js only)。 浏览器端不直接发送邮件/短信,而是通过 API 端点触发服务端 reach.send()。
依赖
| 模块 | 用途 | 是否必需 | 初始化要求 |
|---|
@h-ai/reldb | 数据库(发送日志与模板持久化) | 可选 | 已初始化时自动启用持久化 |
@h-ai/cache | 缓存(DND delay 策略分布式锁) | 可选 | 已初始化时自动启用分布式锁 |
适用场景
- 同时使用邮件、短信、API 回调发送通知
- 发送邮件通知(注册欢迎、密码重置、告警等)
- 发送短信验证码或通知
- 通过 HTTP API 回调触发第三方通知
- 定义和管理消息模板(模板绑定到具体 Provider,支持配置文件定义)
- 免打扰时段控制
- 基于
HaiReachError 做错误分支处理
使用步骤
1. 配置
providers:
- name: email
type: smtp
host: ${HAI_REACH_SMTP_HOST:smtp.example.com}
port: ${HAI_REACH_SMTP_PORT:465}
secure: true
user: ${HAI_REACH_SMTP_USER:}
pass: ${HAI_REACH_SMTP_PASS:}
from: ${HAI_REACH_SMTP_FROM:noreply@example.com}
- name: sms
type: aliyun-sms
accessKeyId: ${HAI_REACH_SMS_ACCESS_KEY:}
accessKeySecret: ${HAI_REACH_SMS_SECRET_KEY:}
signName: ${HAI_REACH_SMS_SIGN_NAME:}
- name: webhook
type: api
url: ${HAI_REACH_WEBHOOK_URL:}
templates:
- name: verification_code
provider: email
subject: '验证码: {code}'
body: '您的验证码是 {code},有效期 {minutes} 分钟。'
- name: sms_code
provider: sms
body: '验证码: {code},{minutes} 分钟内有效。'
dnd:
enabled: true
strategy: delay
start: '22:00'
end: '08:00'
API Provider 只接受 HTTP(S) URL。回调地址若来自不可信输入,业务层必须先做域名 allowlist,并由企业网络出口策略限制内网访问;不要在日志中记录完整 URL、收件人或第三方响应体。
2. 初始化与关闭
import { cache } from '@h-ai/cache'
import { core } from '@h-ai/core'
import { reach } from '@h-ai/reach'
import { reldb } from '@h-ai/reldb'
await reldb.init(core.config.get('db'))
await cache.init(core.config.get('cache'))
await reach.init(core.config.get('reach'))
await reach.close()
reach.config 返回的是脱敏后的配置快照;api.url / endpoint、授权头、SMTP pass、短信密钥等敏感值会自动隐藏。
3. 保存模板(模板绑定 Provider)
await reach.template.save({
name: 'verification_code',
provider: 'email',
subject: '验证码: {code}',
body: '您的验证码是 {code},有效期 {minutes} 分钟。',
})
await reach.template.saveBatch([
{ name: 'welcome', provider: 'email', subject: '欢迎 {userName}', body: '亲爱的 {userName},欢迎使用 {appName}!' },
{ name: 'sms_code', provider: 'sms', body: '验证码: {code},{minutes} 分钟内有效。' },
])
4. 发送消息
const result = await reach.send({
provider: 'email',
to: 'user@example.com',
template: 'verification_code',
vars: { code: '123456', minutes: '5' },
})
await reach.send({
provider: 'email',
to: 'user@example.com',
subject: '通知',
body: '<h1>Hello</h1>',
})
await reach.send({
provider: 'sms',
to: '13800138000',
extra: { templateCode: 'SMS_123456' },
vars: { code: '654321' },
})
await reach.send({
provider: 'webhook',
to: 'user@example.com',
body: '{"event":"signup"}',
})
核心 API
reach 对象
| 方法 / 属性 | 签名 | 说明 |
|---|
reach.init | (config: ReachConfigInput) => Promise<HaiResult<void>> | 初始化(注册多个 Provider) |
reach.send | (message: ReachMessage) => Promise<HaiResult<SendResult>> | 发送消息(通过 provider 字段路由) |
reach.template | ReachTemplateRegistry | 模板注册表 |
reach.config | ReachConfig | null | 当前脱敏配置快照 |
reach.isInitialized | boolean | 是否已初始化 |
reach.close | () => Promise<void> | 关闭所有连接 |
ReachConfigInput
interface ReachConfigInput {
providers: ProviderConfig[]
templates?: TemplateConfig[]
dnd?: {
enabled: boolean
strategy: 'discard' | 'delay'
start: string
end: string
}
}
ReachMessage
| 字段 | 类型 | 必填 | 说明 |
|---|
provider | string | ✅ | 目标 Provider 名称 |
to | string | ✅ | 接收方(邮箱或手机号) |
subject | string | — | 邮件主题(直接发送时) |
body | string | — | 消息正文(直接发送时) |
template | string | — | 模板名称(模板发送时) |
vars | Record<string, string> | — | 模板变量 |
extra | Record<string, unknown> | — | Provider 扩展参数(如短信模板编码) |
ReachTemplate(模板绑定 Provider)
| 字段 | 类型 | 必填 | 说明 |
|---|
name | string | ✅ | 模板名称 |
provider | string | ✅ | 绑定的 Provider 名称 |
subject | string | — | 邮件主题模板 |
body | string | ✅ | 正文模板 |
错误码
| 错误码 | code | 说明 |
|---|
HaiReachError.SEND_FAILED | hai:reach:001 | 发送失败 |
HaiReachError.TEMPLATE_NOT_FOUND | hai:reach:002 | 模板未找到 |
HaiReachError.TEMPLATE_RENDER_FAILED | hai:reach:003 | 模板渲染失败 |
HaiReachError.INVALID_RECIPIENT | hai:reach:004 | 无效接收方 |
HaiReachError.PROVIDER_NOT_FOUND | hai:reach:005 | Provider 未找到 |
HaiReachError.DND_BLOCKED | hai:reach:006 | 免打扰丢弃 |
HaiReachError.DND_DEFERRED | hai:reach:007 | 免打扰延时暂存 |
HaiReachError.NOT_INITIALIZED | hai:reach:010 | 模块未初始化 |
HaiReachError.UNSUPPORTED_TYPE | hai:reach:011 | 不支持的类型 |
HaiReachError.CONFIG_ERROR | hai:reach:012 | 配置错误 |
常见模式
多渠道验证码发送
reach.template.save({
name: 'email_code',
provider: 'email',
subject: '验证码: {code}',
body: '您的验证码是 {code},有效期 {minutes} 分钟。',
})
reach.template.save({
name: 'sms_code',
provider: 'sms',
body: '验证码: {code},{minutes} 分钟内有效。',
})
async function sendCode(channel: 'email' | 'sms', target: string, code: string) {
const template = channel === 'email' ? 'email_code' : 'sms_code'
return reach.send({
provider: channel,
to: target,
template,
vars: { code, minutes: '5' },
})
}
与 IAM 集成(密码重置 / OTP 验证码)
import { iam } from '@h-ai/iam'
import { reach } from '@h-ai/reach'
await reach.init({
providers: [
{ name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
{ name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '...' },
],
templates: [
{ name: 'password_reset', provider: 'email', subject: 'Password Reset', body: 'Token: {token}, expires: {expiresAt}' },
{ name: 'otp_email', provider: 'email', subject: 'Code: {code}', body: 'Your code is {code}' },
{ name: 'otp_sms', provider: 'sms', body: 'Your code is {code}' },
],
})
await iam.init({
db,
cache,
onPasswordResetRequest: async (user, token, expiresAt) => {
await reach.send({ provider: 'email', to: user.email ?? '', template: 'password_reset', vars: { token, expiresAt: expiresAt.toISOString() } })
},
onOtpSendEmail: async (email, code) => {
await reach.send({ provider: 'email', to: email, template: 'otp_email', vars: { code } })
},
onOtpSendSms: async (phone, code) => {
await reach.send({ provider: 'sms', to: phone, template: 'otp_sms', vars: { code } })
},
})
错误处理
import { HaiReachError } from '@h-ai/reach'
const result = await reach.send(message)
if (!result.success) {
switch (result.error.code) {
case HaiReachError.NOT_INITIALIZED.code:
break
case HaiReachError.PROVIDER_NOT_FOUND.code:
break
case HaiReachError.DND_BLOCKED.code:
break
case HaiReachError.TEMPLATE_NOT_FOUND.code:
break
case HaiReachError.SEND_FAILED.code:
break
}
}
相关 Skills
hai-core — 配置加载、日志、HaiResult 模式
hai-reldb — 数据库操作(存储发送记录等)
hai-cache — 缓存操作、分布式锁(DND flush 互斥)