@h-ai/core 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 +546 -0
- package/README.ja.md +546 -0
- package/README.md +4 -0
- package/package.json +1 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
# @h-ai/core
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[Framework home](../../README.en.md)
|
|
6
|
+
|
|
7
|
+
> The core of hai Agent Framework: shared utilities, types, configuration management, and logging.
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
- **HaiResult types**: functional error handling (`ok` / `err`) with a shared result format across modules.
|
|
12
|
+
- **Unified logging**: pino on Node.js and loglevel in browsers, with the same API.
|
|
13
|
+
- **Internationalization (i18n)**: centralized locale management and typed message getters.
|
|
14
|
+
- **Zod validation error mapping**: Zod v3/v4 issue extraction and localization of default English messages.
|
|
15
|
+
- **Configuration management**: YAML loading, convention-based environment mapping, explicit interpolation, Zod validation, and file watching (Node.js only).
|
|
16
|
+
- **ID generation**: nanoid and UUID v4.
|
|
17
|
+
- **Error definitions**: standardized codes and consistent error definitions and instances across modules.
|
|
18
|
+
- **Module utilities**: NotInitializedKit proxies and shared lifecycle infrastructure.
|
|
19
|
+
- **Utility library**: type checks and object / string / array / async / time operations.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### Node.js environment
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { core } from '@h-ai/core'
|
|
27
|
+
|
|
28
|
+
// 初始化:加载配置目录(可选)
|
|
29
|
+
core.init({ configDir: './config', watchConfig: true })
|
|
30
|
+
|
|
31
|
+
// 日志
|
|
32
|
+
core.logger.info('Server started', { port: 3000 })
|
|
33
|
+
core.logger.warn('Slow query', { duration: 2500 })
|
|
34
|
+
core.logger.error('Connection failed', { error })
|
|
35
|
+
|
|
36
|
+
// 创建模块级 logger(独立名称空间)
|
|
37
|
+
const dbLogger = core.logger.create({ name: 'db' })
|
|
38
|
+
dbLogger.info('Connected to PostgreSQL')
|
|
39
|
+
|
|
40
|
+
// 创建子 logger(自动携带固定上下文,适合请求级/操作级追踪)
|
|
41
|
+
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
42
|
+
reqLogger.info('Request started') // 日志自动包含 requestId + userId
|
|
43
|
+
reqLogger.error('Request failed') // 同上
|
|
44
|
+
|
|
45
|
+
// 子 logger 可嵌套
|
|
46
|
+
const opLogger = reqLogger.child({ operation: 'createOrder' })
|
|
47
|
+
opLogger.info('Order created') // 包含 requestId + userId + operation
|
|
48
|
+
|
|
49
|
+
// i18n
|
|
50
|
+
core.i18n.setGlobalLocale('zh-CN')
|
|
51
|
+
const getMessage = core.i18n.createMessageGetter({
|
|
52
|
+
'zh-CN': { welcome: '欢迎 {name}', error: '发生错误:{msg}' },
|
|
53
|
+
'en-US': { welcome: 'Welcome {name}', error: 'Error: {msg}' },
|
|
54
|
+
})
|
|
55
|
+
getMessage('welcome', { params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
56
|
+
|
|
57
|
+
// ID 生成
|
|
58
|
+
const nanoid = core.id.generate()
|
|
59
|
+
const shortId = core.id.short()
|
|
60
|
+
const uuid = core.id.uuid()
|
|
61
|
+
const prefixed = core.id.withPrefix('order_')
|
|
62
|
+
|
|
63
|
+
// 工具函数
|
|
64
|
+
core.object.deepMerge(objA, objB)
|
|
65
|
+
core.sanitize.sanitizeSensitiveFields({ password: 'secret', email: 'user@example.com' })
|
|
66
|
+
core.string.capitalize('hello')
|
|
67
|
+
core.array.unique([1, 1, 2, 3])
|
|
68
|
+
core.array.chunk([1, 2, 3, 4, 5], 2) // [[1,2], [3,4], [5]]
|
|
69
|
+
await core.async.delay(1000)
|
|
70
|
+
await core.async.retry(() => fetchData(), { maxRetries: 3, delay: 1000 })
|
|
71
|
+
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
72
|
+
core.typeUtils.isDefined(value)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Browser environment
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { core } from '@h-ai/core'
|
|
79
|
+
|
|
80
|
+
// 初始化日志级别(浏览器不支持配置加载)
|
|
81
|
+
core.init({ logging: { level: 'warn' } })
|
|
82
|
+
|
|
83
|
+
core.logger.info('Page ready')
|
|
84
|
+
const id = core.id.generate()
|
|
85
|
+
|
|
86
|
+
// 其他工具函数与 Node.js 相同
|
|
87
|
+
core.string.capitalize('hello')
|
|
88
|
+
core.array.unique([1, 1, 2])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The browser retains the `core.config` API shape for type compatibility, but does not support file configuration:
|
|
92
|
+
|
|
93
|
+
- `core.config.get()` returns `undefined`, `has()` returns `false`, and `keys()` returns `[]`.
|
|
94
|
+
- `load()` / `validate()` / `reload()` return `HaiCommonError.SERVICE_UNAVAILABLE`.
|
|
95
|
+
- `watch()` immediately calls its callback with the same error and returns a safe no-op unsubscribe function.
|
|
96
|
+
- Read configuration on the server and deliver it through an API, or use `core.init({ logging })` only to configure browser logging.
|
|
97
|
+
|
|
98
|
+
## Detailed API
|
|
99
|
+
|
|
100
|
+
### Logging (Logger)
|
|
101
|
+
|
|
102
|
+
`core.logger` is both the default Logger instance and the shared entry point for logger management.
|
|
103
|
+
|
|
104
|
+
#### Write logs
|
|
105
|
+
|
|
106
|
+
Six levels: `trace` / `debug` / `info` / `warn` / `error` / `fatal`.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
core.logger.info('User created', { userId: '123' })
|
|
110
|
+
core.logger.error('Failed to connect', { error })
|
|
111
|
+
core.logger.debug('Processing item', { id, step: 'validation' })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Create a module logger
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// 独立名称空间,适合模块内全局使用
|
|
118
|
+
const logger = core.logger.create({ name: 'payment' })
|
|
119
|
+
logger.info('Payment processed')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Child loggers
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// 携带固定上下文,每条日志自动附带这些字段
|
|
126
|
+
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
127
|
+
reqLogger.info('Request started') // 输出包含 requestId + userId
|
|
128
|
+
reqLogger.warn('Slow response') // 同上
|
|
129
|
+
|
|
130
|
+
// 嵌套 child:子上下文递归合并
|
|
131
|
+
const opLogger = reqLogger.child({ operation: 'login' })
|
|
132
|
+
opLogger.info('Login succeeded') // 输出包含 requestId + userId + operation
|
|
133
|
+
opLogger.error('Login failed', { reason: 'invalid_password' })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> **Typical usage**: create `reqLogger` at the request entry, pass it to the service layer, and add operation context with `child`. Logs then include the complete request context.
|
|
137
|
+
|
|
138
|
+
#### Logger management
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// 配置输出格式和级别(Node.js)
|
|
142
|
+
core.logger.configure({
|
|
143
|
+
level: 'info',
|
|
144
|
+
format: 'json', // 'json' | 'pretty'
|
|
145
|
+
context: { app: 'my-service' }, // 全局默认上下文
|
|
146
|
+
redact: ['password', 'token'], // 脱敏字段
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
core.logger.setLevel('debug')
|
|
150
|
+
const level = core.logger.getLevel() // 'debug'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`core.logger` redacts log context by default: credentials such as `password`, `token`, `secret`, `privateKey`, and `apiV3Key` become `[REDACTED]`. Fields such as `url` / `baseUrl` / `endpoint` mask only embedded usernames/passwords. Email addresses and phone numbers are not masked by default.
|
|
154
|
+
|
|
155
|
+
#### Log level conventions
|
|
156
|
+
|
|
157
|
+
| Level | Use case |
|
|
158
|
+
| ------- | --------------------------------------------------- |
|
|
159
|
+
| `trace` | Detailed debugging data inside loops |
|
|
160
|
+
| `debug` | Function entry, parameter summaries, reads |
|
|
161
|
+
| `info` | Business events (initialized, write succeeded) |
|
|
162
|
+
| `warn` | Recoverable problems (validation failures, retries) |
|
|
163
|
+
| `error` | Operation failures requiring investigation |
|
|
164
|
+
| `fatal` | Fatal errors preventing continued service |
|
|
165
|
+
|
|
166
|
+
### Internationalization (i18n)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
170
|
+
core.i18n.setGlobalLocale('en-US')
|
|
171
|
+
const current = core.i18n.getGlobalLocale() // 'en-US'
|
|
172
|
+
|
|
173
|
+
// 为每个模块创建消息获取器
|
|
174
|
+
const getMessage = core.i18n.createMessageGetter({
|
|
175
|
+
'zh-CN': { welcome: '欢迎 {name}', error: '错误:{code}' },
|
|
176
|
+
'en-US': { welcome: 'Welcome {name}', error: 'Error: {code}' },
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// 使用:自动读取全局 locale,支持参数插值
|
|
180
|
+
getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
181
|
+
|
|
182
|
+
// 支持 locale 参数覆盖
|
|
183
|
+
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
184
|
+
|
|
185
|
+
// 常量
|
|
186
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }]
|
|
187
|
+
core.i18n.DEFAULT_LOCALE // 'zh-CN'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Redaction utilities
|
|
191
|
+
|
|
192
|
+
`core.sanitize.sanitizeSensitiveFields()` recursively redacts objects and arrays. By default it handles password, token, secret, privateKey, apiV3Key, and similar credentials, and masks embedded credentials in `url` / `baseUrl` / `endpoint`. Modules can supply custom matchers.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
core.sanitize.sanitizeSensitiveFields({
|
|
196
|
+
email: 'user@example.com',
|
|
197
|
+
password: 'secret',
|
|
198
|
+
baseUrl: 'https://user:pass@example.com/api/v1',
|
|
199
|
+
profile: { token: 'jwt-token' },
|
|
200
|
+
})
|
|
201
|
+
// => { email: 'user@example.com', password: '[REDACTED]', baseUrl: 'https://[REDACTED]:[REDACTED]@example.com/api/v1', profile: { token: '[REDACTED]' } }
|
|
202
|
+
|
|
203
|
+
core.sanitize.sanitizeSensitiveFields({ email: 'user@example.com' }, {
|
|
204
|
+
matcher: /email/i,
|
|
205
|
+
replacement: '***',
|
|
206
|
+
})
|
|
207
|
+
// => { email: '***' }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Zod validation error localization
|
|
211
|
+
|
|
212
|
+
`core.zodValidation` converts Zod `SafeParseError` / `ZodError` into flat form errors and maps Zod's
|
|
213
|
+
default English messages to the caller's own localized messages.
|
|
214
|
+
|
|
215
|
+
- Two public entry points:
|
|
216
|
+
- `createPrefixedZodMessageGetter()`: derive keys such as `serv_validation*` / `kit_validation*` from module prefixes without a manual `KEY_MAP`.
|
|
217
|
+
- `mapZodErrorToFormErrors()`: extract, localize, and flatten in one step.
|
|
218
|
+
- Custom business messages are preserved; only Zod's default English messages are replaced.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import type { ZodMessageGetter } from '@h-ai/core'
|
|
222
|
+
import { core } from '@h-ai/core'
|
|
223
|
+
import { z } from 'zod'
|
|
224
|
+
|
|
225
|
+
const LoginSchema = z.object({
|
|
226
|
+
email: z.string().email(),
|
|
227
|
+
password: z.string().min(8),
|
|
228
|
+
nickname: z.string().min(1, '请填写昵称'),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const getMessage: ZodMessageGetter = (key, params) => {
|
|
232
|
+
switch (key) {
|
|
233
|
+
case 'validationEmail':
|
|
234
|
+
return '请输入合法邮箱地址'
|
|
235
|
+
case 'validationStringMin':
|
|
236
|
+
return `至少输入 ${params?.min} 个字符`
|
|
237
|
+
case 'validationRequired':
|
|
238
|
+
return '此项为必填项'
|
|
239
|
+
default:
|
|
240
|
+
return '输入不合法'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = LoginSchema.safeParse({
|
|
245
|
+
email: 'bad',
|
|
246
|
+
password: '123',
|
|
247
|
+
nickname: '',
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
const getMessage = core.zodValidation.createPrefixedZodMessageGetter(
|
|
252
|
+
'kit',
|
|
253
|
+
(messageKey, params) => {
|
|
254
|
+
if (messageKey === 'kit_validationEmail')
|
|
255
|
+
return '请输入合法邮箱地址'
|
|
256
|
+
if (messageKey === 'kit_validationStringMin')
|
|
257
|
+
return `至少输入 ${params?.min} 个字符`
|
|
258
|
+
return '输入不合法'
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
const errors = core.zodValidation.mapZodErrorToFormErrors(result.error, getMessage)
|
|
262
|
+
|
|
263
|
+
errors[0]?.field // 'email'
|
|
264
|
+
errors[0]?.message // '请输入合法邮箱地址'
|
|
265
|
+
errors[2]?.message // '请填写昵称'(schema 自定义消息原样保留)
|
|
266
|
+
|
|
267
|
+
core.logger.info('Validation failed', { errors })
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Configuration management (Node.js only)
|
|
272
|
+
|
|
273
|
+
> Browsers have no filesystem access. `core.config` offers placeholder APIs only: file-related methods return `SERVICE_UNAVAILABLE` and do not load YAML.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { core, HaiConfigError } from '@h-ai/core'
|
|
277
|
+
import { z } from 'zod'
|
|
278
|
+
|
|
279
|
+
// 方式 1:init 时自动加载整个配置目录
|
|
280
|
+
core.init({
|
|
281
|
+
configDir: './config', // 扫描该目录所有 .yml 文件
|
|
282
|
+
watchConfig: true, // 监听变更并自动重载
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// 方式 2:手动加载单个文件(可选 Schema 校验)
|
|
286
|
+
const AppSchema = z.object({
|
|
287
|
+
name: z.string(),
|
|
288
|
+
port: z.number().default(3000),
|
|
289
|
+
})
|
|
290
|
+
const result = core.config.load('app', './config/app.yml', AppSchema)
|
|
291
|
+
if (result.success) {
|
|
292
|
+
core.logger.info('Config loaded', { name: result.data.name })
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
core.logger.error('Config load failed', { code: result.error.code })
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 校验已加载的配置
|
|
299
|
+
const validResult = core.config.validate('app', AppSchema)
|
|
300
|
+
|
|
301
|
+
// 查询和获取
|
|
302
|
+
if (core.config.has('app')) {
|
|
303
|
+
const cfg = core.config.get<typeof AppSchema>('app') // undefined 或配置
|
|
304
|
+
const cfg2 = core.config.getOrThrow<typeof AppSchema>('app') // 未加载时抛异常
|
|
305
|
+
}
|
|
306
|
+
core.config.keys() // ['core', 'app', 'db', ...]
|
|
307
|
+
|
|
308
|
+
// 重新加载
|
|
309
|
+
const reloadResult = core.config.reload('app')
|
|
310
|
+
|
|
311
|
+
// 监听文件变更
|
|
312
|
+
const unwatch = core.config.watch('app', (newConfig, error) => {
|
|
313
|
+
if (error) {
|
|
314
|
+
core.logger.error('Config reload failed', { error })
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
core.logger.info('Config updated', { config: newConfig })
|
|
318
|
+
})
|
|
319
|
+
unwatch() // 取消监听
|
|
320
|
+
|
|
321
|
+
// 清除缓存(同时停止对应监听)
|
|
322
|
+
core.config.clear('app') // 清除单个
|
|
323
|
+
core.config.clear() // 清除全部
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Environment variable mapping and interpolation
|
|
327
|
+
|
|
328
|
+
`core.config.load(name, ...)` automatically maps each YAML leaf to an
|
|
329
|
+
environment variable named `HAI_<CONFIG_NAME>_<YAML_PATH>`. Configuration names and keys are uppercased, levels are separated by `_`,
|
|
330
|
+
and camelCase is not split. For example, `ai.llm.apiKey` maps to `HAI_AI_LLM_APIKEY`,
|
|
331
|
+
and `ai.llm.maxTokens` to `HAI_AI_LLM_MAXTOKENS`. Array indexes are also levels:
|
|
332
|
+
`ai.llm.models[0].temperature` maps to `HAI_AI_LLM_MODELS_0_TEMPERATURE`.
|
|
333
|
+
|
|
334
|
+
Existing environment variables override YAML and are parsed with YAML scalar rules into numbers, booleans, and other native types.
|
|
335
|
+
Absent variables leave YAML unchanged. Object/array nodes can also be replaced wholesale with same-name JSON variables, such as HAI_DB for connection configuration or HAI_REACH_PROVIDERS for channel arrays. A parent replacement takes precedence over child variables; schema validation still runs afterward.
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
# config/_ai.yml(core.init 扫描后配置名为 ai)
|
|
339
|
+
llm:
|
|
340
|
+
apiKey: ''
|
|
341
|
+
baseUrl: https://api.xiaomimimo.com/v1
|
|
342
|
+
model: mimo-v2.5
|
|
343
|
+
maxTokens: 4096
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
```dotenv
|
|
347
|
+
HAI_AI_LLM_APIKEY=your-api-key
|
|
348
|
+
HAI_AI_LLM_MAXTOKENS=8192
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
For special variable names, use explicit `${VAR}` interpolation. Precedence is fixed:
|
|
352
|
+
Convention-based environment variables > explicit environment variables > YAML defaults.
|
|
353
|
+
|
|
354
|
+
- `${VAR}`: read `process.env.VAR`; return `CONFIG_ENV_VAR_MISSING` if absent.
|
|
355
|
+
- `${VAR:default}`: read `process.env.VAR`; use the value after the colon if absent.
|
|
356
|
+
|
|
357
|
+
**Type restoration:** when the whole value is one variable expression (such as `${PORT:3000}`), YAML rules restore native types (number / boolean, etc.). Mixed text always stays a string.
|
|
358
|
+
|
|
359
|
+
`${VAR:}` or `${VAR}` resolving to an empty string stays `''`, rather than becoming YAML `null`. Modules that treat empty strings as unset should normalize them to `undefined` in their own schemas.
|
|
360
|
+
|
|
361
|
+
```yaml
|
|
362
|
+
# config/app.yml
|
|
363
|
+
debug: ${DEBUG:false} # → boolean false
|
|
364
|
+
port: ${PORT:3000} # → number 3000
|
|
365
|
+
database: ${DB_CONNECTION} # → 环境变量缺失时报错
|
|
366
|
+
url: http://${HOST}:${PORT} # → string(混合文本)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### ID generation
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
const id = core.id.generate() // nanoid 21 字符
|
|
373
|
+
const id8 = core.id.generate(8) // 自定义长度
|
|
374
|
+
const shortId = core.id.short() // 10 字符短 ID
|
|
375
|
+
const prefixed = core.id.withPrefix('user_') // 'user_V1StGXR...'
|
|
376
|
+
const traceId = core.id.trace() // 'trace-V1StGXR...'
|
|
377
|
+
const reqId = core.id.request() // 'req-V1StGXR...'
|
|
378
|
+
const uuid = core.id.uuid() // UUID v4
|
|
379
|
+
|
|
380
|
+
// 校验
|
|
381
|
+
core.id.isValidUUID('f47ac10b-...') // true/false
|
|
382
|
+
core.id.isValidNanoId('abc123', 6) // true/false
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Utilities
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// 类型检查
|
|
389
|
+
core.typeUtils.isDefined(x) // x !== undefined
|
|
390
|
+
core.typeUtils.isObject(x) // plain object
|
|
391
|
+
core.typeUtils.isFunction(x)
|
|
392
|
+
core.typeUtils.isPromise(x)
|
|
393
|
+
core.typeUtils.isString(x)
|
|
394
|
+
core.typeUtils.isNumber(x)
|
|
395
|
+
core.typeUtils.isBoolean(x)
|
|
396
|
+
core.typeUtils.isArray(x)
|
|
397
|
+
|
|
398
|
+
// 对象操作
|
|
399
|
+
core.object.deepClone(obj)
|
|
400
|
+
core.object.deepMerge(a, b) // 递归合并(防原型污染)
|
|
401
|
+
core.object.deepMerge(a, b, c) // 支持多个对象
|
|
402
|
+
core.object.pick(obj, ['a', 'b'])
|
|
403
|
+
core.object.omit(obj, ['c', 'd'])
|
|
404
|
+
core.object.keys(obj) // typed Object.keys
|
|
405
|
+
core.object.values(obj)
|
|
406
|
+
core.object.entries(obj)
|
|
407
|
+
core.object.fromEntries(entries)
|
|
408
|
+
|
|
409
|
+
// 字符串操作
|
|
410
|
+
core.string.capitalize('hello') // 'Hello'
|
|
411
|
+
core.string.kebabCase('helloWorld') // 'hello-world'
|
|
412
|
+
core.string.camelCase('hello-world') // 'helloWorld'
|
|
413
|
+
core.string.snakeCase('helloWorld') // 'hello_world'
|
|
414
|
+
core.string.pascalCase('hello-world') // 'HelloWorld'
|
|
415
|
+
core.string.truncate('long text', 5) // 'long ...'
|
|
416
|
+
core.string.trim(' hello ') // 'hello'
|
|
417
|
+
core.string.isBlank('') // true
|
|
418
|
+
core.string.isNotBlank('x') // true
|
|
419
|
+
core.string.padStart('5', 3, '0') // '005'
|
|
420
|
+
core.string.padEnd('5', 3, '0') // '500'
|
|
421
|
+
core.string.constantTimeEqual(a, b) // 防时序攻击比较
|
|
422
|
+
|
|
423
|
+
// 数组操作
|
|
424
|
+
core.array.unique([1, 1, 2]) // [1, 2]
|
|
425
|
+
core.array.chunk([1, 2, 3, 4], 2) // [[1,2], [3,4]]
|
|
426
|
+
core.array.groupBy(items, item => item.type) // Record<string, T[]>
|
|
427
|
+
core.array.first([1, 2, 3]) // 1
|
|
428
|
+
core.array.last([1, 2, 3]) // 3
|
|
429
|
+
core.array.flatten([[1], [2, 3]]) // [1, 2, 3]
|
|
430
|
+
core.array.compact([1, null, 2]) // [1, 2]
|
|
431
|
+
core.array.shuffle([1, 2, 3]) // 随机排列
|
|
432
|
+
core.array.intersection([1, 2], [2, 3]) // [2]
|
|
433
|
+
core.array.difference([1, 2], [2, 3]) // [1]
|
|
434
|
+
|
|
435
|
+
// 异步操作
|
|
436
|
+
await core.async.delay(1000)
|
|
437
|
+
await core.async.withTimeout(fetch('/api'), 5000)
|
|
438
|
+
await core.async.retry(fn, { maxRetries: 3, delay: 1000 })
|
|
439
|
+
await core.async.parallel(items, processFn, 5) // 并发限制
|
|
440
|
+
await core.async.serial(items, processFn)
|
|
441
|
+
const debouncedFn = core.async.debounce(fn, 300)
|
|
442
|
+
const throttledFn = core.async.throttle(fn, 200)
|
|
443
|
+
|
|
444
|
+
// 时间操作
|
|
445
|
+
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
446
|
+
core.time.timeAgo(new Date(Date.now() - 60000)) // '1 分钟前'
|
|
447
|
+
core.time.now() // Date.now()
|
|
448
|
+
core.time.nowSeconds() // Math.floor(Date.now() / 1000)
|
|
449
|
+
core.time.parseDate('2024-01-01')
|
|
450
|
+
core.time.isValidDate(date)
|
|
451
|
+
core.time.addDays(date, 7)
|
|
452
|
+
core.time.addHours(date, 2)
|
|
453
|
+
core.time.startOfDay(date)
|
|
454
|
+
core.time.endOfDay(date)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Module utilities (module)
|
|
458
|
+
|
|
459
|
+
Shared initialization error handling for modules:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { core } from '@h-ai/core'
|
|
463
|
+
|
|
464
|
+
const notInitialized = core.module.createNotInitializedKit(
|
|
465
|
+
HaiDbError.NOT_INITIALIZED,
|
|
466
|
+
() => dbM('db_notInitialized'),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
// 创建占位 Proxy(异步接口,默认)
|
|
470
|
+
const currentDdl: DdlOperations = notInitialized.proxy<DdlOperations>()
|
|
471
|
+
// 创建占位 Proxy(同步接口)
|
|
472
|
+
const currentHash: HashOperations = notInitialized.proxy<HashOperations>('sync')
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Error handling
|
|
476
|
+
|
|
477
|
+
### HaiResult types
|
|
478
|
+
|
|
479
|
+
All operations that may fail return `HaiResult<T>`:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import type { HaiResult } from '@h-ai/core'
|
|
483
|
+
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
484
|
+
|
|
485
|
+
function divide(a: number, b: number): HaiResult<number> {
|
|
486
|
+
if (b === 0)
|
|
487
|
+
return err(HaiCommonError.VALIDATION_ERROR, 'Division by zero')
|
|
488
|
+
return ok(a / b)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const result = divide(10, 2)
|
|
492
|
+
if (result.success) {
|
|
493
|
+
core.logger.info('Result', { data: result.data })
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
core.logger.error('Error', { error: result.error })
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Custom error codes (per module)
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import type { ErrorInfo } from '@h-ai/core'
|
|
504
|
+
import { core } from '@h-ai/core'
|
|
505
|
+
|
|
506
|
+
// 定义错误码(格式 'code:httpStatus')
|
|
507
|
+
const DbErrorInfo = {
|
|
508
|
+
NOT_INITIALIZED: '001:500',
|
|
509
|
+
CONNECTION_FAILED: '010:500',
|
|
510
|
+
QUERY_FAILED: '011:500',
|
|
511
|
+
} as const satisfies ErrorInfo
|
|
512
|
+
|
|
513
|
+
// 生成错误定义对象
|
|
514
|
+
export const DbError = core.error.buildHaiErrorsDef('db', DbErrorInfo)
|
|
515
|
+
// DbError.CONNECTION_FAILED → { code: 'hai:db:010', httpStatus: 500, system: 'hai', module: 'db' }
|
|
516
|
+
|
|
517
|
+
// 创建错误实例
|
|
518
|
+
const errorInst = core.error.buildHaiErrorInst(
|
|
519
|
+
DbError.CONNECTION_FAILED,
|
|
520
|
+
'Unable to connect to PostgreSQL',
|
|
521
|
+
originalError, // cause(可选)
|
|
522
|
+
'Check connection string', // suggestion(可选)
|
|
523
|
+
)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Configuration error codes
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { HaiConfigError } from '@h-ai/core'
|
|
530
|
+
|
|
531
|
+
HaiConfigError.CONFIG_FILE_NOT_FOUND // hai:core:010 - 配置文件不存在
|
|
532
|
+
HaiConfigError.CONFIG_PARSE_ERROR // hai:core:011 - YAML 解析失败
|
|
533
|
+
HaiConfigError.CONFIG_VALIDATION_ERROR // hai:core:012 - Schema 校验失败
|
|
534
|
+
HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
535
|
+
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Tests
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
pnpm --filter @h-ai/core test
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## License
|
|
545
|
+
|
|
546
|
+
Apache-2.0
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
# @h-ai/core
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[フレームワークのホーム](../../README.ja.md)
|
|
6
|
+
|
|
7
|
+
> hai Agent Framework の中核モジュール。共通ツール、型定義、設定管理、ログ機能を提供します。
|
|
8
|
+
|
|
9
|
+
## 主な機能
|
|
10
|
+
|
|
11
|
+
- **HaiResult 型**:関数型のエラー処理(`ok` / `err`)とモジュール共通の戻り値形式。
|
|
12
|
+
- **統一ログ**:Node.js は pino、ブラウザーは loglevel を使い、API は共通。
|
|
13
|
+
- **国際化(i18n)**:一元化された locale 管理と型安全なメッセージ取得。
|
|
14
|
+
- **Zod 検証エラーの変換**:Zod v3/v4 の issue 抽出と、既定の英語メッセージのローカライズ。
|
|
15
|
+
- **設定管理**:YAML 読み込み、規約に沿う環境変数マッピング、明示的な変数展開、Zod 検証、ファイル監視(Node.js 専用)。
|
|
16
|
+
- **ID 生成**:nanoid と UUID v4。
|
|
17
|
+
- **エラー定義**:標準化されたコードと、モジュール共通のエラー定義・インスタンス生成。
|
|
18
|
+
- **モジュールツール**:NotInitializedKit のプロキシと共通のライフサイクル基盤。
|
|
19
|
+
- **ツール関数**:型確認、オブジェクト・文字列・配列・非同期・時刻の操作。
|
|
20
|
+
|
|
21
|
+
## クイックスタート
|
|
22
|
+
|
|
23
|
+
### Node.js 環境
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { core } from '@h-ai/core'
|
|
27
|
+
|
|
28
|
+
// 初始化:加载配置目录(可选)
|
|
29
|
+
core.init({ configDir: './config', watchConfig: true })
|
|
30
|
+
|
|
31
|
+
// 日志
|
|
32
|
+
core.logger.info('Server started', { port: 3000 })
|
|
33
|
+
core.logger.warn('Slow query', { duration: 2500 })
|
|
34
|
+
core.logger.error('Connection failed', { error })
|
|
35
|
+
|
|
36
|
+
// 创建模块级 logger(独立名称空间)
|
|
37
|
+
const dbLogger = core.logger.create({ name: 'db' })
|
|
38
|
+
dbLogger.info('Connected to PostgreSQL')
|
|
39
|
+
|
|
40
|
+
// 创建子 logger(自动携带固定上下文,适合请求级/操作级追踪)
|
|
41
|
+
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
42
|
+
reqLogger.info('Request started') // 日志自动包含 requestId + userId
|
|
43
|
+
reqLogger.error('Request failed') // 同上
|
|
44
|
+
|
|
45
|
+
// 子 logger 可嵌套
|
|
46
|
+
const opLogger = reqLogger.child({ operation: 'createOrder' })
|
|
47
|
+
opLogger.info('Order created') // 包含 requestId + userId + operation
|
|
48
|
+
|
|
49
|
+
// i18n
|
|
50
|
+
core.i18n.setGlobalLocale('zh-CN')
|
|
51
|
+
const getMessage = core.i18n.createMessageGetter({
|
|
52
|
+
'zh-CN': { welcome: '欢迎 {name}', error: '发生错误:{msg}' },
|
|
53
|
+
'en-US': { welcome: 'Welcome {name}', error: 'Error: {msg}' },
|
|
54
|
+
})
|
|
55
|
+
getMessage('welcome', { params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
56
|
+
|
|
57
|
+
// ID 生成
|
|
58
|
+
const nanoid = core.id.generate()
|
|
59
|
+
const shortId = core.id.short()
|
|
60
|
+
const uuid = core.id.uuid()
|
|
61
|
+
const prefixed = core.id.withPrefix('order_')
|
|
62
|
+
|
|
63
|
+
// 工具函数
|
|
64
|
+
core.object.deepMerge(objA, objB)
|
|
65
|
+
core.sanitize.sanitizeSensitiveFields({ password: 'secret', email: 'user@example.com' })
|
|
66
|
+
core.string.capitalize('hello')
|
|
67
|
+
core.array.unique([1, 1, 2, 3])
|
|
68
|
+
core.array.chunk([1, 2, 3, 4, 5], 2) // [[1,2], [3,4], [5]]
|
|
69
|
+
await core.async.delay(1000)
|
|
70
|
+
await core.async.retry(() => fetchData(), { maxRetries: 3, delay: 1000 })
|
|
71
|
+
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
72
|
+
core.typeUtils.isDefined(value)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### ブラウザー環境
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { core } from '@h-ai/core'
|
|
79
|
+
|
|
80
|
+
// 初始化日志级别(浏览器不支持配置加载)
|
|
81
|
+
core.init({ logging: { level: 'warn' } })
|
|
82
|
+
|
|
83
|
+
core.logger.info('Page ready')
|
|
84
|
+
const id = core.id.generate()
|
|
85
|
+
|
|
86
|
+
// 其他工具函数与 Node.js 相同
|
|
87
|
+
core.string.capitalize('hello')
|
|
88
|
+
core.array.unique([1, 1, 2])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
ブラウザー版は型定義に合わせて `core.config` の API 形状を保持しますが、ファイル設定には対応しません:
|
|
92
|
+
|
|
93
|
+
- `core.config.get()` は `undefined`、`has()` は `false`、`keys()` は `[]` を返します。
|
|
94
|
+
- `load()` / `validate()` / `reload()` は `HaiCommonError.SERVICE_UNAVAILABLE` を返します。
|
|
95
|
+
- `watch()` は同じエラーで直ちにコールバックを呼び、安全な no-op の監視解除関数を返します。
|
|
96
|
+
- 設定が必要な場合はサーバーで読み込み API で渡すか、ブラウザーでは `core.init({ logging })` でログのみ設定してください。
|
|
97
|
+
|
|
98
|
+
## API の詳細
|
|
99
|
+
|
|
100
|
+
### ログ(Logger)
|
|
101
|
+
|
|
102
|
+
`core.logger` は既定の Logger インスタンスであり、ログ管理の共通入口でもあります。
|
|
103
|
+
|
|
104
|
+
#### ログの記録
|
|
105
|
+
|
|
106
|
+
6 つのレベル:`trace` / `debug` / `info` / `warn` / `error` / `fatal`。
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
core.logger.info('User created', { userId: '123' })
|
|
110
|
+
core.logger.error('Failed to connect', { error })
|
|
111
|
+
core.logger.debug('Processing item', { id, step: 'validation' })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### モジュール用 logger の作成
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// 独立名称空间,适合模块内全局使用
|
|
118
|
+
const logger = core.logger.create({ name: 'payment' })
|
|
119
|
+
logger.info('Payment processed')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### 子 logger(child)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// 携带固定上下文,每条日志自动附带这些字段
|
|
126
|
+
const reqLogger = core.logger.child({ requestId: 'req-001', userId: 'u-123' })
|
|
127
|
+
reqLogger.info('Request started') // 输出包含 requestId + userId
|
|
128
|
+
reqLogger.warn('Slow response') // 同上
|
|
129
|
+
|
|
130
|
+
// 嵌套 child:子上下文递归合并
|
|
131
|
+
const opLogger = reqLogger.child({ operation: 'login' })
|
|
132
|
+
opLogger.info('Login succeeded') // 输出包含 requestId + userId + operation
|
|
133
|
+
opLogger.error('Login failed', { reason: 'invalid_password' })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> **典型的な使い方**:リクエスト入口で `reqLogger` を作成してサービス層に渡し、サービス層で `child` に操作コンテキストを追加します。ログに処理全体のコンテキストが自動的に含まれます。
|
|
137
|
+
|
|
138
|
+
#### ログの管理
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// 配置输出格式和级别(Node.js)
|
|
142
|
+
core.logger.configure({
|
|
143
|
+
level: 'info',
|
|
144
|
+
format: 'json', // 'json' | 'pretty'
|
|
145
|
+
context: { app: 'my-service' }, // 全局默认上下文
|
|
146
|
+
redact: ['password', 'token'], // 脱敏字段
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
core.logger.setLevel('debug')
|
|
150
|
+
const level = core.logger.getLevel() // 'debug'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`core.logger` は既定でログの機密情報をマスクします。`password`、`token`、`secret`、`privateKey`、`apiV3Key` などは `[REDACTED]` に置換されます。`url` / `baseUrl` / `endpoint` は URL 内のユーザー名・パスワードのみをマスクします。メールアドレス・電話番号は既定では隠しません。
|
|
154
|
+
|
|
155
|
+
#### ログレベルの規約
|
|
156
|
+
|
|
157
|
+
| レベル | 用途 |
|
|
158
|
+
| ------- | ---------------------------------------- |
|
|
159
|
+
| `trace` | ループ内の詳細なデバッグデータ |
|
|
160
|
+
| `debug` | 関数開始、引数の概要、読み取り操作 |
|
|
161
|
+
| `info` | 業務イベント(初期化完了、書き込み成功) |
|
|
162
|
+
| `warn` | 回復可能な問題(検証失敗、再試行) |
|
|
163
|
+
| `error` | 調査が必要な操作の失敗 |
|
|
164
|
+
| `fatal` | サービス継続不能の致命的エラー |
|
|
165
|
+
|
|
166
|
+
### 国際化(i18n)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
170
|
+
core.i18n.setGlobalLocale('en-US')
|
|
171
|
+
const current = core.i18n.getGlobalLocale() // 'en-US'
|
|
172
|
+
|
|
173
|
+
// 为每个模块创建消息获取器
|
|
174
|
+
const getMessage = core.i18n.createMessageGetter({
|
|
175
|
+
'zh-CN': { welcome: '欢迎 {name}', error: '错误:{code}' },
|
|
176
|
+
'en-US': { welcome: 'Welcome {name}', error: 'Error: {code}' },
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// 使用:自动读取全局 locale,支持参数插值
|
|
180
|
+
getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
181
|
+
|
|
182
|
+
// 支持 locale 参数覆盖
|
|
183
|
+
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
184
|
+
|
|
185
|
+
// 常量
|
|
186
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }]
|
|
187
|
+
core.i18n.DEFAULT_LOCALE // 'zh-CN'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 機密情報のマスク
|
|
191
|
+
|
|
192
|
+
`core.sanitize.sanitizeSensitiveFields()` はオブジェクト・配列内の機密情報を再帰的にマスクします。既定では password、token、secret、privateKey、apiV3Key などを処理し、`url` / `baseUrl` / `endpoint` の認証情報もマスクします。独自の matcher も指定できます。
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
core.sanitize.sanitizeSensitiveFields({
|
|
196
|
+
email: 'user@example.com',
|
|
197
|
+
password: 'secret',
|
|
198
|
+
baseUrl: 'https://user:pass@example.com/api/v1',
|
|
199
|
+
profile: { token: 'jwt-token' },
|
|
200
|
+
})
|
|
201
|
+
// => { email: 'user@example.com', password: '[REDACTED]', baseUrl: 'https://[REDACTED]:[REDACTED]@example.com/api/v1', profile: { token: '[REDACTED]' } }
|
|
202
|
+
|
|
203
|
+
core.sanitize.sanitizeSensitiveFields({ email: 'user@example.com' }, {
|
|
204
|
+
matcher: /email/i,
|
|
205
|
+
replacement: '***',
|
|
206
|
+
})
|
|
207
|
+
// => { email: '***' }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Zod 検証エラーの i18n 変換
|
|
211
|
+
|
|
212
|
+
`core.zodValidation` は Zod の `SafeParseError` / `ZodError` をフラットなフォームエラーに変換し、
|
|
213
|
+
Zod の既定の英語メッセージを呼び出し元の i18n 文言に変換します。
|
|
214
|
+
|
|
215
|
+
- 公開入口は 2 つです:
|
|
216
|
+
- `createPrefixedZodMessageGetter()`:モジュールの接頭辞から `serv_validation*` / `kit_validation*` などの key を生成し、手書きの `KEY_MAP` を省略。
|
|
217
|
+
- `mapZodErrorToFormErrors()`:抽出・ローカライズ・フラット化を一度に実行。
|
|
218
|
+
- 独自の業務メッセージは保持し、Zod の既定の英語メッセージのみを置換します。
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import type { ZodMessageGetter } from '@h-ai/core'
|
|
222
|
+
import { core } from '@h-ai/core'
|
|
223
|
+
import { z } from 'zod'
|
|
224
|
+
|
|
225
|
+
const LoginSchema = z.object({
|
|
226
|
+
email: z.string().email(),
|
|
227
|
+
password: z.string().min(8),
|
|
228
|
+
nickname: z.string().min(1, '请填写昵称'),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const getMessage: ZodMessageGetter = (key, params) => {
|
|
232
|
+
switch (key) {
|
|
233
|
+
case 'validationEmail':
|
|
234
|
+
return '请输入合法邮箱地址'
|
|
235
|
+
case 'validationStringMin':
|
|
236
|
+
return `至少输入 ${params?.min} 个字符`
|
|
237
|
+
case 'validationRequired':
|
|
238
|
+
return '此项为必填项'
|
|
239
|
+
default:
|
|
240
|
+
return '输入不合法'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = LoginSchema.safeParse({
|
|
245
|
+
email: 'bad',
|
|
246
|
+
password: '123',
|
|
247
|
+
nickname: '',
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
const getMessage = core.zodValidation.createPrefixedZodMessageGetter(
|
|
252
|
+
'kit',
|
|
253
|
+
(messageKey, params) => {
|
|
254
|
+
if (messageKey === 'kit_validationEmail')
|
|
255
|
+
return '请输入合法邮箱地址'
|
|
256
|
+
if (messageKey === 'kit_validationStringMin')
|
|
257
|
+
return `至少输入 ${params?.min} 个字符`
|
|
258
|
+
return '输入不合法'
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
const errors = core.zodValidation.mapZodErrorToFormErrors(result.error, getMessage)
|
|
262
|
+
|
|
263
|
+
errors[0]?.field // 'email'
|
|
264
|
+
errors[0]?.message // '请输入合法邮箱地址'
|
|
265
|
+
errors[2]?.message // '请填写昵称'(schema 自定义消息原样保留)
|
|
266
|
+
|
|
267
|
+
core.logger.info('Validation failed', { errors })
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 設定管理(Node.js 専用)
|
|
272
|
+
|
|
273
|
+
> ブラウザーにはファイルシステムがないため、`core.config` は代替 API の形状のみを提供します。ファイル関連メソッドは `SERVICE_UNAVAILABLE` を返し、YAML を読み込みません。
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { core, HaiConfigError } from '@h-ai/core'
|
|
277
|
+
import { z } from 'zod'
|
|
278
|
+
|
|
279
|
+
// 方式 1:init 时自动加载整个配置目录
|
|
280
|
+
core.init({
|
|
281
|
+
configDir: './config', // 扫描该目录所有 .yml 文件
|
|
282
|
+
watchConfig: true, // 监听变更并自动重载
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// 方式 2:手动加载单个文件(可选 Schema 校验)
|
|
286
|
+
const AppSchema = z.object({
|
|
287
|
+
name: z.string(),
|
|
288
|
+
port: z.number().default(3000),
|
|
289
|
+
})
|
|
290
|
+
const result = core.config.load('app', './config/app.yml', AppSchema)
|
|
291
|
+
if (result.success) {
|
|
292
|
+
core.logger.info('Config loaded', { name: result.data.name })
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
core.logger.error('Config load failed', { code: result.error.code })
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 校验已加载的配置
|
|
299
|
+
const validResult = core.config.validate('app', AppSchema)
|
|
300
|
+
|
|
301
|
+
// 查询和获取
|
|
302
|
+
if (core.config.has('app')) {
|
|
303
|
+
const cfg = core.config.get<typeof AppSchema>('app') // undefined 或配置
|
|
304
|
+
const cfg2 = core.config.getOrThrow<typeof AppSchema>('app') // 未加载时抛异常
|
|
305
|
+
}
|
|
306
|
+
core.config.keys() // ['core', 'app', 'db', ...]
|
|
307
|
+
|
|
308
|
+
// 重新加载
|
|
309
|
+
const reloadResult = core.config.reload('app')
|
|
310
|
+
|
|
311
|
+
// 监听文件变更
|
|
312
|
+
const unwatch = core.config.watch('app', (newConfig, error) => {
|
|
313
|
+
if (error) {
|
|
314
|
+
core.logger.error('Config reload failed', { error })
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
core.logger.info('Config updated', { config: newConfig })
|
|
318
|
+
})
|
|
319
|
+
unwatch() // 取消监听
|
|
320
|
+
|
|
321
|
+
// 清除缓存(同时停止对应监听)
|
|
322
|
+
core.config.clear('app') // 清除单个
|
|
323
|
+
core.config.clear() // 清除全部
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### 環境変数のマッピングと展開
|
|
327
|
+
|
|
328
|
+
`core.config.load(name, ...)` は YAML の各末端設定を
|
|
329
|
+
`HAI_<CONFIG_NAME>_<YAML_PATH>` の環境変数に自動対応付けします。設定名・各 key は大文字化し、階層は `_` で区切り、
|
|
330
|
+
camelCase の単語は分割しません。例:`ai.llm.apiKey` は `HAI_AI_LLM_APIKEY`、
|
|
331
|
+
`ai.llm.maxTokens` は `HAI_AI_LLM_MAXTOKENS`。配列の添字も 1 階層として扱い、
|
|
332
|
+
`ai.llm.models[0].temperature` は `HAI_AI_LLM_MODELS_0_TEMPERATURE` です。
|
|
333
|
+
|
|
334
|
+
環境変数が存在すれば YAML を上書きし、YAML のスカラー規則で number / boolean などに変換します。
|
|
335
|
+
変数がなければ YAML 値を保持します。オブジェクト・配列全体も同名の JSON 変数で置換でき、HAI_DB は接続設定全体、HAI_REACH_PROVIDERS は配信チャネル配列を置換します。親ノード全体の上書きが子フィールド変数より優先され、置換後も Schema 検証を行います。
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
# config/_ai.yml(core.init 扫描后配置名为 ai)
|
|
339
|
+
llm:
|
|
340
|
+
apiKey: ''
|
|
341
|
+
baseUrl: https://api.xiaomimimo.com/v1
|
|
342
|
+
model: mimo-v2.5
|
|
343
|
+
maxTokens: 4096
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
```dotenv
|
|
347
|
+
HAI_AI_LLM_APIKEY=your-api-key
|
|
348
|
+
HAI_AI_LLM_MAXTOKENS=8192
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
特殊な変数名が必要な場合は明示的な `${VAR}` を使用します。優先順位は固定です:
|
|
352
|
+
規約に沿う環境変数 > 明示的な環境変数 > YAML の既定値。
|
|
353
|
+
|
|
354
|
+
- `${VAR}`:`process.env.VAR` を読み、存在しなければ `CONFIG_ENV_VAR_MISSING`。
|
|
355
|
+
- `${VAR:default}`:`process.env.VAR` を読み、存在しなければコロン以降の既定値を使用。
|
|
356
|
+
|
|
357
|
+
**型の復元:** 値全体が単一の変数式(`${PORT:3000}` など)の場合、YAML 規則で number / boolean などの型に戻します。文字列との混在は常に文字列です。
|
|
358
|
+
|
|
359
|
+
`${VAR:}` や空文字列の `${VAR}` は `''` を保持し、YAML の `null` には変換しません。空文字列を未設定として扱うモジュールは、自身の Schema で `undefined` に正規化してください。
|
|
360
|
+
|
|
361
|
+
```yaml
|
|
362
|
+
# config/app.yml
|
|
363
|
+
debug: ${DEBUG:false} # → boolean false
|
|
364
|
+
port: ${PORT:3000} # → number 3000
|
|
365
|
+
database: ${DB_CONNECTION} # → 环境变量缺失时报错
|
|
366
|
+
url: http://${HOST}:${PORT} # → string(混合文本)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### ID 生成
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
const id = core.id.generate() // nanoid 21 字符
|
|
373
|
+
const id8 = core.id.generate(8) // 自定义长度
|
|
374
|
+
const shortId = core.id.short() // 10 字符短 ID
|
|
375
|
+
const prefixed = core.id.withPrefix('user_') // 'user_V1StGXR...'
|
|
376
|
+
const traceId = core.id.trace() // 'trace-V1StGXR...'
|
|
377
|
+
const reqId = core.id.request() // 'req-V1StGXR...'
|
|
378
|
+
const uuid = core.id.uuid() // UUID v4
|
|
379
|
+
|
|
380
|
+
// 校验
|
|
381
|
+
core.id.isValidUUID('f47ac10b-...') // true/false
|
|
382
|
+
core.id.isValidNanoId('abc123', 6) // true/false
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### ツール関数
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// 类型检查
|
|
389
|
+
core.typeUtils.isDefined(x) // x !== undefined
|
|
390
|
+
core.typeUtils.isObject(x) // plain object
|
|
391
|
+
core.typeUtils.isFunction(x)
|
|
392
|
+
core.typeUtils.isPromise(x)
|
|
393
|
+
core.typeUtils.isString(x)
|
|
394
|
+
core.typeUtils.isNumber(x)
|
|
395
|
+
core.typeUtils.isBoolean(x)
|
|
396
|
+
core.typeUtils.isArray(x)
|
|
397
|
+
|
|
398
|
+
// 对象操作
|
|
399
|
+
core.object.deepClone(obj)
|
|
400
|
+
core.object.deepMerge(a, b) // 递归合并(防原型污染)
|
|
401
|
+
core.object.deepMerge(a, b, c) // 支持多个对象
|
|
402
|
+
core.object.pick(obj, ['a', 'b'])
|
|
403
|
+
core.object.omit(obj, ['c', 'd'])
|
|
404
|
+
core.object.keys(obj) // typed Object.keys
|
|
405
|
+
core.object.values(obj)
|
|
406
|
+
core.object.entries(obj)
|
|
407
|
+
core.object.fromEntries(entries)
|
|
408
|
+
|
|
409
|
+
// 字符串操作
|
|
410
|
+
core.string.capitalize('hello') // 'Hello'
|
|
411
|
+
core.string.kebabCase('helloWorld') // 'hello-world'
|
|
412
|
+
core.string.camelCase('hello-world') // 'helloWorld'
|
|
413
|
+
core.string.snakeCase('helloWorld') // 'hello_world'
|
|
414
|
+
core.string.pascalCase('hello-world') // 'HelloWorld'
|
|
415
|
+
core.string.truncate('long text', 5) // 'long ...'
|
|
416
|
+
core.string.trim(' hello ') // 'hello'
|
|
417
|
+
core.string.isBlank('') // true
|
|
418
|
+
core.string.isNotBlank('x') // true
|
|
419
|
+
core.string.padStart('5', 3, '0') // '005'
|
|
420
|
+
core.string.padEnd('5', 3, '0') // '500'
|
|
421
|
+
core.string.constantTimeEqual(a, b) // 防时序攻击比较
|
|
422
|
+
|
|
423
|
+
// 数组操作
|
|
424
|
+
core.array.unique([1, 1, 2]) // [1, 2]
|
|
425
|
+
core.array.chunk([1, 2, 3, 4], 2) // [[1,2], [3,4]]
|
|
426
|
+
core.array.groupBy(items, item => item.type) // Record<string, T[]>
|
|
427
|
+
core.array.first([1, 2, 3]) // 1
|
|
428
|
+
core.array.last([1, 2, 3]) // 3
|
|
429
|
+
core.array.flatten([[1], [2, 3]]) // [1, 2, 3]
|
|
430
|
+
core.array.compact([1, null, 2]) // [1, 2]
|
|
431
|
+
core.array.shuffle([1, 2, 3]) // 随机排列
|
|
432
|
+
core.array.intersection([1, 2], [2, 3]) // [2]
|
|
433
|
+
core.array.difference([1, 2], [2, 3]) // [1]
|
|
434
|
+
|
|
435
|
+
// 异步操作
|
|
436
|
+
await core.async.delay(1000)
|
|
437
|
+
await core.async.withTimeout(fetch('/api'), 5000)
|
|
438
|
+
await core.async.retry(fn, { maxRetries: 3, delay: 1000 })
|
|
439
|
+
await core.async.parallel(items, processFn, 5) // 并发限制
|
|
440
|
+
await core.async.serial(items, processFn)
|
|
441
|
+
const debouncedFn = core.async.debounce(fn, 300)
|
|
442
|
+
const throttledFn = core.async.throttle(fn, 200)
|
|
443
|
+
|
|
444
|
+
// 时间操作
|
|
445
|
+
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
446
|
+
core.time.timeAgo(new Date(Date.now() - 60000)) // '1 分钟前'
|
|
447
|
+
core.time.now() // Date.now()
|
|
448
|
+
core.time.nowSeconds() // Math.floor(Date.now() / 1000)
|
|
449
|
+
core.time.parseDate('2024-01-01')
|
|
450
|
+
core.time.isValidDate(date)
|
|
451
|
+
core.time.addDays(date, 7)
|
|
452
|
+
core.time.addHours(date, 2)
|
|
453
|
+
core.time.startOfDay(date)
|
|
454
|
+
core.time.endOfDay(date)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### モジュール共通ツール(module)
|
|
458
|
+
|
|
459
|
+
モジュール共通の初期化エラー処理:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { core } from '@h-ai/core'
|
|
463
|
+
|
|
464
|
+
const notInitialized = core.module.createNotInitializedKit(
|
|
465
|
+
HaiDbError.NOT_INITIALIZED,
|
|
466
|
+
() => dbM('db_notInitialized'),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
// 创建占位 Proxy(异步接口,默认)
|
|
470
|
+
const currentDdl: DdlOperations = notInitialized.proxy<DdlOperations>()
|
|
471
|
+
// 创建占位 Proxy(同步接口)
|
|
472
|
+
const currentHash: HashOperations = notInitialized.proxy<HashOperations>('sync')
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## エラー処理
|
|
476
|
+
|
|
477
|
+
### HaiResult 型
|
|
478
|
+
|
|
479
|
+
失敗する可能性がある操作は `HaiResult<T>` を返します:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import type { HaiResult } from '@h-ai/core'
|
|
483
|
+
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
484
|
+
|
|
485
|
+
function divide(a: number, b: number): HaiResult<number> {
|
|
486
|
+
if (b === 0)
|
|
487
|
+
return err(HaiCommonError.VALIDATION_ERROR, 'Division by zero')
|
|
488
|
+
return ok(a / b)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const result = divide(10, 2)
|
|
492
|
+
if (result.success) {
|
|
493
|
+
core.logger.info('Result', { data: result.data })
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
core.logger.error('Error', { error: result.error })
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### 独自のエラーコード(モジュール単位)
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import type { ErrorInfo } from '@h-ai/core'
|
|
504
|
+
import { core } from '@h-ai/core'
|
|
505
|
+
|
|
506
|
+
// 定义错误码(格式 'code:httpStatus')
|
|
507
|
+
const DbErrorInfo = {
|
|
508
|
+
NOT_INITIALIZED: '001:500',
|
|
509
|
+
CONNECTION_FAILED: '010:500',
|
|
510
|
+
QUERY_FAILED: '011:500',
|
|
511
|
+
} as const satisfies ErrorInfo
|
|
512
|
+
|
|
513
|
+
// 生成错误定义对象
|
|
514
|
+
export const DbError = core.error.buildHaiErrorsDef('db', DbErrorInfo)
|
|
515
|
+
// DbError.CONNECTION_FAILED → { code: 'hai:db:010', httpStatus: 500, system: 'hai', module: 'db' }
|
|
516
|
+
|
|
517
|
+
// 创建错误实例
|
|
518
|
+
const errorInst = core.error.buildHaiErrorInst(
|
|
519
|
+
DbError.CONNECTION_FAILED,
|
|
520
|
+
'Unable to connect to PostgreSQL',
|
|
521
|
+
originalError, // cause(可选)
|
|
522
|
+
'Check connection string', // suggestion(可选)
|
|
523
|
+
)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 設定エラーコード
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { HaiConfigError } from '@h-ai/core'
|
|
530
|
+
|
|
531
|
+
HaiConfigError.CONFIG_FILE_NOT_FOUND // hai:core:010 - 配置文件不存在
|
|
532
|
+
HaiConfigError.CONFIG_PARSE_ERROR // hai:core:011 - YAML 解析失败
|
|
533
|
+
HaiConfigError.CONFIG_VALIDATION_ERROR // hai:core:012 - Schema 校验失败
|
|
534
|
+
HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
535
|
+
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## テスト
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
pnpm --filter @h-ai/core test
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## License
|
|
545
|
+
|
|
546
|
+
Apache-2.0
|
package/README.md
CHANGED
package/package.json
CHANGED