@h-ai/core 0.1.0-alpha8 → 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 ADDED
@@ -0,0 +1,653 @@
1
+ # @h-ai/core
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [Framework home](../../README.en.md)
6
+
7
+ ## Introduction
8
+
9
+ The core of hai Agent Framework: shared utilities, types, configuration management, and logging.
10
+
11
+ ## Core features
12
+
13
+ - **HaiResult types**: functional error handling (`ok` / `err`) with a shared result format across modules.
14
+ - **Unified logging**: pino on Node.js and loglevel in browsers, with the same API.
15
+ - **Internationalization (i18n)**: centralized locale management and typed message getters.
16
+ - **Zod validation error mapping**: Zod v3/v4 issue extraction and localization of default English messages.
17
+ - **Configuration management**: YAML loading, convention-based environment mapping, explicit interpolation, Zod validation, and file watching (Node.js only).
18
+ - **ID generation**: nanoid and UUID v4.
19
+ - **Error definitions**: standardized codes and consistent error definitions and instances across modules.
20
+ - **Module utilities**: NotInitializedKit proxies and shared lifecycle infrastructure.
21
+ - **Utility library**: type checks and object / string / array / async / time operations.
22
+
23
+ ## Getting started
24
+
25
+ ### Quick start
26
+
27
+ #### Node.js environment
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
+ #### Browser environment
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
+ The browser retains the `core.config` API shape for type compatibility, but does not support file configuration:
96
+
97
+ - `core.config.get()` returns `undefined`, `has()` returns `false`, and `keys()` returns `[]`.
98
+ - `loadEnvFiles()` / `load()` / `validate()` / `reload()` return `HaiCommonError.SERVICE_UNAVAILABLE`.
99
+ - `watch()` immediately calls its callback with the same error and returns a safe no-op unsubscribe function.
100
+ - Read configuration on the server and deliver it through an API, or use `core.init({ logging })` only to configure browser logging.
101
+
102
+ ### Detailed API
103
+
104
+ #### Logging (Logger)
105
+
106
+ `core.logger` is both the default Logger instance and the shared entry point for logger management.
107
+
108
+ ##### Write logs
109
+
110
+ Six levels: `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
+ ##### Create a module logger
119
+
120
+ ```typescript
121
+ // 独立名称空间,适合模块内全局使用
122
+ const logger = core.logger.create({ name: 'payment' })
123
+ logger.info('Payment processed')
124
+ ```
125
+
126
+ ##### Child loggers
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
+ > **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.
141
+
142
+ ##### Logger management
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
+ After `core.init()` loads `_core.yml.logging`, or after `configure` / `setLevel`, default, module and nested child loggers use current global configuration for subsequent output, including instances created before initialization. Explicit `create({ level, format, context })` options retain priority; omitted options inherit global configuration dynamically, and per-call context overrides fixed context. Additional `redact` rules apply to all these instances without recreating module loggers.
158
+
159
+ Browser logs still use the DevTools console: `pretty` prints readable messages with context, while `json` prints a JSON string containing `level` / `msg`. Default credential masking and configured path redaction both apply, including nested dot/bracket paths and `*`. Callers remain responsible for redacting message text.
160
+
161
+ Native `Error` values in log context are copied into diagnostics containing `name`, `message`, `stack`, `cause`, `AggregateError.errors`, and enumerable own context before default field/URL masking and configured `redact` paths run. Both backends use the same rules and leave the original error unchanged. Error-subgraph cycles become `[Circular]` and the 32-level limit becomes `[MaxDepth]`; custom replacements in these subgraphs follow the same normalization, while ordinary-object replacement behavior is unchanged. Ordinary-object cycles without an Error retain their original structure and are not guaranteed to be directly JSON-serializable. Error text masks explicitly labeled credentials such as `passwordHash=...` and `secretKey=...`, authorization headers and URL credentials. Arbitrary unlabeled secrets cannot be reliably detected; callers must still clean them or mask whole paths such as `error.message` / `error.stack`.
162
+
163
+ `core.logger` redacts log context by default: credentials such as `password`, `passwordHash`, `passwd`, `token`, `secret`, `secretKey`, `privateKey`, and `apiV3Key` become `[REDACTED]`. Fields such as `url` / `baseUrl` / `endpoint` mask embedded usernames/passwords, sensitive query parameters, and OAuth key/value fragments; malformed URLs return a fixed placeholder. Email addresses and phone numbers are not masked by default.
164
+
165
+ ##### Log level conventions
166
+
167
+ | Level | Use case |
168
+ | ------- | --------------------------------------------------- |
169
+ | `trace` | Detailed debugging data inside loops |
170
+ | `debug` | Function entry, parameter summaries, reads |
171
+ | `info` | Business events (initialized, write succeeded) |
172
+ | `warn` | Recoverable problems (validation failures, retries) |
173
+ | `error` | Operation failures requiring investigation |
174
+ | `fatal` | Fatal errors preventing continued service |
175
+
176
+ #### Internationalization (i18n)
177
+
178
+ Built-in locales are `zh-CN`, `en-US` and `ja-JP`. The `ja` alias normalizes to `ja-JP`; all modules share this setting.
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
+ #### Redaction utilities
203
+
204
+ `core.sanitize.sanitizeSensitiveFields()` recursively redacts objects and arrays. By default it handles password/passwordHash, passwd, token, secret/secretKey, privateKey, apiV3Key, and similar credentials, and masks embedded credentials in `url` / `baseUrl` / `endpoint`. `core.sanitize.sanitizeUrl(url, options)` also handles individual URLs and returns `(invalid url)` for malformed input. Custom matchers apply to parameter names as well. `sanitizeText` masks labeled credentials and URLs and accepts known secret values from the caller; it does not detect arbitrary unlabeled secrets.
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 validation error localization
223
+
224
+ `core.zodValidation` converts Zod `SafeParseError` / `ZodError` into flat form errors and maps Zod's
225
+ default English messages to the caller's own localized messages.
226
+
227
+ Unknown fields rejected by strict schemas map to `validationUnexpectedFields` without exposing internal field names. Custom message getters must handle this key; Kit and Serv provide Chinese, English and Japanese messages. Explicit business messages in a schema remain unchanged.
228
+
229
+ - Two public entry points:
230
+ - `createPrefixedZodMessageGetter()`: derive keys such as `serv_validation*` / `kit_validation*` from module prefixes without a manual `KEY_MAP`.
231
+ - `mapZodErrorToFormErrors()`: extract, localize, and flatten in one step.
232
+ - Custom business messages are preserved; only Zod's default English messages are replaced.
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
+ #### Configuration management (Node.js only)
286
+
287
+ > Browsers have no filesystem access. `core.config` offers placeholder APIs only: file-related methods return `SERVICE_UNAVAILABLE` and do not load YAML.
288
+
289
+ Node automatically loads and watches all `.yml` / `.yaml` files in the configuration directory. The CLI reads `build` / `deploy` from the project root `hai.json`; no YAML filename needs special handling. A failed per-file load result is logged and loading continues; applications must validate required configuration and check `success`. Directory scanning, logger setup and other exceptions may still propagate.
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 files and environment modes
343
+
344
+ Node-only `core.config.loadEnvFiles({ mode, envDir })` returns HaiResult and merges `.env` → `.env.<mode>` → `.env.local`, with existing process variables taking priority. development/production map to dev/prod; missing files are skipped. It does not mutate the process: check success before assigning. Track public environment files and `.env.example`; ignore private `.env.local`. core.init does not load dotenv. Browser calls return 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
+ ##### Environment variable mapping and interpolation
355
+
356
+ `core.config.load(name, ...)` automatically maps each YAML leaf to an
357
+ environment variable named `HAI_<CONFIG_NAME>_<YAML_PATH>`. Configuration names and keys are uppercased, levels are separated by `_`,
358
+ and camelCase is not split. For example, `ai.llm.apiKey` maps to `HAI_AI_LLM_APIKEY`,
359
+ and `ai.llm.maxTokens` to `HAI_AI_LLM_MAXTOKENS`. Array indexes are also levels:
360
+ `ai.llm.models[0].temperature` maps to `HAI_AI_LLM_MODELS_0_TEMPERATURE`.
361
+
362
+ Existing environment variables override YAML and are parsed with YAML scalar rules into numbers, booleans, and other native types.
363
+ 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.
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
+ For special variable names, use explicit `${VAR}` interpolation. Precedence is fixed:
380
+ Convention-based environment variables > explicit environment variables > YAML defaults.
381
+
382
+ - `${VAR}`: read `process.env.VAR`; return `CONFIG_ENV_VAR_MISSING` if absent.
383
+ - `${VAR:default}`: read `process.env.VAR`; use the value after the colon if absent.
384
+
385
+ **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.
386
+
387
+ `${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.
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 generation
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 generation requires cryptographic randomUUID/getRandomValues and throws if neither is available; it never falls back to Math.random.
414
+
415
+ #### Utilities
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
+ The Node entry uses node:crypto.timingSafeEqual over equal-length UTF-16LE code units. Different lengths return false; length is not hidden. The browser implementation avoids content-based early returns but has no JavaScript-engine timing guarantee and is not a server credential boundary. Surrounding branches, encoding and request handling are not made timing-safe by this helper.
488
+
489
+ #### Module utilities (module)
490
+
491
+ Shared initialization error handling for modules:
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
+ ### Error handling
508
+
509
+ #### HaiResult types
510
+
511
+ All operations that may fail return `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
+ #### Custom error codes (per module)
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
+ #### Configuration error codes
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
+ ### Request locale isolation
571
+
572
+ Wrap Node requests and SSR in `core.i18n.runWithLocale(locale, () => handleRequest())`. SvelteKit can use `kit.i18n.runWithLocale(locale, () => resolve(event))`; serv creates the scope automatically from request headers. Async descendants inherit it. Explicit `MessageOptions.locale` takes precedence over the request locale and the startup default. Within a scope, `setGlobalLocale` only changes that request; outside a scope it configures the startup default and must not be called by request hooks.
573
+
574
+ Browsers keep application-level `setGlobalLocale` / `createLocaleStore`. During SSR, UI store initialization and setters only update the store; messages read the request scope. `runWithLocale` requires the Node entry; browser translations use explicit locale options or application state.
575
+
576
+ ## Trusted addresses and request access policies (Node)
577
+
578
+ `core.access` supplies shared IP, time and path checks to HTTP hosts; it does not install HTTP interception itself. Serv uses `http.access`, SvelteKit uses `kit.createHandle({ access })`, and IAM `loginAccess` checks login only.
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
+ `createRequestCheck` validates and compiles at creation. `loadPolicy({ file: './config/security/request-access.yml' })` reads YAML once at startup, with a 1 MiB limit; invalid configuration throws and must stop host startup. Global conditions and all matching rules must pass. Denied IPs take precedence; empty IP and time lists impose no restriction, and the default timezone is UTC. Paths are actual URL pathnames including the deployment prefix, with exact or trailing `/**` matching. GET does not imply HEAD.
591
+
592
+ `core.access.createClientIpResolver()` resolves trusted addresses. Default `auto` ignores forwarded headers on ordinary hosts and uses container networks only in managed Compose topologies with a private backend. Use `[]` to disable proxy trust, or explicit proxy IPs/CIDRs for custom ingress. Never treat arbitrary headers as socket metadata. Request checks perform no file reads, CIDR parsing or full path-rule scan; timezone results are reused within each minute and recomputed across minute boundaries. Only the Node entry exposes `core.access`. Configuration schemas come through `core-config`, public types live in `core-types`, and functions are called through `core.access` registered in `core-main`.
593
+
594
+ Rules accept `encryption: required` or `disabled`; omission inherits the host. The selector uses the path index without evaluating IP or time conditions. Conflicting modes on overlapping paths and methods fail at compilation regardless of declaration order; matching modes and separate methods can be combined. `check.requiresEncryption` tells the host when transport encryption must be installed. Core owns neither keys nor encryption. HTTPS, authentication and access checks remain necessary.
595
+
596
+ See the [request access handbook](../../docs/usage/configuration/request-access.md) for defaults, ingress conditions, policy files, deployment and verification.
597
+
598
+ ## Configuration consumption
599
+
600
+ `_core.yml` accepts only name/version/env/defaultLocale metadata and logging; unknown fields fail loading. Core has no global debug, ID length/prefix, business features or locale registration list. Use logging.level, per-call core.id arguments, and application-owned app.yml with its own schema. defaultLocale is metadata, not automatic translation registration or a Paraglide switch. Importing Logger preserves LANG, LC_ALL and PYTHONIOENCODING, including Windows TTY. See the [configuration-to-consumer map (Chinese)](../../.agents/skills/hai-module-create/configuration.md).
601
+
602
+ ## Request correlation and log observation
603
+
604
+ Collection persistence and upload paths can use `core.context.run({ suppressLogObservers: true }, operation)` to prevent observing their own asynchronous logs. Normal output such as stdout remains enabled; observation resumes outside the scope.
605
+
606
+ `core.logger.observe` receives records permitted by the current log level after default and custom field redaction, and returns an unsubscribe function. Subscribers should collect into bounded memory and persist asynchronously; observation does not imply persistence. Observer failures and synchronous reentry are isolated. Free text and images still require capture-specific privacy rules.
607
+
608
+ On Node, `core.context.run` uses AsyncLocalStorage to isolate concurrent requests and child tasks. Existing module loggers inherit requestId/userId/scopeId/jobId. Only trusted server code supplies identities; correlation IDs do not authorize access. The browser exposes the same API but run only establishes a synchronous scope, without isolation across 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
+ ## Application lifecycle
628
+
629
+ `core.module.createRuntime([{ name, enabled?, start, close }])` starts in declaration order, closes in reverse order and merges concurrent calls. It registers cleanup before partial initialization. Failed close retains that module and dependencies for the next close/start retry. Errors expose module, phase, code and configuration paths, never raw configuration or SDK exceptions. Base/API/fullstack and the admin use this helper.
630
+
631
+ Startup errors explain configuration failures and suggest a fix. With `ext.fields`, `HaiConfigError.CONFIG_ENV_VAR_MISSING` identifies unset environment variables; `CONFIG_VALIDATION_ERROR` or Schema errors identify failed validation. Field paths alone produce a “missing or invalid configuration” hint without guessing the cause. Even when the startup boundary prints only `error.message`, it includes the fields, next steps and underlying error code. A custom initialization hook can report missing required environment variables by throwing an error with `code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code` and `ext: { fields: ['VARIABLE_NAME'] }`; the fullstack template uses this for the initial administrator password.
632
+
633
+ ## Error messages and correlated logs
634
+
635
+ HTTP errors carry safe localized messages and a reference ID. ToastContainer hides the ID by default and reveals it through its diagnostic icon. The same ID remains in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
636
+
637
+ Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
638
+
639
+ ### Public business progress
640
+
641
+ Internal ext is private. Explicitly approved and schema-validated business progress uses publicExt and is serialized as ext; no internal exception should be placed there.
642
+
643
+ ## Development and testing
644
+
645
+ ```bash
646
+ pnpm --filter @h-ai/core test
647
+ ```
648
+
649
+ See the [module JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) for declaration comments and compilable examples. Node core.init is synchronous and returns void. A failed per-file config.load result is logged and loading continues; validate required configuration and check success. Directory scanning and other exceptions may still throw. Business HaiResult APIs require a success check.
650
+
651
+ ## License
652
+
653
+ Apache-2.0