@h-ai/reach 0.1.0-alpha.55 → 0.1.0-alpha.57

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 ADDED
@@ -0,0 +1,175 @@
1
+ # @h-ai/reach
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [Framework home](../../README.en.md) · Related modules: [@h-ai/cache](../cache/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/reldb](../reldb/README.en.md)
6
+
7
+ A notification module supporting multiple providers (email, SMS, API callbacks) at once. Templates bind to providers for scenario-based routing. Includes do-not-disturb (DND) policies and configuration-defined templates.
8
+
9
+ ## Dependencies
10
+
11
+ - `@h-ai/reldb`: optional persistence for delivery logs and templates; enabled automatically when initialized.
12
+ - `@h-ai/cache`: optional distributed locks for DND delay; enabled automatically when initialized.
13
+
14
+ ## Supported providers
15
+
16
+ | Provider | Channel | Description |
17
+ | ------------ | ------- | -------------------------------------------------- |
18
+ | `console` | General | Console output (development/tests) |
19
+ | `smtp` | Email | SMTP email (requires nodemailer) |
20
+ | `aliyun-sms` | SMS | Alibaba Cloud SMS through HTTP API, without an SDK |
21
+ | `api` | General | HTTP API callback (generic webhook) |
22
+
23
+ ## Quick start
24
+
25
+ ### Installation
26
+
27
+ ```bash
28
+ pnpm add @h-ai/reach
29
+ ```
30
+
31
+ Install `nodemailer` for email delivery.
32
+
33
+ ### Initialize multiple providers
34
+
35
+ ```ts
36
+ import { cache } from '@h-ai/cache'
37
+ import { reach } from '@h-ai/reach'
38
+ import { reldb } from '@h-ai/reldb'
39
+
40
+ // 1. 初始化可选依赖(按需)
41
+ await reldb.init({ type: 'sqlite', database: './data.db' }) // 可选,启用发送日志与模板持久化
42
+ await cache.init({ type: 'memory' }) // 可选,启用 DND delay 策略分布式锁
43
+
44
+ // 2. 初始化触达模块(自动检测已初始化的 reldb/cache 单例)
45
+ await reach.init({
46
+ providers: [
47
+ { name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
48
+ { name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '某某科技' },
49
+ { name: 'webhook', type: 'api', url: 'https://api.example.com/notify' },
50
+ ],
51
+ templates: [
52
+ { name: 'welcome_email', provider: 'email', subject: '欢迎 {userName}', body: '亲爱的 {userName},欢迎!' },
53
+ { name: 'sms_code', provider: 'sms', body: '验证码: {code},{minutes} 分钟内有效。' },
54
+ ],
55
+ dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' },
56
+ })
57
+
58
+ // 通过 provider 字段选择发送渠道
59
+ await reach.send({
60
+ provider: 'email',
61
+ to: 'user@example.com',
62
+ template: 'welcome_email',
63
+ vars: { userName: '张三' },
64
+ })
65
+
66
+ // 发送短信(通过 extra 传递 Provider 特有参数)
67
+ await reach.send({
68
+ provider: 'sms',
69
+ to: '13800138000',
70
+ extra: { templateCode: 'SMS_123456' },
71
+ vars: { code: '123456' },
72
+ })
73
+
74
+ // 关闭所有 Provider
75
+ await reach.close()
76
+ ```
77
+
78
+ `reach.config` returns a **redacted configuration snapshot**. Embedded credentials in `api.url` / `endpoint`, `headers.Authorization`, SMTP `pass`, SMS keys, and other secrets are hidden automatically.
79
+
80
+ ## Configuration
81
+
82
+ ### Console (development/tests)
83
+
84
+ ```ts
85
+ const config = { name: 'dev', type: 'console' }
86
+ ```
87
+
88
+ ### SMTP
89
+
90
+ | Field | Type | Required | Default | Description |
91
+ | -------- | --------- | -------- | ------- | ----------------------- |
92
+ | `name` | `string` | ✅ | — | Provider name |
93
+ | `type` | `string` | ✅ | — | Fixed `'smtp'` |
94
+ | `host` | `string` | ✅ | — | SMTP server address |
95
+ | `port` | `number` | — | `465` | SMTP port |
96
+ | `secure` | `boolean` | — | `true` | Whether to use TLS |
97
+ | `user` | `string` | — | — | Authentication username |
98
+ | `pass` | `string` | — | — | Authentication password |
99
+ | `from` | `string` | ✅ | — | Sender address |
100
+
101
+ ### Alibaba Cloud SMS
102
+
103
+ | Field | Type | Required | Default | Description |
104
+ | ----------------- | -------- | -------- | ----------------------- | -------------------- |
105
+ | `name` | `string` | ✅ | — | Provider name |
106
+ | `type` | `string` | ✅ | — | Fixed `'aliyun-sms'` |
107
+ | `accessKeyId` | `string` | ✅ | — | AccessKey ID |
108
+ | `accessKeySecret` | `string` | ✅ | — | AccessKey Secret |
109
+ | `signName` | `string` | ✅ | — | SMS signature |
110
+ | `endpoint` | `string` | — | `dysmsapi.aliyuncs.com` | API endpoint |
111
+
112
+ ### API callbacks
113
+
114
+ | Field | Type | Required | Default | Description |
115
+ | --------- | ------------------------ | -------- | ------- | ----------------------- |
116
+ | `name` | `string` | ✅ | — | Provider name |
117
+ | `type` | `string` | ✅ | — | Fixed `'api'` |
118
+ | `url` | `string` | ✅ | — | HTTP(S) callback URL |
119
+ | `method` | `'POST' \| 'PUT'` | — | `POST` | HTTP method |
120
+ | `headers` | `Record<string, string>` | — | — | Custom request headers |
121
+ | `timeout` | `number` | — | `10000` | Timeout in milliseconds |
122
+
123
+ API callback addresses are trusted server configuration. Logs contain only the callback origin, not URL credentials, query parameters, recipients, or third-party failure bodies. For tenant/user-supplied addresses, applications must enforce a domain allowlist; enterprise deployments should also block internal metadata access through egress proxies or network policies.
124
+
125
+ ### DND (do not disturb)
126
+
127
+ | Field | Type | Required | Default | Description |
128
+ | ---------- | ---------------------- | -------- | --------- | ------------------------------------- |
129
+ | `enabled` | `boolean` | — | `false` | Enable DND |
130
+ | `strategy` | `'discard' \| 'delay'` | — | `discard` | discard: drop / delay: send after DND |
131
+ | `start` | `string` | — | `00:00` | Start time (HH:mm) |
132
+ | `end` | `string` | — | `00:00` | End time (HH:mm) |
133
+
134
+ ## Error handling
135
+
136
+ All operations return `HaiResult<T>`. Error codes are defined in `HaiReachError`:
137
+
138
+ | Error code | code | Description |
139
+ | -------------------------------------- | --------------- | ------------------------- |
140
+ | `HaiReachError.SEND_FAILED` | `hai:reach:001` | Delivery failed |
141
+ | `HaiReachError.TEMPLATE_NOT_FOUND` | `hai:reach:002` | Template not found |
142
+ | `HaiReachError.TEMPLATE_RENDER_FAILED` | `hai:reach:003` | Template rendering failed |
143
+ | `HaiReachError.INVALID_RECIPIENT` | `hai:reach:004` | Invalid recipient |
144
+ | `HaiReachError.PROVIDER_NOT_FOUND` | `hai:reach:005` | Provider not registered |
145
+ | `HaiReachError.DND_BLOCKED` | `hai:reach:006` | Dropped by DND |
146
+ | `HaiReachError.DND_DEFERRED` | `hai:reach:007` | Queued for DND delay |
147
+ | `HaiReachError.NOT_INITIALIZED` | `hai:reach:010` | Not initialized |
148
+ | `HaiReachError.UNSUPPORTED_TYPE` | `hai:reach:011` | Unsupported type |
149
+ | `HaiReachError.CONFIG_ERROR` | `hai:reach:012` | Invalid configuration |
150
+
151
+ ```ts
152
+ const result = await reach.send({ provider: 'email', to: 'user@example.com', body: 'hello' })
153
+ if (!result.success) {
154
+ // result.error.code / result.error.message
155
+ }
156
+ ```
157
+
158
+ ## Distributed locks
159
+
160
+ Across nodes, flushing pending DND-delay messages is protected by `@h-ai/cache` distributed locks, allowing only one node to flush at a time.
161
+
162
+ - Uses `cache.lock.acquire('reach:flush-pending')` with a 300-second TTL.
163
+ - Claims pending records in batches. In-progress records expire, allowing another node to take over after failure and reducing duplicate delivery risk.
164
+ - Without initialized `@h-ai/cache`, distributed locking is disabled automatically; single-node operation continues.
165
+ - A stable process-level owner identifier prevents releasing another owner's lock.
166
+
167
+ ## Tests
168
+
169
+ ```bash
170
+ pnpm --filter @h-ai/reach test
171
+ ```
172
+
173
+ ## License
174
+
175
+ Apache-2.0
package/README.ja.md ADDED
@@ -0,0 +1,175 @@
1
+ # @h-ai/reach
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/cache](../cache/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md)
6
+
7
+ メール・SMS・API 通知など複数の Provider を同時登録できる通知モジュールです。テンプレートと Provider の対応で用途に応じて配信します。通知抑制(DND)と設定ファイルによるテンプレート定義を備えます。
8
+
9
+ ## 依存関係
10
+
11
+ - `@h-ai/reldb`:送信ログ・テンプレートの永続化(任意)。初期化済みなら自動有効化。
12
+ - `@h-ai/cache`:DND delay 用の分散ロック(任意)。初期化済みなら自動有効化。
13
+
14
+ ## 対応 Provider
15
+
16
+ | Provider | チャネル | 説明 |
17
+ | ------------ | -------- | -------------------------------------------------- |
18
+ | `console` | 汎用 | コンソール出力(開発・テスト用) |
19
+ | `smtp` | メール | SMTP メール(nodemailer が必要) |
20
+ | `aliyun-sms` | SMS | Alibaba Cloud SMS(HTTP API を直接使用、SDK 不要) |
21
+ | `api` | 汎用 | HTTP API 通知(汎用 webhook) |
22
+
23
+ ## クイックスタート
24
+
25
+ ### インストール
26
+
27
+ ```bash
28
+ pnpm add @h-ai/reach
29
+ ```
30
+
31
+ メール送信には `nodemailer` をインストールしてください。
32
+
33
+ ### 複数 Provider の初期化
34
+
35
+ ```ts
36
+ import { cache } from '@h-ai/cache'
37
+ import { reach } from '@h-ai/reach'
38
+ import { reldb } from '@h-ai/reldb'
39
+
40
+ // 1. 初始化可选依赖(按需)
41
+ await reldb.init({ type: 'sqlite', database: './data.db' }) // 可选,启用发送日志与模板持久化
42
+ await cache.init({ type: 'memory' }) // 可选,启用 DND delay 策略分布式锁
43
+
44
+ // 2. 初始化触达模块(自动检测已初始化的 reldb/cache 单例)
45
+ await reach.init({
46
+ providers: [
47
+ { name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
48
+ { name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '某某科技' },
49
+ { name: 'webhook', type: 'api', url: 'https://api.example.com/notify' },
50
+ ],
51
+ templates: [
52
+ { name: 'welcome_email', provider: 'email', subject: '欢迎 {userName}', body: '亲爱的 {userName},欢迎!' },
53
+ { name: 'sms_code', provider: 'sms', body: '验证码: {code},{minutes} 分钟内有效。' },
54
+ ],
55
+ dnd: { enabled: true, strategy: 'delay', start: '22:00', end: '08:00' },
56
+ })
57
+
58
+ // 通过 provider 字段选择发送渠道
59
+ await reach.send({
60
+ provider: 'email',
61
+ to: 'user@example.com',
62
+ template: 'welcome_email',
63
+ vars: { userName: '张三' },
64
+ })
65
+
66
+ // 发送短信(通过 extra 传递 Provider 特有参数)
67
+ await reach.send({
68
+ provider: 'sms',
69
+ to: '13800138000',
70
+ extra: { templateCode: 'SMS_123456' },
71
+ vars: { code: '123456' },
72
+ })
73
+
74
+ // 关闭所有 Provider
75
+ await reach.close()
76
+ ```
77
+
78
+ `reach.config` は**機密情報をマスクした設定スナップショット**を返します。`api.url` / `endpoint` 内の認証情報、`headers.Authorization`、SMTP の `pass`、SMS の鍵などは自動的に非表示になります。
79
+
80
+ ## 設定
81
+
82
+ ### Console(開発・テスト用)
83
+
84
+ ```ts
85
+ const config = { name: 'dev', type: 'console' }
86
+ ```
87
+
88
+ ### SMTP
89
+
90
+ | フィールド | 型 | 必須 | 既定値 | 説明 |
91
+ | ---------- | --------- | ---- | ------ | ----------------------- |
92
+ | `name` | `string` | ✅ | — | Provider 名 |
93
+ | `type` | `string` | ✅ | — | 固定値 `'smtp'` |
94
+ | `host` | `string` | ✅ | — | SMTP サーバーのアドレス |
95
+ | `port` | `number` | — | `465` | SMTP ポート |
96
+ | `secure` | `boolean` | — | `true` | TLS を使用するか |
97
+ | `user` | `string` | — | — | 認証ユーザー名 |
98
+ | `pass` | `string` | — | — | 認証パスワード |
99
+ | `from` | `string` | ✅ | — | 送信元アドレス |
100
+
101
+ ### Alibaba Cloud SMS
102
+
103
+ | フィールド | 型 | 必須 | 既定値 | 説明 |
104
+ | ----------------- | -------- | ---- | ----------------------- | --------------------- |
105
+ | `name` | `string` | ✅ | — | Provider 名 |
106
+ | `type` | `string` | ✅ | — | 固定値 `'aliyun-sms'` |
107
+ | `accessKeyId` | `string` | ✅ | — | AccessKey ID |
108
+ | `accessKeySecret` | `string` | ✅ | — | AccessKey Secret |
109
+ | `signName` | `string` | ✅ | — | SMS 署名 |
110
+ | `endpoint` | `string` | — | `dysmsapi.aliyuncs.com` | API endpoint |
111
+
112
+ ### API 通知
113
+
114
+ | フィールド | 型 | 必須 | 既定値 | 説明 |
115
+ | ---------- | ------------------------ | ---- | ------- | ------------------------ |
116
+ | `name` | `string` | ✅ | — | Provider 名 |
117
+ | `type` | `string` | ✅ | — | 固定値 `'api'` |
118
+ | `url` | `string` | ✅ | — | HTTP(S) 通知 URL |
119
+ | `method` | `'POST' \| 'PUT'` | — | `POST` | HTTP メソッド |
120
+ | `headers` | `Record<string, string>` | — | — | 独自のリクエストヘッダー |
121
+ | `timeout` | `number` | — | `10000` | タイムアウト(ミリ秒) |
122
+
123
+ API 通知先は信頼できるサーバー設定です。ログには origin のみを記録し、URL 認証情報、クエリ、宛先、外部の失敗応答本文は残しません。テナント・ユーザー入力の URL にはアプリでドメインの allowlist を設け、企業環境では出口プロキシやネットワーク方針でも内部メタデータへの接続を防いでください。
124
+
125
+ ### DND(通知抑制)
126
+
127
+ | フィールド | 型 | 必須 | 既定値 | 説明 |
128
+ | ---------- | ---------------------- | ---- | --------- | --------------------------------------- |
129
+ | `enabled` | `boolean` | — | `false` | 有効化するか |
130
+ | `strategy` | `'discard' \| 'delay'` | — | `discard` | discard:破棄 / delay:DND 終了後に送信 |
131
+ | `start` | `string` | — | `00:00` | 開始時刻(HH:mm) |
132
+ | `end` | `string` | — | `00:00` | 終了時刻(HH:mm) |
133
+
134
+ ## エラー処理
135
+
136
+ すべての操作は `HaiResult<T>` を返します。エラーコードは `HaiReachError` で定義します:
137
+
138
+ | エラーコード | code | 説明 |
139
+ | -------------------------------------- | --------------- | ------------------------ |
140
+ | `HaiReachError.SEND_FAILED` | `hai:reach:001` | 送信に失敗 |
141
+ | `HaiReachError.TEMPLATE_NOT_FOUND` | `hai:reach:002` | テンプレートが存在しない |
142
+ | `HaiReachError.TEMPLATE_RENDER_FAILED` | `hai:reach:003` | テンプレート描画に失敗 |
143
+ | `HaiReachError.INVALID_RECIPIENT` | `hai:reach:004` | 無効な宛先 |
144
+ | `HaiReachError.PROVIDER_NOT_FOUND` | `hai:reach:005` | Provider が未登録 |
145
+ | `HaiReachError.DND_BLOCKED` | `hai:reach:006` | DND により破棄 |
146
+ | `HaiReachError.DND_DEFERRED` | `hai:reach:007` | DND により一時保留 |
147
+ | `HaiReachError.NOT_INITIALIZED` | `hai:reach:010` | 未初期化 |
148
+ | `HaiReachError.UNSUPPORTED_TYPE` | `hai:reach:011` | 未対応の種類 |
149
+ | `HaiReachError.CONFIG_ERROR` | `hai:reach:012` | 設定エラー |
150
+
151
+ ```ts
152
+ const result = await reach.send({ provider: 'email', to: 'user@example.com', body: 'hello' })
153
+ if (!result.success) {
154
+ // result.error.code / result.error.message
155
+ }
156
+ ```
157
+
158
+ ## 分散ロック
159
+
160
+ 複数ノードでは DND delay の保留メッセージの flush を `@h-ai/cache` の分散ロックで保護し、同時に 1 ノードのみが実行します。
161
+
162
+ - `cache.lock.acquire('reach:flush-pending')` を使用し、TTL は 300 秒。
163
+ - 保留レコードを一括で claim します。処理中レコードには期限があり、ノード異常終了後にほかのノードが引き継いで重複送信のリスクを抑えます。
164
+ - `@h-ai/cache` が未初期化なら分散ロックは自動無効化し、単一ノードで動作します。
165
+ - 安定したプロセス単位の owner 識別子で、ほかの所有者のロックの誤解除を防ぎます。
166
+
167
+ ## テスト
168
+
169
+ ```bash
170
+ pnpm --filter @h-ai/reach test
171
+ ```
172
+
173
+ ## License
174
+
175
+ Apache-2.0
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @h-ai/reach
2
2
 
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [框架首页](../../README.md) · 相关模块:[@h-ai/cache](../cache/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/reldb](../reldb/README.md)
6
+
3
7
  用户触达模块,支持同时注册多个 Provider(邮件、短信、API 回调等),通过模板绑定 Provider 实现按场景路由发送。内置免打扰(DND)机制,支持通过配置文件定义模板。
4
8
 
5
9
  ## 依赖
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/reach",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.55",
4
+ "version": "0.1.0-alpha.57",
5
5
  "description": "Hai Framework user reach module for email, SMS, and webhook channels.",
6
6
  "license": "Apache-2.0",
7
7
  "exports": {
@@ -24,9 +24,9 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "zod": "^4.4.3",
27
- "@h-ai/cache": "0.1.0-alpha.55",
28
- "@h-ai/core": "0.1.0-alpha.55",
29
- "@h-ai/reldb": "0.1.0-alpha.55"
27
+ "@h-ai/core": "0.1.0-alpha.57",
28
+ "@h-ai/cache": "0.1.0-alpha.57",
29
+ "@h-ai/reldb": "0.1.0-alpha.57"
30
30
  },
31
31
  "optionalDependencies": {
32
32
  "nodemailer": "^8.0.1"