@h-ai/core 0.1.0-alpha9 → 1.0.0-beta.1
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 +653 -0
- package/README.ja.md +653 -0
- package/README.md +264 -33
- package/dist/browser.d.ts +11 -31
- package/dist/browser.js +114 -26
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-FVCFFZP6.js → chunk-BEGTB6GL.js} +9760 -3160
- package/dist/chunk-BEGTB6GL.js.map +1 -0
- package/dist/core-types-_JqVAGGT.d.ts +2742 -0
- package/dist/node.d.ts +13 -70
- package/dist/node.js +491 -55
- package/dist/node.js.map +1 -1
- package/package.json +23 -16
- package/dist/chunk-FVCFFZP6.js.map +0 -1
- package/dist/core-util-module-B-wH5_ZT.d.ts +0 -1626
package/README.ja.md
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# @h-ai/core
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[フレームワークのホーム](../../README.ja.md)
|
|
6
|
+
|
|
7
|
+
## 概要
|
|
8
|
+
|
|
9
|
+
hai Agent Framework の中核モジュール。共通ツール、型定義、設定管理、ログ機能を提供します。
|
|
10
|
+
|
|
11
|
+
## 主な機能
|
|
12
|
+
|
|
13
|
+
- **HaiResult 型**:関数型のエラー処理(`ok` / `err`)とモジュール共通の戻り値形式。
|
|
14
|
+
- **統一ログ**:Node.js は pino、ブラウザーは loglevel を使い、API は共通。
|
|
15
|
+
- **国際化(i18n)**:一元化された locale 管理と型安全なメッセージ取得。
|
|
16
|
+
- **Zod 検証エラーの変換**:Zod v3/v4 の issue 抽出と、既定の英語メッセージのローカライズ。
|
|
17
|
+
- **設定管理**:YAML 読み込み、規約に沿う環境変数マッピング、明示的な変数展開、Zod 検証、ファイル監視(Node.js 専用)。
|
|
18
|
+
- **ID 生成**:nanoid と UUID v4。
|
|
19
|
+
- **エラー定義**:標準化されたコードと、モジュール共通のエラー定義・インスタンス生成。
|
|
20
|
+
- **モジュールツール**:NotInitializedKit のプロキシと共通のライフサイクル基盤。
|
|
21
|
+
- **ツール関数**:型確認、オブジェクト・文字列・配列・非同期・時刻の操作。
|
|
22
|
+
|
|
23
|
+
## 使い始める
|
|
24
|
+
|
|
25
|
+
### クイックスタート
|
|
26
|
+
|
|
27
|
+
#### Node.js 環境
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { core } from '@h-ai/core'
|
|
31
|
+
|
|
32
|
+
// 初始化:加载配置目录(可选)
|
|
33
|
+
core.init({ configDir: './config', watchConfig: true })
|
|
34
|
+
|
|
35
|
+
// 日志
|
|
36
|
+
core.logger.info('Server started', { port: 3000 })
|
|
37
|
+
core.logger.warn('Slow query', { duration: 2500 })
|
|
38
|
+
core.logger.error('Connection failed', { error })
|
|
39
|
+
|
|
40
|
+
// 创建模块级 logger(独立名称空间)
|
|
41
|
+
const dbLogger = core.logger.create({ name: 'db' })
|
|
42
|
+
dbLogger.info('Connected to PostgreSQL')
|
|
43
|
+
|
|
44
|
+
// 创建子 logger(自动携带固定上下文,适合请求级/操作级追踪)
|
|
45
|
+
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
46
|
+
reqLogger.info('Request started') // 日志自动包含 requestId + userId
|
|
47
|
+
reqLogger.error('Request failed') // 同上
|
|
48
|
+
|
|
49
|
+
// 子 logger 可嵌套
|
|
50
|
+
const opLogger = reqLogger.child({ operation: 'createOrder' })
|
|
51
|
+
opLogger.info('Order created') // 包含 requestId + userId + operation
|
|
52
|
+
|
|
53
|
+
// i18n
|
|
54
|
+
core.i18n.setGlobalLocale('zh-CN')
|
|
55
|
+
const getMessage = core.i18n.createMessageGetter({
|
|
56
|
+
'zh-CN': { welcome: '欢迎 {name}', error: '发生错误:{msg}' },
|
|
57
|
+
'en-US': { welcome: 'Welcome {name}', error: 'Error: {msg}' },
|
|
58
|
+
})
|
|
59
|
+
getMessage('welcome', { params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
60
|
+
|
|
61
|
+
// ID 生成
|
|
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_')
|
|
66
|
+
|
|
67
|
+
// 工具函数
|
|
68
|
+
core.object.deepMerge(objA, objB)
|
|
69
|
+
core.sanitize.sanitizeSensitiveFields({ password: 'secret', email: 'user@example.com' })
|
|
70
|
+
core.string.capitalize('hello')
|
|
71
|
+
core.array.unique([1, 1, 2, 3])
|
|
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)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### ブラウザー環境
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { core } from '@h-ai/core'
|
|
83
|
+
|
|
84
|
+
// 初始化日志级别(浏览器不支持配置加载)
|
|
85
|
+
core.init({ logging: { level: 'warn' } })
|
|
86
|
+
|
|
87
|
+
core.logger.info('Page ready')
|
|
88
|
+
const id = core.id.generate()
|
|
89
|
+
|
|
90
|
+
// 其他工具函数与 Node.js 相同
|
|
91
|
+
core.string.capitalize('hello')
|
|
92
|
+
core.array.unique([1, 1, 2])
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
ブラウザー版は型定義に合わせて `core.config` の API 形状を保持しますが、ファイル設定には対応しません:
|
|
96
|
+
|
|
97
|
+
- `core.config.get()` は `undefined`、`has()` は `false`、`keys()` は `[]` を返します。
|
|
98
|
+
- `loadEnvFiles()` / `load()` / `validate()` / `reload()` は `HaiCommonError.SERVICE_UNAVAILABLE` を返します。
|
|
99
|
+
- `watch()` は同じエラーで直ちにコールバックを呼び、安全な no-op の監視解除関数を返します。
|
|
100
|
+
- 設定が必要な場合はサーバーで読み込み API で渡すか、ブラウザーでは `core.init({ logging })` でログのみ設定してください。
|
|
101
|
+
|
|
102
|
+
### API の詳細
|
|
103
|
+
|
|
104
|
+
#### ログ(Logger)
|
|
105
|
+
|
|
106
|
+
`core.logger` は既定の Logger インスタンスであり、ログ管理の共通入口でもあります。
|
|
107
|
+
|
|
108
|
+
##### ログの記録
|
|
109
|
+
|
|
110
|
+
6 つのレベル:`trace` / `debug` / `info` / `warn` / `error` / `fatal`。
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
core.logger.info('User created', { userId: '123' })
|
|
114
|
+
core.logger.error('Failed to connect', { error })
|
|
115
|
+
core.logger.debug('Processing item', { id, step: 'validation' })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
##### モジュール用 logger の作成
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// 独立名称空间,适合模块内全局使用
|
|
122
|
+
const logger = core.logger.create({ name: 'payment' })
|
|
123
|
+
logger.info('Payment processed')
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
##### 子 logger(child)
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// 携带固定上下文,每条日志自动附带这些字段
|
|
130
|
+
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
131
|
+
reqLogger.info('Request started') // 输出包含 requestId + userId
|
|
132
|
+
reqLogger.warn('Slow response') // 同上
|
|
133
|
+
|
|
134
|
+
// 嵌套 child:子上下文递归合并
|
|
135
|
+
const opLogger = reqLogger.child({ operation: 'login' })
|
|
136
|
+
opLogger.info('Login succeeded') // 输出包含 requestId + userId + operation
|
|
137
|
+
opLogger.error('Login failed', { reason: 'invalid_password' })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> **典型的な使い方**:リクエスト入口で `reqLogger` を作成してサービス層に渡し、サービス層で `child` に操作コンテキストを追加します。ログに処理全体のコンテキストが自動的に含まれます。
|
|
141
|
+
|
|
142
|
+
##### ログの管理
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
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
|
+
})
|
|
152
|
+
|
|
153
|
+
core.logger.setLevel('debug')
|
|
154
|
+
const level = core.logger.getLevel() // 'debug'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`core.init()` で `_core.yml.logging` を読み込んだ後、または `configure` / `setLevel` の後は、既定・モジュール・ネストした child Logger の次の出力に現在の共通設定を適用します。初期化前に作成したインスタンスも対象です。`create({ level, format, context })` の明示指定は優先され、省略項目は共通設定を動的に継承します。呼び出し時の context は固定 context を上書きし、追加 `redact` 規則も全インスタンスに適用するため、モジュール Logger の再作成は不要です。
|
|
158
|
+
|
|
159
|
+
ブラウザーでは引き続き DevTools console に出力します。`pretty` は読みやすいメッセージと context、`json` は `level` / `msg` を含む JSON 文字列です。既定の認証情報マスキングと設定したパスのマスキングを併用し、ネストしたドット・括弧パスと `*` に対応します。メッセージ本文の機密情報は呼び出し側で除去してください。
|
|
160
|
+
|
|
161
|
+
ログ文脈のネイティブ `Error` は、`name`、`message`、`stack`、`cause`、`AggregateError.errors` と列挙可能な自身の文脈を持つコピーに変換してから、既定のフィールド/URL マスクと設定済み `redact` を適用します。両環境で同じ規則を使い、元の例外は変更しません。例外サブグラフの循環は `[Circular]`、32 階層の上限は `[MaxDepth]` になり、この範囲の独自置換値にも同じ正規化を適用します。通常オブジェクトの置換動作は変えません。Error を含まない通常オブジェクトの循環は元の構造を保持し、直接 JSON 化できる保証はありません。例外文中の `passwordHash=...` や `secretKey=...` など明示的な認証情報、認証ヘッダー、URL の認証情報はマスクします。ラベルのない任意の秘密は検出できないため、呼び出し側で除去するか `error.message` / `error.stack` などのパス全体を隠してください。
|
|
162
|
+
|
|
163
|
+
`core.logger` は既定でログの機密情報をマスクします。`password`、`passwordHash`、`passwd`、`token`、`secret`、`secretKey`、`privateKey`、`apiV3Key` などは `[REDACTED]` に置換されます。`url` / `baseUrl` / `endpoint` は URL 内のユーザー名・パスワード、機密クエリ値、OAuth 形式の fragment をマスクします。不正な URL は固定値を返します。メールアドレス・電話番号は既定では隠しません。
|
|
164
|
+
|
|
165
|
+
##### ログレベルの規約
|
|
166
|
+
|
|
167
|
+
| レベル | 用途 |
|
|
168
|
+
| ------- | ---------------------------------------- |
|
|
169
|
+
| `trace` | ループ内の詳細なデバッグデータ |
|
|
170
|
+
| `debug` | 関数開始、引数の概要、読み取り操作 |
|
|
171
|
+
| `info` | 業務イベント(初期化完了、書き込み成功) |
|
|
172
|
+
| `warn` | 回復可能な問題(検証失敗、再試行) |
|
|
173
|
+
| `error` | 調査が必要な操作の失敗 |
|
|
174
|
+
| `fatal` | サービス継続不能の致命的エラー |
|
|
175
|
+
|
|
176
|
+
#### 国際化(i18n)
|
|
177
|
+
|
|
178
|
+
組み込みの言語は `zh-CN`、`en-US`、`ja-JP` です。略記 `ja` は `ja-JP` に正規化され、すべてのモジュールで設定を共有します。
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
182
|
+
core.i18n.setGlobalLocale('en-US')
|
|
183
|
+
const current = core.i18n.getGlobalLocale() // 'en-US'
|
|
184
|
+
|
|
185
|
+
// 为每个模块创建消息获取器
|
|
186
|
+
const getMessage = core.i18n.createMessageGetter({
|
|
187
|
+
'zh-CN': { welcome: '欢迎 {name}', error: '错误:{code}' },
|
|
188
|
+
'en-US': { welcome: 'Welcome {name}', error: 'Error: {code}' },
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// 使用:自动读取全局 locale,支持参数插值
|
|
192
|
+
getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
193
|
+
|
|
194
|
+
// 支持 locale 参数覆盖
|
|
195
|
+
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
196
|
+
|
|
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
|
+
```
|
|
201
|
+
|
|
202
|
+
#### 機密情報のマスク
|
|
203
|
+
|
|
204
|
+
`core.sanitize.sanitizeSensitiveFields()` はオブジェクト・配列内の機密情報を再帰的にマスクします。既定では password/passwordHash、passwd、token、secret/secretKey、privateKey、apiV3Key などを処理し、`url` / `baseUrl` / `endpoint` の認証情報もマスクします。`core.sanitize.sanitizeUrl(url, options)` は単独の URL にも使用でき、不正な URL は `(invalid url)` を返します。独自の matcher はクエリ名にも適用されます。`sanitizeText` は明示的な認証情報と URL を処理し、既知の秘密値も指定できますが、ラベルのない任意の秘密は検出しません。
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
core.sanitize.sanitizeSensitiveFields({
|
|
208
|
+
email: 'user@example.com',
|
|
209
|
+
password: 'secret',
|
|
210
|
+
baseUrl: 'https://user:pass@example.com/api/v1',
|
|
211
|
+
profile: { token: 'jwt-token' },
|
|
212
|
+
})
|
|
213
|
+
// => { email: 'user@example.com', password: '[REDACTED]', baseUrl: 'https://[REDACTED]:[REDACTED]@example.com/api/v1', profile: { token: '[REDACTED]' } }
|
|
214
|
+
|
|
215
|
+
core.sanitize.sanitizeSensitiveFields({ email: 'user@example.com' }, {
|
|
216
|
+
matcher: /email/i,
|
|
217
|
+
replacement: '***',
|
|
218
|
+
})
|
|
219
|
+
// => { email: '***' }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Zod 検証エラーの i18n 変換
|
|
223
|
+
|
|
224
|
+
`core.zodValidation` は Zod の `SafeParseError` / `ZodError` をフラットなフォームエラーに変換し、
|
|
225
|
+
Zod の既定の英語メッセージを呼び出し元の i18n 文言に変換します。
|
|
226
|
+
|
|
227
|
+
厳密なスキーマで拒否された未知の項目は `validationUnexpectedFields` に変換し、内部の項目名をユーザーに表示しません。独自のメッセージ取得関数はこの key に対応してください。Kit と Serv は中英日文のメッセージを提供します。スキーマに明示した業務メッセージは保持します。
|
|
228
|
+
|
|
229
|
+
- 公開入口は 2 つです:
|
|
230
|
+
- `createPrefixedZodMessageGetter()`:モジュールの接頭辞から `serv_validation*` / `kit_validation*` などの key を生成し、手書きの `KEY_MAP` を省略。
|
|
231
|
+
- `mapZodErrorToFormErrors()`:抽出・ローカライズ・フラット化を一度に実行。
|
|
232
|
+
- 独自の業務メッセージは保持し、Zod の既定の英語メッセージのみを置換します。
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import type { ZodMessageGetter } from '@h-ai/core'
|
|
236
|
+
import { core } from '@h-ai/core'
|
|
237
|
+
import { z } from 'zod'
|
|
238
|
+
|
|
239
|
+
const LoginSchema = z.object({
|
|
240
|
+
email: z.string().email(),
|
|
241
|
+
password: z.string().min(8),
|
|
242
|
+
nickname: z.string().min(1, '请填写昵称'),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const getMessage: ZodMessageGetter = (key, params) => {
|
|
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
|
+
}
|
|
257
|
+
|
|
258
|
+
const result = LoginSchema.safeParse({
|
|
259
|
+
email: 'bad',
|
|
260
|
+
password: '123',
|
|
261
|
+
nickname: '',
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
if (!result.success) {
|
|
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 自定义消息原样保留)
|
|
280
|
+
|
|
281
|
+
core.logger.info('Validation failed', { errors })
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### 設定管理(Node.js 専用)
|
|
286
|
+
|
|
287
|
+
> ブラウザーにはファイルシステムがないため、`core.config` は代替 API の形状のみを提供します。ファイル関連メソッドは `SERVICE_UNAVAILABLE` を返し、YAML を読み込みません。
|
|
288
|
+
|
|
289
|
+
Node は設定ディレクトリー内のすべての `.yml` / `.yaml` を自動読み込み・監視します。CLI はプロジェクトルートの `hai.json` から `build` / `deploy` を読み取り、特別扱いする YAML ファイル名はありません。単一ファイルの失敗結果はログに記録して続行するため、アプリは必須設定を validate して `success` を確認してください。ディレクトリー走査や Logger 設定などの例外は伝播する場合があります。
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { core, HaiConfigError } from '@h-ai/core'
|
|
293
|
+
import { z } from 'zod'
|
|
294
|
+
|
|
295
|
+
// 方式 1:init 时自动加载运行时配置
|
|
296
|
+
core.init({
|
|
297
|
+
configDir: './config', // 扫描配置目录内所有 .yml / .yaml
|
|
298
|
+
watchConfig: true, // 监听变更并自动重载
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// 方式 2:手动加载单个文件(可选 Schema 校验)
|
|
302
|
+
const AppSchema = z.object({
|
|
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
|
+
}
|
|
313
|
+
|
|
314
|
+
// 校验已加载的配置
|
|
315
|
+
const validResult = core.config.validate('app', AppSchema)
|
|
316
|
+
|
|
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', ...]
|
|
323
|
+
|
|
324
|
+
// 重新加载
|
|
325
|
+
const reloadResult = core.config.reload('app')
|
|
326
|
+
|
|
327
|
+
// 监听文件变更
|
|
328
|
+
const unwatch = core.config.watch('app', (newConfig, error) => {
|
|
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() // 取消监听
|
|
336
|
+
|
|
337
|
+
// 清除缓存(同时停止对应监听)
|
|
338
|
+
core.config.clear('app') // 清除单个
|
|
339
|
+
core.config.clear() // 清除全部
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
##### dotenv ファイルと環境切り替え
|
|
343
|
+
|
|
344
|
+
Node 専用の `core.config.loadEnvFiles({ mode, envDir })` は HaiResult を返し、`.env` → `.env.<mode>` → `.env.local` の順で読み込みます。既存のプロセス変数が最優先です。development/production は dev/prod に対応し、存在しないファイルは省略します。プロセスは変更しないため、success を確認してから代入します。公開設定と `.env.example` は Git 登録し、私有の `.env.local` は除外します。core.init は dotenv を読み込まず、ブラウザーでは SERVICE_UNAVAILABLE を返します。
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const env = core.config.loadEnvFiles({ mode: 'dev', envDir: '.' })
|
|
348
|
+
if (!env.success)
|
|
349
|
+
throw new Error(env.error.message)
|
|
350
|
+
Object.assign(process.env, env.data)
|
|
351
|
+
core.init({ configDir: './config' })
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
##### 環境変数のマッピングと展開
|
|
355
|
+
|
|
356
|
+
`core.config.load(name, ...)` は YAML の各末端設定を
|
|
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` です。
|
|
361
|
+
|
|
362
|
+
環境変数が存在すれば YAML を上書きし、YAML のスカラー規則で number / boolean などに変換します。
|
|
363
|
+
変数がなければ YAML 値を保持します。オブジェクト・配列全体も同名の JSON 変数で置換でき、HAI_DB は接続設定全体、HAI_REACH_PROVIDERS は配信チャネル配列を置換します。親ノード全体の上書きが子フィールド変数より優先され、置換後も Schema 検証を行います。
|
|
364
|
+
|
|
365
|
+
```yaml
|
|
366
|
+
# config/_ai.yml(core.init 扫描后配置名为 ai)
|
|
367
|
+
llm:
|
|
368
|
+
apiKey: ''
|
|
369
|
+
baseUrl: https://api.xiaomimimo.com/v1
|
|
370
|
+
model: mimo-v2.5
|
|
371
|
+
maxTokens: 4096
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```dotenv
|
|
375
|
+
HAI_AI_LLM_APIKEY=your-api-key
|
|
376
|
+
HAI_AI_LLM_MAXTOKENS=8192
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
特殊な変数名が必要な場合は明示的な `${VAR}` を使用します。優先順位は固定です:
|
|
380
|
+
規約に沿う環境変数 > 明示的な環境変数 > YAML の既定値。
|
|
381
|
+
|
|
382
|
+
- `${VAR}`:`process.env.VAR` を読み、存在しなければ `CONFIG_ENV_VAR_MISSING`。
|
|
383
|
+
- `${VAR:default}`:`process.env.VAR` を読み、存在しなければコロン以降の既定値を使用。
|
|
384
|
+
|
|
385
|
+
**型の復元:** 値全体が単一の変数式(`${PORT:3000}` など)の場合、YAML 規則で number / boolean などの型に戻します。文字列との混在は常に文字列です。
|
|
386
|
+
|
|
387
|
+
`${VAR:}` や空文字列の `${VAR}` は `''` を保持し、YAML の `null` には変換しません。空文字列を未設定として扱うモジュールは、自身の Schema で `undefined` に正規化してください。
|
|
388
|
+
|
|
389
|
+
```yaml
|
|
390
|
+
# config/app.yml
|
|
391
|
+
debug: ${DEBUG:false} # → boolean false
|
|
392
|
+
port: ${PORT:3000} # → number 3000
|
|
393
|
+
database: ${DB_CONNECTION} # → 环境变量缺失时报错
|
|
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
|
|
494
|
+
import { core } from '@h-ai/core'
|
|
495
|
+
|
|
496
|
+
const notInitialized = core.module.createNotInitializedKit(
|
|
497
|
+
HaiDbError.NOT_INITIALIZED,
|
|
498
|
+
() => dbM('db_notInitialized'),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
// 创建占位 Proxy(异步接口,默认)
|
|
502
|
+
const currentDdl: DdlOperations = notInitialized.proxy<DdlOperations>()
|
|
503
|
+
// 创建占位 Proxy(同步接口)
|
|
504
|
+
const currentHash: HashOperations = notInitialized.proxy<HashOperations>('sync')
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### エラー処理
|
|
508
|
+
|
|
509
|
+
#### HaiResult 型
|
|
510
|
+
|
|
511
|
+
失敗する可能性がある操作は `HaiResult<T>` を返します:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
import type { HaiResult } from '@h-ai/core'
|
|
515
|
+
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
516
|
+
|
|
517
|
+
function divide(a: number, b: number): HaiResult<number> {
|
|
518
|
+
if (b === 0)
|
|
519
|
+
return err(HaiCommonError.VALIDATION_ERROR, 'Division by zero')
|
|
520
|
+
return ok(a / b)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const result = divide(10, 2)
|
|
524
|
+
if (result.success) {
|
|
525
|
+
core.logger.info('Result', { data: result.data })
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
core.logger.error('Error', { error: result.error })
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### 独自のエラーコード(モジュール単位)
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import type { ErrorInfo } from '@h-ai/core'
|
|
536
|
+
import { core } from '@h-ai/core'
|
|
537
|
+
|
|
538
|
+
// 定义错误码(格式 'code:httpStatus')
|
|
539
|
+
const DbErrorInfo = {
|
|
540
|
+
NOT_INITIALIZED: '001:500',
|
|
541
|
+
CONNECTION_FAILED: '010:500',
|
|
542
|
+
QUERY_FAILED: '011:500',
|
|
543
|
+
} as const satisfies ErrorInfo
|
|
544
|
+
|
|
545
|
+
// 生成错误定义对象
|
|
546
|
+
export const DbError = core.error.buildHaiErrorsDef('db', DbErrorInfo)
|
|
547
|
+
// DbError.CONNECTION_FAILED → { code: 'hai:db:010', httpStatus: 500, system: 'hai', module: 'db' }
|
|
548
|
+
|
|
549
|
+
// 创建错误实例
|
|
550
|
+
const errorInst = core.error.buildHaiErrorInst(
|
|
551
|
+
DbError.CONNECTION_FAILED,
|
|
552
|
+
'Unable to connect to PostgreSQL',
|
|
553
|
+
originalError, // cause(可选)
|
|
554
|
+
'Check connection string', // suggestion(可选)
|
|
555
|
+
)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### 設定エラーコード
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { HaiConfigError } from '@h-ai/core'
|
|
562
|
+
|
|
563
|
+
HaiConfigError.CONFIG_FILE_NOT_FOUND // hai:core:010 - 配置文件不存在
|
|
564
|
+
HaiConfigError.CONFIG_PARSE_ERROR // hai:core:011 - YAML 解析失败
|
|
565
|
+
HaiConfigError.CONFIG_VALIDATION_ERROR // hai:core:012 - Schema 校验失败
|
|
566
|
+
HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
567
|
+
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### リクエスト言語の分離
|
|
571
|
+
|
|
572
|
+
Node リクエストと SSR は `core.i18n.runWithLocale(locale, () => handleRequest())` で囲みます。SvelteKit は `kit.i18n.runWithLocale(locale, () => resolve(event))` を使えます。serv はリクエストヘッダーから自動設定します。非同期の子処理も言語を継承し、明示的な `MessageOptions.locale`、リクエスト言語、起動時の既定値の順で優先します。コンテキスト内の `setGlobalLocale` はそのリクエストだけを変更します。コンテキスト外の設定は起動時に限定し、リクエスト hook から呼びません。
|
|
573
|
+
|
|
574
|
+
ブラウザーはアプリ全体の `setGlobalLocale` / `createLocaleStore` を使います。SSR の UI store 初期化と setter は自身の状態のみ変更し、メッセージはリクエストコンテキストを参照します。`runWithLocale` は Node 入口専用です。
|
|
575
|
+
|
|
576
|
+
## 信頼する接続元とリクエスト制御(Node)
|
|
577
|
+
|
|
578
|
+
`core.access` は HTTP ホストが共用する IP・時間・パスの判定を提供し、HTTP 処理への登録は行いません。Serv は `http.access`、SvelteKit は `kit.createHandle({ access })` を使用します。IAM の `loginAccess` はログイン時だけの判定です。
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
import { core } from '@h-ai/core'
|
|
582
|
+
|
|
583
|
+
const check = core.access.createRequestCheck({
|
|
584
|
+
rules: [{ id: 'external-api', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'], encryption: 'disabled' }],
|
|
585
|
+
})
|
|
586
|
+
const decision = check('/api/v1/external/sync', 'POST', '192.0.2.10')
|
|
587
|
+
const mode = check.encryption('/api/v1/external/sync', 'POST') // disabled
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
生成時に設定を検証・コンパイルします。`loadPolicy({ file: './config/security/request-access.yml' })` は最大 1 MiB の YAML を起動時に一度読み、不正な設定では例外によりホストの起動を停止します。全体条件と一致するすべてのルールを満たす必要があり、拒否 IP が優先します。空の IP・時間リストは制限なし、既定のタイムゾーンは UTC です。パスは配備プレフィックスを含む実際の URL pathname で、完全一致と末尾 `/**` に対応します。GET に HEAD は含まれません。
|
|
591
|
+
|
|
592
|
+
接続元の解決は `core.access.createClientIpResolver()` を使用します。既定の `auto` は通常のホストで転送ヘッダーを無視し、管理対象 Compose の非公開バックエンド構成だけでコンテナのネットワークを採用します。`[]` は信頼を無効化し、独自の入口では実際の代理 IP/CIDR を指定します。任意のヘッダーを socket の接続元として渡してはいけません。要求中にファイル読取、CIDR 解析、全パスルールの走査は行いません。同じ分の時刻結果を再利用し、分境界で再判定します。`core.access` は Node のみです。設定 Schema は `core-config`、公開型は `core-types`、関数呼出は `core-main` に登録した `core.access` に集約します。
|
|
593
|
+
|
|
594
|
+
パスルールの `encryption` は `required` / `disabled` を指定でき、省略時はホスト設定に従います。選択はパス索引だけを参照し、IP や時刻を再計算しません。同じパスとメソッドに反対の指定が重なると、宣言順に関係なく起動時に失敗します。同じモードや異なるメソッドは組み合わせられます。`check.requiresEncryption` はホストに必要な暗号化を知らせます。Core は鍵や暗号処理を持たず、HTTPS、認証、アクセス条件は別途必要です。
|
|
595
|
+
|
|
596
|
+
既定値・代理条件・独立ファイル・配備と確認は[リクエスト制御手引き](../../docs/usage/configuration/request-access.md)を参照してください。
|
|
597
|
+
|
|
598
|
+
## 設定の消費範囲
|
|
599
|
+
|
|
600
|
+
`_core.yml` は name/version/env/defaultLocale のメタデータと logging のみを受け付け、不明なフィールドでは読み込みに失敗します。グローバルな debug、ID 長/接頭辞、業務 features、言語登録リストは提供しません。logging.level、core.id の呼び出し引数、独自 Schema で検証する app.yml を使用してください。defaultLocale はメタデータであり、翻訳登録や Paraglide の切り替えを自動実行しません。Logger の import は Windows TTY でも LANG、LC_ALL、PYTHONIOENCODING を変更しません。[設定と消費点の対応表(中国語)](../../.agents/skills/hai-module-create/configuration.md)を参照してください。
|
|
601
|
+
|
|
602
|
+
## リクエスト関連付けとログの監視
|
|
603
|
+
|
|
604
|
+
収集側の保存・アップロード処理は `core.context.run({ suppressLogObservers: true }, operation)` で自身の非同期ログ監視を一時停止できます。stdout など通常の出力は維持し、スコープ外では監視を再開します。
|
|
605
|
+
|
|
606
|
+
`core.logger.observe` は現在のレベルと既定・追加フィールドのマスキングを通過したレコードを受け取り、購読解除関数を返します。購読側は上限付きメモリに収集して非同期で保存してください。監視は永続化の完了を意味しません。コールバックの失敗と同期再入は業務処理から隔離されます。自由文や画像には収集側の追加プライバシー処理が必要です。
|
|
607
|
+
|
|
608
|
+
Node の `core.context.run` は AsyncLocalStorage で並行リクエストと子タスクを隔離し、既存 logger に requestId/userId/scopeId/jobId を付加します。認証範囲は信頼できるサーバーからのみ設定し、関連 ID を認可に使用しません。ブラウザーは同じ API を公開しますが、run は同期スコープのみで await をまたぐ隔離を提供しません。
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
import type { LogRecord } from '@h-ai/core'
|
|
612
|
+
import { core } from '@h-ai/core'
|
|
613
|
+
|
|
614
|
+
const records: LogRecord[] = []
|
|
615
|
+
const unsubscribe = core.logger.observe((record) => {
|
|
616
|
+
if (records.length === 100)
|
|
617
|
+
records.shift()
|
|
618
|
+
records.push(record)
|
|
619
|
+
})
|
|
620
|
+
await core.context.run({ requestId: 'request-1', scopeId: 'tenant-1', userId: 'user-1' }, async () => {
|
|
621
|
+
await Promise.resolve()
|
|
622
|
+
core.logger.warn('Business operation failed')
|
|
623
|
+
})
|
|
624
|
+
unsubscribe()
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## アプリケーションのライフサイクル
|
|
628
|
+
|
|
629
|
+
`core.module.createRuntime([{ name, enabled?, start, close }])` は宣言順で起動し、逆順で終了して並行呼出しを統合します。部分初期化前に清理義務を登録し、終了失敗時はモジュールと依存を保持して次回 close/start で再試行します。エラーはモジュール・段階・コード・設定パスのみを示し、値や SDK 例外を公開しません。base/API/fullstack と管理画面が利用します。
|
|
630
|
+
|
|
631
|
+
起動エラーには設定の原因と対処方法を含めます。`ext.fields` がある場合、`HaiConfigError.CONFIG_ENV_VAR_MISSING` は環境変数の未設定、`CONFIG_VALIDATION_ERROR` または Schema エラーは検証失敗を示します。設定パスだけの場合は原因を推測せず「設定が不足しているか無効」と案内します。起動境界が `error.message` だけを表示しても、設定項目・対処方法・元のエラーコードを確認できます。独自の初期化フックで必須環境変数が不足した場合は、`code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code` と `ext: { fields: ['VARIABLE_NAME'] }` を持つ例外を使用できます。fullstack テンプレートの初期管理者パスワード検査もこの方式を使用します。
|
|
632
|
+
|
|
633
|
+
## エラー表示とログの関連付け
|
|
634
|
+
|
|
635
|
+
HTTP エラーには安全な翻訳文と追跡番号を含めます。ToastContainer は番号を既定で非表示にし、調査情報のアイコンから表示・コピーできます。同じ ID を error.traceId と X-Trace-Id に返します。requestId を traceId として共有し、ブラウザーとサーバーのログを関連付けます。脱機密化した例外・スタック・操作情報はログに残し、内部 cause/ext は公開しません。項目検証は翻訳済みの項目名とメッセージだけを返します。
|
|
636
|
+
|
|
637
|
+
HTTP/UI 境界には core.error.toPublic(error, traceId?)、再翻訳後の表示には core.error.formatMessage(error) を使い、ID を重複追加しません。ログには完全な Error を渡してください。DB ログにはドライバーコード、脱機密化したメッセージ・スタック、表・列を残し、SQL 本文、バインド値と接続認証情報を記録しません。
|
|
638
|
+
|
|
639
|
+
### 公開する業務進捗
|
|
640
|
+
|
|
641
|
+
内部 ext は非公開です。スキーマ検証済みの業務進捗だけを publicExt で明示し、ext として公開します。内部例外を入れないでください。
|
|
642
|
+
|
|
643
|
+
## 開発とテスト
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
pnpm --filter @h-ai/core test
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
宣言の注釈と型検査できる例は [JSDoc 規約(中国語)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) を参照してください。Node core.init は同期的に void を返します。単一ファイルの config.load 失敗結果は記録して続行するため、必須設定は validate して success を確認してください。ディレクトリー走査などの例外は伝播する場合があります。業務 HaiResult API は success を先に確認します。
|
|
650
|
+
|
|
651
|
+
## ライセンス
|
|
652
|
+
|
|
653
|
+
Apache-2.0
|