@h-ai/core 1.0.0-beta.2 → 1.0.0-beta.21
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 +715 -474
- package/README.ja.md +714 -473
- package/README.md +714 -473
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +39 -11
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-BEGTB6GL.js → chunk-WU6HUTBO.js} +262 -59
- package/dist/chunk-WU6HUTBO.js.map +1 -0
- package/dist/{core-types-_JqVAGGT.d.ts → core-types-Dp3Vr2uI.d.ts} +161 -15
- package/dist/node.d.ts +2 -2
- package/dist/node.js +495 -123
- package/dist/node.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-BEGTB6GL.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -4,649 +4,890 @@
|
|
|
4
4
|
|
|
5
5
|
[Framework home](../../README.en.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`@h-ai/core` is the shared foundation of Hai Framework. It gives applications and framework modules consistent configuration, errors, logging, and internationalization. Utilities work directly; call `core.init()` when a Node application needs file configuration.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Quick Start
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Each Core feature below has a purpose, examples, and relevant limits. Start with initialization when loading configuration; logging, messages, and general utilities work directly.
|
|
12
12
|
|
|
13
|
-
|
|
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.
|
|
13
|
+
Call runtime functions through `core.xxx()` or `core.<feature>.xxx()`. Import the result constructors `ok` / `err`, types, schemas, and error constants separately from the package entry. Their sections below show the usage.
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @h-ai/core
|
|
17
|
+
```
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
The custom configuration and form-validation examples also use Zod; install it with `pnpm add zod` before running them. Database and cache examples list their additional dependencies in their section.
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
### Initialization: `core.init`
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
import { core } from '@h-ai/core'
|
|
23
|
+
Load YAML and configure logging at Node startup, or configure logging in a browser. Initializing Core does not start databases or other modules.
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
core.init({ configDir: './config', watchConfig: true })
|
|
25
|
+
#### Node.js
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
core.logger.info('Server started', { port: 3000 })
|
|
37
|
-
core.logger.warn('Slow query', { duration: 2500 })
|
|
38
|
-
core.logger.error('Connection failed', { error })
|
|
27
|
+
Create `config/_core.yml`:
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
```yaml
|
|
30
|
+
# 应用元信息,供启动代码和页面读取。
|
|
31
|
+
name: orders
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
env: development
|
|
34
|
+
# 默认消息语言;启动代码读取后调用 setGlobalLocale 应用。
|
|
35
|
+
defaultLocale: zh-CN
|
|
36
|
+
logging:
|
|
37
|
+
level: info # 输出 info 及以上级别。
|
|
38
|
+
format: pretty # 本地开发使用便于阅读的日志。
|
|
39
|
+
```
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
reqLogger.info('Request started') // 日志自动包含 requestId + userId
|
|
47
|
-
reqLogger.error('Request failed') // 同上
|
|
41
|
+
```ts
|
|
42
|
+
import { core, CoreConfigSchema } from '@h-ai/core'
|
|
48
43
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
opLogger.info('Order created') // 包含 requestId + userId + operation
|
|
44
|
+
// 1. 读取 config 目录中的 YAML,_core.yml 对应名称 core。
|
|
45
|
+
core.init({ configDir: './config' }) // 返回 undefined;配置缓存中可读取 core。
|
|
52
46
|
|
|
53
|
-
//
|
|
54
|
-
core.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'en-US': { welcome: 'Welcome {name}', error: 'Error: {msg}' },
|
|
58
|
-
})
|
|
59
|
-
getMessage('welcome', { params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
47
|
+
// 2. 检查应用必需的配置;失败时停止启动,保留原始错误供排查。
|
|
48
|
+
const config = core.config.validate('core', CoreConfigSchema)
|
|
49
|
+
if (!config.success)
|
|
50
|
+
throw new Error(config.error.message, { cause: config.error })
|
|
60
51
|
|
|
61
|
-
//
|
|
62
|
-
const nanoid = core.id.generate()
|
|
63
|
-
const shortId = core.id.short()
|
|
64
|
-
const uuid = core.id.uuid()
|
|
65
|
-
const prefixed = core.id.withPrefix('order_')
|
|
52
|
+
// 成功时 config.success 为 true;config.data.name 为 'orders',defaultLocale 为 'zh-CN'。
|
|
66
53
|
|
|
67
|
-
//
|
|
68
|
-
core.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
core.
|
|
72
|
-
core.array.chunk([1, 2, 3, 4, 5], 2) // [[1,2], [3,4], [5]]
|
|
73
|
-
await core.async.delay(1000)
|
|
74
|
-
await core.async.retry(() => fetchData(), { maxRetries: 3, delay: 1000 })
|
|
75
|
-
core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
76
|
-
core.typeUtils.isDefined(value)
|
|
54
|
+
// 3. 将配置中的语言应用到默认消息;读取 YAML 本身不会切换语言。
|
|
55
|
+
core.i18n.setGlobalLocale(config.data.defaultLocale) // 返回 undefined;后续默认消息使用中文。
|
|
56
|
+
|
|
57
|
+
// 4. 记录已验证的应用名称,确认启动完成。
|
|
58
|
+
core.logger.info('Application ready', { name: config.data.name }) // 返回 undefined;info 日志包含 name: 'orders'。
|
|
77
59
|
```
|
|
78
60
|
|
|
79
|
-
|
|
61
|
+
The startup boundary stops when required configuration is unavailable. `core.init()` logs individual load failures and continues, so completion does not mean every configuration is valid. Directory scanning and logger setup can still throw.
|
|
62
|
+
|
|
63
|
+
#### Browser
|
|
80
64
|
|
|
81
|
-
```
|
|
65
|
+
```ts
|
|
82
66
|
import { core } from '@h-ai/core'
|
|
83
67
|
|
|
84
|
-
//
|
|
85
|
-
core.init({
|
|
68
|
+
// 浏览器不读取 YAML;此处只设置日志,输出 warn 及以上级别。
|
|
69
|
+
core.init({
|
|
70
|
+
logging: { level: 'warn' }
|
|
71
|
+
}) // 返回 undefined;之后 info 日志被过滤。
|
|
86
72
|
|
|
87
|
-
|
|
88
|
-
|
|
73
|
+
// 设置当前页面使用的默认消息语言。
|
|
74
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;页面默认消息使用中文。
|
|
89
75
|
|
|
90
|
-
//
|
|
91
|
-
core.
|
|
92
|
-
core.
|
|
76
|
+
// 给一次请求生成编号,后续日志可用同一编号关联排查。
|
|
77
|
+
const requestId = core.id.request() // 返回随机字符串,如 'req-V1StGXR8_Z5jdHi6B-myT'。
|
|
78
|
+
core.logger.warn('Request needs attention', { requestId }) // 返回 undefined;浏览器输出 warn 消息及同一个 requestId。
|
|
93
79
|
```
|
|
94
80
|
|
|
95
|
-
The
|
|
81
|
+
The host supplies public configuration fields; never send complete server settings or secrets. Browser `core.config.get()` / `has()` / `keys()` return `undefined` / `false` / `[]`. `loadEnvFiles()`, `load()`, `validate()`, and `reload()` return `SERVICE_UNAVAILABLE`; `getOrThrow()` throws. `watch()` immediately calls back with the same unavailable error and returns a no-op unsubscribe function.
|
|
96
82
|
|
|
97
|
-
|
|
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.
|
|
83
|
+
## Features
|
|
101
84
|
|
|
102
|
-
|
|
85
|
+
| Capability | Purpose | Main entry points |
|
|
86
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
87
|
+
| Configuration and startup | Load YAML, merge environment overrides, validate settings, and orchestrate module startup and shutdown | `core.config`, `core.init`, `core.module.createRuntime` |
|
|
88
|
+
| Results and errors | Express business success/failure, define error codes, and produce safe messages at API boundaries | `HaiResult`, `ok` / `err`, `core.error` |
|
|
89
|
+
| Logging and diagnostics | Structured logs, request correlation, credential masking, and log observation | `core.logger`, `core.context`, `core.sanitize` |
|
|
90
|
+
| Internationalization and validation messages | Manage message languages, isolate concurrent request locales, and localize Zod field errors | `core.i18n`, `core.zodValidation` |
|
|
91
|
+
| Request access control | Resolve trusted client addresses and check paths, methods, IPs, and time windows | `core.access` (Node only) |
|
|
92
|
+
| General utilities | Generate identifiers and reuse type, object, string, array, async, and time operations, plus consistent errors before module initialization | `core.id`, `core.typeUtils`, `core.object`, and others |
|
|
103
93
|
|
|
104
|
-
|
|
94
|
+
### Node and browser boundaries
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
Both environments use `import { core } from '@h-ai/core'`; the bundler selects the code for the target environment.
|
|
107
97
|
|
|
108
|
-
|
|
98
|
+
| Capability | Node.js | Browser |
|
|
99
|
+
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
100
|
+
| Logging, results/errors, messages, utilities, and ordered module startup/shutdown | Supported | Supported |
|
|
101
|
+
| YAML, dotenv, environment mapping, file watching | Reads and watches local configuration files | Cannot read or watch local files; loading methods return failure |
|
|
102
|
+
| Request correlation with `core.context.run` | Each request retains its own IDs after asynchronous work, without mixing them with other requests | Values exist only during synchronous callback code and are no longer available after `await` |
|
|
103
|
+
| Request locale with `core.i18n.runWithLocale` | Each request keeps its chosen language after asynchronous work; other requests do not change it | Throws; set the page language with `setGlobalLocale()` or pass a locale when getting a message |
|
|
104
|
+
| Request policies with `core.access` | Checks request addresses, paths, and time windows | This tool is unavailable |
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
`core.init()` configures Core settings and logging synchronously, returns `void`, and Core provides no `close()` method. Applications starting databases, caches, or other modules can manage them with `core.module.createRuntime()`: its returned object's `start()` initializes them in order, and `close()` cleans them up in reverse. A complete example follows under “Application startup and shutdown”.
|
|
111
107
|
|
|
112
|
-
|
|
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
|
-
```
|
|
108
|
+
### Files and environment settings: `core.config`
|
|
117
109
|
|
|
118
|
-
|
|
110
|
+
Read YAML, apply environment overrides, validate configuration types, and manage cached settings and file watchers. The file and environment operations below require Node.
|
|
119
111
|
|
|
120
|
-
|
|
121
|
-
// 独立名称空间,适合模块内全局使用
|
|
122
|
-
const logger = core.logger.create({ name: 'payment' })
|
|
123
|
-
logger.info('Payment processed')
|
|
124
|
-
```
|
|
112
|
+
#### Loading, validating, and reading
|
|
125
113
|
|
|
126
|
-
|
|
114
|
+
`core.init({ configDir, configSchemas?, watchConfig? })` scans direct `.yml` / `.yaml` children of the directory. `_core.yml` becomes `core`, `_db.yml` becomes `db`, and `app.yml` becomes `app`. The CLI reads build/deploy settings from root `hai.json`; they are separate from runtime YAML.
|
|
127
115
|
|
|
128
|
-
|
|
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') // 同上
|
|
116
|
+
`_core.yml` is validated with `CoreConfigSchema`, accepting only `name`, `version`, `env`, `defaultLocale`, and `logging`. The metadata defaults are `hai Admin`, `0.1.0`, `development`, and `zh-CN`; `logging` is optional. Metadata does not set `NODE_ENV`, switch message languages, or register application translations. Configure logging here; explicit `core.init({ logging })` overrides the application of YAML logging settings during that initialization.
|
|
133
117
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
For other files, host-supplied `configSchemas` provide structural and type hints during scanning. Full business validation stays in module initialization or explicit `validate()`. A direct `load(name, file, schema)` validates immediately.
|
|
119
|
+
|
|
120
|
+
The following file examples use this `config/app.yml` with no additional environment overrides:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# config/app.yml
|
|
124
|
+
port: 3000
|
|
138
125
|
```
|
|
139
126
|
|
|
140
|
-
|
|
127
|
+
load() reads a file and can validate it immediately; validate() validates cached settings. get() returns undefined for a missing entry, while getOrThrow() throws. Generic arguments help TypeScript and do not validate data. has()/keys() show which entries exist.
|
|
141
128
|
|
|
142
|
-
|
|
129
|
+
```ts
|
|
130
|
+
import { core } from '@h-ai/core'
|
|
131
|
+
import { z } from 'zod'
|
|
143
132
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
level: 'info',
|
|
148
|
-
format: 'json', // 'json' | 'pretty'
|
|
149
|
-
context: { app: 'my-service' }, // 全局默认上下文
|
|
150
|
-
redact: ['password', 'token'], // 脱敏字段
|
|
133
|
+
// 读取上方 config/app.yml;Schema 同时描述类型和校验规则。
|
|
134
|
+
const AppSchema = z.object({
|
|
135
|
+
port: z.number().int().min(1).max(65535)
|
|
151
136
|
})
|
|
137
|
+
const loaded = core.config.load('app', './config/app.yml', AppSchema)
|
|
138
|
+
if (!loaded.success)
|
|
139
|
+
throw new Error(loaded.error.message, { cause: loaded.error })
|
|
152
140
|
|
|
153
|
-
|
|
154
|
-
const level = core.logger.getLevel() // 'debug'
|
|
155
|
-
```
|
|
141
|
+
// loaded:{ success: true, data: { port: 3000 } }。
|
|
156
142
|
|
|
157
|
-
|
|
143
|
+
// validate 用于重新校验缓存;加载时未传 Schema 的配置也可在这里校验。
|
|
144
|
+
const validated = core.config.validate('app', AppSchema)
|
|
145
|
+
if (!validated.success)
|
|
146
|
+
throw new Error(validated.error.message, { cause: validated.error })
|
|
158
147
|
|
|
159
|
-
|
|
148
|
+
// validated:{ success: true, data: { port: 3000 } }。
|
|
160
149
|
|
|
161
|
-
|
|
150
|
+
// get 适合可选配置;getOrThrow 适合已确认存在的必需配置。
|
|
151
|
+
if (core.config.has('app')) { // true:上方已成功加载 app。
|
|
152
|
+
const optional = core.config.get<z.infer<typeof AppSchema>>('app') // { port: 3000 }。
|
|
153
|
+
const required = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // { port: 3000 };缺失则抛错。
|
|
154
|
+
core.logger.info('App config ready', { port: required.port, available: optional !== undefined })
|
|
155
|
+
}
|
|
156
|
+
core.config.keys() // 仅运行本节 load 示例时为 ['app']。
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Reloading, watching, and cleanup
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
Use reload() to reread a file or watch() to receive new values and errors. Watching survives ordinary saves, atomic replacement, and file deletion/recreation. Failures retain the last successful cache. unwatch() stops watchers; clear() also removes the cache. Omitting the name affects all entries.
|
|
164
162
|
|
|
165
|
-
|
|
163
|
+
watchConfig defaults to false. Reloading settings does not reinitialize running database modules. Use the cleanup calls below when the setting is no longer needed.
|
|
166
164
|
|
|
167
|
-
|
|
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 |
|
|
165
|
+
```ts
|
|
166
|
+
import { core } from '@h-ai/core'
|
|
175
167
|
|
|
176
|
-
|
|
168
|
+
// 前提:app 已由 init/load 加载。回调需先处理读取或校验失败。
|
|
169
|
+
const stop = core.config.watch('app', (_settings, error) => {
|
|
170
|
+
// 文件改成 port: 4000 后,成功回调收到 { port: 4000 }、error 为 undefined;失败时为 null 和错误对象。
|
|
171
|
+
if (error)
|
|
172
|
+
core.logger.error('Config reload failed', { error })
|
|
173
|
+
else
|
|
174
|
+
core.logger.info('Config reloaded')
|
|
175
|
+
})
|
|
176
|
+
// watch 返回取消函数 stop,不立即返回配置值。
|
|
177
|
+
core.config.isWatching('app') // true。
|
|
178
|
+
const reloaded = core.config.reload('app') // 文件仍为 port: 3000 时返回 { success: true, data: { port: 3000 } }。
|
|
179
|
+
if (!reloaded.success)
|
|
180
|
+
core.logger.warn('Manual reload failed', { error: reloaded.error })
|
|
181
|
+
|
|
182
|
+
stop() // 返回 undefined;取消当前回调,没有其他回调时停止底层监听。
|
|
183
|
+
core.config.unwatch('app') // 返回 undefined;isWatching('app') 变为 false。
|
|
184
|
+
core.config.clear('app') // 返回 undefined;get('app') 变为 undefined,has('app') 变为 false。
|
|
185
|
+
```
|
|
177
186
|
|
|
178
|
-
|
|
187
|
+
#### How environment variables change YAML settings
|
|
179
188
|
|
|
180
|
-
|
|
181
|
-
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
182
|
-
core.i18n.setGlobalLocale('en-US')
|
|
183
|
-
const current = core.i18n.getGlobalLocale() // 'en-US'
|
|
189
|
+
The framework derives environment override names from the configuration name and field path. For app.yml, port maps to HAI_APP_PORT and http.maxRetries maps to HAI_APP_HTTP_MAXRETRIES. Segments are uppercased and joined with underscores without splitting camelCase. YAML does not need a ${...} expression for this override to work.
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
const getMessage = core.i18n.createMessageGetter({
|
|
187
|
-
'zh-CN': { welcome: '欢迎 {name}', error: '错误:{code}' },
|
|
188
|
-
'en-US': { welcome: 'Welcome {name}', error: 'Error: {code}' },
|
|
189
|
-
})
|
|
191
|
+
For a fixed YAML value `port: 3000`, the result is 3000 without HAI_APP_PORT and 5000 after setting HAI_APP_PORT=5000.
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
193
|
+
Write `${VARIABLE}` in YAML to replace the expression with its environment value when loading. Text after the colon is the fallback when the variable is unset:
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
| YAML | Environment input | Result |
|
|
196
|
+
| ---------------------------- | ---------------------------- | ------------------------------------------------------- |
|
|
197
|
+
| `port: ${PORT}` | PORT=4000 | Uses 4000; an unset PORT returns CONFIG_ENV_VAR_MISSING |
|
|
198
|
+
| `port: ${PORT:3000}` | No PORT | Uses the expression's fallback, 3000 |
|
|
199
|
+
| `port: ${PORT:3000}` | PORT=4000, HAI_APP_PORT=5000 | Uses 5000: the convention variable wins |
|
|
200
|
+
| `label: server-${PORT:3000}` | PORT=4000 | Produces the string server-4000 |
|
|
196
201
|
|
|
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
|
-
```
|
|
202
|
+
The loader first checks HAI_APP_PORT. If set, it replaces this field and does not evaluate its ${PORT...} expression. Otherwise it reads YAML: an expression looks up PORT, while a fixed value is used directly. Therefore PORT need not exist when HAI_APP_PORT already overrides the field.
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
Within one source, parent JSON can supply a base and child variables refine it. HAI_APP='{"port":3000,"label":"local"}' plus HAI_APP_PORT=4000 produces port=4000 and label=local regardless of line order. A higher-priority whole-object replacement removes lower-priority child overrides. Array JSON replaces the list; indexed variables only update existing items and out-of-range indexes fail.
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
#### Reading .env files
|
|
205
207
|
|
|
206
|
-
|
|
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]' } }
|
|
208
|
+
core.init() does not read .env files. Call loadEnvFiles(), check success, assign its results to process.env, then load YAML. development/production map to dev/prod; missing files are skipped and duplicate keys in a file fail. Highest priority first: **process environment > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML**. Keep private .local files out of Git.
|
|
214
209
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
replacement: '***',
|
|
218
|
-
})
|
|
219
|
-
// => { email: '***' }
|
|
220
|
-
```
|
|
210
|
+
```ts
|
|
211
|
+
import { core } from '@h-ai/core'
|
|
221
212
|
|
|
222
|
-
|
|
213
|
+
// 前提:.env 写 HAI_APP_PORT=4000,没有其他来源覆盖这个变量。
|
|
214
|
+
// 先读取并合并 .env 文件与进程环境,失败时停止启动。
|
|
215
|
+
const result = core.config.loadEnvFiles({ mode: 'dev', envDir: '.' })
|
|
216
|
+
if (!result.success)
|
|
217
|
+
throw new Error(result.error.message, { cause: result.error })
|
|
218
|
+
// 成功时 result.success 为 true,result.data.HAI_APP_PORT 为 '4000';其他键来自合并后的环境。
|
|
219
|
+
// 返回值不会自动写入进程;显式应用后再初始化。
|
|
220
|
+
Object.assign(process.env, result.data)
|
|
221
|
+
core.init({ configDir: './config' }) // 返回 undefined;app.yml 中 port: 3000 被覆盖为数字 4000。
|
|
222
|
+
```
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
default English messages to the caller's own localized messages.
|
|
224
|
+
#### What type does the replacement produce?
|
|
226
225
|
|
|
227
|
-
|
|
226
|
+
Environment variables start as text. Pass a Zod schema to `load()` / `resolve()` to convert replacements by field type: `z.number()` produces numbers, `z.boolean()` produces booleans, and `z.string()` preserves strings.
|
|
228
227
|
|
|
229
|
-
-
|
|
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.
|
|
228
|
+
A full-value ${PORT:3000} can produce number 3000, and ${ENABLED:false} boolean false. Mixed text such as server-${PORT:3000} stays a string. A string field code preserves the fallback 00123 as '00123', including leading zeros. This example supplies the equivalent YAML values and environment in memory without reading a file:
|
|
233
229
|
|
|
234
|
-
```
|
|
235
|
-
import type { ZodMessageGetter } from '@h-ai/core'
|
|
230
|
+
```ts
|
|
236
231
|
import { core } from '@h-ai/core'
|
|
237
232
|
import { z } from 'zod'
|
|
238
233
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
234
|
+
// 用显式环境数据演示覆盖顺序、数值/布尔转换与字符串保留。
|
|
235
|
+
const AppSchema = z.object({
|
|
236
|
+
port: z.number(),
|
|
237
|
+
enabled: z.boolean(),
|
|
238
|
+
label: z.string(),
|
|
239
|
+
code: z.string()
|
|
243
240
|
})
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
email: 'bad',
|
|
260
|
-
password: '123',
|
|
261
|
-
nickname: '',
|
|
241
|
+
const resolved = core.config.resolve({
|
|
242
|
+
name: 'app',
|
|
243
|
+
|
|
244
|
+
value: {
|
|
245
|
+
// eslint-disable-next-line no-template-curly-in-string -- 保留供配置解析器处理的字面占位符。
|
|
246
|
+
port: '${PORT:3000}',
|
|
247
|
+
// eslint-disable-next-line no-template-curly-in-string -- 保留供配置解析器处理的字面占位符。
|
|
248
|
+
enabled: '${ENABLED:false}',
|
|
249
|
+
// eslint-disable-next-line no-template-curly-in-string -- 保留供配置解析器处理的字面占位符。
|
|
250
|
+
label: 'server-${PORT:3000}',
|
|
251
|
+
// eslint-disable-next-line no-template-curly-in-string -- 保留供配置解析器处理的字面占位符。
|
|
252
|
+
code: '${APP_CODE:00123}'
|
|
253
|
+
},
|
|
254
|
+
environment: { PORT: '4000', HAI_APP_PORT: '5000' },
|
|
255
|
+
schema: AppSchema,
|
|
262
256
|
})
|
|
257
|
+
if (!resolved.success)
|
|
258
|
+
throw new Error(resolved.error.message, { cause: resolved.error })
|
|
263
259
|
|
|
264
|
-
|
|
265
|
-
const getMessage = core.zodValidation.createPrefixedZodMessageGetter(
|
|
266
|
-
'kit',
|
|
267
|
-
(messageKey, params) => {
|
|
268
|
-
if (messageKey === 'kit_validationEmail')
|
|
269
|
-
return '请输入合法邮箱地址'
|
|
270
|
-
if (messageKey === 'kit_validationStringMin')
|
|
271
|
-
return `至少输入 ${params?.min} 个字符`
|
|
272
|
-
return '输入不合法'
|
|
273
|
-
},
|
|
274
|
-
)
|
|
275
|
-
const errors = core.zodValidation.mapZodErrorToFormErrors(result.error, getMessage)
|
|
276
|
-
|
|
277
|
-
errors[0]?.field // 'email'
|
|
278
|
-
errors[0]?.message // '请输入合法邮箱地址'
|
|
279
|
-
errors[2]?.message // '请填写昵称'(schema 自定义消息原样保留)
|
|
260
|
+
// resolved:{ success: true, data: { port: 5000, enabled: false, label: 'server-4000', code: '00123' } }。
|
|
280
261
|
|
|
281
|
-
|
|
282
|
-
|
|
262
|
+
// resolve 只做替换和类型还原;再用 Schema 确认最终数据符合要求。
|
|
263
|
+
const validated = AppSchema.safeParse(resolved.data)
|
|
264
|
+
if (!validated.success)
|
|
265
|
+
throw new Error(validated.error.message, { cause: validated.error })
|
|
266
|
+
core.logger.info('Config resolved', { settings: validated.data })
|
|
267
|
+
// settings: { port: 5000, enabled: false, label: 'server-4000', code: '00123' }
|
|
283
268
|
```
|
|
284
269
|
|
|
285
|
-
|
|
270
|
+
An empty string means the variable is set with no characters; it is not missing, null, false, or zero. APP_CODE='' leaves code as '' instead of using 00123. The schema decides whether this is allowed; numeric fields usually reject empty strings.
|
|
286
271
|
|
|
287
|
-
|
|
272
|
+
Substitution processes expressions in YAML; `server-${PORT}` inside an environment value or parent JSON remains literal text. For example, HAI_APP_LABEL='server-${PORT}' stays server-${PORT}. Put the expression in YAML when substitution is intended.
|
|
288
273
|
|
|
289
|
-
|
|
274
|
+
Without a schema, paths come from existing YAML/JSON structure and single expressions use YAML scalar rules for numbers/booleans. configSchemas can supply optional legal paths missing from YAML. Final business validation still belongs in validate() or module initialization.
|
|
290
275
|
|
|
291
|
-
|
|
292
|
-
import { core, HaiConfigError } from '@h-ai/core'
|
|
293
|
-
import { z } from 'zod'
|
|
276
|
+
#### Tools for generating configuration files
|
|
294
277
|
|
|
295
|
-
|
|
296
|
-
core.init({
|
|
297
|
-
configDir: './config', // 扫描配置目录内所有 .yml / .yaml
|
|
298
|
-
watchConfig: true, // 监听变更并自动重载
|
|
299
|
-
})
|
|
278
|
+
resolve() also lets build/deploy tools calculate settings in memory. toEnv() converts objects to environment variables, mergeEnv() merges sources, and stringifyEnv()/parseEnv() encode/decode literal .env text. These tools do not mask secrets.
|
|
300
279
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
280
|
+
```ts
|
|
281
|
+
import { core } from '@h-ai/core'
|
|
282
|
+
|
|
283
|
+
// 配置生成器把普通对象转成 HAI_APP_PORT 等变量;数组保留为 JSON。
|
|
284
|
+
const values = core.config.toEnv({
|
|
285
|
+
prefix: 'HAI_APP',
|
|
286
|
+
value: {
|
|
287
|
+
port: 3000,
|
|
288
|
+
tags: ['local']
|
|
289
|
+
}
|
|
305
290
|
})
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
291
|
+
// values:{ HAI_APP_PORT: '3000', HAI_APP_TAGS: '["local"]' }。
|
|
292
|
+
const merged = core.config.mergeEnv([
|
|
293
|
+
values,
|
|
294
|
+
{ HAI_APP_PORT: '4000' }
|
|
295
|
+
])
|
|
296
|
+
// merged:{ HAI_APP_PORT: '4000', HAI_APP_TAGS: '["local"]' }。
|
|
297
|
+
const text = core.config.stringifyEnv(merged) // 编码为可写入 .env 的文本。
|
|
298
|
+
// text 含以下两行,每行以换行符结尾:
|
|
299
|
+
// HAI_APP_PORT='4000'
|
|
300
|
+
// HAI_APP_TAGS='["local"]'
|
|
301
|
+
const restored = core.config.parseEnv(text) // 解析回字面字符串,不执行其中的变量。
|
|
302
|
+
// restored:{ HAI_APP_PORT: '4000', HAI_APP_TAGS: '["local"]' }。
|
|
303
|
+
core.logger.info('Generated config verified', { port: restored.HAI_APP_PORT })
|
|
304
|
+
```
|
|
313
305
|
|
|
314
|
-
|
|
315
|
-
const validResult = core.config.validate('app', AppSchema)
|
|
306
|
+
Plain objects expand by default; arrays, dynamic dictionaries, and empty objects retain JSON. See the [runtime configuration guide](../../docs/usage/configuration/runtime.md) for replacement generation rules, full precedence, and application integration.
|
|
316
307
|
|
|
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', ...]
|
|
308
|
+
### Logging: `core.logger`
|
|
323
309
|
|
|
324
|
-
|
|
325
|
-
const reloadResult = core.config.reload('app')
|
|
310
|
+
core.logger records structured logs. Levels progress from detail to severity: trace, debug, info, warn, error, fatal. fatal records a log; the application decides whether to exit.
|
|
326
311
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
312
|
+
`core.logger` supports `trace` / `debug` / `info` / `warn` / `error` / `fatal`. Node uses pino and browsers use loglevel. Unconfigured loggers default to `info` / `pretty`; a `logging` object parsed by `LoggingConfigSchema` defaults its format to `json`. Node falls back to JSON when optional `pino-pretty` is unavailable.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { core } from '@h-ai/core'
|
|
316
|
+
|
|
317
|
+
// 先设置级别;低于当前级别的日志不会输出。
|
|
318
|
+
core.logger.setLevel('trace') // 返回 undefined;以下六个级别均会输出。
|
|
319
|
+
core.logger.trace('Processing detail', { step: 1 })
|
|
320
|
+
core.logger.debug('Read started')
|
|
321
|
+
core.logger.info('Order created', { orderId: 'order-1' })
|
|
322
|
+
core.logger.warn('Retry scheduled')
|
|
323
|
+
core.logger.error('Operation failed', {
|
|
324
|
+
error: new Error('Example failure')
|
|
334
325
|
})
|
|
335
|
-
|
|
326
|
+
core.logger.fatal('Service cannot continue')
|
|
327
|
+
// 六次日志调用均返回 undefined;分别输出对应级别、上面的消息及所传字段。
|
|
328
|
+
// 例如 info 日志的消息为 'Order created',字段 orderId 为 'order-1'。
|
|
329
|
+
```
|
|
336
330
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
331
|
+
create() makes a named logger; child() attaches fixed context. configure() sets output and extra redaction paths; setLevel()/getLevel() change or inspect the global level.
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import { core } from '@h-ai/core'
|
|
335
|
+
|
|
336
|
+
// 全局配置可作用于已经创建的 Logger;redact 补充业务隐私字段。
|
|
337
|
+
core.logger.configure({
|
|
338
|
+
level: 'info',
|
|
339
|
+
format: 'json',
|
|
340
|
+
redact: ['customer.email']
|
|
341
|
+
}) // 返回 undefined;后续日志按此配置输出。
|
|
342
|
+
const orders = core.logger.create({ name: 'orders' }) // 返回 Logger 对象,可调用 info/warn 等方法。
|
|
343
|
+
const request = core.logger.child({ requestId: 'request-1' }) // 返回带固定 requestId 的 Logger。
|
|
344
|
+
orders.info('Order created', {
|
|
345
|
+
customer: { email: 'user@example.com' }
|
|
346
|
+
}) // 返回 undefined;输出中 customer.email 为 '[REDACTED]'。
|
|
347
|
+
request.warn('Request needs attention') // 返回 undefined;输出中 requestId 为 'request-1'。
|
|
348
|
+
core.logger.getLevel() // 'info'。
|
|
340
349
|
```
|
|
341
350
|
|
|
342
|
-
|
|
351
|
+
Existing default, module, and nested child loggers use current global settings on subsequent writes; `setLevel()` also takes effect immediately. Explicit `create()` level/format/context options take precedence; unspecified options inherit dynamically. Per-call context overrides fixed context, then `core.context` adds current request correlation fields. Browser JSON includes `level` / `msg`; pretty output includes readable text and context.
|
|
343
352
|
|
|
344
|
-
|
|
353
|
+
observe() supports diagnostics collection, receiving records after level filtering and masking and returning an unsubscribe function. Collect bounded memory data in the callback and persist asynchronously elsewhere. Observation is not persistence; exceptions and synchronous reentry are isolated.
|
|
345
354
|
|
|
346
355
|
```ts
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
core.
|
|
356
|
+
import { core } from '@h-ai/core'
|
|
357
|
+
|
|
358
|
+
// 观察经过级别过滤和脱敏的记录;内存收集设置上限。
|
|
359
|
+
const recentMessages: string[] = []
|
|
360
|
+
const stop = core.logger.observe((record) => {
|
|
361
|
+
if (recentMessages.length === 100)
|
|
362
|
+
recentMessages.shift()
|
|
363
|
+
recentMessages.push(record.message)
|
|
364
|
+
})
|
|
365
|
+
// observe 返回取消函数 stop;本例 warn 日志触发一次回调,record.message 为 'Observed event'。
|
|
366
|
+
core.logger.warn('Observed event')
|
|
367
|
+
recentMessages // ['Observed event']。
|
|
368
|
+
stop() // 返回 undefined;后续日志不再进入这个回调。
|
|
352
369
|
```
|
|
353
370
|
|
|
354
|
-
|
|
371
|
+
### Request correlation: `core.context`
|
|
355
372
|
|
|
356
|
-
|
|
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`.
|
|
373
|
+
run() gives a request/task its requestId, userId, scopeId, jobId, and other correlation fields; get() reads them. On Node they remain available after database or other asynchronous work, without another request overwriting them. Existing loggers automatically include these fields.
|
|
361
374
|
|
|
362
|
-
|
|
363
|
-
|
|
375
|
+
```ts
|
|
376
|
+
import { core } from '@h-ai/core'
|
|
364
377
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
378
|
+
// Node 示例:同一请求等待异步处理后,仍能取得自己的编号。
|
|
379
|
+
await core.context.run({ requestId: 'request-1', userId: 'user-1' }, async () => {
|
|
380
|
+
await core.async.delay(1)
|
|
381
|
+
const current = core.context.get() // 包含 requestId: 'request-1'、traceId: 'request-1'、userId: 'user-1';其他字段为 undefined。
|
|
382
|
+
core.logger.info('Request continued', { requestId: current.requestId })
|
|
383
|
+
return current.requestId
|
|
384
|
+
}) // run 返回回调的结果;此处 await 后得到 'request-1'。
|
|
385
|
+
core.context.get() // {},回到外层后不保留上面请求的上下文。
|
|
386
|
+
|
|
387
|
+
// 采集器自身写日志时,可暂停观察回调,正常日志输出仍然执行。
|
|
388
|
+
core.context.run({ suppressLogObservers: true }, () => core.logger.warn('Collector diagnostic'))
|
|
389
|
+
// 返回 undefined;输出 warn 日志,但不会触发日志观察回调。
|
|
372
390
|
```
|
|
373
391
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
392
|
+
In browsers values exist only in synchronous callback code and are not retained after await. Correlation IDs do not authorize access; trusted server code must provide identity and tenant scope.
|
|
393
|
+
|
|
394
|
+
### Cleaning diagnostic data: `core.sanitize`
|
|
395
|
+
|
|
396
|
+
Clean credentials before logs, diagnostic exports, or public errors. Check sensitive names, mask a single value or object tree, clean URL credentials, or remove labeled/known secrets from text.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { core } from '@h-ai/core'
|
|
400
|
+
|
|
401
|
+
core.sanitize.isSensitiveKey('apiKey') // true。
|
|
402
|
+
core.sanitize.sanitizeSensitiveValue('example-only', 'password') // '[REDACTED]'。
|
|
403
|
+
const safe = core.sanitize.sanitizeSensitiveFields({ password: 'example-only', email: 'user@example.com' })
|
|
404
|
+
// safe:{ password: '[REDACTED]', email: 'user@example.com' },原对象不变。
|
|
405
|
+
// 默认仅遮蔽凭据;额外业务隐私可在已处理凭据的副本上继续清理。
|
|
406
|
+
core.sanitize.sanitizeSensitiveFields(safe, {
|
|
407
|
+
matcher: ['email'],
|
|
408
|
+
replacement: '***'
|
|
409
|
+
})
|
|
410
|
+
// => { password: '[REDACTED]', email: '***' }
|
|
411
|
+
core.sanitize.sanitizeUrl('https://user:example-only@example.com/?token=example-only')
|
|
412
|
+
// => 'https://[REDACTED]:[REDACTED]@example.com/?token=[REDACTED]'
|
|
413
|
+
core.sanitize.sanitizeText('Bearer example-only') // '[REDACTED]'。
|
|
414
|
+
core.sanitize.sanitizeText('Request failed with a known private value', ['private value'])
|
|
415
|
+
// => 'Request failed with a known [REDACTED]'
|
|
377
416
|
```
|
|
378
417
|
|
|
379
|
-
|
|
380
|
-
Convention-based environment variables > explicit environment variables > YAML defaults.
|
|
418
|
+
defaultSensitiveKeyRegex/defaultSensitiveReplacement expose the default matching rule and [REDACTED] value. A custom matcher replaces the default rule, so retain default credential masking before applying extra business privacy rules.
|
|
381
419
|
|
|
382
|
-
|
|
383
|
-
- `${VAR:default}`: read `process.env.VAR`; use the value after the colon if absent.
|
|
420
|
+
Logs mask credential fields such as password/passwordHash, token, secretKey, and privateKey, plus URL userinfo, sensitive query parameters, and OAuth key-value fragments. Invalid URLs return `(invalid url)`. `redact` adds business privacy paths using dots, brackets, and single-level `*`, case sensitively. Email addresses and phone numbers remain visible by default.
|
|
384
421
|
|
|
385
|
-
|
|
422
|
+
Native `Error` values are copied with name/message/stack/cause, aggregate errors, and enumerable context before masking; originals are unchanged. Error-subgraph cycles and the 32-level depth limit use `[Circular]` / `[MaxDepth]`. Ordinary object cycles remain and may prevent JSON serialization. Message and error text masking handles explicit credential labels and URLs, not arbitrary unlabeled secrets; callers must clean these or redact the whole path.
|
|
386
423
|
|
|
387
|
-
|
|
424
|
+
Masked results contain data only. Plain objects and class instances contribute their own enumerable data properties; prototypes, accessors, and custom `toJSON()` methods are not retained or invoked. Dates become ISO strings and URLs become masked strings, without changing the original. Still select only useful diagnostic fields from SDK/business objects and explicitly clean unlabeled secrets and business privacy data.
|
|
388
425
|
|
|
389
|
-
|
|
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'
|
|
426
|
+
### Results and errors: `core.error`
|
|
495
427
|
|
|
496
|
-
|
|
497
|
-
HaiDbError.NOT_INITIALIZED,
|
|
498
|
-
() => dbM('db_notInitialized'),
|
|
499
|
-
)
|
|
428
|
+
core.error defines module errors, creates failure instances, and builds safe HTTP/UI messages. Codes stay stable and messages can follow the current locale. Serv/Kit or another HTTP framework handles response status and headers.
|
|
500
429
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// 创建占位 Proxy(同步接口)
|
|
504
|
-
const currentHash: HashOperations = notInitialized.proxy<HashOperations>('sync')
|
|
505
|
-
```
|
|
430
|
+
```ts
|
|
431
|
+
import { core } from '@h-ai/core'
|
|
506
432
|
|
|
507
|
-
|
|
433
|
+
// 定义稳定错误码,消息在创建实例时根据当前语言获取。
|
|
434
|
+
const OrderError = core.error.buildHaiErrorsDef('orders', { NOT_FOUND: '300:404' })
|
|
435
|
+
// OrderError.NOT_FOUND:{ code: 'hai:orders:300', httpStatus: 404, system: 'hai', module: 'orders' }。
|
|
436
|
+
const failure = core.error.buildHaiErrorInst(OrderError.NOT_FOUND, core.i18n.coreM('core_errorNotFound', { locale: 'zh-CN' }))
|
|
437
|
+
// failure:上面的错误定义加上 message: '资源不存在'。
|
|
438
|
+
const publicError = core.error.toPublic(failure, 'request-1')
|
|
439
|
+
// publicError:保留 code/httpStatus/system/module,增加 traceId: 'request-1';
|
|
440
|
+
// message 为 '资源不存在 (traceId: request-1)'。
|
|
441
|
+
const message = core.error.formatMessage(publicError) // '资源不存在 (traceId: request-1)',编号不会重复追加。
|
|
442
|
+
core.logger.warn('Order unavailable', { error: failure, publicMessage: message })
|
|
443
|
+
```
|
|
508
444
|
|
|
509
|
-
####
|
|
445
|
+
#### Shared result type and standalone constructors
|
|
510
446
|
|
|
511
|
-
|
|
447
|
+
`HaiResult<T>` uses success to distinguish data from error. Import `ok` / `err` to construct successful or failed results. Check success after business operations. Getters, utilities, and factories follow their direct-value or throwing signatures.
|
|
512
448
|
|
|
513
|
-
```
|
|
449
|
+
```ts
|
|
514
450
|
import type { HaiResult } from '@h-ai/core'
|
|
515
451
|
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
516
452
|
|
|
453
|
+
// 业务失败返回 err,成功返回 ok;共用构造器保持独立导入。
|
|
517
454
|
function divide(a: number, b: number): HaiResult<number> {
|
|
518
455
|
if (b === 0)
|
|
519
|
-
return err(HaiCommonError.VALIDATION_ERROR, '
|
|
520
|
-
return ok(a / b)
|
|
456
|
+
return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM('core_errorValidation', { locale: 'zh-CN' }))
|
|
457
|
+
return ok(a / b) // 如 ok(5) 返回 { success: true, data: 5 }。
|
|
521
458
|
}
|
|
522
459
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
else
|
|
528
|
-
core.logger.
|
|
529
|
-
|
|
460
|
+
// 先检查 success,再读取 data 或 error。
|
|
461
|
+
const result = divide(10, 2) // { success: true, data: 5 }。
|
|
462
|
+
if (!result.success)
|
|
463
|
+
core.logger.warn('Calculation failed', { error: result.error })
|
|
464
|
+
else
|
|
465
|
+
core.logger.info('Calculation completed', { value: result.data })
|
|
466
|
+
|
|
467
|
+
const failed = divide(10, 0)
|
|
468
|
+
// failed:{ success: false, error: { code: 'hai:common:200', httpStatus: 400,
|
|
469
|
+
// system: 'hai', module: 'common', message: '验证失败' } },无 data。
|
|
470
|
+
if (!failed.success)
|
|
471
|
+
core.logger.warn('Calculation rejected', { code: failed.error.code })
|
|
530
472
|
```
|
|
531
473
|
|
|
532
|
-
|
|
474
|
+
Use `core.error.toPublic(error, traceId?, locale?)` at HTTP/UI boundaries. It replaces 5xx details with a localized generic message, removes known credentials from other messages, and retains safe fields. Correlation IDs are selected from the explicit argument, context traceId/requestId, or the error's traceId, otherwise generated. `formatMessage()` appends a valid ID without duplicating the same suffix.
|
|
475
|
+
|
|
476
|
+
Internal `cause` / `ext` are private by default. Field errors for 400/422 can retain `field` / `message`; explicitly approved, business-schema-validated `publicExt` is masked and exposed as `ext`. Core does not validate its business safety automatically. Serv/Kit/UI hosts handle HTTP status, headers, and message components.
|
|
533
477
|
|
|
534
|
-
|
|
535
|
-
|
|
478
|
+
| Configuration error | Code | Meaning |
|
|
479
|
+
| ------------------------- | -------------- | -------------------------------------------- |
|
|
480
|
+
| `CONFIG_FILE_NOT_FOUND` | `hai:core:010` | Missing file |
|
|
481
|
+
| `CONFIG_PARSE_ERROR` | `hai:core:011` | YAML/dotenv reading or parsing failed |
|
|
482
|
+
| `CONFIG_VALIDATION_ERROR` | `hai:core:012` | Invalid schema input or environment override |
|
|
483
|
+
| `CONFIG_ENV_VAR_MISSING` | `hai:core:013` | Required interpolation variable missing |
|
|
484
|
+
| `CONFIG_NOT_LOADED` | `hai:core:014` | Configuration not loaded |
|
|
485
|
+
|
|
486
|
+
These constants belong to `HaiConfigError`; cross-module failures use `HaiCommonError`.
|
|
487
|
+
|
|
488
|
+
`HaiCommonError.RATE_LIMIT_EXCEEDED` maps to `hai:common:203` / HTTP 429. Kit shortcut responses and guards use the same common error definitions.
|
|
489
|
+
|
|
490
|
+
HTTP/UI boundaries expose business messages only for explicit valid 4xx statuses. Missing or invalid `httpStatus` is treated as 500 with a safe message. Custom business errors must carry a complete error definition or an explicit status.
|
|
491
|
+
|
|
492
|
+
### Messages and locales: `core.i18n`
|
|
493
|
+
|
|
494
|
+
Create message getters with parameter interpolation, read built-in Core messages, and manage locale selection. Constants list framework locales and do not register application translations.
|
|
495
|
+
|
|
496
|
+
```ts
|
|
536
497
|
import { core } from '@h-ai/core'
|
|
537
498
|
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
499
|
+
// 页面语言或服务进程的启动默认值;请求中使用 runWithLocale 单独选择语言。
|
|
500
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;后续默认消息使用中文。
|
|
501
|
+
core.i18n.getGlobalLocale() // 'zh-CN'。
|
|
502
|
+
core.i18n.coreM('core_errorNotFound') // '资源不存在'。
|
|
503
|
+
core.i18n.DEFAULT_LOCALE // zh-CN。
|
|
504
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]。
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Message locales are `zh-CN`, `en-US`, and `ja-JP`; `zh` / `en` / `ja` aliases are normalized. Explicit locale takes priority over Node request locale and then the application default. Unsupported locales fall back to `zh-CN`; missing keys return the key itself.
|
|
544
508
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
// DbError.CONNECTION_FAILED → { code: 'hai:db:010', httpStatus: 500, system: 'hai', module: 'db' }
|
|
509
|
+
```ts
|
|
510
|
+
import { core } from '@h-ai/core'
|
|
548
511
|
|
|
549
|
-
//
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
'
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
)
|
|
512
|
+
// 业务字典按语言组织;占位参数在取消息时传入。
|
|
513
|
+
const message = core.i18n.createMessageGetter({
|
|
514
|
+
'zh-CN': { welcome: '欢迎 {name}' },
|
|
515
|
+
'en-US': { welcome: 'Welcome {name}' },
|
|
516
|
+
'ja-JP': { welcome: 'ようこそ {name}' },
|
|
517
|
+
})
|
|
518
|
+
// createMessageGetter 返回消息函数;如 message('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) 得到 '欢迎 Alice'。
|
|
519
|
+
const greeting = await core.i18n.runWithLocale('en-US', async () => {
|
|
520
|
+
await Promise.resolve()
|
|
521
|
+
return message('welcome', {
|
|
522
|
+
params: { name: 'Alice' }
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
// greeting:'Welcome Alice';runWithLocale 返回回调的结果,此处等待 Promise 得到字符串。
|
|
526
|
+
core.logger.info('Greeting prepared', { greeting })
|
|
556
527
|
```
|
|
557
528
|
|
|
558
|
-
|
|
529
|
+
Wrap actual asynchronous Node request/SSR work in `runWithLocale()`. Serv establishes the request locale automatically; Kit provides the same entry point. Within a request, `setGlobalLocale()` changes only that request. Outside it, use it only for startup defaults. Browsers use application-level `setGlobalLocale()` or explicit locales. SSR UI stores should not modify process defaults.
|
|
530
|
+
|
|
531
|
+
### Field validation messages: `core.zodValidation`
|
|
532
|
+
|
|
533
|
+
Convert Zod errors into { field, message }[] for form hints. createPrefixedZodMessageGetter() selects translation keys by prefix; mapZodErrorToFormErrors() extracts, translates, and flattens field paths.
|
|
559
534
|
|
|
560
|
-
```
|
|
561
|
-
import {
|
|
535
|
+
```ts
|
|
536
|
+
import { core } from '@h-ai/core'
|
|
537
|
+
import { z } from 'zod'
|
|
562
538
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
539
|
+
// 此例复用 Core 已有翻译;业务表单可注入自己的消息获取器。
|
|
540
|
+
const message = core.zodValidation.createPrefixedZodMessageGetter('core', key =>
|
|
541
|
+
key === 'core_validationEmail'
|
|
542
|
+
? core.i18n.coreM('core_validationEmail', { locale: 'zh-CN' })
|
|
543
|
+
: core.i18n.coreM('core_errorValidation', { locale: 'zh-CN' }))
|
|
544
|
+
// 返回消息函数;message('validationEmail') 得到 '请输入有效的邮箱地址'。
|
|
545
|
+
const parsed = z.object({
|
|
546
|
+
email: z.email()
|
|
547
|
+
}).safeParse({ email: 'invalid' })
|
|
548
|
+
if (!parsed.success) {
|
|
549
|
+
const errors = core.zodValidation.mapZodErrorToFormErrors(parsed.error, message)
|
|
550
|
+
// errors:[{ field: 'email', message: '请输入有效的邮箱地址' }]。
|
|
551
|
+
core.logger.info('Validation failed', { errors })
|
|
552
|
+
}
|
|
568
553
|
```
|
|
569
554
|
|
|
570
|
-
|
|
555
|
+
Supports Zod v3/v4 issues/errors. Default English messages are translated; explicit business messages remain unchanged. Paths use dots and global errors use _. Unknown fields in strict schemas use validationUnexpectedFields without exposing internal names.
|
|
556
|
+
|
|
557
|
+
### Module startup and helpers: `core.module`
|
|
571
558
|
|
|
572
|
-
|
|
559
|
+
Applications and module implementations can reuse these helpers to manage several modules or return consistent failures before initialization. The application chooses its actual modules.
|
|
573
560
|
|
|
574
|
-
|
|
561
|
+
#### Application startup and shutdown
|
|
575
562
|
|
|
576
|
-
|
|
563
|
+
Applications using several modules need to initialize resources such as databases before handling business work, then release them afterward. `core.module.createRuntime()` accepts each module's startup and cleanup functions and returns an object the application can call. Creating it does not start the modules.
|
|
577
564
|
|
|
578
|
-
|
|
565
|
+
This local development example additionally requires `@h-ai/reldb` and `@h-ai/cache`. Both use in-memory settings for demonstration; production must use the shared backends required by each module.
|
|
579
566
|
|
|
580
567
|
```ts
|
|
568
|
+
import { cache } from '@h-ai/cache'
|
|
581
569
|
import { core } from '@h-ai/core'
|
|
570
|
+
import { reldb } from '@h-ai/reldb'
|
|
571
|
+
|
|
572
|
+
// 按需要的启动顺序,列出各模块的初始化和关闭函数。
|
|
573
|
+
const runtime = core.module.createRuntime([
|
|
574
|
+
{
|
|
575
|
+
name: 'database',
|
|
576
|
+
start: () => reldb.init({ type: 'sqlite', database: ':memory:' }),
|
|
577
|
+
close: () => reldb.close(),
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: 'cache',
|
|
581
|
+
start: () => cache.init({ type: 'memory' }),
|
|
582
|
+
close: () => cache.close(),
|
|
583
|
+
},
|
|
584
|
+
])
|
|
585
|
+
|
|
586
|
+
// createRuntime 返回带 start()/close() 的对象;到这里数据库和缓存尚未启动。
|
|
587
|
+
|
|
588
|
+
// 依次启动数据库、缓存;失败时自动清理已尝试启动的模块。
|
|
589
|
+
const started = await runtime.start() // 成功为 { success: true, data: undefined };两个模块均已初始化。
|
|
590
|
+
if (!started.success)
|
|
591
|
+
throw new Error(started.error.message, { cause: started.error })
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
// 启动成功后再执行应用业务;这里演示记录启动结果。
|
|
595
|
+
core.logger.info('Application modules ready')
|
|
596
|
+
}
|
|
597
|
+
finally {
|
|
598
|
+
// 演示结束后先关闭缓存,再关闭数据库,并检查清理结果。
|
|
599
|
+
const closed = await runtime.close() // 成功为 { success: true, data: undefined };两个模块均已关闭。
|
|
600
|
+
if (!closed.success)
|
|
601
|
+
core.logger.error('Application cleanup failed', { error: closed.error })
|
|
602
|
+
}
|
|
603
|
+
```
|
|
582
604
|
|
|
583
|
-
|
|
584
|
-
|
|
605
|
+
Startup here is database → cache; shutdown is cache → database. This demonstration cleans up immediately afterward. A long-running service should call the same `runtime.close()` when exiting.
|
|
606
|
+
|
|
607
|
+
A startup failure cleans up modules whose startup was attempted. A shutdown failure stops cleanup at that module and retains dependencies it still needs; the next `close()` or `start()` retries cleanup. Simultaneous `start()` calls do not initialize twice. An optional `enabled()` returning false skips its step.
|
|
608
|
+
|
|
609
|
+
Both methods return `HaiResult`; check `success`. Errors include the module, phase, code, and necessary configuration field hints while keeping configuration secrets and raw SDK exceptions out of public startup errors.
|
|
610
|
+
|
|
611
|
+
#### Returning errors before initialization
|
|
612
|
+
|
|
613
|
+
createNotInitializedKit() provides error/result/proxy forms. Proxies return a consistent sync/async failure until the implementation switches to real operations. The message function reads the locale at call time.
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
import type { HaiResult } from '@h-ai/core'
|
|
617
|
+
import { core, HaiCommonError } from '@h-ai/core'
|
|
618
|
+
|
|
619
|
+
const message = core.i18n.createMessageGetter({
|
|
620
|
+
'zh-CN': { notReady: '请先初始化模块' },
|
|
621
|
+
'en-US': { notReady: 'Initialize the module first' },
|
|
622
|
+
'ja-JP': { notReady: '先にモジュールを初期化してください' },
|
|
585
623
|
})
|
|
586
|
-
|
|
587
|
-
|
|
624
|
+
// 本例固定使用中文消息,便于观察返回值。
|
|
625
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined。
|
|
626
|
+
const waiting = core.module.createNotInitializedKit(HaiCommonError.NOT_INITIALIZED, () => message('notReady'))
|
|
627
|
+
// waiting 是带 error/result/proxy 的对象。
|
|
628
|
+
waiting.error() // { code: 'hai:common:001', httpStatus: 500, system: 'hai', module: 'common', message: '请先初始化模块' }。
|
|
629
|
+
waiting.result<string>() // { success: false, error: 上方错误对象 },无 data。
|
|
630
|
+
|
|
631
|
+
// 模块实现可把尚未就绪的操作暂时指向这些代理。
|
|
632
|
+
const asyncOperations = waiting.proxy<{ read: () => Promise<HaiResult<string>> }>()
|
|
633
|
+
const syncOperations = waiting.proxy<{ read: () => HaiResult<string> }>('sync')
|
|
634
|
+
// 两次 proxy 调用分别返回异步和同步操作对象。
|
|
635
|
+
const result = await asyncOperations.read() // { success: false, error: 上方错误对象 }。
|
|
636
|
+
if (!result.success)
|
|
637
|
+
core.logger.warn('Read before initialization', { error: result.error })
|
|
638
|
+
syncOperations.read() // 直接返回 { success: false, error: 上方错误对象 },不返回 Promise。
|
|
588
639
|
```
|
|
589
640
|
|
|
590
|
-
|
|
641
|
+
#### Checking shared-state requirements
|
|
591
642
|
|
|
592
|
-
|
|
643
|
+
requiresSharedState() decides whether a production process should reject backends limited to one host. It only returns a decision; it does not connect or switch databases. Server code supplies the real environment.
|
|
593
644
|
|
|
594
|
-
|
|
645
|
+
```ts
|
|
646
|
+
import { core } from '@h-ai/core'
|
|
595
647
|
|
|
596
|
-
|
|
648
|
+
// 此例用显式数据演示;服务端实际使用时传入 process.env。
|
|
649
|
+
core.module.requiresSharedState({ NODE_ENV: 'production' }) // true:应使用跨主机共享后端。
|
|
650
|
+
core.module.requiresSharedState({ NODE_ENV: 'production', HAI_E2E: '1' }) // false:显式 E2E 例外。
|
|
651
|
+
```
|
|
597
652
|
|
|
598
|
-
|
|
653
|
+
### Request access checks: `core.access` (Node)
|
|
599
654
|
|
|
600
|
-
|
|
655
|
+
At HTTP entry points, resolve a trusted client IP and check addresses, paths, methods, and time windows. Core returns decisions; Serv http.access or Kit createHandle({ access }) connects them to request processing.
|
|
601
656
|
|
|
602
|
-
|
|
657
|
+
#### Resolving and checking client addresses
|
|
603
658
|
|
|
604
|
-
|
|
659
|
+
```ts
|
|
660
|
+
import { AccessConditionSchema, core } from '@h-ai/core'
|
|
605
661
|
|
|
606
|
-
|
|
662
|
+
core.access.normalizeClientIp('::ffff:198.51.100.23') // 198.51.100.23。
|
|
663
|
+
const resolve = core.access.createClientIpResolver(['192.0.2.10']) // 返回 IP 解析函数,只信任这个代理。
|
|
664
|
+
const request = new Request('https://example.test/api/orders', {
|
|
665
|
+
headers: { 'x-forwarded-for': '198.51.100.23' }
|
|
666
|
+
})
|
|
667
|
+
// 第二参数必须来自真实 socket/可信平台;这里用测试地址模拟代理对端。
|
|
668
|
+
const clientIp = resolve(request, '192.0.2.10') // '198.51.100.23',读取受信代理的转发头。
|
|
669
|
+
const check = core.access.createConditionCheck(AccessConditionSchema.parse({
|
|
670
|
+
allowIps: ['198.51.100.0/24']
|
|
671
|
+
}))
|
|
672
|
+
// createConditionCheck 返回检查函数;check(clientIp) 得到 { allowed: true }。
|
|
673
|
+
core.logger.info('Address checked', { allowed: check(clientIp).allowed })
|
|
674
|
+
```
|
|
607
675
|
|
|
608
|
-
|
|
676
|
+
#### Loading policies and checking requests
|
|
609
677
|
|
|
610
678
|
```ts
|
|
611
|
-
import type { LogRecord } from '@h-ai/core'
|
|
612
679
|
import { core } from '@h-ai/core'
|
|
613
680
|
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
681
|
+
// 可改为 loadPolicy({ file: './config/security/request-access.yml' }) 在启动时读独立文件。
|
|
682
|
+
const policy = core.access.loadPolicy({
|
|
683
|
+
rules: [
|
|
684
|
+
{
|
|
685
|
+
id: 'external-api',
|
|
686
|
+
paths: ['/api/external/**'],
|
|
687
|
+
methods: ['POST'],
|
|
688
|
+
allowIps: ['192.0.2.0/24'],
|
|
689
|
+
encryption: 'disabled'
|
|
690
|
+
}
|
|
691
|
+
],
|
|
623
692
|
})
|
|
624
|
-
|
|
693
|
+
// policy.rules[0] 保留上方规则,并补 denyIps: []、windows: []、timezone: 'UTC'。
|
|
694
|
+
const check = core.access.createRequestCheck(policy) // 返回请求检查函数,另带 encryption 方法和 requiresEncryption 属性。
|
|
695
|
+
const decision = check('/api/external/sync', 'POST', '192.0.2.10') // { allowed: true }。
|
|
696
|
+
check.encryption('/api/external/sync', 'POST') // disabled,供 HTTP 框架选择传输模式。
|
|
697
|
+
check.requiresEncryption // false;本例没有 required 规则。
|
|
698
|
+
core.logger.info('Request access checked', { allowed: decision.allowed })
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
- Creating a checker validates and compiles rules. `loadPolicy({ file })` reads independent YAML at startup, limited to 1 MiB. Invalid configuration throws and must stop the host. File references cannot be mixed with inline rules.
|
|
702
|
+
- Global conditions and all matching rules must pass; deny addresses take priority. Empty IP/time lists leave that dimension unrestricted, and timezone defaults to UTC. Paths include deployment prefixes, match exactly or end in `/**`, and GET does not imply HEAD.
|
|
703
|
+
- Default `auto` in `createClientIpResolver()` ignores forwarded headers on ordinary hosts; only managed private Compose uses the container subnet. `[]` disables proxy trust. Self-managed hosts explicitly list proxy IPs/CIDRs; peer addresses must come from sockets or trusted platforms.
|
|
704
|
+
- Path `encryption` can be `required` / `disabled`; omission inherits the host setting. Opposing modes on overlapping paths and methods fail at creation. Hosts use `check.encryption()` / `requiresEncryption` to wire transport behavior. Core does not hold keys or encrypt/decrypt; HTTPS and authentication are still required.
|
|
705
|
+
|
|
706
|
+
See the [request access guide](../../docs/usage/configuration/request-access.md) for defaults, policy files, proxy topology, and deployment steps.
|
|
707
|
+
|
|
708
|
+
### Creating and checking identifiers: `core.id`
|
|
709
|
+
|
|
710
|
+
Create business identifiers and log correlation IDs, or validate UUID v4/nanoid inputs.
|
|
711
|
+
|
|
712
|
+
```ts
|
|
713
|
+
import { core } from '@h-ai/core'
|
|
714
|
+
|
|
715
|
+
// 以下随机值仅展示格式,每次执行都不同。
|
|
716
|
+
core.id.generate() // 如 'V1StGXR8_Z5jdHi6B-myT',默认 21 字符;generate(8) 返回 8 字符。
|
|
717
|
+
const shortId = core.id.short() // 如 'V1StGXR8_Z',10 字符。
|
|
718
|
+
core.id.withPrefix('order_') // 如 'order_V1StGXR8_Z5jdHi6B-myT',前缀后接 21 字符。
|
|
719
|
+
core.id.trace() // 如 'trace-V1StGXR8_Z5jdHi6B-myT'。
|
|
720
|
+
core.id.request() // 如 'req-V1StGXR8_Z5jdHi6B-myT'。
|
|
721
|
+
const uuid = core.id.uuid() // 如 '550e8400-e29b-41d4-a716-446655440000',UUID v4。
|
|
722
|
+
core.id.isValidUUID(uuid) // true。
|
|
723
|
+
core.id.isValidNanoId(shortId, 10) // true。
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
IDs are not secrets. UUID requires cryptographic randomness and throws if unavailable. _core.yml has no global ID length/prefix settings; pass options per call.
|
|
727
|
+
|
|
728
|
+
### Checking value types: `core.typeUtils`
|
|
729
|
+
|
|
730
|
+
Check boundary values and narrow their TypeScript types in branches. These checks do not replace business schema validation.
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
import { core } from '@h-ai/core'
|
|
734
|
+
|
|
735
|
+
core.typeUtils.isDefined(null) // false,同时排除 null 和 undefined。
|
|
736
|
+
core.typeUtils.isObject({ name: 'orders' }) // true;排除 null/数组,但也接受 Date 和类实例。
|
|
737
|
+
core.typeUtils.isFunction(() => {}) // true。
|
|
738
|
+
core.typeUtils.isPromise(Promise.resolve('ready')) // true。
|
|
739
|
+
core.typeUtils.isString('orders') // true。
|
|
740
|
+
core.typeUtils.isNumber(3000) // true;传 NaN 返回 false。
|
|
741
|
+
core.typeUtils.isBoolean(false) // true。
|
|
742
|
+
core.typeUtils.isArray([1, 2]) // true。
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Objects: `core.object`
|
|
746
|
+
|
|
747
|
+
Clone, merge, and select object fields, with typed keys/values/entries and fromEntries.
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
import { core } from '@h-ai/core'
|
|
751
|
+
|
|
752
|
+
const defaults = { name: 'orders', port: 3000 }
|
|
753
|
+
const copy = core.object.deepClone(defaults) // { name: 'orders', port: 3000 },新副本。
|
|
754
|
+
// 合并局部覆盖时,显式保留包含 name/port 的完整配置类型。
|
|
755
|
+
const settings = core.object.deepMerge<typeof defaults>(copy, { port: 4000 }) // { name: 'orders', port: 4000 }。
|
|
756
|
+
core.object.pick(settings, ['name']) // { name: 'orders' }。
|
|
757
|
+
core.object.omit(settings, ['port']) // { name: 'orders' }。
|
|
758
|
+
core.object.keys(settings) // ['name', 'port'],元素类型为 'name' | 'port'。
|
|
759
|
+
core.object.values(settings) // ['orders', 4000]。
|
|
760
|
+
core.object.entries(settings) // [['name', 'orders'], ['port', 4000]]。
|
|
761
|
+
core.object.fromEntries([
|
|
762
|
+
['mode', 'local']
|
|
763
|
+
]) // { mode: 'local' }。
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
deepClone uses structuredClone for cloneable Date/Map/Set/cyclic values and throws for unsupported values such as functions. deepMerge recursively merges plain objects only; later arrays, dates, collections, and class instances replace earlier values without modifying inputs. It does not clone instances.
|
|
767
|
+
|
|
768
|
+
### Strings: `core.string`
|
|
769
|
+
|
|
770
|
+
Convert naming formats, prepare display strings, detect whitespace, and compare strings.
|
|
771
|
+
|
|
772
|
+
```ts
|
|
773
|
+
import { core } from '@h-ai/core'
|
|
774
|
+
|
|
775
|
+
core.string.capitalize('hello') // 'Hello'。
|
|
776
|
+
core.string.kebabCase('helloWorld') // 'hello-world'。
|
|
777
|
+
core.string.camelCase('hello-world') // 'helloWorld'。
|
|
778
|
+
core.string.snakeCase('helloWorld') // 'hello_world'。
|
|
779
|
+
core.string.pascalCase('hello-world') // 'HelloWorld'。
|
|
780
|
+
core.string.truncate('abcdef', 3) // 'abc...'。
|
|
781
|
+
core.string.trim(' hello ') // 'hello'。
|
|
782
|
+
core.string.isBlank(' ') // true。
|
|
783
|
+
core.string.isNotBlank('hello') // true。
|
|
784
|
+
core.string.padStart('5', 3, '0') // '005'。
|
|
785
|
+
core.string.padEnd('5', 3, '0') // '500'。
|
|
786
|
+
core.string.constantTimeEqual('same', 'same') // true;安全边界见下方说明。
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Node constantTimeEqual uses native timingSafeEqual on equal-length UTF-16 bytes and rejects unequal lengths immediately. Browser engines provide no constant-time guarantee; do not use this for server credential checks.
|
|
790
|
+
|
|
791
|
+
### Arrays: `core.array`
|
|
792
|
+
|
|
793
|
+
Deduplicate, chunk, group, remove nullish values, or calculate intersections/differences while retaining the input array.
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
import { core } from '@h-ai/core'
|
|
797
|
+
|
|
798
|
+
core.array.unique([1, 1, 2]) // [1, 2]。
|
|
799
|
+
core.array.chunk([1, 2, 3], 2) // [[1, 2], [3]]。
|
|
800
|
+
core.array.groupBy([
|
|
801
|
+
{ role: 'admin' },
|
|
802
|
+
{ role: 'user' }
|
|
803
|
+
], item => item.role)
|
|
804
|
+
// => { admin: [{ role: 'admin' }], user: [{ role: 'user' }] }
|
|
805
|
+
core.array.first([1, 2, 3]) // 1;空数组为 undefined。
|
|
806
|
+
core.array.last([1, 2, 3]) // 3;空数组为 undefined。
|
|
807
|
+
core.array.flatten([
|
|
808
|
+
[1],
|
|
809
|
+
[2, 3]
|
|
810
|
+
]) // [1, 2, 3],只展开一层。
|
|
811
|
+
core.array.compact([0, null, 1, undefined]) // [0, 1],保留 0/false/空字符串。
|
|
812
|
+
core.array.shuffle([1, 2, 3]) // 如 [2, 3, 1],次序随机;新数组不修改输入。
|
|
813
|
+
core.array.intersection([1, 2], [2, 3]) // [2]。
|
|
814
|
+
core.array.difference([1, 2], [2, 3]) // [1]。
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
groupBy supports prototype names such as `constructor` and `__proto__`. chunk expects a positive integer size and returns an empty array for size<=0.
|
|
818
|
+
|
|
819
|
+
### Asynchronous work: `core.async`
|
|
820
|
+
|
|
821
|
+
Schedule delays, timeouts, retries, bounded/serial work, or debounce/throttle input and scroll events.
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
import { core } from '@h-ai/core'
|
|
825
|
+
|
|
826
|
+
await core.async.delay(1) // 等待约 1ms 后得到 undefined;计时以运行环境为准。
|
|
827
|
+
await core.async.withTimeout(Promise.resolve('ready'), 1000) // 'ready';超时则 reject 为 Error。
|
|
828
|
+
await core.async.retry(async () => 'ready', { maxRetries: 3, delay: 10 }) // 'ready',本例首次即成功。
|
|
829
|
+
await core.async.parallel([1, 2, 3], async value => value * 2, 2) // [2, 4, 6],最多同时执行两项。
|
|
830
|
+
await core.async.serial([1, 2, 3], async value => value * 2) // [2, 4, 6],按输入顺序逐项执行。
|
|
831
|
+
|
|
832
|
+
// 两种包装保留回调参数类型;调用 search/scroll 时传入同样类型的参数。
|
|
833
|
+
const search = core.async.debounce((term: string) => core.logger.debug('Search requested', { term }), 100)
|
|
834
|
+
const scroll = core.async.throttle((position: number) => core.logger.debug('Scroll position', { position }), 100)
|
|
835
|
+
// debounce/throttle 均返回包装函数,调用时返回 undefined。
|
|
836
|
+
core.logger.setLevel('debug') // 返回 undefined;确保下面的 debug 日志可见。
|
|
837
|
+
search('orders') // 约 100ms 后回调收到 'orders';100ms 内连续调用只处理最后一次。
|
|
838
|
+
scroll(100) // 本例第一次调用立即回调,position 为 100;100ms 内最多执行一次。
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
withTimeout limits waiting but does not cancel work. retry handles throws/rejections, not failed HaiResult values; current maxRetries includes the first attempt, so 3 means at most three executions. parallel requires positive safe integer concurrency; invalid values reject with RangeError before any task runs. Result order follows input order.
|
|
842
|
+
|
|
843
|
+
### Dates and time: `core.time`
|
|
844
|
+
|
|
845
|
+
Convert timestamps, Date values, and display text, and calculate local calendar boundaries or relative time.
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
import { core } from '@h-ai/core'
|
|
849
|
+
|
|
850
|
+
// date 与以下日期运算使用本机时区;parseDate 中的 Z 表示 UTC。
|
|
851
|
+
const date = new Date(2026, 0, 1, 12, 0, 0)
|
|
852
|
+
core.time.formatDate(date, 'YYYY-MM-DD HH:mm:ss') // '2026-01-01 12:00:00'。
|
|
853
|
+
core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;相对时间使用中文。
|
|
854
|
+
core.time.timeAgo(new Date(Date.now() - 60_000)) // '1 分钟前'(立即执行时)。
|
|
855
|
+
core.time.now() // 当前毫秒时间戳,如 1767268800000;每次执行随时间变化。
|
|
856
|
+
core.time.nowSeconds() // 当前秒时间戳,如 1767268800;相当于当前毫秒时间戳除以 1000 后向下取整。
|
|
857
|
+
const parsed = core.time.parseDate('2026-01-01T00:00:00Z') // Date 对象,toISOString() 为 '2026-01-01T00:00:00.000Z'。
|
|
858
|
+
core.time.isValidDate(parsed) // true;Invalid Date 返回 false。
|
|
859
|
+
core.time.addDays(date, 7) // 新 Date,本机时间 2026-01-08 12:00:00。
|
|
860
|
+
core.time.addHours(date, 2) // 新 Date,本机时间 2026-01-01 14:00:00。
|
|
861
|
+
core.time.startOfDay(date) // 新 Date,本机时间 2026-01-01 00:00:00.000。
|
|
862
|
+
core.time.endOfDay(date) // 新 Date,本机时间 2026-01-01 23:59:59.999。
|
|
625
863
|
```
|
|
626
864
|
|
|
627
|
-
|
|
865
|
+
formatDate supports YYYY/MM/DD/HH/mm/ss tokens and defaults to YYYY-MM-DD. Date arithmetic returns new Date values. Invalid parseDate input produces an invalid date rather than automatically throwing; check isValidDate.
|
|
628
866
|
|
|
629
|
-
|
|
867
|
+
See the [Core reference](../cli/templates/skills/hai-core/reference.md) for complete parameters and less common options. Current published declarations define the contract.
|
|
630
868
|
|
|
631
|
-
|
|
869
|
+
## Configuration
|
|
632
870
|
|
|
633
|
-
|
|
871
|
+
core.init takes configuration directory, schemas and logging options, returns void synchronously and has no close. See Quick Start and core.config for file loading, environment precedence and field examples; init completing does not prove all business configuration is valid.
|
|
634
872
|
|
|
635
|
-
|
|
873
|
+
[Per-option purpose, use cases and examples](../../docs/usage/configuration/runtime.md).
|
|
636
874
|
|
|
637
|
-
|
|
875
|
+
## Error Handling
|
|
638
876
|
|
|
639
|
-
|
|
877
|
+
Check result.success for domain operations. Handle failures with the module error definitions and safe messages; retain codes, correlation IDs and sanitized causes in server logs. Synchronous tools, factories, stream iteration and lifecycle calls follow their own signatures. A completed Promise alone is not business success. See each feature for recovery limits.
|
|
640
878
|
|
|
641
|
-
|
|
879
|
+
## Development
|
|
642
880
|
|
|
643
|
-
|
|
881
|
+
Run from the repository root (use `pnpm.cmd` in PowerShell):
|
|
644
882
|
|
|
645
883
|
```bash
|
|
884
|
+
pnpm --filter @h-ai/core typecheck
|
|
885
|
+
pnpm --filter @h-ai/core lint
|
|
886
|
+
pnpm --filter @h-ai/core build
|
|
646
887
|
pnpm --filter @h-ai/core test
|
|
647
888
|
```
|
|
648
889
|
|
|
649
|
-
See the [module JSDoc conventions
|
|
890
|
+
See the [module JSDoc conventions](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) for declaration and example checks.
|
|
650
891
|
|
|
651
892
|
## License
|
|
652
893
|
|