@h-ai/core 1.0.0-beta.1 → 1.0.0-beta.11
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 +640 -473
- package/README.ja.md +639 -472
- package/README.md +640 -473
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-BEGTB6GL.js → chunk-MYNB4HIZ.js} +42 -19
- package/dist/chunk-MYNB4HIZ.js.map +1 -0
- package/dist/{core-types-_JqVAGGT.d.ts → core-types-DM_AiIaS.d.ts} +154 -11
- package/dist/node.d.ts +2 -2
- package/dist/node.js +325 -99
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BEGTB6GL.js.map +0 -1
package/README.ja.md
CHANGED
|
@@ -6,647 +6,814 @@
|
|
|
6
6
|
|
|
7
7
|
## 概要
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`@h-ai/core` は Hai Framework の共通基盤です。アプリケーションと他のモジュールに、設定、エラー、ログ、国際化の共通機能を提供します。ツールはそのまま利用でき、Node アプリでファイル設定が必要な場合に `core.init()` を呼びます。
|
|
10
10
|
|
|
11
11
|
## 主な機能
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
| 機能 | 用途 | 主な入口 |
|
|
14
|
+
| ---------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
15
|
+
| 設定と起動 | YAML の読み込み、環境変数の上書き、設定検証、依存順に沿ったモジュールの起動と終了 | `core.config`、`core.init`、`core.module.createRuntime` |
|
|
16
|
+
| 結果とエラー | 業務処理の成否、エラーコード、API 境界で公開する安全なメッセージ | `HaiResult`、`ok` / `err`、`core.error` |
|
|
17
|
+
| ログと診断 | 構造化ログ、リクエスト関連付け、認証情報のマスキング、ログ監視 | `core.logger`、`core.context`、`core.sanitize` |
|
|
18
|
+
| 国際化と検証メッセージ | メッセージ言語、並行リクエストの言語分離、Zod フィールドエラーの翻訳 | `core.i18n`、`core.zodValidation` |
|
|
19
|
+
| リクエストアクセス制御 | 信頼できるクライアント IP の解決と、パス・メソッド・IP・時間帯の検査 | `core.access`(Node 専用) |
|
|
20
|
+
| 共通ツール | ID、型確認、オブジェクト、文字列、配列、非同期処理、日時操作、未初期化時の共通エラー応答 | `core.id`、`core.typeUtils`、`core.object` など |
|
|
21
|
+
|
|
22
|
+
### Node とブラウザーの境界
|
|
23
|
+
|
|
24
|
+
両環境とも `import { core } from '@h-ai/core'` を使い、ビルドツールが対象環境のコードを選びます。
|
|
25
|
+
|
|
26
|
+
| 機能 | Node.js | ブラウザー |
|
|
27
|
+
| --------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
28
|
+
| ログ、結果/エラー、メッセージ、共通ツール、モジュールの順次起動と終了 | 対応 | 対応 |
|
|
29
|
+
| YAML、dotenv、環境変数マッピング、ファイル監視 | ローカル設定ファイルを読み込み、監視できる | ローカルファイルを読み込み・監視できず、読み込みメソッドは失敗を返す |
|
|
30
|
+
| `core.context.run` による関連付け | 非同期処理の待機後も各リクエストの ID を保持し、他のリクエストと混在しない | コールバックの同期処理中だけ保持し、`await` 後には保持しない |
|
|
31
|
+
| `core.i18n.runWithLocale` によるリクエスト言語 | 非同期処理の待機後も選んだ言語を保持し、他のリクエストに変更されない | 例外となる。ページ言語は `setGlobalLocale()`、またはメッセージ取得時の locale で指定する |
|
|
32
|
+
| `core.access` によるアクセス制御 | リクエストのアドレス、パス、時間帯を検査できる | このツールは利用不可 |
|
|
33
|
+
|
|
34
|
+
`core.init()` は Core の設定とログを同期的に準備し、`void` を返します。Core に `close()` メソッドはありません。データベースやキャッシュなども使うアプリは `core.module.createRuntime()` でまとめて管理できます。返されたオブジェクトの `start()` で順に初期化し、`close()` で逆順に解放します。下の「アプリケーションの起動と終了」に例を示します。
|
|
22
35
|
|
|
23
36
|
## 使い始める
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
以下では Core の全機能について用途、例、制約を説明します。設定を読み込む場合は初期化から確認してください。ログ、メッセージ、汎用ツールは直接利用できます。
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
実行機能は `core.xxx()` または `core.<子機能>.xxx()` で呼びます。結果構築関数 `ok` / `err`、型、Schema、エラー定数はパッケージの入口から個別に import します。使い方は該当節で説明します。
|
|
28
41
|
|
|
29
|
-
```
|
|
30
|
-
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @h-ai/core
|
|
44
|
+
```
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
core.init({ configDir: './config', watchConfig: true })
|
|
46
|
+
独自設定やフォーム検証の例では Zod も使用するため、実行前に `pnpm add zod` でインストールしてください。データベース・キャッシュの追加依存は該当節で説明します。
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
core.logger.info('Server started', { port: 3000 })
|
|
37
|
-
core.logger.warn('Slow query', { duration: 2500 })
|
|
38
|
-
core.logger.error('Connection failed', { error })
|
|
48
|
+
### 初期化:`core.init`
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
const dbLogger = core.logger.create({ name: 'db' })
|
|
42
|
-
dbLogger.info('Connected to PostgreSQL')
|
|
50
|
+
Node 起動時に YAML を読み込み、ログを設定します。ブラウザーではログのみを設定します。Core の初期化ではデータベースなどの他モジュールは起動しません。
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
46
|
-
reqLogger.info('Request started') // 日志自动包含 requestId + userId
|
|
47
|
-
reqLogger.error('Request failed') // 同上
|
|
52
|
+
#### Node.js
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
const opLogger = reqLogger.child({ operation: 'createOrder' })
|
|
51
|
-
opLogger.info('Order created') // 包含 requestId + userId + operation
|
|
54
|
+
`config/_core.yml` を用意します。
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
```yaml
|
|
57
|
+
# 应用元信息,供启动代码和页面读取。
|
|
58
|
+
name: orders
|
|
59
|
+
version: 0.1.0
|
|
60
|
+
env: development
|
|
61
|
+
# 默认消息语言;启动代码读取后调用 setGlobalLocale 应用。
|
|
62
|
+
defaultLocale: zh-CN
|
|
63
|
+
logging:
|
|
64
|
+
level: info # 输出 info 及以上级别。
|
|
65
|
+
format: pretty # 本地开发使用便于阅读的日志。
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { core, CoreConfigSchema } from '@h-ai/core'
|
|
70
|
+
|
|
71
|
+
// 1. 读取 config 目录中的 YAML,_core.yml 对应名称 core。
|
|
72
|
+
core.init({ configDir: './config' }) // 返回 undefined;配置缓存中可读取 core。
|
|
73
|
+
|
|
74
|
+
// 2. 检查应用必需的配置;失败时停止启动,保留原始错误供排查。
|
|
75
|
+
const config = core.config.validate('core', CoreConfigSchema)
|
|
76
|
+
if (!config.success)
|
|
77
|
+
throw new Error(config.error.message, { cause: config.error })
|
|
60
78
|
|
|
61
|
-
//
|
|
62
|
-
const nanoid = core.id.generate()
|
|
63
|
-
const shortId = core.id.short()
|
|
64
|
-
const uuid = core.id.uuid()
|
|
65
|
-
const prefixed = core.id.withPrefix('order_')
|
|
79
|
+
// 成功时 config.success 为 true;config.data.name 为 'orders',defaultLocale 为 'zh-CN'。
|
|
66
80
|
|
|
67
|
-
//
|
|
68
|
-
core.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
core.
|
|
72
|
-
core.array.chunk([1, 2, 3, 4, 5], 2) // [[1,2], [3,4], [5]]
|
|
73
|
-
await core.async.delay(1000)
|
|
74
|
-
await core.async.retry(() => fetchData(), { maxRetries: 3, delay: 1000 })
|
|
75
|
-
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
76
|
-
core.typeUtils.isDefined(value)
|
|
81
|
+
// 3. 将配置中的语言应用到默认消息;读取 YAML 本身不会切换语言。
|
|
82
|
+
core.i18n.setGlobalLocale(config.data.defaultLocale) // 返回 undefined;后续默认消息使用中文。
|
|
83
|
+
|
|
84
|
+
// 4. 记录已验证的应用名称,确认启动完成。
|
|
85
|
+
core.logger.info('Application ready', { name: config.data.name }) // 返回 undefined;info 日志包含 name: 'orders'。
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
必須設定を取得できない場合、起動境界で停止します。`core.init()` は個々の読み込み失敗をログに記録して続行するため、完了してもすべての設定が有効とは限りません。ディレクトリー走査やログ設定では例外が伝播する場合があります。
|
|
89
|
+
|
|
90
|
+
#### ブラウザー
|
|
80
91
|
|
|
81
|
-
```
|
|
92
|
+
```ts
|
|
82
93
|
import { core } from '@h-ai/core'
|
|
83
94
|
|
|
84
|
-
//
|
|
85
|
-
core.init({ logging: { level: 'warn' } })
|
|
95
|
+
// 浏览器不读取 YAML;此处只设置日志,输出 warn 及以上级别。
|
|
96
|
+
core.init({ logging: { level: 'warn' } }) // 返回 undefined;之后 info 日志被过滤。
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
// 设置当前页面使用的默认消息语言。
|
|
99
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;页面默认消息使用中文。
|
|
89
100
|
|
|
90
|
-
//
|
|
91
|
-
core.
|
|
92
|
-
core.
|
|
101
|
+
// 给一次请求生成编号,后续日志可用同一编号关联排查。
|
|
102
|
+
const requestId = core.id.request() // 返回随机字符串,如 'req-V1StGXR8_Z5jdHi6B-myT'。
|
|
103
|
+
core.logger.warn('Request needs attention', { requestId }) // 返回 undefined;浏览器输出 warn 消息及同一个 requestId。
|
|
93
104
|
```
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
ホストは公開可能な設定項目だけを渡し、サーバー設定全体や秘密情報は送りません。ブラウザーの `core.config.get()` / `has()` / `keys()` は `undefined` / `false` / `[]` を返します。`loadEnvFiles()`、`load()`、`validate()`、`reload()` は `SERVICE_UNAVAILABLE` を返し、`getOrThrow()` は例外を送出します。`watch()` は同じ利用不可エラーを即座に通知し、何もしない解除関数を返します。
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
- `loadEnvFiles()` / `load()` / `validate()` / `reload()` は `HaiCommonError.SERVICE_UNAVAILABLE` を返します。
|
|
99
|
-
- `watch()` は同じエラーで直ちにコールバックを呼び、安全な no-op の監視解除関数を返します。
|
|
100
|
-
- 設定が必要な場合はサーバーで読み込み API で渡すか、ブラウザーでは `core.init({ logging })` でログのみ設定してください。
|
|
108
|
+
### ファイルと環境設定:`core.config`
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
YAML の読み込み、環境変数による上書き、型の検証、設定キャッシュとファイル監視を管理します。以下のファイル・環境操作は Node 専用です。
|
|
103
111
|
|
|
104
|
-
####
|
|
112
|
+
#### 読み込み・検証・取得
|
|
105
113
|
|
|
106
|
-
`core.
|
|
114
|
+
`core.init({ configDir, configSchemas?, watchConfig? })` は指定ディレクトリー直下の `.yml` / `.yaml` を走査します。`_core.yml` は `core`、`_db.yml` は `db`、`app.yml` は `app` として登録されます。ビルド・デプロイ設定は CLI がルートの `hai.json` から読み、実行時 YAML には含まれません。
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
`_core.yml` は `CoreConfigSchema` で検証し、`name`、`version`、`env`、`defaultLocale`、`logging` だけを受け付けます。メタデータの既定値は順に `hai Admin`、`0.1.0`、`development`、`zh-CN` で、`logging` は省略可能です。メタデータは `NODE_ENV` の変更、メッセージ言語の切り替え、業務翻訳の登録を行いません。通常はここでログを設定します。明示的な `core.init({ logging })` は、その初期化における YAML ログ設定の適用を上書きします。
|
|
109
117
|
|
|
110
|
-
|
|
118
|
+
その他のファイルでは、ホストが渡す `configSchemas` を構造と型のヒントとして使います。完全な業務検証はモジュール初期化または明示的な `validate()` で行います。`load(name, file, schema)` は即座に検証します。
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
以下のファイル例では、この `config/app.yml` を使い、環境変数による追加の上書きがないものとします。
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# config/app.yml
|
|
124
|
+
port: 3000
|
|
116
125
|
```
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
load() はファイルを読み、任意で即時検証します。validate() はキャッシュを検証します。get() は未登録時に undefined、getOrThrow() は例外です。型引数は TypeScript の補助であり、実データを検証しません。has()/keys() で登録を確認できます。
|
|
119
128
|
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
```ts
|
|
130
|
+
import { core } from '@h-ai/core'
|
|
131
|
+
import { z } from 'zod'
|
|
132
|
+
|
|
133
|
+
// 读取上方 config/app.yml;Schema 同时描述类型和校验规则。
|
|
134
|
+
const AppSchema = z.object({ port: z.number().int().min(1).max(65535) })
|
|
135
|
+
const loaded = core.config.load('app', './config/app.yml', AppSchema)
|
|
136
|
+
if (!loaded.success)
|
|
137
|
+
throw new Error(loaded.error.message, { cause: loaded.error })
|
|
125
138
|
|
|
126
|
-
|
|
139
|
+
// loaded:{ success: true, data: { port: 3000 } }。
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
reqLogger.warn('Slow response') // 同上
|
|
141
|
+
// validate 用于重新校验缓存;加载时未传 Schema 的配置也可在这里校验。
|
|
142
|
+
const validated = core.config.validate('app', AppSchema)
|
|
143
|
+
if (!validated.success)
|
|
144
|
+
throw new Error(validated.error.message, { cause: validated.error })
|
|
133
145
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
// validated:{ success: true, data: { port: 3000 } }。
|
|
147
|
+
|
|
148
|
+
// get 适合可选配置;getOrThrow 适合已确认存在的必需配置。
|
|
149
|
+
if (core.config.has('app')) { // true:上方已成功加载 app。
|
|
150
|
+
const optional = core.config.get<z.infer<typeof AppSchema>>('app') // { port: 3000 }。
|
|
151
|
+
const required = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // { port: 3000 };缺失则抛错。
|
|
152
|
+
core.logger.info('App config ready', { port: required.port, available: optional !== undefined })
|
|
153
|
+
}
|
|
154
|
+
core.config.keys() // 仅运行本节 load 示例时为 ['app']。
|
|
138
155
|
```
|
|
139
156
|
|
|
140
|
-
|
|
157
|
+
#### 再読み込み・監視・解放
|
|
141
158
|
|
|
142
|
-
|
|
159
|
+
reload() で再読み込み、watch() で新しい値やエラーを受け取ります。通常保存、原子的な置換、削除後の再作成に対応し、失敗時は最後の成功値を保持します。unwatch() は監視停止、clear() はキャッシュも削除し、名前省略時は全件が対象です。
|
|
143
160
|
|
|
144
|
-
|
|
145
|
-
// 配置输出格式和级别(Node.js / Browser)
|
|
146
|
-
core.logger.configure({
|
|
147
|
-
level: 'info',
|
|
148
|
-
format: 'json', // 'json' | 'pretty'
|
|
149
|
-
context: { app: 'my-service' }, // 全局默认上下文
|
|
150
|
-
redact: ['password', 'token'], // 脱敏字段
|
|
151
|
-
})
|
|
161
|
+
watchConfig の既定値は false です。設定を再読込しても稼働中の DB モジュールは再初期化しません。以下の解放処理は設定が不要になった時に呼びます。
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
```ts
|
|
164
|
+
import { core } from '@h-ai/core'
|
|
165
|
+
|
|
166
|
+
// 前提:app 已由 init/load 加载。回调需先处理读取或校验失败。
|
|
167
|
+
const stop = core.config.watch('app', (_settings, error) => {
|
|
168
|
+
// 文件改成 port: 4000 后,成功回调收到 { port: 4000 }、error 为 undefined;失败时为 null 和错误对象。
|
|
169
|
+
if (error)
|
|
170
|
+
core.logger.error('Config reload failed', { error })
|
|
171
|
+
else
|
|
172
|
+
core.logger.info('Config reloaded')
|
|
173
|
+
})
|
|
174
|
+
// watch 返回取消函数 stop,不立即返回配置值。
|
|
175
|
+
core.config.isWatching('app') // true。
|
|
176
|
+
const reloaded = core.config.reload('app') // 文件仍为 port: 3000 时返回 { success: true, data: { port: 3000 } }。
|
|
177
|
+
if (!reloaded.success)
|
|
178
|
+
core.logger.warn('Manual reload failed', { error: reloaded.error })
|
|
179
|
+
|
|
180
|
+
stop() // 返回 undefined;取消当前回调,没有其他回调时停止底层监听。
|
|
181
|
+
core.config.unwatch('app') // 返回 undefined;isWatching('app') 变为 false。
|
|
182
|
+
core.config.clear('app') // 返回 undefined;get('app') 变为 undefined,has('app') 变为 false。
|
|
155
183
|
```
|
|
156
184
|
|
|
157
|
-
|
|
185
|
+
#### 環境変数で YAML の値を変える
|
|
158
186
|
|
|
159
|
-
|
|
187
|
+
設定名と項目の場所から、上書き用の環境変数名が自動で決まります。app.yml の port は HAI_APP_PORT、http.maxRetries は HAI_APP_HTTP_MAXRETRIES です。階層を大文字と_ で連結し、camelCase は分割しません。上書きのために YAML に ${...} を書く必要はありません。
|
|
160
188
|
|
|
161
|
-
|
|
189
|
+
固定値の `port: 3000` は、HAI_APP_PORT がなければ 3000、HAI_APP_PORT=5000 を設定すれば 5000 になります。
|
|
162
190
|
|
|
163
|
-
|
|
191
|
+
YAML に `${変数名}` を書くと、読み込み時に環境変数の値で置き換えられます。コロンの後は変数が未設定の場合の既定値です。
|
|
164
192
|
|
|
165
|
-
|
|
193
|
+
| YAML | 環境入力 | 結果 |
|
|
194
|
+
| ---------------------------- | ---------------------------- | --------------------------------------------------- |
|
|
195
|
+
| `port: ${PORT}` | PORT=4000 | 4000 を使用。PORT がなければ CONFIG_ENV_VAR_MISSING |
|
|
196
|
+
| `port: ${PORT:3000}` | PORT なし | 式内の既定値 3000 |
|
|
197
|
+
| `port: ${PORT:3000}` | PORT=4000、HAI_APP_PORT=5000 | 規約変数を優先して 5000 |
|
|
198
|
+
| `label: server-${PORT:3000}` | PORT=4000 | 文字列 server-4000 |
|
|
166
199
|
|
|
167
|
-
|
|
168
|
-
| ------- | ---------------------------------------- |
|
|
169
|
-
| `trace` | ループ内の詳細なデバッグデータ |
|
|
170
|
-
| `debug` | 関数開始、引数の概要、読み取り操作 |
|
|
171
|
-
| `info` | 業務イベント(初期化完了、書き込み成功) |
|
|
172
|
-
| `warn` | 回復可能な問題(検証失敗、再試行) |
|
|
173
|
-
| `error` | 調査が必要な操作の失敗 |
|
|
174
|
-
| `fatal` | サービス継続不能の致命的エラー |
|
|
200
|
+
最初に HAI_APP_PORT を確認し、値があれば項目全体を置換して ${PORT...} は評価しません。なければ YAML を読み、式の場合だけ PORT を参照し、固定値ならそのまま使います。HAI_APP_PORT で上書き済みなら PORT は不要です。
|
|
175
201
|
|
|
176
|
-
|
|
202
|
+
同じ設定元では、親 JSON を先に使い、細かな項目で上書きできます。HAI_APP='{"port":3000,"label":"local"}' と HAI_APP_PORT=4000 は行順に関係なく port=4000、label=local です。高優先度のオブジェクト全体の置換は低優先度の子項目上書きを残しません。配列 JSON はリスト全体を置換し、添字変数は既存要素だけを変更し、範囲外はエラーです。
|
|
177
203
|
|
|
178
|
-
|
|
204
|
+
#### .env ファイルを読み込む
|
|
179
205
|
|
|
180
|
-
|
|
181
|
-
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
182
|
-
core.i18n.setGlobalLocale('en-US')
|
|
183
|
-
const current = core.i18n.getGlobalLocale() // 'en-US'
|
|
206
|
+
core.init() は .env を読みません。loadEnvFiles() の success を確認し、結果を process.env に反映してから YAML を読みます。development/production は dev/prod に対応し、存在しないファイルは無視、同一ファイルの重複キーはエラーです。優先順位は **プロセス環境 > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML** です。秘密を含む .local は Git に登録しません。
|
|
184
207
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
'zh-CN': { welcome: '欢迎 {name}', error: '错误:{code}' },
|
|
188
|
-
'en-US': { welcome: 'Welcome {name}', error: 'Error: {code}' },
|
|
189
|
-
})
|
|
208
|
+
```ts
|
|
209
|
+
import { core } from '@h-ai/core'
|
|
190
210
|
|
|
191
|
-
//
|
|
192
|
-
|
|
211
|
+
// 前提:.env 写 HAI_APP_PORT=4000,没有其他来源覆盖这个变量。
|
|
212
|
+
// 先读取并合并 .env 文件与进程环境,失败时停止启动。
|
|
213
|
+
const result = core.config.loadEnvFiles({ mode: 'dev', envDir: '.' })
|
|
214
|
+
if (!result.success)
|
|
215
|
+
throw new Error(result.error.message, { cause: result.error })
|
|
216
|
+
// 成功时 result.success 为 true,result.data.HAI_APP_PORT 为 '4000';其他键来自合并后的环境。
|
|
217
|
+
// 返回值不会自动写入进程;显式应用后再初始化。
|
|
218
|
+
Object.assign(process.env, result.data)
|
|
219
|
+
core.init({ configDir: './config' }) // 返回 undefined;app.yml 中 port: 3000 被覆盖为数字 4000。
|
|
220
|
+
```
|
|
193
221
|
|
|
194
|
-
|
|
195
|
-
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
222
|
+
#### 置換後の値の型
|
|
196
223
|
|
|
197
|
-
|
|
198
|
-
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]
|
|
199
|
-
core.i18n.DEFAULT_LOCALE // 'zh-CN'
|
|
200
|
-
```
|
|
224
|
+
環境変数はテキストです。`load()` / `resolve()` に Zod Schema を渡すと、項目の型に従って置換結果を変換します。`z.number()` は数値、`z.boolean()` は真偽値となり、`z.string()` は文字列を保持します。
|
|
201
225
|
|
|
202
|
-
|
|
226
|
+
値全体が ${PORT:3000} なら数値 3000、${ENABLED:false} なら真偽値 false にできます。server-${PORT:3000} のような混合テキストは文字列です。文字列項目 code の 00123 は先頭の 0 を保った '00123' になります。以下はファイルを読まず、同等の YAML 値と環境データを直接渡す例です。
|
|
203
227
|
|
|
204
|
-
|
|
228
|
+
```ts
|
|
229
|
+
import { core } from '@h-ai/core'
|
|
230
|
+
import { z } from 'zod'
|
|
205
231
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
232
|
+
// 用显式环境数据演示覆盖顺序、数值/布尔转换与字符串保留。
|
|
233
|
+
const AppSchema = z.object({ port: z.number(), enabled: z.boolean(), label: z.string(), code: z.string() })
|
|
234
|
+
const resolved = core.config.resolve({
|
|
235
|
+
name: 'app',
|
|
236
|
+
// eslint-disable-next-line no-template-curly-in-string -- 保留 YAML 中等待框架解析的字面表达式。
|
|
237
|
+
value: { port: '${PORT:3000}', enabled: '${ENABLED:false}', label: 'server-${PORT:3000}', code: '${APP_CODE:00123}' },
|
|
238
|
+
environment: { PORT: '4000', HAI_APP_PORT: '5000' },
|
|
239
|
+
schema: AppSchema,
|
|
212
240
|
})
|
|
213
|
-
|
|
241
|
+
if (!resolved.success)
|
|
242
|
+
throw new Error(resolved.error.message, { cause: resolved.error })
|
|
214
243
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
// resolved:{ success: true, data: { port: 5000, enabled: false, label: 'server-4000', code: '00123' } }。
|
|
245
|
+
|
|
246
|
+
// resolve 只做替换和类型还原;再用 Schema 确认最终数据符合要求。
|
|
247
|
+
const validated = AppSchema.safeParse(resolved.data)
|
|
248
|
+
if (!validated.success)
|
|
249
|
+
throw new Error(validated.error.message, { cause: validated.error })
|
|
250
|
+
core.logger.info('Config resolved', { settings: validated.data })
|
|
251
|
+
// settings: { port: 5000, enabled: false, label: 'server-4000', code: '00123' }
|
|
220
252
|
```
|
|
221
253
|
|
|
222
|
-
|
|
254
|
+
空文字列は「設定済みだが文字がない」値で、未設定、null、false、0 ではありません。APP_CODE='' は 00123 に戻らず '' のままです。許可するかは Schema が決め、数値項目の空文字列は通常検証に失敗します。
|
|
255
|
+
|
|
256
|
+
変数の置換は YAML 内の式に適用されます。環境値や親 JSON 内の `server-${PORT}` は原文のまま保持されます。HAI_APP_LABEL='server-${PORT}' の最終値は server-${PORT} です。置換したい式は YAML に書きます。
|
|
223
257
|
|
|
224
|
-
|
|
225
|
-
Zod の既定の英語メッセージを呼び出し元の i18n 文言に変換します。
|
|
258
|
+
Schema がなければ既存 YAML/JSON 構造からパスを判断し、単独の式は YAML の規則で数値・真偽値を解釈します。YAML にない合法な省略項目は configSchemas で示せます。最終的な業務検証は validate() またはモジュール初期化で行います。
|
|
226
259
|
|
|
227
|
-
|
|
260
|
+
#### 設定ファイル生成用のツール
|
|
228
261
|
|
|
229
|
-
|
|
230
|
-
- `createPrefixedZodMessageGetter()`:モジュールの接頭辞から `serv_validation*` / `kit_validation*` などの key を生成し、手書きの `KEY_MAP` を省略。
|
|
231
|
-
- `mapZodErrorToFormErrors()`:抽出・ローカライズ・フラット化を一度に実行。
|
|
232
|
-
- 独自の業務メッセージは保持し、Zod の既定の英語メッセージのみを置換します。
|
|
262
|
+
resolve() はビルド・デプロイ時のメモリ上の設定計算にも使えます。toEnv() はオブジェクトを環境変数へ、mergeEnv() は複数ソースの統合、stringifyEnv()/parseEnv() は .env の符号化・解析を行います。秘密のマスキングは行いません。
|
|
233
263
|
|
|
234
|
-
```
|
|
235
|
-
import type { ZodMessageGetter } from '@h-ai/core'
|
|
264
|
+
```ts
|
|
236
265
|
import { core } from '@h-ai/core'
|
|
237
|
-
import { z } from 'zod'
|
|
238
266
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
267
|
+
// 配置生成器把普通对象转成 HAI_APP_PORT 等变量;数组保留为 JSON。
|
|
268
|
+
const values = core.config.toEnv({ prefix: 'HAI_APP', value: { port: 3000, tags: ['local'] } })
|
|
269
|
+
// values:{ HAI_APP_PORT: '3000', HAI_APP_TAGS: '["local"]' }。
|
|
270
|
+
const merged = core.config.mergeEnv([values, { HAI_APP_PORT: '4000' }])
|
|
271
|
+
// merged:{ HAI_APP_PORT: '4000', HAI_APP_TAGS: '["local"]' }。
|
|
272
|
+
const text = core.config.stringifyEnv(merged) // 编码为可写入 .env 的文本。
|
|
273
|
+
// text 含以下两行,每行以换行符结尾:
|
|
274
|
+
// HAI_APP_PORT='4000'
|
|
275
|
+
// HAI_APP_TAGS='["local"]'
|
|
276
|
+
const restored = core.config.parseEnv(text) // 解析回字面字符串,不执行其中的变量。
|
|
277
|
+
// restored:{ HAI_APP_PORT: '4000', HAI_APP_TAGS: '["local"]' }。
|
|
278
|
+
core.logger.info('Generated config verified', { port: restored.HAI_APP_PORT })
|
|
279
|
+
```
|
|
244
280
|
|
|
245
|
-
|
|
246
|
-
switch (key) {
|
|
247
|
-
case 'validationEmail':
|
|
248
|
-
return '请输入合法邮箱地址'
|
|
249
|
-
case 'validationStringMin':
|
|
250
|
-
return `至少输入 ${params?.min} 个字符`
|
|
251
|
-
case 'validationRequired':
|
|
252
|
-
return '此项为必填项'
|
|
253
|
-
default:
|
|
254
|
-
return '输入不合法'
|
|
255
|
-
}
|
|
256
|
-
}
|
|
281
|
+
通常オブジェクトは展開し、配列・動的辞書・空オブジェクトは JSON を保持します。全体置換の生成規則、優先順位、組み込みは[基本設定ガイド](../../docs/usage/configuration/runtime.md)を参照してください。
|
|
257
282
|
|
|
258
|
-
|
|
259
|
-
email: 'bad',
|
|
260
|
-
password: '123',
|
|
261
|
-
nickname: '',
|
|
262
|
-
})
|
|
283
|
+
### ログ:`core.logger`
|
|
263
284
|
|
|
264
|
-
|
|
265
|
-
const getMessage = core.zodValidation.createPrefixedZodMessageGetter(
|
|
266
|
-
'kit',
|
|
267
|
-
(messageKey, params) => {
|
|
268
|
-
if (messageKey === 'kit_validationEmail')
|
|
269
|
-
return '请输入合法邮箱地址'
|
|
270
|
-
if (messageKey === 'kit_validationStringMin')
|
|
271
|
-
return `至少输入 ${params?.min} 个字符`
|
|
272
|
-
return '输入不合法'
|
|
273
|
-
},
|
|
274
|
-
)
|
|
275
|
-
const errors = core.zodValidation.mapZodErrorToFormErrors(result.error, getMessage)
|
|
276
|
-
|
|
277
|
-
errors[0]?.field // 'email'
|
|
278
|
-
errors[0]?.message // '请输入合法邮箱地址'
|
|
279
|
-
errors[2]?.message // '请填写昵称'(schema 自定义消息原样保留)
|
|
285
|
+
core.logger は構造化ログを記録します。詳細から重大の順に trace、debug、info、warn、error、fatal です。fatal はログを記録し、終了するかはアプリが決めます。
|
|
280
286
|
|
|
281
|
-
|
|
282
|
-
|
|
287
|
+
`core.logger` は `trace` / `debug` / `info` / `warn` / `error` / `fatal` に対応します。Node は pino、ブラウザーは loglevel を使います。未設定時は `info` / `pretty` で、`LoggingConfigSchema` で解析した `logging` オブジェクトの形式既定値は `json` です。Node で任意依存の `pino-pretty` がなければ JSON 出力になります。
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { core } from '@h-ai/core'
|
|
291
|
+
|
|
292
|
+
// 先设置级别;低于当前级别的日志不会输出。
|
|
293
|
+
core.logger.setLevel('trace') // 返回 undefined;以下六个级别均会输出。
|
|
294
|
+
core.logger.trace('Processing detail', { step: 1 })
|
|
295
|
+
core.logger.debug('Read started')
|
|
296
|
+
core.logger.info('Order created', { orderId: 'order-1' })
|
|
297
|
+
core.logger.warn('Retry scheduled')
|
|
298
|
+
core.logger.error('Operation failed', { error: new Error('Example failure') })
|
|
299
|
+
core.logger.fatal('Service cannot continue')
|
|
300
|
+
// 六次日志调用均返回 undefined;分别输出对应级别、上面的消息及所传字段。
|
|
301
|
+
// 例如 info 日志的消息为 'Order created',字段 orderId 为 'order-1'。
|
|
283
302
|
```
|
|
284
303
|
|
|
285
|
-
|
|
304
|
+
create() は名前付き Logger、child() は固定項目付き Logger を作ります。configure() は出力と追加マスキング、setLevel()/getLevel() はグローバルレベルの変更・取得です。
|
|
286
305
|
|
|
287
|
-
|
|
306
|
+
```ts
|
|
307
|
+
import { core } from '@h-ai/core'
|
|
288
308
|
|
|
289
|
-
|
|
309
|
+
// 全局配置可作用于已经创建的 Logger;redact 补充业务隐私字段。
|
|
310
|
+
core.logger.configure({ level: 'info', format: 'json', redact: ['customer.email'] }) // 返回 undefined;后续日志按此配置输出。
|
|
311
|
+
const orders = core.logger.create({ name: 'orders' }) // 返回 Logger 对象,可调用 info/warn 等方法。
|
|
312
|
+
const request = core.logger.child({ requestId: 'request-1' }) // 返回带固定 requestId 的 Logger。
|
|
313
|
+
orders.info('Order created', { customer: { email: 'user@example.com' } }) // 返回 undefined;输出中 customer.email 为 '[REDACTED]'。
|
|
314
|
+
request.warn('Request needs attention') // 返回 undefined;输出中 requestId 为 'request-1'。
|
|
315
|
+
core.logger.getLevel() // 'info'。
|
|
316
|
+
```
|
|
290
317
|
|
|
291
|
-
|
|
292
|
-
import { core, HaiConfigError } from '@h-ai/core'
|
|
293
|
-
import { z } from 'zod'
|
|
318
|
+
既存の既定・モジュール・入れ子 child Logger も、その後の出力で最新のグローバル設定を使います。`setLevel()` も即座に反映します。`create()` の明示的な level/format/context が優先し、省略項目は動的に継承します。呼び出し時の context が固定値を上書きし、最後に `core.context` のリクエスト関連項目を追加します。ブラウザーの JSON は `level` / `msg`、pretty は読みやすい文面と context を出力します。
|
|
294
319
|
|
|
295
|
-
|
|
296
|
-
core.init({
|
|
297
|
-
configDir: './config', // 扫描配置目录内所有 .yml / .yaml
|
|
298
|
-
watchConfig: true, // 监听变更并自动重载
|
|
299
|
-
})
|
|
320
|
+
observe() はレベル判定・マスキング済みレコードを診断収集に通知し、解除関数を返します。コールバックでは上限付きメモリ収集のみ行い、保存は別途非同期に行います。観測は保存完了を意味せず、例外と同期再入は分離します。
|
|
300
321
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
name: z.string(),
|
|
304
|
-
port: z.number().default(3000),
|
|
305
|
-
})
|
|
306
|
-
const result = core.config.load('app', './config/app.yml', AppSchema)
|
|
307
|
-
if (result.success) {
|
|
308
|
-
core.logger.info('Config loaded', { name: result.data.name })
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
core.logger.error('Config load failed', { code: result.error.code })
|
|
312
|
-
}
|
|
322
|
+
```ts
|
|
323
|
+
import { core } from '@h-ai/core'
|
|
313
324
|
|
|
314
|
-
//
|
|
315
|
-
const
|
|
325
|
+
// 观察经过级别过滤和脱敏的记录;内存收集设置上限。
|
|
326
|
+
const recentMessages: string[] = []
|
|
327
|
+
const stop = core.logger.observe((record) => {
|
|
328
|
+
if (recentMessages.length === 100)
|
|
329
|
+
recentMessages.shift()
|
|
330
|
+
recentMessages.push(record.message)
|
|
331
|
+
})
|
|
332
|
+
// observe 返回取消函数 stop;本例 warn 日志触发一次回调,record.message 为 'Observed event'。
|
|
333
|
+
core.logger.warn('Observed event')
|
|
334
|
+
recentMessages // ['Observed event']。
|
|
335
|
+
stop() // 返回 undefined;后续日志不再进入这个回调。
|
|
336
|
+
```
|
|
316
337
|
|
|
317
|
-
|
|
318
|
-
if (core.config.has('app')) {
|
|
319
|
-
const cfg = core.config.get<z.infer<typeof AppSchema>>('app') // undefined 或配置
|
|
320
|
-
const cfg2 = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // 未加载时抛异常
|
|
321
|
-
}
|
|
322
|
-
core.config.keys() // ['core', 'app', 'db', ...]
|
|
338
|
+
### リクエストの関連付け:`core.context`
|
|
323
339
|
|
|
324
|
-
|
|
325
|
-
const reloadResult = core.config.reload('app')
|
|
340
|
+
run() は requestId、userId、scopeId、jobId などをリクエストやタスクに持たせ、get() は現在値を読みます。Node では DB などの非同期処理後も他のリクエストに上書きされず読み取れます。既存 Logger にも自動で付加します。
|
|
326
341
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (error) {
|
|
330
|
-
core.logger.error('Config reload failed', { error })
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
core.logger.info('Config updated', { config: newConfig })
|
|
334
|
-
})
|
|
335
|
-
unwatch() // 取消监听
|
|
342
|
+
```ts
|
|
343
|
+
import { core } from '@h-ai/core'
|
|
336
344
|
|
|
337
|
-
//
|
|
338
|
-
core.
|
|
339
|
-
core.
|
|
345
|
+
// Node 示例:同一请求等待异步处理后,仍能取得自己的编号。
|
|
346
|
+
await core.context.run({ requestId: 'request-1', userId: 'user-1' }, async () => {
|
|
347
|
+
await core.async.delay(1)
|
|
348
|
+
const current = core.context.get() // 包含 requestId: 'request-1'、traceId: 'request-1'、userId: 'user-1';其他字段为 undefined。
|
|
349
|
+
core.logger.info('Request continued', { requestId: current.requestId })
|
|
350
|
+
return current.requestId
|
|
351
|
+
}) // run 返回回调的结果;此处 await 后得到 'request-1'。
|
|
352
|
+
core.context.get() // {},回到外层后不保留上面请求的上下文。
|
|
353
|
+
|
|
354
|
+
// 采集器自身写日志时,可暂停观察回调,正常日志输出仍然执行。
|
|
355
|
+
core.context.run({ suppressLogObservers: true }, () => core.logger.warn('Collector diagnostic'))
|
|
356
|
+
// 返回 undefined;输出 warn 日志,但不会触发日志观察回调。
|
|
340
357
|
```
|
|
341
358
|
|
|
342
|
-
|
|
359
|
+
ブラウザーでは同期コールバック中だけ保持し、await 後は保持しません。関連 ID は認可情報ではなく、主体とテナント範囲は信頼できるサーバーコードが渡します。
|
|
360
|
+
|
|
361
|
+
### 診断データのマスキング:`core.sanitize`
|
|
343
362
|
|
|
344
|
-
|
|
363
|
+
ログ、診断パッケージ、公開エラーの前に認証情報を隠します。項目名の判定、単一値・オブジェクト全体・URL の処理、ラベル付きまたは既知の秘密のテキスト除去に使えます。
|
|
345
364
|
|
|
346
365
|
```ts
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
core.
|
|
366
|
+
import { core } from '@h-ai/core'
|
|
367
|
+
|
|
368
|
+
core.sanitize.isSensitiveKey('apiKey') // true。
|
|
369
|
+
core.sanitize.sanitizeSensitiveValue('example-only', 'password') // '[REDACTED]'。
|
|
370
|
+
const safe = core.sanitize.sanitizeSensitiveFields({ password: 'example-only', email: 'user@example.com' })
|
|
371
|
+
// safe:{ password: '[REDACTED]', email: 'user@example.com' },原对象不变。
|
|
372
|
+
// 默认仅遮蔽凭据;额外业务隐私可在已处理凭据的副本上继续清理。
|
|
373
|
+
core.sanitize.sanitizeSensitiveFields(safe, { matcher: ['email'], replacement: '***' })
|
|
374
|
+
// => { password: '[REDACTED]', email: '***' }
|
|
375
|
+
core.sanitize.sanitizeUrl('https://user:example-only@example.com/?token=example-only')
|
|
376
|
+
// => 'https://[REDACTED]:[REDACTED]@example.com/?token=[REDACTED]'
|
|
377
|
+
core.sanitize.sanitizeText('Bearer example-only') // '[REDACTED]'。
|
|
378
|
+
core.sanitize.sanitizeText('Request failed with a known private value', ['private value'])
|
|
379
|
+
// => 'Request failed with a known [REDACTED]'
|
|
352
380
|
```
|
|
353
381
|
|
|
354
|
-
|
|
382
|
+
defaultSensitiveKeyRegex/defaultSensitiveReplacement は既定の照合と [REDACTED] を提供します。独自 matcher は既定の規則を置換するため、業務上の個人情報を追加処理する前に既定の認証情報マスキングを残します。
|
|
355
383
|
|
|
356
|
-
`
|
|
357
|
-
`HAI_<CONFIG_NAME>_<YAML_PATH>` の環境変数に自動対応付けします。設定名・各 key は大文字化し、階層は `_` で区切り、
|
|
358
|
-
camelCase の単語は分割しません。例:`ai.llm.apiKey` は `HAI_AI_LLM_APIKEY`、
|
|
359
|
-
`ai.llm.maxTokens` は `HAI_AI_LLM_MAXTOKENS`。配列の添字も 1 階層として扱い、
|
|
360
|
-
`ai.llm.models[0].temperature` は `HAI_AI_LLM_MODELS_0_TEMPERATURE` です。
|
|
384
|
+
ログは password/passwordHash、token、secretKey、privateKey などの認証項目と、URL のユーザー名・パスワード・機密クエリー・OAuth のキー値 fragment を隠します。不正 URL は `(invalid url)` になります。`redact` は業務上の個人情報パスを追加し、ドット・括弧・単一階層 `*` に対応し、大文字小文字を区別します。メールアドレスと電話番号は既定では保持します。
|
|
361
385
|
|
|
362
|
-
|
|
363
|
-
変数がなければ YAML 値を保持します。オブジェクト・配列全体も同名の JSON 変数で置換でき、HAI_DB は接続設定全体、HAI_REACH_PROVIDERS は配信チャネル配列を置換します。親ノード全体の上書きが子フィールド変数より優先され、置換後も Schema 検証を行います。
|
|
386
|
+
ネイティブ `Error` は name/message/stack/cause、集約エラー、列挙可能な context を含むコピーにしてから隠し、元の例外は変更しません。例外内の循環と 32 階層の上限は `[Circular]` / `[MaxDepth]` に置換します。通常オブジェクトの循環は保持され、JSON 化できるとは限りません。文面内では明示的な認証情報ラベルと URL を処理しますが、ラベルのない任意の秘密は識別できないため、呼び出し側で除去するかパス全体を隠します。
|
|
364
387
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
388
|
+
マスキング結果はデータだけを保持します。通常オブジェクトとクラスインスタンスの自有・列挙可能なデータ項目をコピーし、元のプロトタイプ、アクセサー、独自の `toJSON()` を保持・実行しません。日付は ISO 文字列、URL はマスキング済み文字列に変換し、元の値は変更しません。SDK や業務オブジェクトからは診断に必要な項目だけを選び、ラベルのない秘密や業務上の個人情報は引き続き明示的に除去してください。
|
|
389
|
+
|
|
390
|
+
### 結果とエラー:`core.error`
|
|
391
|
+
|
|
392
|
+
core.error はエラーコード定義、実際の失敗インスタンス、安全な HTTP/UI メッセージを扱います。コードは安定し、文面は現在の言語で生成できます。HTTP ステータスとヘッダーは Serv/Kit などが扱います。
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import { core } from '@h-ai/core'
|
|
396
|
+
|
|
397
|
+
// 定义稳定错误码,消息在创建实例时根据当前语言获取。
|
|
398
|
+
const OrderError = core.error.buildHaiErrorsDef('orders', { NOT_FOUND: '300:404' })
|
|
399
|
+
// OrderError.NOT_FOUND:{ code: 'hai:orders:300', httpStatus: 404, system: 'hai', module: 'orders' }。
|
|
400
|
+
const failure = core.error.buildHaiErrorInst(OrderError.NOT_FOUND, core.i18n.coreM('core_errorNotFound', { locale: 'zh-CN' }))
|
|
401
|
+
// failure:上面的错误定义加上 message: '资源不存在'。
|
|
402
|
+
const publicError = core.error.toPublic(failure, 'request-1')
|
|
403
|
+
// publicError:保留 code/httpStatus/system/module,增加 traceId: 'request-1';
|
|
404
|
+
// message 为 '资源不存在 (traceId: request-1)'。
|
|
405
|
+
const message = core.error.formatMessage(publicError) // '资源不存在 (traceId: request-1)',编号不会重复追加。
|
|
406
|
+
core.logger.warn('Order unavailable', { error: failure, publicMessage: message })
|
|
372
407
|
```
|
|
373
408
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
409
|
+
#### 共通結果型と独立した構築関数
|
|
410
|
+
|
|
411
|
+
`HaiResult<T>` は success で data/error を区別します。`ok` / `err` を個別に import して成功・失敗の結果を構築します。業務 API 後は success を確認し、getter・ツール・ファクトリーはそれぞれの戻り値・例外契約に従います。
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import type { HaiResult } from '@h-ai/core'
|
|
415
|
+
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
416
|
+
|
|
417
|
+
// 业务失败返回 err,成功返回 ok;共用构造器保持独立导入。
|
|
418
|
+
function divide(a: number, b: number): HaiResult<number> {
|
|
419
|
+
if (b === 0)
|
|
420
|
+
return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM('core_errorValidation', { locale: 'zh-CN' }))
|
|
421
|
+
return ok(a / b) // 如 ok(5) 返回 { success: true, data: 5 }。
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 先检查 success,再读取 data 或 error。
|
|
425
|
+
const result = divide(10, 2) // { success: true, data: 5 }。
|
|
426
|
+
if (!result.success)
|
|
427
|
+
core.logger.warn('Calculation failed', { error: result.error })
|
|
428
|
+
else
|
|
429
|
+
core.logger.info('Calculation completed', { value: result.data })
|
|
430
|
+
|
|
431
|
+
const failed = divide(10, 0)
|
|
432
|
+
// failed:{ success: false, error: { code: 'hai:common:200', httpStatus: 400,
|
|
433
|
+
// system: 'hai', module: 'common', message: '验证失败' } },无 data。
|
|
434
|
+
if (!failed.success)
|
|
435
|
+
core.logger.warn('Calculation rejected', { code: failed.error.code })
|
|
377
436
|
```
|
|
378
437
|
|
|
379
|
-
|
|
380
|
-
規約に沿う環境変数 > 明示的な環境変数 > YAML の既定値。
|
|
438
|
+
HTTP/UI 境界では `core.error.toPublic(error, traceId?, locale?)` を使います。5xx は翻訳済みの一般的な案内に置換し、その他の文面から既知の認証情報を除き、安全な項目だけを保持します。追跡 ID は明示引数、コンテキストの traceId/requestId、既存 error.traceId の順で選び、なければ生成します。`formatMessage()` は有効な ID を追記し、同じ末尾を重複させません。
|
|
381
439
|
|
|
382
|
-
|
|
383
|
-
- `${VAR:default}`:`process.env.VAR` を読み、存在しなければコロン以降の既定値を使用。
|
|
440
|
+
内部 `cause` / `ext` は既定で非公開です。400/422 のフィールドエラーは `field` / `message` を保持できます。明示的に公開を許可し、業務 Schema で検証した `publicExt` は、マスキング後に公開 `ext` となります。Core は業務上の公開可否を自動検証しません。HTTP ステータス、ヘッダー、通知 UI は Serv/Kit/UI ホストが組み込みます。
|
|
384
441
|
|
|
385
|
-
|
|
442
|
+
| 設定エラー | コード | 内容 |
|
|
443
|
+
| ------------------------- | -------------- | -------------------------------- |
|
|
444
|
+
| `CONFIG_FILE_NOT_FOUND` | `hai:core:010` | ファイルなし |
|
|
445
|
+
| `CONFIG_PARSE_ERROR` | `hai:core:011` | YAML/dotenv の読み取り・解析失敗 |
|
|
446
|
+
| `CONFIG_VALIDATION_ERROR` | `hai:core:012` | Schema または環境上書きが無効 |
|
|
447
|
+
| `CONFIG_ENV_VAR_MISSING` | `hai:core:013` | 必須展開変数の欠落 |
|
|
448
|
+
| `CONFIG_NOT_LOADED` | `hai:core:014` | 設定未読み込み |
|
|
386
449
|
|
|
387
|
-
|
|
450
|
+
これらは `HaiConfigError` の定数です。モジュール共通のエラーは `HaiCommonError` を使います。
|
|
388
451
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
url: http://${HOST}:${PORT} # → string(混合文本)
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
#### ID 生成
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
const id = core.id.generate() // nanoid 21 字符
|
|
401
|
-
const id8 = core.id.generate(8) // 自定义长度
|
|
402
|
-
const shortId = core.id.short() // 10 字符短 ID
|
|
403
|
-
const prefixed = core.id.withPrefix('user_') // 'user_V1StGXR...'
|
|
404
|
-
const traceId = core.id.trace() // 'trace-V1StGXR...'
|
|
405
|
-
const reqId = core.id.request() // 'req-V1StGXR...'
|
|
406
|
-
const uuid = core.id.uuid() // UUID v4
|
|
407
|
-
|
|
408
|
-
// 校验
|
|
409
|
-
core.id.isValidUUID('f47ac10b-...') // true/false
|
|
410
|
-
core.id.isValidNanoId('abc123', 6) // true/false
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
UUID は暗号学的乱数の randomUUID/getRandomValues のみを使用し、どちらも利用できない場合は例外を送出します。Math.random にはフォールバックしません。
|
|
414
|
-
|
|
415
|
-
#### ツール関数
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
// 类型检查
|
|
419
|
-
core.typeUtils.isDefined(x) // x !== undefined
|
|
420
|
-
core.typeUtils.isObject(x) // plain object
|
|
421
|
-
core.typeUtils.isFunction(x)
|
|
422
|
-
core.typeUtils.isPromise(x)
|
|
423
|
-
core.typeUtils.isString(x)
|
|
424
|
-
core.typeUtils.isNumber(x)
|
|
425
|
-
core.typeUtils.isBoolean(x)
|
|
426
|
-
core.typeUtils.isArray(x)
|
|
427
|
-
|
|
428
|
-
// 对象操作
|
|
429
|
-
core.object.deepClone(obj)
|
|
430
|
-
core.object.deepMerge(a, b) // 递归合并(防原型污染)
|
|
431
|
-
core.object.deepMerge(a, b, c) // 支持多个对象
|
|
432
|
-
core.object.pick(obj, ['a', 'b'])
|
|
433
|
-
core.object.omit(obj, ['c', 'd'])
|
|
434
|
-
core.object.keys(obj) // typed Object.keys
|
|
435
|
-
core.object.values(obj)
|
|
436
|
-
core.object.entries(obj)
|
|
437
|
-
core.object.fromEntries(entries)
|
|
438
|
-
|
|
439
|
-
// 字符串操作
|
|
440
|
-
core.string.capitalize('hello') // 'Hello'
|
|
441
|
-
core.string.kebabCase('helloWorld') // 'hello-world'
|
|
442
|
-
core.string.camelCase('hello-world') // 'helloWorld'
|
|
443
|
-
core.string.snakeCase('helloWorld') // 'hello_world'
|
|
444
|
-
core.string.pascalCase('hello-world') // 'HelloWorld'
|
|
445
|
-
core.string.truncate('long text', 5) // 'long ...'
|
|
446
|
-
core.string.trim(' hello ') // 'hello'
|
|
447
|
-
core.string.isBlank('') // true
|
|
448
|
-
core.string.isNotBlank('x') // true
|
|
449
|
-
core.string.padStart('5', 3, '0') // '005'
|
|
450
|
-
core.string.padEnd('5', 3, '0') // '500'
|
|
451
|
-
core.string.constantTimeEqual(a, b) // Node 使用原生等长比较;浏览器不保证恒时
|
|
452
|
-
|
|
453
|
-
// 数组操作
|
|
454
|
-
core.array.unique([1, 1, 2]) // [1, 2]
|
|
455
|
-
core.array.chunk([1, 2, 3, 4], 2) // [[1,2], [3,4]]
|
|
456
|
-
core.array.groupBy(items, item => item.type) // Record<string, T[]>
|
|
457
|
-
core.array.first([1, 2, 3]) // 1
|
|
458
|
-
core.array.last([1, 2, 3]) // 3
|
|
459
|
-
core.array.flatten([[1], [2, 3]]) // [1, 2, 3]
|
|
460
|
-
core.array.compact([1, null, 2]) // [1, 2]
|
|
461
|
-
core.array.shuffle([1, 2, 3]) // 随机排列
|
|
462
|
-
core.array.intersection([1, 2], [2, 3]) // [2]
|
|
463
|
-
core.array.difference([1, 2], [2, 3]) // [1]
|
|
464
|
-
|
|
465
|
-
// 异步操作
|
|
466
|
-
await core.async.delay(1000)
|
|
467
|
-
await core.async.withTimeout(fetch('/api'), 5000)
|
|
468
|
-
await core.async.retry(fn, { maxRetries: 3, delay: 1000 })
|
|
469
|
-
await core.async.parallel(items, processFn, 5) // 并发限制
|
|
470
|
-
await core.async.serial(items, processFn)
|
|
471
|
-
const debouncedFn = core.async.debounce(fn, 300)
|
|
472
|
-
const throttledFn = core.async.throttle(fn, 200)
|
|
473
|
-
|
|
474
|
-
// 时间操作
|
|
475
|
-
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
476
|
-
core.time.timeAgo(new Date(Date.now() - 60000)) // '1 分钟前'
|
|
477
|
-
core.time.now() // Date.now()
|
|
478
|
-
core.time.nowSeconds() // Math.floor(Date.now() / 1000)
|
|
479
|
-
core.time.parseDate('2024-01-01')
|
|
480
|
-
core.time.isValidDate(date)
|
|
481
|
-
core.time.addDays(date, 7)
|
|
482
|
-
core.time.addHours(date, 2)
|
|
483
|
-
core.time.startOfDay(date)
|
|
484
|
-
core.time.endOfDay(date)
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
Node 入口は元の UTF-16 コード単位を UTF-16LE に変換し、同じバイト長を node:crypto.timingSafeEqual で比較します。長さが違えば false となり、長さ自体は隠しません。ブラウザー実装には JavaScript エンジンの定時間保証がなく、サーバーの認証境界には使用しません。周辺の分岐・変換・リクエスト処理まで定時間になるわけではありません。
|
|
488
|
-
|
|
489
|
-
#### モジュール共通ツール(module)
|
|
490
|
-
|
|
491
|
-
モジュール共通の初期化エラー処理:
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
452
|
+
### メッセージと言語:`core.i18n`
|
|
453
|
+
|
|
454
|
+
パラメーター置換付きのメッセージ取得関数を作り、Core 内蔵メッセージや言語を取得・設定します。定数は内蔵言語一覧であり、アプリ翻訳の自動登録は行いません。
|
|
455
|
+
|
|
456
|
+
```ts
|
|
494
457
|
import { core } from '@h-ai/core'
|
|
495
458
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
)
|
|
459
|
+
// 页面语言或服务进程的启动默认值;请求中使用 runWithLocale 单独选择语言。
|
|
460
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;后续默认消息使用中文。
|
|
461
|
+
core.i18n.getGlobalLocale() // 'zh-CN'。
|
|
462
|
+
core.i18n.coreM('core_errorNotFound') // '资源不存在'。
|
|
463
|
+
core.i18n.DEFAULT_LOCALE // zh-CN。
|
|
464
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]。
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
対応言語は `zh-CN`、`en-US`、`ja-JP` で、略記 `zh` / `en` / `ja` を正規化します。明示 locale、Node リクエスト言語、アプリ既定言語の順に参照します。未対応言語は `zh-CN`、未定義キーはキー自体にフォールバックします。
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { core } from '@h-ai/core'
|
|
500
471
|
|
|
501
|
-
//
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
472
|
+
// 业务字典按语言组织;占位参数在取消息时传入。
|
|
473
|
+
const message = core.i18n.createMessageGetter({
|
|
474
|
+
'zh-CN': { welcome: '欢迎 {name}' },
|
|
475
|
+
'en-US': { welcome: 'Welcome {name}' },
|
|
476
|
+
'ja-JP': { welcome: 'ようこそ {name}' },
|
|
477
|
+
})
|
|
478
|
+
// createMessageGetter 返回消息函数;如 message('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) 得到 '欢迎 Alice'。
|
|
479
|
+
const greeting = await core.i18n.runWithLocale('en-US', async () => {
|
|
480
|
+
await Promise.resolve()
|
|
481
|
+
return message('welcome', { params: { name: 'Alice' } })
|
|
482
|
+
})
|
|
483
|
+
// greeting:'Welcome Alice';runWithLocale 返回回调的结果,此处等待 Promise 得到字符串。
|
|
484
|
+
core.logger.info('Greeting prepared', { greeting })
|
|
505
485
|
```
|
|
506
486
|
|
|
507
|
-
|
|
487
|
+
Node のリクエスト/SSR では、実際の非同期処理を `runWithLocale()` で囲みます。Serv は自動でリクエスト言語を設定し、Kit は同名の入口を提供します。リクエスト内の `setGlobalLocale()` はそのリクエストだけを変更し、外側では起動時の既定値にのみ使います。ブラウザーはアプリの `setGlobalLocale()` または明示 locale を使用します。SSR の UI store はプロセス既定言語を変更しません。
|
|
508
488
|
|
|
509
|
-
|
|
489
|
+
### フィールド検証メッセージ:`core.zodValidation`
|
|
510
490
|
|
|
511
|
-
|
|
491
|
+
Zod エラーをフォームに表示できる { field, message }[] に変換します。createPrefixedZodMessageGetter() は接頭辞で翻訳キーを選び、mapZodErrorToFormErrors() は抽出、翻訳、フィールドパス変換を行います。
|
|
512
492
|
|
|
513
|
-
```
|
|
514
|
-
import
|
|
515
|
-
import {
|
|
493
|
+
```ts
|
|
494
|
+
import { core } from '@h-ai/core'
|
|
495
|
+
import { z } from 'zod'
|
|
516
496
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
497
|
+
// 此例复用 Core 已有翻译;业务表单可注入自己的消息获取器。
|
|
498
|
+
const message = core.zodValidation.createPrefixedZodMessageGetter('core', key =>
|
|
499
|
+
key === 'core_validationEmail'
|
|
500
|
+
? core.i18n.coreM('core_validationEmail', { locale: 'zh-CN' })
|
|
501
|
+
: core.i18n.coreM('core_errorValidation', { locale: 'zh-CN' }))
|
|
502
|
+
// 返回消息函数;message('validationEmail') 得到 '请输入有效的邮箱地址'。
|
|
503
|
+
const parsed = z.object({ email: z.email() }).safeParse({ email: 'invalid' })
|
|
504
|
+
if (!parsed.success) {
|
|
505
|
+
const errors = core.zodValidation.mapZodErrorToFormErrors(parsed.error, message)
|
|
506
|
+
// errors:[{ field: 'email', message: '请输入有效的邮箱地址' }]。
|
|
507
|
+
core.logger.info('Validation failed', { errors })
|
|
521
508
|
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Zod v3/v4 の issues/errors に対応し、既定英語は翻訳、業務の独自メッセージは保持します。パスはドット、全体エラーは _ です。厳密 Schema の未知項目は validationUnexpectedFields を使い、内部項目名を公開しません。
|
|
512
|
+
|
|
513
|
+
### モジュールの起動と補助機能:`core.module`
|
|
514
|
+
|
|
515
|
+
アプリ起動コードやモジュール実装で、複数モジュールの管理と未初期化時の共通エラーを再利用します。どのモジュールを使うかはアプリが選びます。
|
|
516
|
+
|
|
517
|
+
#### アプリケーションの起動と終了
|
|
518
|
+
|
|
519
|
+
複数のモジュールを使うアプリは、業務処理の前にデータベースなどを初期化し、処理後に解放する必要があります。`core.module.createRuntime()` は各モジュールの起動・終了関数を受け取り、アプリから呼び出すオブジェクトを返します。この作成時点ではモジュールを起動しません。
|
|
522
520
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
521
|
+
以下はローカル開発用の例で、`@h-ai/reldb` と `@h-ai/cache` の追加インストールが必要です。両方とも説明用のメモリ設定を使います。本番では各モジュールが要求する共有バックエンドを設定してください。
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { cache } from '@h-ai/cache'
|
|
525
|
+
import { core } from '@h-ai/core'
|
|
526
|
+
import { reldb } from '@h-ai/reldb'
|
|
527
|
+
|
|
528
|
+
// 按需要的启动顺序,列出各模块的初始化和关闭函数。
|
|
529
|
+
const runtime = core.module.createRuntime([
|
|
530
|
+
{
|
|
531
|
+
name: 'database',
|
|
532
|
+
start: () => reldb.init({ type: 'sqlite', database: ':memory:' }),
|
|
533
|
+
close: () => reldb.close(),
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'cache',
|
|
537
|
+
start: () => cache.init({ type: 'memory' }),
|
|
538
|
+
close: () => cache.close(),
|
|
539
|
+
},
|
|
540
|
+
])
|
|
541
|
+
|
|
542
|
+
// createRuntime 返回带 start()/close() 的对象;到这里数据库和缓存尚未启动。
|
|
543
|
+
|
|
544
|
+
// 依次启动数据库、缓存;失败时自动清理已尝试启动的模块。
|
|
545
|
+
const started = await runtime.start() // 成功为 { success: true, data: undefined };两个模块均已初始化。
|
|
546
|
+
if (!started.success)
|
|
547
|
+
throw new Error(started.error.message, { cause: started.error })
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
// 启动成功后再执行应用业务;这里演示记录启动结果。
|
|
551
|
+
core.logger.info('Application modules ready')
|
|
526
552
|
}
|
|
527
|
-
|
|
528
|
-
|
|
553
|
+
finally {
|
|
554
|
+
// 演示结束后先关闭缓存,再关闭数据库,并检查清理结果。
|
|
555
|
+
const closed = await runtime.close() // 成功为 { success: true, data: undefined };两个模块均已关闭。
|
|
556
|
+
if (!closed.success)
|
|
557
|
+
core.logger.error('Application cleanup failed', { error: closed.error })
|
|
529
558
|
}
|
|
530
559
|
```
|
|
531
560
|
|
|
532
|
-
|
|
561
|
+
この例はデータベース → キャッシュの順に起動し、キャッシュ → データベースの順に終了します。例では実行直後に解放しますが、常駐サービスでは終了時に同じ `runtime.close()` を呼びます。
|
|
533
562
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
563
|
+
起動失敗時は起動を試みたモジュールを解放します。終了失敗時はそのモジュールで止まり、まだ必要な依存先を保持し、次の `close()` / `start()` で解放を再試行します。同時に複数の `start()` を呼んでも初期化は重複しません。任意の `enabled()` が false を返す項目は実行しません。
|
|
564
|
+
|
|
565
|
+
両メソッドは `HaiResult` を返すため、`success` を確認します。エラーはモジュール名、段階、コード、必要な設定項目の案内を含み、秘密の設定値や SDK の元例外を公開の起動エラーに含めません。
|
|
537
566
|
|
|
538
|
-
|
|
539
|
-
const DbErrorInfo = {
|
|
540
|
-
NOT_INITIALIZED: '001:500',
|
|
541
|
-
CONNECTION_FAILED: '010:500',
|
|
542
|
-
QUERY_FAILED: '011:500',
|
|
543
|
-
} as const satisfies ErrorInfo
|
|
567
|
+
#### 未初期化時のエラー応答
|
|
544
568
|
|
|
545
|
-
|
|
546
|
-
export const DbError = core.error.buildHaiErrorsDef('db', DbErrorInfo)
|
|
547
|
-
// DbError.CONNECTION_FAILED → { code: 'hai:db:010', httpStatus: 500, system: 'hai', module: 'db' }
|
|
569
|
+
createNotInitializedKit() は error/result/proxy を返します。proxy は実際の操作へ切り替わるまで同期・非同期の共通失敗結果を返し、メッセージ関数は呼び出し時に現在言語を参照します。
|
|
548
570
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
'
|
|
555
|
-
|
|
571
|
+
```ts
|
|
572
|
+
import type { HaiResult } from '@h-ai/core'
|
|
573
|
+
import { core, HaiCommonError } from '@h-ai/core'
|
|
574
|
+
|
|
575
|
+
const message = core.i18n.createMessageGetter({
|
|
576
|
+
'zh-CN': { notReady: '请先初始化模块' },
|
|
577
|
+
'en-US': { notReady: 'Initialize the module first' },
|
|
578
|
+
'ja-JP': { notReady: '先にモジュールを初期化してください' },
|
|
579
|
+
})
|
|
580
|
+
// 本例固定使用中文消息,便于观察返回值。
|
|
581
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined。
|
|
582
|
+
const waiting = core.module.createNotInitializedKit(HaiCommonError.NOT_INITIALIZED, () => message('notReady'))
|
|
583
|
+
// waiting 是带 error/result/proxy 的对象。
|
|
584
|
+
waiting.error() // { code: 'hai:common:001', httpStatus: 500, system: 'hai', module: 'common', message: '请先初始化模块' }。
|
|
585
|
+
waiting.result<string>() // { success: false, error: 上方错误对象 },无 data。
|
|
586
|
+
|
|
587
|
+
// 模块实现可把尚未就绪的操作暂时指向这些代理。
|
|
588
|
+
const asyncOperations = waiting.proxy<{ read: () => Promise<HaiResult<string>> }>()
|
|
589
|
+
const syncOperations = waiting.proxy<{ read: () => HaiResult<string> }>('sync')
|
|
590
|
+
// 两次 proxy 调用分别返回异步和同步操作对象。
|
|
591
|
+
const result = await asyncOperations.read() // { success: false, error: 上方错误对象 }。
|
|
592
|
+
if (!result.success)
|
|
593
|
+
core.logger.warn('Read before initialization', { error: result.error })
|
|
594
|
+
syncOperations.read() // 直接返回 { success: false, error: 上方错误对象 },不返回 Promise。
|
|
556
595
|
```
|
|
557
596
|
|
|
558
|
-
####
|
|
597
|
+
#### 共有状態の必要性を判断
|
|
598
|
+
|
|
599
|
+
requiresSharedState() は本番で単一ホスト限定の業務状態を拒否すべきかを判断します。判定のみで、DB 接続や切り替えは行いません。実際の環境はサーバーから渡します。
|
|
559
600
|
|
|
560
|
-
```
|
|
561
|
-
import {
|
|
601
|
+
```ts
|
|
602
|
+
import { core } from '@h-ai/core'
|
|
562
603
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
567
|
-
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
604
|
+
// 此例用显式数据演示;服务端实际使用时传入 process.env。
|
|
605
|
+
core.module.requiresSharedState({ NODE_ENV: 'production' }) // true:应使用跨主机共享后端。
|
|
606
|
+
core.module.requiresSharedState({ NODE_ENV: 'production', HAI_E2E: '1' }) // false:显式 E2E 例外。
|
|
568
607
|
```
|
|
569
608
|
|
|
570
|
-
###
|
|
609
|
+
### リクエストアクセス検査:`core.access`(Node)
|
|
571
610
|
|
|
572
|
-
|
|
611
|
+
HTTP 入口で信頼できるクライアント IP を解決し、アドレス、パス、メソッド、時間帯を検査します。Core は判定だけを提供し、Serv の http.access または Kit の createHandle({ access }) がリクエスト処理へ組み込みます。
|
|
573
612
|
|
|
574
|
-
|
|
613
|
+
#### アドレスの解決と検査
|
|
575
614
|
|
|
576
|
-
|
|
615
|
+
```ts
|
|
616
|
+
import { AccessConditionSchema, core } from '@h-ai/core'
|
|
617
|
+
|
|
618
|
+
core.access.normalizeClientIp('::ffff:198.51.100.23') // 198.51.100.23。
|
|
619
|
+
const resolve = core.access.createClientIpResolver(['192.0.2.10']) // 返回 IP 解析函数,只信任这个代理。
|
|
620
|
+
const request = new Request('https://example.test/api/orders', { headers: { 'x-forwarded-for': '198.51.100.23' } })
|
|
621
|
+
// 第二参数必须来自真实 socket/可信平台;这里用测试地址模拟代理对端。
|
|
622
|
+
const clientIp = resolve(request, '192.0.2.10') // '198.51.100.23',读取受信代理的转发头。
|
|
623
|
+
const check = core.access.createConditionCheck(AccessConditionSchema.parse({ allowIps: ['198.51.100.0/24'] }))
|
|
624
|
+
// createConditionCheck 返回检查函数;check(clientIp) 得到 { allowed: true }。
|
|
625
|
+
core.logger.info('Address checked', { allowed: check(clientIp).allowed })
|
|
626
|
+
```
|
|
577
627
|
|
|
578
|
-
|
|
628
|
+
#### ポリシー読み込みとリクエスト検査
|
|
579
629
|
|
|
580
630
|
```ts
|
|
581
631
|
import { core } from '@h-ai/core'
|
|
582
632
|
|
|
583
|
-
|
|
584
|
-
|
|
633
|
+
// 可改为 loadPolicy({ file: './config/security/request-access.yml' }) 在启动时读独立文件。
|
|
634
|
+
const policy = core.access.loadPolicy({
|
|
635
|
+
rules: [{ id: 'external-api', paths: ['/api/external/**'], methods: ['POST'], allowIps: ['192.0.2.0/24'], encryption: 'disabled' }],
|
|
585
636
|
})
|
|
586
|
-
|
|
587
|
-
const
|
|
637
|
+
// policy.rules[0] 保留上方规则,并补 denyIps: []、windows: []、timezone: 'UTC'。
|
|
638
|
+
const check = core.access.createRequestCheck(policy) // 返回请求检查函数,另带 encryption 方法和 requiresEncryption 属性。
|
|
639
|
+
const decision = check('/api/external/sync', 'POST', '192.0.2.10') // { allowed: true }。
|
|
640
|
+
check.encryption('/api/external/sync', 'POST') // disabled,供 HTTP 框架选择传输模式。
|
|
641
|
+
check.requiresEncryption // false;本例没有 required 规则。
|
|
642
|
+
core.logger.info('Request access checked', { allowed: decision.allowed })
|
|
588
643
|
```
|
|
589
644
|
|
|
590
|
-
|
|
645
|
+
- 検査器作成時に規則を検証・コンパイルします。`loadPolicy({ file })` は起動時に最大 1 MiB の独立 YAML を読みます。設定不正は例外となり、ホストの起動を停止します。ファイル指定とインライン規則は混在できません。
|
|
646
|
+
- グローバル条件とすべての一致規則を満たす必要があり、拒否 IP を優先します。空の IP/時間リストはその軸を制限せず、既定時区は UTC です。パスにはデプロイ接頭辞を含め、完全一致または末尾 `/**` を使います。GET は HEAD を含みません。
|
|
647
|
+
- `createClientIpResolver()` の既定 `auto` は通常ホストで転送ヘッダーを無視し、管理された非公開 Compose だけでコンテナー網を使います。`[]` はプロキシ信頼を無効にします。独自管理の入口では実際のプロキシ IP/CIDR を指定し、対向アドレスは socket または信頼できる基盤から取得します。
|
|
648
|
+
- パスの `encryption` は `required` / `disabled` で、省略時はホストに従います。パスとメソッドが重なる逆の指定は作成時に失敗します。ホストは `check.encryption()` / `requiresEncryption` で通信モードを組み込みます。Core は鍵を保持せず暗号化も行わないため、HTTPS と認証は引き続き必要です。
|
|
591
649
|
|
|
592
|
-
|
|
650
|
+
既定値、独立ファイル、プロキシ構成、デプロイ手順は[アクセス制御ガイド](../../docs/usage/configuration/request-access.md)を参照してください。
|
|
593
651
|
|
|
594
|
-
|
|
652
|
+
### ID の生成と検証:`core.id`
|
|
595
653
|
|
|
596
|
-
|
|
654
|
+
業務 ID とログ関連番号を作り、UUID v4/nanoid の形式を確認します。
|
|
597
655
|
|
|
598
|
-
|
|
656
|
+
```ts
|
|
657
|
+
import { core } from '@h-ai/core'
|
|
599
658
|
|
|
600
|
-
|
|
659
|
+
// 以下随机值仅展示格式,每次执行都不同。
|
|
660
|
+
core.id.generate() // 如 'V1StGXR8_Z5jdHi6B-myT',默认 21 字符;generate(8) 返回 8 字符。
|
|
661
|
+
const shortId = core.id.short() // 如 'V1StGXR8_Z',10 字符。
|
|
662
|
+
core.id.withPrefix('order_') // 如 'order_V1StGXR8_Z5jdHi6B-myT',前缀后接 21 字符。
|
|
663
|
+
core.id.trace() // 如 'trace-V1StGXR8_Z5jdHi6B-myT'。
|
|
664
|
+
core.id.request() // 如 'req-V1StGXR8_Z5jdHi6B-myT'。
|
|
665
|
+
const uuid = core.id.uuid() // 如 '550e8400-e29b-41d4-a716-446655440000',UUID v4。
|
|
666
|
+
core.id.isValidUUID(uuid) // true。
|
|
667
|
+
core.id.isValidNanoId(shortId, 10) // true。
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
ID を秘密鍵には使いません。UUID は暗号学的乱数を必要とし、なければ例外です。_core.yml に全体の長さ・接頭辞設定はなく、呼び出しごとに指定します。
|
|
671
|
+
|
|
672
|
+
### 値の型確認:`core.typeUtils`
|
|
601
673
|
|
|
602
|
-
|
|
674
|
+
境界の値を型判定し、分岐内で TypeScript の型を絞り込みます。業務 Schema の検証は別途必要です。
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
import { core } from '@h-ai/core'
|
|
603
678
|
|
|
604
|
-
|
|
679
|
+
core.typeUtils.isDefined(null) // false,同时排除 null 和 undefined。
|
|
680
|
+
core.typeUtils.isObject({ name: 'orders' }) // true;排除 null/数组,但也接受 Date 和类实例。
|
|
681
|
+
core.typeUtils.isFunction(() => {}) // true。
|
|
682
|
+
core.typeUtils.isPromise(Promise.resolve('ready')) // true。
|
|
683
|
+
core.typeUtils.isString('orders') // true。
|
|
684
|
+
core.typeUtils.isNumber(3000) // true;传 NaN 返回 false。
|
|
685
|
+
core.typeUtils.isBoolean(false) // true。
|
|
686
|
+
core.typeUtils.isArray([1, 2]) // true。
|
|
687
|
+
```
|
|
605
688
|
|
|
606
|
-
|
|
689
|
+
### オブジェクト:`core.object`
|
|
607
690
|
|
|
608
|
-
|
|
691
|
+
複製、結合、項目選択と、型付き keys/values/entries、fromEntries を提供します。
|
|
609
692
|
|
|
610
693
|
```ts
|
|
611
|
-
import type { LogRecord } from '@h-ai/core'
|
|
612
694
|
import { core } from '@h-ai/core'
|
|
613
695
|
|
|
614
|
-
const
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
unsubscribe()
|
|
696
|
+
const defaults = { name: 'orders', port: 3000 }
|
|
697
|
+
const copy = core.object.deepClone(defaults) // { name: 'orders', port: 3000 },新副本。
|
|
698
|
+
// 合并局部覆盖时,显式保留包含 name/port 的完整配置类型。
|
|
699
|
+
const settings = core.object.deepMerge<typeof defaults>(copy, { port: 4000 }) // { name: 'orders', port: 4000 }。
|
|
700
|
+
core.object.pick(settings, ['name']) // { name: 'orders' }。
|
|
701
|
+
core.object.omit(settings, ['port']) // { name: 'orders' }。
|
|
702
|
+
core.object.keys(settings) // ['name', 'port'],元素类型为 'name' | 'port'。
|
|
703
|
+
core.object.values(settings) // ['orders', 4000]。
|
|
704
|
+
core.object.entries(settings) // [['name', 'orders'], ['port', 4000]]。
|
|
705
|
+
core.object.fromEntries([['mode', 'local']]) // { mode: 'local' }。
|
|
625
706
|
```
|
|
626
707
|
|
|
627
|
-
|
|
708
|
+
deepClone は structuredClone で Date/Map/Set/循環など複製可能な値を扱い、関数など非対応値では例外です。deepMerge は通常オブジェクトのみ再帰結合し、配列・Date・集合・クラスは後の値で置換し、入力は変更しません。インスタンスの複製ではありません。
|
|
628
709
|
|
|
629
|
-
|
|
710
|
+
### 文字列:`core.string`
|
|
630
711
|
|
|
631
|
-
|
|
712
|
+
命名形式の変換、表示文字列の処理、空白判定、文字列比較に使います。
|
|
632
713
|
|
|
633
|
-
|
|
714
|
+
```ts
|
|
715
|
+
import { core } from '@h-ai/core'
|
|
716
|
+
|
|
717
|
+
core.string.capitalize('hello') // 'Hello'。
|
|
718
|
+
core.string.kebabCase('helloWorld') // 'hello-world'。
|
|
719
|
+
core.string.camelCase('hello-world') // 'helloWorld'。
|
|
720
|
+
core.string.snakeCase('helloWorld') // 'hello_world'。
|
|
721
|
+
core.string.pascalCase('hello-world') // 'HelloWorld'。
|
|
722
|
+
core.string.truncate('abcdef', 3) // 'abc...'。
|
|
723
|
+
core.string.trim(' hello ') // 'hello'。
|
|
724
|
+
core.string.isBlank(' ') // true。
|
|
725
|
+
core.string.isNotBlank('hello') // true。
|
|
726
|
+
core.string.padStart('5', 3, '0') // '005'。
|
|
727
|
+
core.string.padEnd('5', 3, '0') // '500'。
|
|
728
|
+
core.string.constantTimeEqual('same', 'same') // true;安全边界见下方说明。
|
|
729
|
+
```
|
|
634
730
|
|
|
635
|
-
|
|
731
|
+
Node の constantTimeEqual は同じ長さの UTF-16 バイト列をネイティブ timingSafeEqual で比較し、長さが違えば即座に false です。ブラウザーは定時間保証がなく、サーバー認証の検査には使いません。
|
|
636
732
|
|
|
637
|
-
|
|
733
|
+
### 配列:`core.array`
|
|
638
734
|
|
|
639
|
-
|
|
735
|
+
入力配列を保持しながら、重複除去、分割、分類、nullish 値の除外、積集合・差集合を計算します。
|
|
640
736
|
|
|
641
|
-
|
|
737
|
+
```ts
|
|
738
|
+
import { core } from '@h-ai/core'
|
|
739
|
+
|
|
740
|
+
core.array.unique([1, 1, 2]) // [1, 2]。
|
|
741
|
+
core.array.chunk([1, 2, 3], 2) // [[1, 2], [3]]。
|
|
742
|
+
core.array.groupBy([{ role: 'admin' }, { role: 'user' }], item => item.role)
|
|
743
|
+
// => { admin: [{ role: 'admin' }], user: [{ role: 'user' }] }
|
|
744
|
+
core.array.first([1, 2, 3]) // 1;空数组为 undefined。
|
|
745
|
+
core.array.last([1, 2, 3]) // 3;空数组为 undefined。
|
|
746
|
+
core.array.flatten([[1], [2, 3]]) // [1, 2, 3],只展开一层。
|
|
747
|
+
core.array.compact([0, null, 1, undefined]) // [0, 1],保留 0/false/空字符串。
|
|
748
|
+
core.array.shuffle([1, 2, 3]) // 如 [2, 3, 1],次序随机;新数组不修改输入。
|
|
749
|
+
core.array.intersection([1, 2], [2, 3]) // [2]。
|
|
750
|
+
core.array.difference([1, 2], [2, 3]) // [1]。
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
groupBy は `constructor`/`__proto__` などと同名のキーにも対応します。chunk の大きさは正の整数で、size<=0 は空配列を返します。
|
|
754
|
+
|
|
755
|
+
### 非同期処理:`core.async`
|
|
756
|
+
|
|
757
|
+
遅延、タイムアウト、再試行、並行制限・直列処理と、入力やスクロールの debounce/throttle を行います。
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
import { core } from '@h-ai/core'
|
|
761
|
+
|
|
762
|
+
await core.async.delay(1) // 等待约 1ms 后得到 undefined;计时以运行环境为准。
|
|
763
|
+
await core.async.withTimeout(Promise.resolve('ready'), 1000) // 'ready';超时则 reject 为 Error。
|
|
764
|
+
await core.async.retry(async () => 'ready', { maxRetries: 3, delay: 10 }) // 'ready',本例首次即成功。
|
|
765
|
+
await core.async.parallel([1, 2, 3], async value => value * 2, 2) // [2, 4, 6],最多同时执行两项。
|
|
766
|
+
await core.async.serial([1, 2, 3], async value => value * 2) // [2, 4, 6],按输入顺序逐项执行。
|
|
767
|
+
|
|
768
|
+
// 两种包装保留回调参数类型;调用 search/scroll 时传入同样类型的参数。
|
|
769
|
+
const search = core.async.debounce((term: string) => core.logger.debug('Search requested', { term }), 100)
|
|
770
|
+
const scroll = core.async.throttle((position: number) => core.logger.debug('Scroll position', { position }), 100)
|
|
771
|
+
// debounce/throttle 均返回包装函数,调用时返回 undefined。
|
|
772
|
+
core.logger.setLevel('debug') // 返回 undefined;确保下面的 debug 日志可见。
|
|
773
|
+
search('orders') // 约 100ms 后回调收到 'orders';100ms 内连续调用只处理最后一次。
|
|
774
|
+
scroll(100) // 本例第一次调用立即回调,position 为 100;100ms 内最多执行一次。
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
withTimeout は待機だけを制限し、元の処理を中止しません。retry は例外/reject が対象で、失敗 HaiResult は再試行しません。現在の maxRetries は初回を含み、3 は最大 3 回です。parallel は 0 より大きい安全な整数の並行数が必要で、不正値は実行前に RangeError で失敗します。結果順序は入力と同じです。
|
|
778
|
+
|
|
779
|
+
### 日時:`core.time`
|
|
780
|
+
|
|
781
|
+
タイムスタンプ、Date、表示テキストを変換し、地域時刻の日付境界と相対時間を計算します。
|
|
782
|
+
|
|
783
|
+
```ts
|
|
784
|
+
import { core } from '@h-ai/core'
|
|
785
|
+
|
|
786
|
+
// date 与以下日期运算使用本机时区;parseDate 中的 Z 表示 UTC。
|
|
787
|
+
const date = new Date(2026, 0, 1, 12, 0, 0)
|
|
788
|
+
core.time.formatDate(date, 'YYYY-MM-DD HH:mm:ss') // '2026-01-01 12:00:00'。
|
|
789
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;相对时间使用中文。
|
|
790
|
+
core.time.timeAgo(new Date(Date.now() - 60_000)) // '1 分钟前'(立即执行时)。
|
|
791
|
+
core.time.now() // 当前毫秒时间戳,如 1767268800000;每次执行随时间变化。
|
|
792
|
+
core.time.nowSeconds() // 当前秒时间戳,如 1767268800;相当于当前毫秒时间戳除以 1000 后向下取整。
|
|
793
|
+
const parsed = core.time.parseDate('2026-01-01T00:00:00Z') // Date 对象,toISOString() 为 '2026-01-01T00:00:00.000Z'。
|
|
794
|
+
core.time.isValidDate(parsed) // true;Invalid Date 返回 false。
|
|
795
|
+
core.time.addDays(date, 7) // 新 Date,本机时间 2026-01-08 12:00:00。
|
|
796
|
+
core.time.addHours(date, 2) // 新 Date,本机时间 2026-01-01 14:00:00。
|
|
797
|
+
core.time.startOfDay(date) // 新 Date,本机时间 2026-01-01 00:00:00.000。
|
|
798
|
+
core.time.endOfDay(date) // 新 Date,本机时间 2026-01-01 23:59:59.999。
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
formatDate は YYYY/MM/DD/HH/mm/ss に対応し、既定は YYYY-MM-DD です。日付計算は新しい Date を返します。parseDate の不正入力は自動例外ではなく無効な Date になるため isValidDate で確認します。
|
|
802
|
+
|
|
803
|
+
詳しい引数と低頻度の選択肢は [Core 利用リファレンス](../cli/templates/skills/hai-core/reference.md)を参照し、公開契約は現在の宣言に従ってください。
|
|
642
804
|
|
|
643
805
|
## 開発とテスト
|
|
644
806
|
|
|
807
|
+
リポジトリールートで実行します(PowerShell は `pnpm.cmd`)。
|
|
808
|
+
|
|
645
809
|
```bash
|
|
810
|
+
pnpm --filter @h-ai/core typecheck
|
|
811
|
+
pnpm --filter @h-ai/core lint
|
|
812
|
+
pnpm --filter @h-ai/core build
|
|
646
813
|
pnpm --filter @h-ai/core test
|
|
647
814
|
```
|
|
648
815
|
|
|
649
|
-
|
|
816
|
+
公開宣言と例の検証は[モジュール JSDoc 規約](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)を参照してください。
|
|
650
817
|
|
|
651
818
|
## ライセンス
|
|
652
819
|
|