@h-ai/cli 0.1.0-alpha5

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 (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +167 -0
  3. package/dist/chunk-2H7UOFLK.js +10 -0
  4. package/dist/chunk-2H7UOFLK.js.map +1 -0
  5. package/dist/chunk-LANIDJC7.js +659 -0
  6. package/dist/chunk-LANIDJC7.js.map +1 -0
  7. package/dist/chunk-YV7WPUI6.js +206 -0
  8. package/dist/chunk-YV7WPUI6.js.map +1 -0
  9. package/dist/config-templates-S5RI2MC6.js +5 -0
  10. package/dist/config-templates-S5RI2MC6.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +1645 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/skill-templates-N5DU5FNX.js +5 -0
  15. package/dist/skill-templates-N5DU5FNX.js.map +1 -0
  16. package/dist/src-XJ6YXNAF.js +15030 -0
  17. package/dist/src-XJ6YXNAF.js.map +1 -0
  18. package/package.json +59 -0
  19. package/templates/README.md +126 -0
  20. package/templates/apps/admin/messages/en-US.json +33 -0
  21. package/templates/apps/admin/messages/zh-CN.json +33 -0
  22. package/templates/apps/admin/src/routes/+layout.svelte.hbs +30 -0
  23. package/templates/apps/admin/src/routes/+page.svelte.hbs +35 -0
  24. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +44 -0
  25. package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +45 -0
  26. package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +37 -0
  27. package/templates/apps/admin/src/routes/api/health/+server.ts +12 -0
  28. package/templates/apps/android-app/.env.hbs +3 -0
  29. package/templates/apps/android-app/capacitor.config.ts.hbs +12 -0
  30. package/templates/apps/android-app/messages/en-US.json +24 -0
  31. package/templates/apps/android-app/messages/zh-CN.json +24 -0
  32. package/templates/apps/android-app/src/lib/api.ts.hbs +30 -0
  33. package/templates/apps/android-app/src/lib/capacitor.ts +21 -0
  34. package/templates/apps/android-app/src/routes/+layout.svelte.hbs +42 -0
  35. package/templates/apps/android-app/src/routes/+layout.ts +7 -0
  36. package/templates/apps/android-app/src/routes/+page.svelte.hbs +27 -0
  37. package/templates/apps/api/src/routes/+layout.svelte +14 -0
  38. package/templates/apps/api/src/routes/+page.svelte +11 -0
  39. package/templates/apps/api/src/routes/api/v1/health/+server.ts +14 -0
  40. package/templates/apps/api/src/routes/api/v1/items/+server.ts +37 -0
  41. package/templates/apps/api/src/routes/api/v1/items/[id]/+server.ts +30 -0
  42. package/templates/apps/h5/messages/en-US.json +37 -0
  43. package/templates/apps/h5/messages/zh-CN.json +37 -0
  44. package/templates/apps/h5/src/routes/+layout.svelte.hbs +57 -0
  45. package/templates/apps/h5/src/routes/+page.svelte.hbs +27 -0
  46. package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +27 -0
  47. package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +27 -0
  48. package/templates/apps/website/messages/en-US.json +32 -0
  49. package/templates/apps/website/messages/zh-CN.json +32 -0
  50. package/templates/apps/website/src/routes/+layout.svelte.hbs +66 -0
  51. package/templates/apps/website/src/routes/+page.svelte.hbs +33 -0
  52. package/templates/apps/website/src/routes/about/+page.svelte.hbs +27 -0
  53. package/templates/apps/website/src/routes/api/health/+server.ts +12 -0
  54. package/templates/apps/website/src/routes/contact/+page.svelte.hbs +59 -0
  55. package/templates/apps/website/src/routes/services/+page.svelte.hbs +63 -0
  56. package/templates/base/e2e/helpers.ts +49 -0
  57. package/templates/base/package.json.hbs +67 -0
  58. package/templates/base/playwright.config.ts +32 -0
  59. package/templates/base/src/app.css +2 -0
  60. package/templates/base/src/app.d.ts.hbs +18 -0
  61. package/templates/base/src/app.html.hbs +16 -0
  62. package/templates/base/src/hooks.server.ts.hbs +123 -0
  63. package/templates/base/src/lib/server/init.ts.hbs +190 -0
  64. package/templates/base/svelte.config.js.hbs +30 -0
  65. package/templates/base/tsconfig.json +8 -0
  66. package/templates/base/vite.config.ts.hbs +27 -0
  67. package/templates/features/ai/routes/api/chat/+server.ts +26 -0
  68. package/templates/features/datapipe/routes/api/datapipe/pipeline/+server.ts +41 -0
  69. package/templates/features/iam/routes-admin/(auth)/+layout.svelte.hbs +21 -0
  70. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +98 -0
  71. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +124 -0
  72. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +98 -0
  73. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +114 -0
  74. package/templates/features/iam/routes-shared/api/auth/login/+server.ts +32 -0
  75. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +15 -0
  76. package/templates/features/iam/routes-shared/api/auth/me/+server.ts +12 -0
  77. package/templates/features/iam/routes-shared/api/auth/register/+server.ts +26 -0
  78. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +30 -0
  79. package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +42 -0
  80. package/templates/features/payment/routes/api/v1/payment/query/[orderNo]/+server.ts +25 -0
  81. package/templates/features/storage/routes/api/upload/+server.ts +35 -0
  82. package/templates/features/vecdb/routes/api/vecdb/search/+server.ts +36 -0
  83. package/templates/i18n/project.inlang/settings.json +11 -0
  84. package/templates/skills/AGENTS.md +26 -0
  85. package/templates/skills/CLAUDE.md +12 -0
  86. package/templates/skills/copilot-instructions.md +61 -0
  87. package/templates/skills/hai-ai/SKILL.md +659 -0
  88. package/templates/skills/hai-api-client/SKILL.md +346 -0
  89. package/templates/skills/hai-app-create/SKILL.md +371 -0
  90. package/templates/skills/hai-app-review/SKILL.md +156 -0
  91. package/templates/skills/hai-app-tests/SKILL.md +468 -0
  92. package/templates/skills/hai-audit/SKILL.md +141 -0
  93. package/templates/skills/hai-build/SKILL.md +319 -0
  94. package/templates/skills/hai-cache/SKILL.md +184 -0
  95. package/templates/skills/hai-capacitor/SKILL.md +389 -0
  96. package/templates/skills/hai-core/SKILL.md +544 -0
  97. package/templates/skills/hai-crypto/SKILL.md +183 -0
  98. package/templates/skills/hai-datapipe/SKILL.md +214 -0
  99. package/templates/skills/hai-deploy/SKILL.md +210 -0
  100. package/templates/skills/hai-iam/SKILL.md +355 -0
  101. package/templates/skills/hai-kit/SKILL.md +319 -0
  102. package/templates/skills/hai-payment/SKILL.md +289 -0
  103. package/templates/skills/hai-reach/SKILL.md +325 -0
  104. package/templates/skills/hai-reldb/SKILL.md +459 -0
  105. package/templates/skills/hai-scheduler/SKILL.md +242 -0
  106. package/templates/skills/hai-storage/SKILL.md +230 -0
  107. package/templates/skills/hai-ui/SKILL.md +275 -0
  108. package/templates/skills/hai-vecdb/SKILL.md +273 -0
  109. package/templates/skills/opencode.json +11 -0
@@ -0,0 +1,289 @@
1
+ ---
2
+ name: hai-payment
3
+ description: 使用 @h-ai/payment 接入微信支付、支付宝、Stripe 统一支付;支持创建订单、回调验签、订单查询与退款;当需求涉及支付接入、订单创建、支付回调、退款或客户端调起支付时使用。
4
+ ---
5
+
6
+ # hai-payment
7
+
8
+ > `@h-ai/payment` 是 hai-framework 的统一支付模块,通过 Provider 模式支持微信支付、支付宝、Stripe。服务端处理订单和回调,客户端调起支付。
9
+
10
+ ---
11
+
12
+ ## 依赖
13
+
14
+ | 模块 | 用途 | 是否必需 | 初始化要求 |
15
+ | --- | --- | --- | --- |
16
+ | `@h-ai/audit` | 审计日志(支付操作自动审计) | 可选 | 已初始化时自动写入审计日志 |
17
+
18
+ ---
19
+
20
+ ## 适用场景
21
+
22
+ - 接入微信支付(JSAPI / H5 / Native / App)
23
+ - 接入支付宝(H5 / App / PC)
24
+ - 接入 Stripe(Checkout Session)
25
+ - 支付回调验签与通知处理
26
+ - 订单查询与退款
27
+ - 客户端调起支付(Web / H5 / App)
28
+
29
+ ---
30
+
31
+ ## 使用步骤
32
+
33
+ ### 1. 配置
34
+
35
+ ```yaml
36
+ # config/_payment.yml(自行创建)
37
+ wechat:
38
+ appId: ${HAI_PAYMENT_WECHAT_APP_ID}
39
+ mchId: ${HAI_PAYMENT_WECHAT_MCH_ID}
40
+ apiV3Key: ${HAI_PAYMENT_WECHAT_API_V3_KEY}
41
+ privateKey: ${HAI_PAYMENT_WECHAT_PRIVATE_KEY}
42
+ serialNo: ${HAI_PAYMENT_WECHAT_SERIAL_NO}
43
+ platformCert: ${HAI_PAYMENT_WECHAT_PLATFORM_CERT} # 可选,回调验签需要
44
+
45
+ alipay:
46
+ appId: ${HAI_PAYMENT_ALIPAY_APP_ID}
47
+ privateKey: ${HAI_PAYMENT_ALIPAY_PRIVATE_KEY}
48
+ alipayPublicKey: ${HAI_PAYMENT_ALIPAY_PUBLIC_KEY}
49
+ signType: RSA2 # 可选,默认 RSA2
50
+ sandbox: false # 可选,默认 false
51
+
52
+ stripe:
53
+ secretKey: ${HAI_PAYMENT_STRIPE_SECRET_KEY}
54
+ webhookSecret: ${HAI_PAYMENT_STRIPE_WEBHOOK_SECRET}
55
+ ```
56
+
57
+ ### 2. 初始化(服务端)
58
+
59
+ ```typescript
60
+ import { payment } from '@h-ai/payment'
61
+
62
+ const initResult = await payment.init({
63
+ wechat: {
64
+ appId: 'wx1234567890',
65
+ mchId: '1600000000',
66
+ apiV3Key: 'your-api-key-v3',
67
+ privateKey: '-----BEGIN RSA PRIVATE KEY-----...',
68
+ serialNo: 'CERT_SERIAL_NO',
69
+ },
70
+ alipay: {
71
+ appId: '2021000000000000',
72
+ privateKey: '-----BEGIN RSA PRIVATE KEY-----...',
73
+ alipayPublicKey: '-----BEGIN PUBLIC KEY-----...',
74
+ },
75
+ })
76
+ if (!initResult.success) {
77
+ // 处理初始化错误
78
+ }
79
+ ```
80
+
81
+ ### 3. 创建订单
82
+
83
+ ```typescript
84
+ const result = await payment.createOrder('wechat', {
85
+ orderNo: 'ORDER_20250101_001',
86
+ amount: 9900, // 单位:分
87
+ description: '商品名称',
88
+ tradeType: 'jsapi',
89
+ userId: 'user_openid',
90
+ notifyUrl: 'https://example.com/payment/notify/wechat',
91
+ metadata: { source: 'web' },
92
+ })
93
+
94
+ if (result.success) {
95
+ // result.data: PaymentOrder { provider, tradeType, clientParams, prepayId? }
96
+ // clientParams 包含调起支付所需参数
97
+ }
98
+ ```
99
+
100
+ ### 4. 处理支付回调
101
+
102
+ ```typescript
103
+ // API 路由:POST /payment/notify/:provider
104
+ const result = await payment.handleNotify('wechat', {
105
+ body: await request.text(),
106
+ headers: Object.fromEntries(request.headers),
107
+ })
108
+
109
+ if (result.success) {
110
+ // result.data: { orderNo, transactionId, amount, status, paidAt? }
111
+ // 更新订单状态...
112
+ }
113
+ ```
114
+
115
+ ### 5. 查询订单
116
+
117
+ ```typescript
118
+ const result = await payment.queryOrder('wechat', 'ORDER_20250101_001')
119
+
120
+ if (result.success) {
121
+ // result.data.status: 'pending' | 'paid' | 'closed' | 'refunded' | 'failed'
122
+ }
123
+ ```
124
+
125
+ ### 6. 退款
126
+
127
+ ```typescript
128
+ const result = await payment.refund('wechat', {
129
+ orderNo: 'ORDER_20250101_001',
130
+ refundNo: 'REFUND_20250101_001',
131
+ amount: 9900,
132
+ totalAmount: 9900, // 微信退款必填
133
+ reason: '用户退款',
134
+ })
135
+ ```
136
+
137
+ ### 7. 客户端调起支付(浏览器)
138
+
139
+ ```typescript
140
+ import { invokePayment } from '@h-ai/payment/client'
141
+
142
+ // 前端拿到服务端返回的 PaymentOrder 后调起支付
143
+ const payResult = await invokePayment(orderResult.data)
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 核心 API(服务端)
149
+
150
+ | API | 用途 | 返回值 |
151
+ | ------------------------------------------------ | -------- | ----------------------------- |
152
+ | `payment.init(config)` | 初始化 | `HaiResult<void>` |
153
+ | `payment.close()` | 关闭 | `void` |
154
+ | `payment.createOrder(providerName, input)` | 创建订单 | `HaiResult<PaymentOrder>` |
155
+ | `payment.handleNotify(providerName, request)` | 处理回调 | `HaiResult<PaymentNotifyResult>` |
156
+ | `payment.queryOrder(providerName, orderNo)` | 查询订单 | `HaiResult<OrderStatus>` |
157
+ | `payment.refund(providerName, input)` | 退款 | `HaiResult<RefundResult>` |
158
+ | `payment.closeOrder(providerName, orderNo)` | 关闭订单 | `HaiResult<void>` |
159
+ | `payment.getProvider(name)` | 获取 Provider | `PaymentProvider \| undefined` |
160
+ | `payment.registerProvider(provider)` | 注册自定义 Provider | `void` |
161
+
162
+ ## 客户端 API
163
+
164
+ | API | 用途 | 返回值 |
165
+ | ------------------------ | -------- | ----------------------------- |
166
+ | `invokePayment(options)` | 调起支付 | `HaiResult<InvokePaymentResult>` |
167
+
168
+ ---
169
+
170
+ ## 错误码 — `HaiPaymentError`
171
+
172
+ | 错误码 | code | 说明 |
173
+ |--------|------|------|
174
+ | `HaiPaymentError.CREATE_ORDER_FAILED` | `hai:payment:001` | 创建订单失败 |
175
+ | `HaiPaymentError.QUERY_ORDER_FAILED` | `hai:payment:002` | 查询订单失败 |
176
+ | `HaiPaymentError.REFUND_FAILED` | `hai:payment:003` | 退款失败 |
177
+ | `HaiPaymentError.CLOSE_ORDER_FAILED` | `hai:payment:004` | 关闭订单失败 |
178
+ | `HaiPaymentError.NOT_INITIALIZED` | `hai:payment:010` | 模块未初始化 |
179
+ | `HaiPaymentError.SIGN_FAILED` | `hai:payment:020` | 签名失败 |
180
+ | `HaiPaymentError.PROVIDER_NOT_FOUND` | `hai:payment:030` | Provider 未注册 |
181
+ | `HaiPaymentError.INVALID_AMOUNT` | `hai:payment:040` | 金额无效 |
182
+ | `HaiPaymentError.NOTIFY_VERIFY_FAILED` | `hai:payment:050` | 回调验签失败 |
183
+ | `HaiPaymentError.NOTIFY_PARSE_FAILED` | `hai:payment:051` | 回调解析失败 |
184
+ | `HaiPaymentError.INVOKE_WEB_FAILED` | `hai:payment:060` | Web 端调起支付失败 |
185
+ | `HaiPaymentError.INVOKE_APP_FAILED` | `hai:payment:061` | App 端调起支付失败 |
186
+ | `HaiPaymentError.CONFIG_ERROR` | `hai:payment:070` | 配置无效 |
187
+
188
+ ---
189
+
190
+ ## Provider 模式
191
+
192
+ ### PaymentProvider 接口
193
+
194
+ ```typescript
195
+ interface PaymentProvider {
196
+ readonly name: string
197
+ createOrder: (input: CreateOrderInput) => Promise<HaiResult<PaymentOrder>>
198
+ handleNotify: (request: PaymentNotifyRequest) => Promise<HaiResult<PaymentNotifyResult>>
199
+ queryOrder: (outTradeNo: string) => Promise<HaiResult<OrderStatus>>
200
+ refund: (input: RefundInput) => Promise<HaiResult<RefundResult>>
201
+ closeOrder: (outTradeNo: string) => Promise<HaiResult<void>>
202
+ }
203
+ ```
204
+
205
+ 内置 Provider:
206
+
207
+ - **wechat** — 微信支付 API v3(RSA-SHA256 签名、AES-256-GCM 回调解密)
208
+ - **alipay** — 支付宝开放平台(RSA2 签名验签)
209
+ - **stripe** — Stripe Checkout Session 模式
210
+
211
+ ### API 契约
212
+
213
+ ```typescript
214
+ import { paymentEndpoints } from '@h-ai/payment/api'
215
+
216
+ // paymentEndpoints.createOrder — POST /payment/create
217
+ // paymentEndpoints.queryOrder — GET /payment/query
218
+ // paymentEndpoints.notifyWechat — POST /payment/notify/wechat
219
+ // paymentEndpoints.notifyAlipay — POST /payment/notify/alipay
220
+ // paymentEndpoints.notifyStripe — POST /payment/notify/stripe
221
+ // paymentEndpoints.refund — POST /payment/refund
222
+ ```
223
+
224
+ ---
225
+
226
+ ## 常见模式
227
+
228
+ ### 服务端路由(SvelteKit)
229
+
230
+ ```typescript
231
+ import { kit } from '@h-ai/kit'
232
+ // src/routes/api/v1/payment/create/+server.ts
233
+ import { payment } from '@h-ai/payment'
234
+
235
+ export const POST = kit.handler(async ({ request, locals }) => {
236
+ const body = await request.json()
237
+ const result = await payment.createOrder(body.provider, {
238
+ orderNo: body.orderNo,
239
+ amount: body.amount,
240
+ description: body.description,
241
+ tradeType: body.tradeType,
242
+ userId: body.userId,
243
+ notifyUrl: body.notifyUrl,
244
+ })
245
+
246
+ if (!result.success) {
247
+ return kit.response.internalError(result.error.message)
248
+ }
249
+
250
+ return kit.response.ok(result.data, locals.requestId)
251
+ })
252
+ ```
253
+
254
+ ### 完整支付流程
255
+
256
+ ```
257
+ 1. 客户端 → 服务端:创建订单(api.call(paymentEndpoints.createOrder, {...}))
258
+ 2. 服务端 → 支付平台:调用 Provider 创建预支付
259
+ 3. 服务端 → 客户端:返回 providerData(含调起参数)
260
+ 4. 客户端:invokePayment(providerData) 调起支付
261
+ 5. 支付平台 → 服务端:异步回调通知
262
+ 6. 服务端:handleNotify() 验签 + 更新订单状态
263
+ 7. 客户端:轮询或回调查询订单状态
264
+ ```
265
+
266
+ ---
267
+
268
+ ## 审计日志
269
+
270
+ 关键支付操作成功后自动写入审计日志(依赖 `@h-ai/audit`),无需额外配置。失败仅 warn,不影响支付流程。
271
+
272
+ | 操作 | `action` | `resource` | `resourceId` | `details` |
273
+ | --- | --- | --- | --- | --- |
274
+ | 创建订单 | `create_order` | `payment` | `orderNo` | `{ provider, amount, tradeType }` |
275
+ | 支付回调 | `payment_notify` | `payment` | `orderNo` | `{ provider, transactionId, status, amount }` |
276
+ | 退款 | `refund` | `payment` | `orderNo` | `{ provider, refundNo, amount }` |
277
+ | 关闭订单 | `close_order` | `payment` | `orderNo` | `{ provider }` |
278
+
279
+ > `queryOrder` 为只读操作,不写审计日志。
280
+
281
+ ---
282
+
283
+ ## 相关 Skills
284
+
285
+ - `hai-kit`:服务端 API 路由与契约处理
286
+ - `hai-api-client`:客户端契约调用
287
+ - `hai-reldb`:订单持久化存储
288
+ - `hai-audit`:审计日志(payment 内部自动调用)
289
+ - `hai-core`:HaiResult 类型、日志、配置
@@ -0,0 +1,325 @@
1
+ ---
2
+ name: hai-reach
3
+ description: 使用 @h-ai/reach 进行邮件、短信和 API 回调发送;当需求涉及用户触达、消息通知、验证码发送、多渠道模板管理时使用。
4
+ ---
5
+
6
+ # hai-reach
7
+
8
+ > `@h-ai/reach` 提供统一的用户触达接口,支持同时注册多个 Provider(SMTP 邮件、短信、API 回调),内置模板引擎与免打扰(DND)机制。
9
+
10
+ ---
11
+
12
+ ## 依赖
13
+
14
+ | 模块 | 用途 | 是否必需 | 初始化要求 |
15
+ | --- | --- | --- | --- |
16
+ | `@h-ai/reldb` | 数据库(发送日志与模板持久化) | 可选 | 已初始化时自动启用持久化 |
17
+ | `@h-ai/cache` | 缓存(DND delay 策略分布式锁) | 可选 | 已初始化时自动启用分布式锁 |
18
+
19
+ ---
20
+
21
+ ## 适用场景
22
+
23
+ - 同时使用邮件、短信、API 回调发送通知
24
+ - 发送邮件通知(注册欢迎、密码重置、告警等)
25
+ - 发送短信验证码或通知
26
+ - 通过 HTTP API 回调触发第三方通知
27
+ - 定义和管理消息模板(模板绑定到具体 Provider,支持配置文件定义)
28
+ - 免打扰时段控制
29
+ - 基于 `HaiReachError` 做错误分支处理
30
+
31
+ ---
32
+
33
+ ## 使用步骤
34
+
35
+ ### 1. 配置
36
+
37
+ ```yaml
38
+ # config/_reach.yml
39
+ providers:
40
+ - name: email
41
+ type: smtp
42
+ host: ${HAI_REACH_SMTP_HOST:smtp.example.com}
43
+ port: ${HAI_REACH_SMTP_PORT:465}
44
+ secure: true
45
+ user: ${HAI_REACH_SMTP_USER:}
46
+ pass: ${HAI_REACH_SMTP_PASS:}
47
+ from: ${HAI_REACH_SMTP_FROM:noreply@example.com}
48
+ - name: sms
49
+ type: aliyun-sms
50
+ accessKeyId: ${HAI_REACH_SMS_ACCESS_KEY:}
51
+ accessKeySecret: ${HAI_REACH_SMS_SECRET_KEY:}
52
+ signName: ${HAI_REACH_SMS_SIGN_NAME:}
53
+ - name: webhook
54
+ type: api
55
+ url: ${HAI_REACH_WEBHOOK_URL:}
56
+
57
+ # 模板(可选,也可通过代码注册)
58
+ templates:
59
+ - name: verification_code
60
+ provider: email
61
+ subject: '验证码: {code}'
62
+ body: '您的验证码是 {code},有效期 {minutes} 分钟。'
63
+ - name: sms_code
64
+ provider: sms
65
+ body: '验证码: {code},{minutes} 分钟内有效。'
66
+
67
+ # 免打扰(可选)
68
+ dnd:
69
+ enabled: true
70
+ strategy: delay # discard(丢弃)或 delay(延时,DND 结束后集中发送)
71
+ start: '22:00'
72
+ end: '08:00'
73
+ ```
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
+ ### 3. 保存模板(模板绑定 Provider)
94
+
95
+ ```typescript
96
+ // 通过代码保存(配置文件中的模板在 init 时自动注册)
97
+ await reach.template.save({
98
+ name: 'verification_code',
99
+ provider: 'email',
100
+ subject: '验证码: {code}',
101
+ body: '您的验证码是 {code},有效期 {minutes} 分钟。',
102
+ })
103
+
104
+ await reach.template.saveBatch([
105
+ { name: 'welcome', provider: 'email', subject: '欢迎 {userName}', body: '亲爱的 {userName},欢迎使用 {appName}!' },
106
+ { name: 'sms_code', provider: 'sms', body: '验证码: {code},{minutes} 分钟内有效。' },
107
+ ])
108
+ ```
109
+
110
+ ### 4. 发送消息
111
+
112
+ ```typescript
113
+ // 使用模板发送邮件(指定 provider)
114
+ const result = await reach.send({
115
+ provider: 'email',
116
+ to: 'user@example.com',
117
+ template: 'verification_code',
118
+ vars: { code: '123456', minutes: '5' },
119
+ })
120
+
121
+ // 直接发送邮件(无模板)
122
+ await reach.send({
123
+ provider: 'email',
124
+ to: 'user@example.com',
125
+ subject: '通知',
126
+ body: '<h1>Hello</h1>',
127
+ })
128
+
129
+ // 发送短信(通过 extra 传递 Provider 特有参数)
130
+ await reach.send({
131
+ provider: 'sms',
132
+ to: '13800138000',
133
+ extra: { templateCode: 'SMS_123456' },
134
+ vars: { code: '654321' },
135
+ })
136
+
137
+ // API 回调
138
+ await reach.send({
139
+ provider: 'webhook',
140
+ to: 'user@example.com',
141
+ body: '{"event":"signup"}',
142
+ })
143
+ ```
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
+ ## 错误码
199
+
200
+ | 错误码 | code | 说明 |
201
+ |--------|------|------|
202
+ | `HaiReachError.SEND_FAILED` | `hai:reach:001` | 发送失败 |
203
+ | `HaiReachError.TEMPLATE_NOT_FOUND` | `hai:reach:002` | 模板未找到 |
204
+ | `HaiReachError.TEMPLATE_RENDER_FAILED` | `hai:reach:003` | 模板渲染失败 |
205
+ | `HaiReachError.INVALID_RECIPIENT` | `hai:reach:004` | 无效接收方 |
206
+ | `HaiReachError.PROVIDER_NOT_FOUND` | `hai:reach:005` | Provider 未找到 |
207
+ | `HaiReachError.DND_BLOCKED` | `hai:reach:006` | 免打扰丢弃 |
208
+ | `HaiReachError.DND_DEFERRED` | `hai:reach:007` | 免打扰延时暂存 |
209
+ | `HaiReachError.NOT_INITIALIZED` | `hai:reach:010` | 模块未初始化 |
210
+ | `HaiReachError.UNSUPPORTED_TYPE` | `hai:reach:011` | 不支持的类型 |
211
+ | `HaiReachError.CONFIG_ERROR` | `hai:reach:012` | 配置错误 |
212
+
213
+ ---
214
+
215
+ ## 常见模式
216
+
217
+ ### 多渠道验证码发送
218
+
219
+ ```typescript
220
+ reach.template.save({
221
+ name: 'email_code',
222
+ provider: 'email',
223
+ subject: '验证码: {code}',
224
+ body: '您的验证码是 {code},有效期 {minutes} 分钟。',
225
+ })
226
+
227
+ reach.template.save({
228
+ name: 'sms_code',
229
+ provider: 'sms',
230
+ body: '验证码: {code},{minutes} 分钟内有效。',
231
+ })
232
+
233
+ // 根据用户选择的渠道发送
234
+ async function sendCode(channel: 'email' | 'sms', target: string, code: string) {
235
+ const template = channel === 'email' ? 'email_code' : 'sms_code'
236
+ return reach.send({
237
+ provider: channel,
238
+ to: target,
239
+ template,
240
+ vars: { code, minutes: '5' },
241
+ })
242
+ }
243
+ ```
244
+
245
+ ### 与 IAM 集成(密码重置 / OTP 验证码)
246
+
247
+ ```typescript
248
+ import { iam } from '@h-ai/iam'
249
+ import { reach } from '@h-ai/reach'
250
+
251
+ // 初始化 reach(注册 email 和 sms Provider)
252
+ await reach.init({
253
+ providers: [
254
+ { name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
255
+ { name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '...' },
256
+ ],
257
+ templates: [
258
+ { name: 'password_reset', provider: 'email', subject: 'Password Reset', body: 'Token: {token}, expires: {expiresAt}' },
259
+ { name: 'otp_email', provider: 'email', subject: 'Code: {code}', body: 'Your code is {code}' },
260
+ { name: 'otp_sms', provider: 'sms', body: 'Your code is {code}' },
261
+ ],
262
+ })
263
+
264
+ // 初始化 IAM,使用 reach 发送通知
265
+ await iam.init({
266
+ db,
267
+ cache,
268
+ onPasswordResetRequest: async (user, token, expiresAt) => {
269
+ await reach.send({ provider: 'email', to: user.email ?? '', template: 'password_reset', vars: { token, expiresAt: expiresAt.toISOString() } })
270
+ },
271
+ onOtpSendEmail: async (email, code) => {
272
+ await reach.send({ provider: 'email', to: email, template: 'otp_email', vars: { code } })
273
+ },
274
+ onOtpSendSms: async (phone, code) => {
275
+ await reach.send({ provider: 'sms', to: phone, template: 'otp_sms', vars: { code } })
276
+ },
277
+ })
278
+ ```
279
+
280
+ ### 错误处理
281
+
282
+ ```typescript
283
+ import { HaiReachError } from '@h-ai/reach'
284
+
285
+ const result = await reach.send(message)
286
+ if (!result.success) {
287
+ switch (result.error.code) {
288
+ case HaiReachError.NOT_INITIALIZED.code:
289
+ break
290
+ case HaiReachError.PROVIDER_NOT_FOUND.code:
291
+ break
292
+ case HaiReachError.DND_BLOCKED.code:
293
+ // 免打扰时段(discard 策略),消息已丢弃
294
+ break
295
+ case HaiReachError.TEMPLATE_NOT_FOUND.code:
296
+ break
297
+ case HaiReachError.SEND_FAILED.code:
298
+ break
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### 分布式锁(DND flush 保护)
304
+
305
+ > 多节点部署时,DND(delay 策略)的 pending 消息 flush 通过 `@h-ai/cache` 分布式锁保护,确保同一时刻只有一个节点执行 flush。
306
+
307
+ ```typescript
308
+ import { cache } from '@h-ai/cache'
309
+
310
+ // 初始化 cache 后,reach 自动使用分布式锁保护 flush 操作
311
+ await cache.init({ type: 'redis', host: 'localhost', port: 6379 })
312
+ await reach.init({ /* ... */ })
313
+ ```
314
+
315
+ - 锁键:`reach:flush-pending`,TTL 60 秒
316
+ - 若 cache 未初始化,分布式锁自动禁用,不影响单节点运行
317
+ - 使用稳定的进程级 owner 标识,防止误释放他人锁
318
+
319
+ ---
320
+
321
+ ## 相关 Skills
322
+
323
+ - `hai-core` — 配置加载、日志、HaiResult 模式
324
+ - `hai-reldb` — 数据库操作(存储发送记录等)
325
+ - `hai-cache` — 缓存操作、分布式锁(DND flush 互斥)