@h-ai/core 1.0.0-beta.1 → 1.0.0-beta.10

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.md CHANGED
@@ -6,647 +6,814 @@
6
6
 
7
7
  ## 简介
8
8
 
9
- hai Agent Framework 核心模块 — 提供统一的基础工具、类型定义、配置管理和日志功能。
9
+ `@h-ai/core` 是 Hai Framework 的公共基础层,为应用和其他框架模块提供一致的配置、错误、日志和国际化能力。常用工具可直接使用;Node 应用需要加载配置时再调用 `core.init()`。
10
10
 
11
- ## 核心功能清单
11
+ ## 核心能力
12
12
 
13
- - **HaiResult 类型** — 函数式错误处理(`ok` / `err`),所有模块统一返回值
14
- - **统一日志** — Node.js 基于 pino,浏览器基于 loglevel,API 一致
15
- - **国际化(i18n)** — 集中式 locale 管理 + 类型安全的消息获取器
16
- - **Zod 校验错误映射** — 兼容 Zod v3/v4 的 issue 提取与默认英文消息本地化
17
- - **配置管理** — YAML 加载、约定式环境变量映射、显式环境变量插值、Zod 校验、文件监听(Node.js 专用)
18
- - **ID 生成** — nanoid 与 UUID v4
19
- - **错误定义** — 标准化错误码体系,支持跨模块统一的错误定义与实例创建
20
- - **模块工具** — NotInitializedKit 代理模式,各模块共用的生命周期基础设施
21
- - **工具函数库** — 类型检查、对象 / 字符串 / 数组 / 异步 / 时间操作
13
+ | 能力 | 解决的问题 | 主要入口 |
14
+ | ---------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
15
+ | 配置与启动 | 加载 YAML、合并环境覆盖、校验配置,按依赖顺序编排模块启动与关闭 | `core.config`、`core.init`、`core.module.createRuntime` |
16
+ | 结果与错误 | 用统一结果表达业务成败,定义错误码,在接口边界生成安全提示 | `HaiResult`、`ok` / `err`、`core.error` |
17
+ | 日志与诊断 | 结构化日志、请求关联、凭据脱敏及日志观察 | `core.logger`、`core.context`、`core.sanitize` |
18
+ | 国际化与校验提示 | 管理消息语言,隔离并发请求语言,把 Zod 错误转换为本地化字段提示 | `core.i18n`、`core.zodValidation` |
19
+ | 请求访问控制 | 解析可信客户端地址,按路径、方法、IP 和时段检查请求 | `core.access`(Node 专属) |
20
+ | 通用工具 | 复用标识生成、类型检查、对象、字符串、数组、异步和时间操作,以及模块未初始化时的统一错误返回 | `core.id`、`core.typeUtils`、`core.object` 等 |
21
+
22
+ ### Node 与浏览器的边界
23
+
24
+ 两端都通过 `import { core } from '@h-ai/core'` 使用,由打包工具选择对应环境的代码。
25
+
26
+ | 能力 | Node.js | 浏览器 |
27
+ | --------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ |
28
+ | 日志、结果/错误、消息、通用工具,以及模块的顺序启动与关闭 | 支持 | 支持 |
29
+ | YAML、dotenv、环境映射与文件监听 | 可以读取和监听本地配置文件 | 不能读取或监听本地文件;调用加载方法会返回失败 |
30
+ | 请求关联 `core.context.run` | 每个请求保留自己的编号等信息,等待异步操作后仍能读取,互不串用 | 只在回调的同步代码中保留信息,`await` 后不再保留 |
31
+ | 请求语言 `core.i18n.runWithLocale` | 请求使用自己的语言,等待异步操作后也不会受其他请求影响 | 调用会抛错;用 `setGlobalLocale()` 设置页面语言,或在取消息时指定 locale |
32
+ | 请求策略 `core.access` | 可以检查请求地址、路径和时段 | 不提供此工具 |
33
+
34
+ `core.init()` 只负责 Core 的配置和日志设置,同步返回 `void`,Core 没有 `close()` 方法。应用如果还需要启动数据库、缓存等模块,可用 `core.module.createRuntime()` 集中管理它们:调用返回对象的 `start()` 依次初始化,调用 `close()` 按相反顺序清理。下方“应用启动与关闭”给出完整示例。
22
35
 
23
36
  ## 使用入门
24
37
 
25
- ### 快速开始
38
+ 下面按 `core` 对象上的全部子功能介绍用途、示例与必要限制。需要加载配置时先看初始化;日志、消息和通用工具可直接使用。
26
39
 
27
- #### Node.js 环境
40
+ 运行功能通过 `core.xxx()` 或 `core.<子功能>.xxx()` 调用。结果构造器 `ok` / `err`,以及类型、Schema 和错误常量,从包入口单独导入,具体用法见对应小节。
28
41
 
29
- ```typescript
30
- import { core } from '@h-ai/core'
42
+ ```bash
43
+ pnpm add @h-ai/core
44
+ ```
31
45
 
32
- // 初始化:加载配置目录(可选)
33
- core.init({ configDir: './config', watchConfig: true })
46
+ 下方自定义配置和表单校验示例还使用 Zod,运行这些示例前安装 `pnpm add zod`。数据库、缓存示例的额外依赖在相应小节说明。
34
47
 
35
- // 日志
36
- core.logger.info('Server started', { port: 3000 })
37
- core.logger.warn('Slow query', { duration: 2500 })
38
- core.logger.error('Connection failed', { error })
48
+ ### 初始化:`core.init`
39
49
 
40
- // 创建模块级 logger(独立名称空间)
41
- const dbLogger = core.logger.create({ name: 'db' })
42
- dbLogger.info('Connected to PostgreSQL')
50
+ Node 启动时加载 YAML 并设置日志;浏览器只设置日志。初始化 Core 不会启动数据库等其他模块。
43
51
 
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') // 同上
52
+ #### Node.js
48
53
 
49
- // 子 logger 可嵌套
50
- const opLogger = reqLogger.child({ operation: 'createOrder' })
51
- opLogger.info('Order created') // 包含 requestId + userId + operation
54
+ 准备 `config/_core.yml`:
52
55
 
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'
56
+ ```yaml
57
+ # 应用元信息,供启动代码和页面读取。
58
+ name: orders
59
+ version: 0.1.0
60
+ env: development
61
+ # 默认消息语言;启动代码读取后调用 setGlobalLocale 应用。
62
+ defaultLocale: zh-CN
63
+ logging:
64
+ level: info # 输出 info 及以上级别。
65
+ format: pretty # 本地开发使用便于阅读的日志。
66
+ ```
67
+
68
+ ```ts
69
+ import { core, CoreConfigSchema } from '@h-ai/core'
70
+
71
+ // 1. 读取 config 目录中的 YAML,_core.yml 对应名称 core。
72
+ core.init({ configDir: './config' }) // 返回 undefined;配置缓存中可读取 core。
73
+
74
+ // 2. 检查应用必需的配置;失败时停止启动,保留原始错误供排查。
75
+ const config = core.config.validate('core', CoreConfigSchema)
76
+ if (!config.success)
77
+ throw new Error(config.error.message, { cause: config.error })
60
78
 
61
- // 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_')
79
+ // 成功时 config.success 为 true;config.data.name 为 'orders',defaultLocale 为 'zh-CN'。
66
80
 
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)
81
+ // 3. 将配置中的语言应用到默认消息;读取 YAML 本身不会切换语言。
82
+ core.i18n.setGlobalLocale(config.data.defaultLocale) // 返回 undefined;后续默认消息使用中文。
83
+
84
+ // 4. 记录已验证的应用名称,确认启动完成。
85
+ core.logger.info('Application ready', { name: config.data.name }) // 返回 undefined;info 日志包含 name: 'orders'。
77
86
  ```
78
87
 
79
- #### 浏览器环境
88
+ 启动边界在必需配置失败时中止启动。`core.init()` 对单项加载失败记日志并继续,所以不能把调用结束视为所有配置校验成功;目录扫描、日志设置等异常仍可能抛出。
89
+
90
+ #### 浏览器
80
91
 
81
- ```typescript
92
+ ```ts
82
93
  import { core } from '@h-ai/core'
83
94
 
84
- // 初始化日志级别(浏览器不支持配置加载)
85
- core.init({ logging: { level: 'warn' } })
95
+ // 浏览器不读取 YAML;此处只设置日志,输出 warn 及以上级别。
96
+ core.init({ logging: { level: 'warn' } }) // 返回 undefined;之后 info 日志被过滤。
86
97
 
87
- core.logger.info('Page ready')
88
- const id = core.id.generate()
98
+ // 设置当前页面使用的默认消息语言。
99
+ core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;页面默认消息使用中文。
89
100
 
90
- // 其他工具函数与 Node.js 相同
91
- core.string.capitalize('hello')
92
- core.array.unique([1, 1, 2])
101
+ // 给一次请求生成编号,后续日志可用同一编号关联排查。
102
+ const requestId = core.id.request() // 返回随机字符串,如 'req-V1StGXR8_Z5jdHi6B-myT'。
103
+ core.logger.warn('Request needs attention', { requestId }) // 返回 undefined;浏览器输出 warn 消息及同一个 requestId。
93
104
  ```
94
105
 
95
- 浏览器端会保留 `core.config` API 形态以匹配类型定义,但不支持文件配置:
106
+ 浏览器配置由宿主通过公开字段下发,不传完整服务端配置或密钥。`core.config.get()` / `has()` / `keys()` 分别返回 `undefined` / `false` / `[]`;`loadEnvFiles()`、`load()`、`validate()`、`reload()` 返回 `SERVICE_UNAVAILABLE`,`getOrThrow()` 抛错。`watch()` 立即回调同一不可用错误并返回空取消函数。
96
107
 
97
- - `core.config.get()` 返回 `undefined`,`has()` 返回 `false`,`keys()` 返回 `[]`
98
- - `loadEnvFiles()` / `load()` / `validate()` / `reload()` 返回 `HaiCommonError.SERVICE_UNAVAILABLE`
99
- - `watch()` 会立即用同一错误调用回调,并返回安全的 no-op 取消函数
100
- - 需要配置时请由服务端读取后通过接口下发,或只在浏览器端用 `core.init({ logging })` 配置日志
108
+ ### 文件与环境配置:`core.config`
101
109
 
102
- ### 详细 API
110
+ 读取 YAML、应用环境变量覆盖、检查配置类型,并管理配置缓存与文件监听。下列文件与环境操作仅在 Node 中可用。
103
111
 
104
- #### 日志(Logger)
112
+ #### 加载、校验与读取
105
113
 
106
- `core.logger` 既是默认 Logger 实例,也是日志管理的统一入口。
114
+ `core.init({ configDir, configSchemas?, watchConfig? })` 扫描指定目录直属的 `.yml` / `.yaml` 文件;`_core.yml` 注册为 `core`,`_db.yml` 注册为 `db`,`app.yml` 注册为 `app`。构建/部署设置由 CLI 从项目根 `hai.json` 读取,不属于运行时 YAML。
107
115
 
108
- ##### 日志记录
116
+ `_core.yml` 使用 `CoreConfigSchema` 校验,只接受 `name`、`version`、`env`、`defaultLocale` 和 `logging`。默认值依次为 `hai Admin`、`0.1.0`、`development`、`zh-CN`;`logging` 可省略。元数据不会自行修改 `NODE_ENV`、切换消息语言或注册业务翻译。日志应从此处配置;显式传入 `core.init({ logging })` 会覆盖本次初始化对 YAML 日志设置的应用。
109
117
 
110
- 6 个级别:`trace` / `debug` / `info` / `warn` / `error` / `fatal`
118
+ 其他配置在扫描时仅借助宿主提供的 `configSchemas` 识别结构和类型,完整业务校验留给模块初始化或显式 `validate()`。直接 `load(name, file, schema)` 会立即校验。
111
119
 
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' })
120
+ 以下文件示例使用这份 `config/app.yml`,并假设没有额外环境变量覆盖:
121
+
122
+ ```yaml
123
+ # config/app.yml
124
+ port: 3000
116
125
  ```
117
126
 
118
- ##### 创建模块级 logger
127
+ `load()` 读取文件并可立即校验;`validate()` 校验已加载的缓存。`get()` 找不到时返回 undefined,`getOrThrow()` 找不到时抛错;泛型只帮助 TypeScript,不会替代实际校验。`has()` / `keys()` 用于检查有哪些配置。
119
128
 
120
- ```typescript
121
- // 独立名称空间,适合模块内全局使用
122
- const logger = core.logger.create({ name: 'payment' })
123
- logger.info('Payment processed')
124
- ```
129
+ ```ts
130
+ import { core } from '@h-ai/core'
131
+ import { z } from 'zod'
132
+
133
+ // 读取上方 config/app.yml;Schema 同时描述类型和校验规则。
134
+ const AppSchema = z.object({ port: z.number().int().min(1).max(65535) })
135
+ const loaded = core.config.load('app', './config/app.yml', AppSchema)
136
+ if (!loaded.success)
137
+ throw new Error(loaded.error.message, { cause: loaded.error })
125
138
 
126
- ##### 子 logger(child)
139
+ // loaded:{ success: true, data: { port: 3000 } }。
127
140
 
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') // 同上
141
+ // validate 用于重新校验缓存;加载时未传 Schema 的配置也可在这里校验。
142
+ const validated = core.config.validate('app', AppSchema)
143
+ if (!validated.success)
144
+ throw new Error(validated.error.message, { cause: validated.error })
133
145
 
134
- // 嵌套 child:子上下文递归合并
135
- const opLogger = reqLogger.child({ operation: 'login' })
136
- opLogger.info('Login succeeded') // 输出包含 requestId + userId + operation
137
- opLogger.error('Login failed', { reason: 'invalid_password' })
146
+ // validated:{ success: true, data: { port: 3000 } }。
147
+
148
+ // get 适合可选配置;getOrThrow 适合已确认存在的必需配置。
149
+ if (core.config.has('app')) { // true:上方已成功加载 app。
150
+ const optional = core.config.get<z.infer<typeof AppSchema>>('app') // { port: 3000 }。
151
+ const required = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // { port: 3000 };缺失则抛错。
152
+ core.logger.info('App config ready', { port: required.port, available: optional !== undefined })
153
+ }
154
+ core.config.keys() // 仅运行本节 load 示例时为 ['app']。
138
155
  ```
139
156
 
140
- > **典型用法**:在请求入口创建 `reqLogger`,传入 service 层,service 层再 `child` 添加操作上下文,最终日志自动串联完整链路。
157
+ #### 重载、监听与清理
141
158
 
142
- ##### 日志管理
159
+ 修改配置后可用 `reload()` 主动读盘,也可用 `watch()` 接收新值或错误。监听支持普通保存、文件原子替换以及删除后重建;失败保留最近一次成功的缓存。`unwatch()` 停止监听,`clear()` 同时清缓存;二者省略名称时作用于全部配置。
143
160
 
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
- })
161
+ `watchConfig` 默认 false;重载缓存不会自动重新初始化数据库等已启动模块。下面的清理操作应在不再需要这份配置时调用。
152
162
 
153
- core.logger.setLevel('debug')
154
- const level = core.logger.getLevel() // 'debug'
163
+ ```ts
164
+ import { core } from '@h-ai/core'
165
+
166
+ // 前提:app 已由 init/load 加载。回调需先处理读取或校验失败。
167
+ const stop = core.config.watch('app', (_settings, error) => {
168
+ // 文件改成 port: 4000 后,成功回调收到 { port: 4000 }、error 为 undefined;失败时为 null 和错误对象。
169
+ if (error)
170
+ core.logger.error('Config reload failed', { error })
171
+ else
172
+ core.logger.info('Config reloaded')
173
+ })
174
+ // watch 返回取消函数 stop,不立即返回配置值。
175
+ core.config.isWatching('app') // true。
176
+ const reloaded = core.config.reload('app') // 文件仍为 port: 3000 时返回 { success: true, data: { port: 3000 } }。
177
+ if (!reloaded.success)
178
+ core.logger.warn('Manual reload failed', { error: reloaded.error })
179
+
180
+ stop() // 返回 undefined;取消当前回调,没有其他回调时停止底层监听。
181
+ core.config.unwatch('app') // 返回 undefined;isWatching('app') 变为 false。
182
+ core.config.clear('app') // 返回 undefined;get('app') 变为 undefined,has('app') 变为 false。
155
183
  ```
156
184
 
157
- `core.init()` 加载 `_core.yml.logging` 后,或调用 `configure` / `setLevel` 后,默认、模块和嵌套 child Logger 会在后续输出中使用当前全局配置,即使这些实例在初始化前创建。`create({ level, format, context })` 的显式级别、格式和本地上下文仍优先;未指定的选项动态继承全局配置,调用时上下文覆盖固定上下文。附加 `redact` 对这些实例统一生效,不需要重建模块 Logger。
185
+ #### 环境变量如何改变 YAML 配置
158
186
 
159
- 浏览器仍输出到 DevTools console:`pretty` 使用可读消息与上下文,`json` 输出包含 `level` / `msg` 的 JSON 字符串。默认凭据脱敏与配置的字段路径脱敏同时生效,支持嵌套点/括号路径和 `*`;消息文本本身需调用方脱敏。
187
+ 框架根据配置名和字段位置自动确定用于覆盖配置的环境变量名。例如文件 `app.yml` 的 `port` 对应 `HAI_APP_PORT`,嵌套的 `http.maxRetries` 对应 `HAI_APP_HTTP_MAXRETRIES`。每层转大写并用 `_` 连接,camelCase 不拆词;YAML 不必写 `${...}` 才能被覆盖。
160
188
 
161
- 日志上下文中的原生 `Error` 会先转为包含 `name`、`message`、`stack`、`cause`、`AggregateError.errors` 与可枚举自有上下文的副本,再执行默认字段/URL 脱敏与配置的 `redact`;两端规则一致,原异常不变。异常子图循环返回 `[Circular]`,32 层上限返回 `[MaxDepth]`;其中的自定义替换值也经同一规范化处理,普通对象的替换语义不变。无 Error 的普通对象循环仍保留原结构,不保证可直接 JSON 序列化。异常文本会清理显式标注的凭据(如 `passwordHash=...`、`secretKey=...`)、认证头和 URL 凭据;无法保证识别任意无标签秘密,调用方仍应清理或用 `error.message` / `error.stack` 等路径整体遮蔽。
189
+ 先看一个固定值的例子:YAML 写 `port: 3000`,没有 `HAI_APP_PORT` 时得到 3000,设置 `HAI_APP_PORT=5000` 后得到 5000。
162
190
 
163
- `core.logger` 默认会对日志上下文执行安全脱敏:`password`、`passwordHash`、`passwd`、`token`、`secret`、`secretKey`、`privateKey`、`apiV3Key` 等凭证类字段会替换为 `[REDACTED]`,`url` / `baseUrl` / `endpoint` 这类字段会掩码 URL 中的用户名、密码和敏感查询参数(含 OAuth 键值 fragment);非法 URL 返回固定占位符;邮箱、手机号默认不会被遮盖。
191
+ 在 YAML 中写 `${变量名}`,读取时就会用对应环境变量的值替换这段表达式。冒号后面的文字是在变量没有设置时使用的默认值:
164
192
 
165
- ##### 日志级别规范
193
+ | YAML 写法 | 环境输入 | 读取结果 |
194
+ | ---------------------------- | -------------------------------- | -------------------------------------------------------- |
195
+ | `port: ${PORT}` | `PORT=4000` | 使用 4000;若 PORT 没设置,返回 `CONFIG_ENV_VAR_MISSING` |
196
+ | `port: ${PORT:3000}` | 没有 PORT | 使用表达式中的默认值 3000 |
197
+ | `port: ${PORT:3000}` | `PORT=4000`、`HAI_APP_PORT=5000` | 使用 5000,约定变量优先 |
198
+ | `label: server-${PORT:3000}` | `PORT=4000` | 拼成字符串 `server-4000` |
166
199
 
167
- | 级别 | 适用场景 |
168
- | ------- | ---------------------------------- |
169
- | `trace` | 循环内详细调试数据 |
170
- | `debug` | 函数进入、参数概要、读操作 |
171
- | `info` | 业务事件(初始化完成、写操作成功) |
172
- | `warn` | 异常但可恢复(校验失败、重试) |
173
- | `error` | 操作失败(需人工排查) |
174
- | `fatal` | 致命错误(服务无法继续) |
200
+ 实际读取顺序是:先检查 `HAI_APP_PORT`;有值就直接覆盖这一项,不再解析该项的 `${PORT...}`。否则读取 YAML:写了表达式才查 PORT,写的是固定值就直接用固定值。因此已有 HAI_APP_PORT 覆盖时,不要求 PORT 也存在。
175
201
 
176
- #### 国际化(i18n)
202
+ 同一份环境来源可以先给父对象 JSON,再覆盖具体字段:`HAI_APP='{"port":3000,"label":"local"}'` 配合 `HAI_APP_PORT=4000`,结果是 port=4000、label=local,与行顺序无关。高优先级来源整体替换对象时,不会继续保留低来源的细项覆盖。数组 JSON 替换整个列表,索引变量只修改已有项,越界报错。
177
203
 
178
- 内置语言为 `zh-CN`、`en-US`、`ja-JP`;`ja` 简写会规范化为 `ja-JP`,所有模块共用此语言设置。
204
+ #### 从 .env 文件读取变量
179
205
 
180
- ```typescript
181
- // 设置全局语言(所有 getMessage 函数自动响应)
182
- core.i18n.setGlobalLocale('en-US')
183
- const current = core.i18n.getGlobalLocale() // 'en-US'
206
+ `core.init()` 不自行读取 .env 文件。需要时先调用 `loadEnvFiles()`,确认 success 后把结果放入 process.env,再加载 YAML。`development` / `production` 映射为 `dev` / `prod`;缺失文件跳过,同文件重复键报错。优先级从高到低是:**进程环境 > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML**。私有 .local 文件不提交 Git。
184
207
 
185
- // 为每个模块创建消息获取器
186
- const getMessage = core.i18n.createMessageGetter({
187
- 'zh-CN': { welcome: '欢迎 {name}', error: '错误:{code}' },
188
- 'en-US': { welcome: 'Welcome {name}', error: 'Error: {code}' },
189
- })
208
+ ```ts
209
+ import { core } from '@h-ai/core'
190
210
 
191
- // 使用:自动读取全局 locale,支持参数插值
192
- getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
211
+ // 前提:.env 写 HAI_APP_PORT=4000,没有其他来源覆盖这个变量。
212
+ // 先读取并合并 .env 文件与进程环境,失败时停止启动。
213
+ const result = core.config.loadEnvFiles({ mode: 'dev', envDir: '.' })
214
+ if (!result.success)
215
+ throw new Error(result.error.message, { cause: result.error })
216
+ // 成功时 result.success 为 true,result.data.HAI_APP_PORT 为 '4000';其他键来自合并后的环境。
217
+ // 返回值不会自动写入进程;显式应用后再初始化。
218
+ Object.assign(process.env, result.data)
219
+ core.init({ configDir: './config' }) // 返回 undefined;app.yml 中 port: 3000 被覆盖为数字 4000。
220
+ ```
193
221
 
194
- // 支持 locale 参数覆盖
195
- getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
222
+ #### 替换后的值是什么类型
196
223
 
197
- // 常量
198
- core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]
199
- core.i18n.DEFAULT_LOCALE // 'zh-CN'
200
- ```
224
+ 环境变量原本都是文本。给 `load()` / `resolve()` 传入 Zod Schema 后,框架会按字段类型转换替换结果:`z.number()` 对应数字,`z.boolean()` 对应布尔值,`z.string()` 保留字符串。
201
225
 
202
- #### 脱敏工具
226
+ 例如 `${PORT:3000}` 占据整个值时,可得到数字 3000;`${ENABLED:false}` 可得到布尔值 false。`server-${PORT:3000}` 带有其他文字,拼接后仍是字符串。字符串字段 `code` 的默认文本 00123 保持为 `'00123'`,不会丢掉前导零。下面不用读文件,直接给出相同的 YAML 内容和环境数据来演示:
203
227
 
204
- `core.sanitize.sanitizeSensitiveFields()` 可递归脱敏对象/数组中的敏感字段;默认会处理 password/passwordHash、passwd、token、secret/secretKey、privateKey、apiV3Key 等凭证类字段,并自动掩码 `url` / `baseUrl` / `endpoint` 字段中的内嵌凭证、敏感查询参数和 OAuth 键值 fragment。`core.sanitize.sanitizeUrl(url, options)` 可单独处理 URL,非法 URL 返回 `(invalid url)`;自定义 matcher 同样适用于参数名。`sanitizeText` 清理显式凭据标签和 URL,也可传入调用方已知的秘密值;它不识别任意无标签秘密。
228
+ ```ts
229
+ import { core } from '@h-ai/core'
230
+ import { z } from 'zod'
205
231
 
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' },
232
+ // 用显式环境数据演示覆盖顺序、数值/布尔转换与字符串保留。
233
+ const AppSchema = z.object({ port: z.number(), enabled: z.boolean(), label: z.string(), code: z.string() })
234
+ const resolved = core.config.resolve({
235
+ name: 'app',
236
+ // eslint-disable-next-line no-template-curly-in-string -- 保留 YAML 中等待框架解析的字面表达式。
237
+ value: { port: '${PORT:3000}', enabled: '${ENABLED:false}', label: 'server-${PORT:3000}', code: '${APP_CODE:00123}' },
238
+ environment: { PORT: '4000', HAI_APP_PORT: '5000' },
239
+ schema: AppSchema,
212
240
  })
213
- // => { email: 'user@example.com', password: '[REDACTED]', baseUrl: 'https://[REDACTED]:[REDACTED]@example.com/api/v1', profile: { token: '[REDACTED]' } }
241
+ if (!resolved.success)
242
+ throw new Error(resolved.error.message, { cause: resolved.error })
214
243
 
215
- core.sanitize.sanitizeSensitiveFields({ email: 'user@example.com' }, {
216
- matcher: /email/i,
217
- replacement: '***',
218
- })
219
- // => { email: '***' }
244
+ // resolved:{ success: true, data: { port: 5000, enabled: false, label: 'server-4000', code: '00123' } }。
245
+
246
+ // resolve 只做替换和类型还原;再用 Schema 确认最终数据符合要求。
247
+ const validated = AppSchema.safeParse(resolved.data)
248
+ if (!validated.success)
249
+ throw new Error(validated.error.message, { cause: validated.error })
250
+ core.logger.info('Config resolved', { settings: validated.data })
251
+ // settings: { port: 5000, enabled: false, label: 'server-4000', code: '00123' }
220
252
  ```
221
253
 
222
- #### Zod 校验错误 i18n 映射
254
+ 空字符串表示“变量已经设置,只是内容为空”,不会被当成缺失、null、false 或 0。例如 `APP_CODE=''` 会使 code 保持 `''`,不会改用 00123;是否允许空串由 Schema 决定。数字字段收到空串则通常校验失败。
255
+
256
+ 变量替换只处理 YAML 中的表达式;环境变量或父 JSON 中的 `server-${PORT}` 会按原文保留。例如 `HAI_APP_LABEL='server-${PORT}'` 最终仍为 `server-${PORT}`。需要替换时,把表达式写在 YAML 中。
223
257
 
224
- `core.zodValidation` 用于把 Zod `SafeParseError` / `ZodError` 转为扁平表单错误,并将 Zod
225
- 默认英文消息映射为调用方自己的 i18n 文案。
258
+ 没有 Schema 时,框架按现有 YAML/JSON 结构识别路径,并对单个表达式按 YAML 规则解释数字/布尔值;缺少的合法可选路径可由 `configSchemas` 提供。最终业务校验仍应通过 `validate()` 或模块 init 执行。
226
259
 
227
- 严格 schema 的未知字段错误映射为 `validationUnexpectedFields`,不向用户暴露内部字段名。自定义消息获取器需要处理此 key;Kit、Serv 已提供中英日文提示。Schema 显式指定的业务消息仍原样保留。
260
+ #### 生成配置文件时使用的工具
228
261
 
229
- - 两个公开入口:
230
- - `createPrefixedZodMessageGetter()`:按模块前缀自动派生 `serv_validation*` / `kit_validation*` 这类 key,省掉手写 `KEY_MAP`
231
- - `mapZodErrorToFormErrors()`:一步完成提取 + 本地化 + 扁平化
232
- - 自定义业务消息不会被覆盖;只有 Zod 默认英文消息才会被替换
262
+ `resolve()` 也可供构建/部署工具在内存中计算配置。`toEnv()` 把对象转为环境变量,`mergeEnv()` 合并多个来源,`stringifyEnv()` / `parseEnv()` 编解码 .env 文本;这些工具不自动脱敏。
233
263
 
234
- ```typescript
235
- import type { ZodMessageGetter } from '@h-ai/core'
264
+ ```ts
236
265
  import { core } from '@h-ai/core'
237
- import { z } from 'zod'
238
266
 
239
- const LoginSchema = z.object({
240
- email: z.string().email(),
241
- password: z.string().min(8),
242
- nickname: z.string().min(1, '请填写昵称'),
243
- })
267
+ // 配置生成器把普通对象转成 HAI_APP_PORT 等变量;数组保留为 JSON。
268
+ const values = core.config.toEnv({ prefix: 'HAI_APP', value: { port: 3000, tags: ['local'] } })
269
+ // values:{ HAI_APP_PORT: '3000', HAI_APP_TAGS: '["local"]' }。
270
+ const merged = core.config.mergeEnv([values, { HAI_APP_PORT: '4000' }])
271
+ // merged:{ HAI_APP_PORT: '4000', HAI_APP_TAGS: '["local"]' }。
272
+ const text = core.config.stringifyEnv(merged) // 编码为可写入 .env 的文本。
273
+ // text 含以下两行,每行以换行符结尾:
274
+ // HAI_APP_PORT='4000'
275
+ // HAI_APP_TAGS='["local"]'
276
+ const restored = core.config.parseEnv(text) // 解析回字面字符串,不执行其中的变量。
277
+ // restored:{ HAI_APP_PORT: '4000', HAI_APP_TAGS: '["local"]' }。
278
+ core.logger.info('Generated config verified', { port: restored.HAI_APP_PORT })
279
+ ```
244
280
 
245
- 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
- }
281
+ 普通对象默认展开;数组、动态字典和空对象保留 JSON。整体替换对象的生成规则、完整来源优先级与应用接入见[基础配置手册](../../docs/usage/configuration/runtime.md)。
257
282
 
258
- const result = LoginSchema.safeParse({
259
- email: 'bad',
260
- password: '123',
261
- nickname: '',
262
- })
283
+ ### 记录日志:`core.logger`
263
284
 
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 自定义消息原样保留)
285
+ `core.logger` 用于记录结构化日志。六个级别从详细到严重依次为 trace、debug、info、warn、error、fatal;fatal 记录日志,是否退出由应用决定。
280
286
 
281
- core.logger.info('Validation failed', { errors })
282
- }
287
+ `core.logger` 支持 `trace` / `debug` / `info` / `warn` / `error` / `fatal`。Node 使用 pino,浏览器使用 loglevel;未配置时为 `info` / `pretty`。若提供经 `LoggingConfigSchema` 解析的 `logging` 对象,其格式默认 `json`。Node 缺少可选 `pino-pretty` 时使用 JSON 输出。
288
+
289
+ ```ts
290
+ import { core } from '@h-ai/core'
291
+
292
+ // 先设置级别;低于当前级别的日志不会输出。
293
+ core.logger.setLevel('trace') // 返回 undefined;以下六个级别均会输出。
294
+ core.logger.trace('Processing detail', { step: 1 })
295
+ core.logger.debug('Read started')
296
+ core.logger.info('Order created', { orderId: 'order-1' })
297
+ core.logger.warn('Retry scheduled')
298
+ core.logger.error('Operation failed', { error: new Error('Example failure') })
299
+ core.logger.fatal('Service cannot continue')
300
+ // 六次日志调用均返回 undefined;分别输出对应级别、上面的消息及所传字段。
301
+ // 例如 info 日志的消息为 'Order created',字段 orderId 为 'order-1'。
283
302
  ```
284
303
 
285
- #### 配置管理(Node.js 专用)
304
+ `create()` 创建有名称的 Logger,`child()` 给后续日志附带固定字段。`configure()` 设置输出与附加脱敏路径,`setLevel()` / `getLevel()` 调整或查看全局级别。
286
305
 
287
- > 浏览器端没有文件系统,`core.config` 仅提供占位 API:文件相关方法返回 `SERVICE_UNAVAILABLE`,不会加载 YAML。
306
+ ```ts
307
+ import { core } from '@h-ai/core'
288
308
 
289
- Node 自动加载和监听配置目录内所有 `.yml` / `.yaml`,没有保留文件名例外。项目根 `hai.json` 中的 `build` / `deploy` 由 CLI 读取,不进入运行时 YAML 配置。单项加载返回失败时记录错误并继续,必需配置由应用调用 `core.config.validate` 并检查 `success`;目录扫描、日志设置等异常仍可能传播。
309
+ // 全局配置可作用于已经创建的 Logger;redact 补充业务隐私字段。
310
+ core.logger.configure({ level: 'info', format: 'json', redact: ['customer.email'] }) // 返回 undefined;后续日志按此配置输出。
311
+ const orders = core.logger.create({ name: 'orders' }) // 返回 Logger 对象,可调用 info/warn 等方法。
312
+ const request = core.logger.child({ requestId: 'request-1' }) // 返回带固定 requestId 的 Logger。
313
+ orders.info('Order created', { customer: { email: 'user@example.com' } }) // 返回 undefined;输出中 customer.email 为 '[REDACTED]'。
314
+ request.warn('Request needs attention') // 返回 undefined;输出中 requestId 为 'request-1'。
315
+ core.logger.getLevel() // 'info'。
316
+ ```
290
317
 
291
- ```typescript
292
- import { core, HaiConfigError } from '@h-ai/core'
293
- import { z } from 'zod'
318
+ 已有默认、模块和嵌套 child Logger 的后续输出会使用当前全局配置,`setLevel()` 也即时生效。`create()` 显式指定的 level/format/context 优先;未指定项动态继承,调用时上下文覆盖固定上下文,当前请求关联字段再由 `core.context` 附加。浏览器 JSON 输出包含 `level` / `msg`,pretty 输出可读消息和上下文。
294
319
 
295
- // 方式 1:init 时自动加载运行时配置
296
- core.init({
297
- configDir: './config', // 扫描配置目录内所有 .yml / .yaml
298
- watchConfig: true, // 监听变更并自动重载
299
- })
320
+ `observe()` 用于诊断采集,接收经过当前级别过滤与脱敏的记录,返回解除订阅的函数。回调只做有界内存收集,持久化由应用另行异步完成;它不代表日志已保存。回调异常与同步重入被隔离。
300
321
 
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
- }
322
+ ```ts
323
+ import { core } from '@h-ai/core'
313
324
 
314
- // 校验已加载的配置
315
- const validResult = core.config.validate('app', AppSchema)
325
+ // 观察经过级别过滤和脱敏的记录;内存收集设置上限。
326
+ const recentMessages: string[] = []
327
+ const stop = core.logger.observe((record) => {
328
+ if (recentMessages.length === 100)
329
+ recentMessages.shift()
330
+ recentMessages.push(record.message)
331
+ })
332
+ // observe 返回取消函数 stop;本例 warn 日志触发一次回调,record.message 为 'Observed event'。
333
+ core.logger.warn('Observed event')
334
+ recentMessages // ['Observed event']。
335
+ stop() // 返回 undefined;后续日志不再进入这个回调。
336
+ ```
316
337
 
317
- // 查询和获取
318
- if (core.config.has('app')) {
319
- const cfg = core.config.get<z.infer<typeof AppSchema>>('app') // undefined 或配置
320
- const cfg2 = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // 未加载时抛异常
321
- }
322
- core.config.keys() // ['core', 'app', 'db', ...]
338
+ ### 关联同一次请求:`core.context`
323
339
 
324
- // 重新加载
325
- const reloadResult = core.config.reload('app')
340
+ `run()` 让一次请求或任务携带 requestId、userId、scopeId、jobId 等关联信息,`get()` 读取当前值。Node 中等待数据库或其他异步操作后,仍能读取这次请求自己的信息,其他请求不会覆盖它。已有 Logger 会自动带上这些字段。
326
341
 
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() // 取消监听
342
+ ```ts
343
+ import { core } from '@h-ai/core'
336
344
 
337
- // 清除缓存(同时停止对应监听)
338
- core.config.clear('app') // 清除单个
339
- core.config.clear() // 清除全部
345
+ // Node 示例:同一请求等待异步处理后,仍能取得自己的编号。
346
+ await core.context.run({ requestId: 'request-1', userId: 'user-1' }, async () => {
347
+ await core.async.delay(1)
348
+ const current = core.context.get() // 包含 requestId: 'request-1'、traceId: 'request-1'、userId: 'user-1';其他字段为 undefined。
349
+ core.logger.info('Request continued', { requestId: current.requestId })
350
+ return current.requestId
351
+ }) // run 返回回调的结果;此处 await 后得到 'request-1'。
352
+ core.context.get() // {},回到外层后不保留上面请求的上下文。
353
+
354
+ // 采集器自身写日志时,可暂停观察回调,正常日志输出仍然执行。
355
+ core.context.run({ suppressLogObservers: true }, () => core.logger.warn('Collector diagnostic'))
356
+ // 返回 undefined;输出 warn 日志,但不会触发日志观察回调。
340
357
  ```
341
358
 
342
- ##### dotenv 文件与环境切换
359
+ 浏览器只能在 run 回调的同步代码中保留信息,await 后不再保留。关联编号不是身份授权,用户和租户范围必须由可信服务端代码提供。
360
+
361
+ ### 清理诊断数据:`core.sanitize`
343
362
 
344
- Node 专用的 `core.config.loadEnvFiles({ mode, envDir })` 返回 HaiResult,读取 `.env` → `.env.<mode>` → `.env.local`,已有进程环境最高;development/production 映射为 dev/prod,文件不存在时跳过。该函数不修改进程,调用方检查 success 后注入。除 `.env.local` 外的公共环境文件及完整说明 `.env.example` 提交 Git。`core.init` 不自动读取 dotenv,浏览器调用返回 SERVICE_UNAVAILABLE。
363
+ 用于日志、诊断包或公开错误输出前清理凭据。可以判断字段名是否敏感、清理单个值、递归处理对象、遮蔽 URL 凭据,或清理文本中带标签的凭据和调用方已知的秘密值。
345
364
 
346
365
  ```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' })
366
+ import { core } from '@h-ai/core'
367
+
368
+ core.sanitize.isSensitiveKey('apiKey') // true。
369
+ core.sanitize.sanitizeSensitiveValue('example-only', 'password') // '[REDACTED]'。
370
+ const safe = core.sanitize.sanitizeSensitiveFields({ password: 'example-only', email: 'user@example.com' })
371
+ // safe:{ password: '[REDACTED]', email: 'user@example.com' },原对象不变。
372
+ // 默认仅遮蔽凭据;额外业务隐私可在已处理凭据的副本上继续清理。
373
+ core.sanitize.sanitizeSensitiveFields(safe, { matcher: ['email'], replacement: '***' })
374
+ // => { password: '[REDACTED]', email: '***' }
375
+ core.sanitize.sanitizeUrl('https://user:example-only@example.com/?token=example-only')
376
+ // => 'https://[REDACTED]:[REDACTED]@example.com/?token=[REDACTED]'
377
+ core.sanitize.sanitizeText('Bearer example-only') // '[REDACTED]'。
378
+ core.sanitize.sanitizeText('Request failed with a known private value', ['private value'])
379
+ // => 'Request failed with a known [REDACTED]'
352
380
  ```
353
381
 
354
- ##### 环境变量映射与插值
382
+ `defaultSensitiveKeyRegex` / `defaultSensitiveReplacement` 提供默认匹配规则和 `[REDACTED]` 替换值。传入自定义 matcher 会替换默认匹配规则,所以额外清理业务隐私时应先保留默认凭据清理结果。
355
383
 
356
- `core.config.load(name, ...)` 会为每个 YAML 叶子配置项自动映射
357
- `HAI_<配置名>_<YAML 路径>` 环境变量。配置名和每层 key 转成大写,层级以 `_`
358
- 分隔,camelCase 不拆词;例如 `ai.llm.apiKey` 对应 `HAI_AI_LLM_APIKEY`,
359
- `ai.llm.maxTokens` 对应 `HAI_AI_LLM_MAXTOKENS`。数组索引也作为一层,例如
360
- `ai.llm.models[0].temperature` 对应 `HAI_AI_LLM_MODELS_0_TEMPERATURE`。
384
+ 日志默认遮蔽 password/passwordHash、token、secretKey、privateKey 等凭据字段,以及 URL 的用户名、密码、敏感查询参数和 OAuth 键值 fragment;非法 URL 返回 `(invalid url)`。`redact` 叠加业务隐私路径,支持点/括号和单层 `*`,区分大小写;邮箱、手机号默认保留。
361
385
 
362
- 环境变量存在时覆盖 YAML 值,并按 YAML 标量规则还原 number / boolean 等类型;
363
- 环境变量不存在时保留 YAML 值。对象或数组节点也可用同名变量整体替换(JSON),例如 HAI_DB 切换完整连接配置、HAI_REACH_PROVIDERS 替换渠道数组。父节点整体覆盖优先于其子字段环境变量;替换后仍执行 Schema 校验。
386
+ 原生 `Error` 先转为含 name/message/stack/cause、聚合错误和枚举上下文的副本再脱敏,不修改原异常;异常子图循环和 32 层深度分别使用 `[Circular]` / `[MaxDepth]`。普通对象循环保留,不保证可直接 JSON 序列化。消息与异常文本会清理显式凭据标签和 URL,无法识别任意无标签秘密,调用方仍需清理或整体遮蔽相应路径。
364
387
 
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
388
+ 脱敏结果只保留数据:普通对象和类实例复制自有、可枚举的数据属性,不保留类原型,不执行访问器或自定义 `toJSON()`。日期转为 ISO 字符串,URL 转为已脱敏的字符串;原对象保持不变。记录 SDK/业务对象时仍应只选取排查所需字段,未标注的秘密和业务隐私需由应用明确清理。
389
+
390
+ ### 结果与错误:`core.error`
391
+
392
+ 用 `core.error` 定义模块错误、创建本次失败的实例,并在 HTTP/UI 边界生成安全提示。错误码稳定,消息可以按当前语言产生;接口响应状态和响应头由 Serv/Kit 等 HTTP 框架处理。
393
+
394
+ ```ts
395
+ import { core } from '@h-ai/core'
396
+
397
+ // 定义稳定错误码,消息在创建实例时根据当前语言获取。
398
+ const OrderError = core.error.buildHaiErrorsDef('orders', { NOT_FOUND: '300:404' })
399
+ // OrderError.NOT_FOUND:{ code: 'hai:orders:300', httpStatus: 404, system: 'hai', module: 'orders' }。
400
+ const failure = core.error.buildHaiErrorInst(OrderError.NOT_FOUND, core.i18n.coreM('core_errorNotFound', { locale: 'zh-CN' }))
401
+ // failure:上面的错误定义加上 message: '资源不存在'。
402
+ const publicError = core.error.toPublic(failure, 'request-1')
403
+ // publicError:保留 code/httpStatus/system/module,增加 traceId: 'request-1';
404
+ // message 为 '资源不存在 (traceId: request-1)'。
405
+ const message = core.error.formatMessage(publicError) // '资源不存在 (traceId: request-1)',编号不会重复追加。
406
+ core.logger.warn('Order unavailable', { error: failure, publicMessage: message })
372
407
  ```
373
408
 
374
- ```dotenv
375
- HAI_AI_LLM_APIKEY=your-api-key
376
- HAI_AI_LLM_MAXTOKENS=8192
409
+ #### 共用结果类型与独立构造器
410
+
411
+ `HaiResult<T>` 用 success 区分 data 和 error。单独导入 `ok` / `err` 可构造成功或失败的结果。调用业务 API 后先检查 success;getter、工具与工厂按各自签名返回直接值或抛错。
412
+
413
+ ```ts
414
+ import type { HaiResult } from '@h-ai/core'
415
+ import { core, err, HaiCommonError, ok } from '@h-ai/core'
416
+
417
+ // 业务失败返回 err,成功返回 ok;共用构造器保持独立导入。
418
+ function divide(a: number, b: number): HaiResult<number> {
419
+ if (b === 0)
420
+ return err(HaiCommonError.VALIDATION_ERROR, core.i18n.coreM('core_errorValidation', { locale: 'zh-CN' }))
421
+ return ok(a / b) // 如 ok(5) 返回 { success: true, data: 5 }。
422
+ }
423
+
424
+ // 先检查 success,再读取 data 或 error。
425
+ const result = divide(10, 2) // { success: true, data: 5 }。
426
+ if (!result.success)
427
+ core.logger.warn('Calculation failed', { error: result.error })
428
+ else
429
+ core.logger.info('Calculation completed', { value: result.data })
430
+
431
+ const failed = divide(10, 0)
432
+ // failed:{ success: false, error: { code: 'hai:common:200', httpStatus: 400,
433
+ // system: 'hai', module: 'common', message: '验证失败' } },无 data。
434
+ if (!failed.success)
435
+ core.logger.warn('Calculation rejected', { code: failed.error.code })
377
436
  ```
378
437
 
379
- 确实需要特殊变量名时,继续使用显式 `${VAR}` 语法。优先级固定为:
380
- 约定环境变量 > 显式环境变量 > YAML 默认值。
438
+ 在 HTTP/UI 边界使用 `core.error.toPublic(error, traceId?, locale?)`:5xx 改为本地化通用提示,清理其他消息中的已知凭据,仅保留安全字段。追踪编号依次取显式参数、上下文 traceId/requestId、已有 error.traceId,否则生成;`formatMessage()` 在消息中附加合法编号,重复调用不会重复追加同一编号。
381
439
 
382
- - `${VAR}` — 读取 `process.env.VAR`;若不存在则返回 `CONFIG_ENV_VAR_MISSING` 错误
383
- - `${VAR:default}` — 读取 `process.env.VAR`;若不存在则使用冒号后的默认值
440
+ 内部 `cause` / `ext` 默认不公开;400/422 的字段校验可保留 `field` / `message`,明确批准且经业务 Schema 校验的 `publicExt` 会脱敏后转为公开 `ext`。Core 不自动验证 `publicExt` 的业务安全性。HTTP 状态、响应头和提示组件由 Serv/Kit/UI 宿主装配。
384
441
 
385
- **类型还原:** 当整个值恰好是单个变量表达式(如 `${PORT:3000}`)时,插值结果按 YAML 规则还原为原生类型(number / boolean 等)。混合文本始终为字符串。
442
+ | 配置错误 | 错误码 | 场景 |
443
+ | ------------------------- | -------------- | -------------------------- |
444
+ | `CONFIG_FILE_NOT_FOUND` | `hai:core:010` | 文件不存在 |
445
+ | `CONFIG_PARSE_ERROR` | `hai:core:011` | YAML/dotenv 读取或解析失败 |
446
+ | `CONFIG_VALIDATION_ERROR` | `hai:core:012` | Schema 或环境覆盖无效 |
447
+ | `CONFIG_ENV_VAR_MISSING` | `hai:core:013` | 必需插值变量缺失 |
448
+ | `CONFIG_NOT_LOADED` | `hai:core:014` | 配置未加载 |
386
449
 
387
- `${VAR:}` 或值为空字符串的 `${VAR}` 会保留为 `''`,不会被 YAML 转成 `null`。需要表达“未配置”的模块应在自身 Schema 中把空字符串规范化为 `undefined`。
450
+ 以上常量来自 `HaiConfigError`;跨模块通用错误使用 `HaiCommonError`。
388
451
 
389
- ```yaml
390
- # config/app.yml
391
- debug: ${DEBUG:false} # → boolean false
392
- port: ${PORT:3000} # → number 3000
393
- database: ${DB_CONNECTION} # → 环境变量缺失时报错
394
- url: http://${HOST}:${PORT} # → string(混合文本)
395
- ```
396
-
397
- #### ID 生成
398
-
399
- ```typescript
400
- const id = core.id.generate() // nanoid 21 字符
401
- const id8 = core.id.generate(8) // 自定义长度
402
- const shortId = core.id.short() // 10 字符短 ID
403
- const prefixed = core.id.withPrefix('user_') // 'user_V1StGXR...'
404
- const traceId = core.id.trace() // 'trace-V1StGXR...'
405
- const reqId = core.id.request() // 'req-V1StGXR...'
406
- const uuid = core.id.uuid() // UUID v4
407
-
408
- // 校验
409
- core.id.isValidUUID('f47ac10b-...') // true/false
410
- core.id.isValidNanoId('abc123', 6) // true/false
411
- ```
412
-
413
- UUID 只使用密码学安全的 randomUUID/getRandomValues;均不可用时明确抛错,不生成 Math.random 标识。
414
-
415
- #### 工具函数
416
-
417
- ```typescript
418
- // 类型检查
419
- core.typeUtils.isDefined(x) // x !== undefined
420
- core.typeUtils.isObject(x) // plain object
421
- core.typeUtils.isFunction(x)
422
- core.typeUtils.isPromise(x)
423
- core.typeUtils.isString(x)
424
- core.typeUtils.isNumber(x)
425
- core.typeUtils.isBoolean(x)
426
- core.typeUtils.isArray(x)
427
-
428
- // 对象操作
429
- core.object.deepClone(obj)
430
- core.object.deepMerge(a, b) // 递归合并(防原型污染)
431
- core.object.deepMerge(a, b, c) // 支持多个对象
432
- core.object.pick(obj, ['a', 'b'])
433
- core.object.omit(obj, ['c', 'd'])
434
- core.object.keys(obj) // typed Object.keys
435
- core.object.values(obj)
436
- core.object.entries(obj)
437
- core.object.fromEntries(entries)
438
-
439
- // 字符串操作
440
- core.string.capitalize('hello') // 'Hello'
441
- core.string.kebabCase('helloWorld') // 'hello-world'
442
- core.string.camelCase('hello-world') // 'helloWorld'
443
- core.string.snakeCase('helloWorld') // 'hello_world'
444
- core.string.pascalCase('hello-world') // 'HelloWorld'
445
- core.string.truncate('long text', 5) // 'long ...'
446
- core.string.trim(' hello ') // 'hello'
447
- core.string.isBlank('') // true
448
- core.string.isNotBlank('x') // true
449
- core.string.padStart('5', 3, '0') // '005'
450
- core.string.padEnd('5', 3, '0') // '500'
451
- core.string.constantTimeEqual(a, b) // Node 使用原生等长比较;浏览器不保证恒时
452
-
453
- // 数组操作
454
- core.array.unique([1, 1, 2]) // [1, 2]
455
- core.array.chunk([1, 2, 3, 4], 2) // [[1,2], [3,4]]
456
- core.array.groupBy(items, item => item.type) // Record<string, T[]>
457
- core.array.first([1, 2, 3]) // 1
458
- core.array.last([1, 2, 3]) // 3
459
- core.array.flatten([[1], [2, 3]]) // [1, 2, 3]
460
- core.array.compact([1, null, 2]) // [1, 2]
461
- core.array.shuffle([1, 2, 3]) // 随机排列
462
- core.array.intersection([1, 2], [2, 3]) // [2]
463
- core.array.difference([1, 2], [2, 3]) // [1]
464
-
465
- // 异步操作
466
- await core.async.delay(1000)
467
- await core.async.withTimeout(fetch('/api'), 5000)
468
- await core.async.retry(fn, { maxRetries: 3, delay: 1000 })
469
- await core.async.parallel(items, processFn, 5) // 并发限制
470
- await core.async.serial(items, processFn)
471
- const debouncedFn = core.async.debounce(fn, 300)
472
- const throttledFn = core.async.throttle(fn, 200)
473
-
474
- // 时间操作
475
- core.time.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
476
- core.time.timeAgo(new Date(Date.now() - 60000)) // '1 分钟前'
477
- core.time.now() // Date.now()
478
- core.time.nowSeconds() // Math.floor(Date.now() / 1000)
479
- core.time.parseDate('2024-01-01')
480
- core.time.isValidDate(date)
481
- core.time.addDays(date, 7)
482
- core.time.addHours(date, 2)
483
- core.time.startOfDay(date)
484
- core.time.endOfDay(date)
485
- ```
486
-
487
- Node 入口的 core.string.constantTimeEqual 将原始 UTF-16 单元编码后使用 node:crypto.timingSafeEqual 比较等长字节,长度不相同时直接 false,长度本身不受保护。浏览器实现仅避免按内容提前返回,不提供 JavaScript 引擎级恒时保证,不用于服务端凭证校验。外围分支、编码和请求处理不因此自动成为恒时。
488
-
489
- #### 模块基础工具(module)
490
-
491
- 提供各模块共用的初始化错误处理:
492
-
493
- ```typescript
452
+ ### 消息与语言:`core.i18n`
453
+
454
+ 为业务字典创建带参数替换的消息获取器,也可取得 Core 内置消息和当前语言。常量列出框架内置语言,不会自动为应用注册翻译。
455
+
456
+ ```ts
494
457
  import { core } from '@h-ai/core'
495
458
 
496
- const notInitialized = core.module.createNotInitializedKit(
497
- HaiDbError.NOT_INITIALIZED,
498
- () => dbM('db_notInitialized'),
499
- )
459
+ // 页面语言或服务进程的启动默认值;请求中使用 runWithLocale 单独选择语言。
460
+ core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;后续默认消息使用中文。
461
+ core.i18n.getGlobalLocale() // 'zh-CN'。
462
+ core.i18n.coreM('core_errorNotFound') // '资源不存在'。
463
+ core.i18n.DEFAULT_LOCALE // zh-CN。
464
+ core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]。
465
+ ```
466
+
467
+ 消息语言支持 `zh-CN`、`en-US`、`ja-JP`,简写 `zh` / `en` / `ja` 会规范化。消息优先使用显式 locale,其次为 Node 请求语言,最后为应用默认语言;不支持的语言回退 `zh-CN`,缺失键返回键本身。
468
+
469
+ ```ts
470
+ import { core } from '@h-ai/core'
500
471
 
501
- // 创建占位 Proxy(异步接口,默认)
502
- const currentDdl: DdlOperations = notInitialized.proxy<DdlOperations>()
503
- // 创建占位 Proxy(同步接口)
504
- const currentHash: HashOperations = notInitialized.proxy<HashOperations>('sync')
472
+ // 业务字典按语言组织;占位参数在取消息时传入。
473
+ const message = core.i18n.createMessageGetter({
474
+ 'zh-CN': { welcome: '欢迎 {name}' },
475
+ 'en-US': { welcome: 'Welcome {name}' },
476
+ 'ja-JP': { welcome: 'ようこそ {name}' },
477
+ })
478
+ // createMessageGetter 返回消息函数;如 message('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) 得到 '欢迎 Alice'。
479
+ const greeting = await core.i18n.runWithLocale('en-US', async () => {
480
+ await Promise.resolve()
481
+ return message('welcome', { params: { name: 'Alice' } })
482
+ })
483
+ // greeting:'Welcome Alice';runWithLocale 返回回调的结果,此处等待 Promise 得到字符串。
484
+ core.logger.info('Greeting prepared', { greeting })
505
485
  ```
506
486
 
507
- ### 错误处理
487
+ Node 请求/SSR 用 `runWithLocale()` 包裹实际异步处理;Serv 自动建立请求语言上下文,Kit 提供同名入口。在请求上下文内 `setGlobalLocale()` 只修改当前请求;上下文外仅用于启动默认值。浏览器使用应用级 `setGlobalLocale()` 或显式 locale;SSR 的 UI store 不应修改进程默认语言。
508
488
 
509
- #### HaiResult 类型
489
+ ### 字段校验提示:`core.zodValidation`
510
490
 
511
- 所有可能失败的操作返回 `HaiResult<T>`:
491
+ 把 Zod 校验错误转换为可显示在表单字段旁的 `{ field, message }[]`。`createPrefixedZodMessageGetter()` 按前缀选择翻译键,`mapZodErrorToFormErrors()` 完成提取、翻译和字段路径转换。
512
492
 
513
- ```typescript
514
- import type { HaiResult } from '@h-ai/core'
515
- import { core, err, HaiCommonError, ok } from '@h-ai/core'
493
+ ```ts
494
+ import { core } from '@h-ai/core'
495
+ import { z } from 'zod'
516
496
 
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)
497
+ // 此例复用 Core 已有翻译;业务表单可注入自己的消息获取器。
498
+ const message = core.zodValidation.createPrefixedZodMessageGetter('core', key =>
499
+ key === 'core_validationEmail'
500
+ ? core.i18n.coreM('core_validationEmail', { locale: 'zh-CN' })
501
+ : core.i18n.coreM('core_errorValidation', { locale: 'zh-CN' }))
502
+ // 返回消息函数;message('validationEmail') 得到 '请输入有效的邮箱地址'。
503
+ const parsed = z.object({ email: z.email() }).safeParse({ email: 'invalid' })
504
+ if (!parsed.success) {
505
+ const errors = core.zodValidation.mapZodErrorToFormErrors(parsed.error, message)
506
+ // errors:[{ field: 'email', message: '请输入有效的邮箱地址' }]。
507
+ core.logger.info('Validation failed', { errors })
521
508
  }
509
+ ```
510
+
511
+ 支持 Zod v3/v4 的 issues/errors 结构;默认英文消息会翻译,Schema 显式提供的业务消息保留。字段路径用点号,全局错误用 `_`。严格 Schema 的未知字段使用 validationUnexpectedFields,不公开内部字段名。
512
+
513
+ ### 模块启动与辅助工具:`core.module`
514
+
515
+ 这些工具供应用启动代码和框架模块实现复用,既能管理多个模块,也能在模块尚未初始化时返回一致错误。它们不会替应用决定使用哪些模块。
516
+
517
+ #### 应用启动与关闭
518
+
519
+ 应用使用多个模块时,通常需要先完成数据库等资源的初始化,处理业务后再释放资源。`core.module.createRuntime()` 接收这些模块的启动与关闭函数,返回一个供应用调用的对象;创建这个对象时还不会启动模块。
522
520
 
523
- const result = divide(10, 2)
524
- if (result.success) {
525
- core.logger.info('Result', { data: result.data })
521
+ 下面是本地开发示例,需额外安装 `@h-ai/reldb` 和 `@h-ai/cache`。数据库和缓存都使用内存配置,仅用于演示,生产环境应按模块要求配置共享后端。
522
+
523
+ ```ts
524
+ import { cache } from '@h-ai/cache'
525
+ import { core } from '@h-ai/core'
526
+ import { reldb } from '@h-ai/reldb'
527
+
528
+ // 按需要的启动顺序,列出各模块的初始化和关闭函数。
529
+ const runtime = core.module.createRuntime([
530
+ {
531
+ name: 'database',
532
+ start: () => reldb.init({ type: 'sqlite', database: ':memory:' }),
533
+ close: () => reldb.close(),
534
+ },
535
+ {
536
+ name: 'cache',
537
+ start: () => cache.init({ type: 'memory' }),
538
+ close: () => cache.close(),
539
+ },
540
+ ])
541
+
542
+ // createRuntime 返回带 start()/close() 的对象;到这里数据库和缓存尚未启动。
543
+
544
+ // 依次启动数据库、缓存;失败时自动清理已尝试启动的模块。
545
+ const started = await runtime.start() // 成功为 { success: true, data: undefined };两个模块均已初始化。
546
+ if (!started.success)
547
+ throw new Error(started.error.message, { cause: started.error })
548
+
549
+ try {
550
+ // 启动成功后再执行应用业务;这里演示记录启动结果。
551
+ core.logger.info('Application modules ready')
526
552
  }
527
- else {
528
- core.logger.error('Error', { error: result.error })
553
+ finally {
554
+ // 演示结束后先关闭缓存,再关闭数据库,并检查清理结果。
555
+ const closed = await runtime.close() // 成功为 { success: true, data: undefined };两个模块均已关闭。
556
+ if (!closed.success)
557
+ core.logger.error('Application cleanup failed', { error: closed.error })
529
558
  }
530
559
  ```
531
560
 
532
- #### 自定义错误码(模块级)
561
+ 本例的启动顺序是数据库 → 缓存,关闭顺序是缓存 → 数据库。示例在演示结束后立即清理;长期运行的服务应在退出时执行同样的 `runtime.close()`。
533
562
 
534
- ```typescript
535
- import type { ErrorInfo } from '@h-ai/core'
536
- import { core } from '@h-ai/core'
563
+ 某一步启动失败,会清理已经尝试启动的模块;某一步关闭失败,会先停在该处,保留它仍需使用的依赖,再次调用 `close()` 或 `start()` 时重试清理。多处同时调用 `start()` 不会重复初始化。可选的 `enabled()` 返回 false 时跳过该项。
564
+
565
+ `start()` / `close()` 都返回 `HaiResult`,应用必须检查 `success`。错误包含模块名、失败阶段、错误码和必要的配置字段提示,避免把配置秘密和 SDK 原始异常作为启动错误公开。
537
566
 
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
567
+ #### 模块尚未初始化时返回错误
544
568
 
545
- // 生成错误定义对象
546
- export const DbError = core.error.buildHaiErrorsDef('db', DbErrorInfo)
547
- // DbError.CONNECTION_FAILED → { code: 'hai:db:010', httpStatus: 500, system: 'hai', module: 'db' }
569
+ `createNotInitializedKit()` 返回 error/result/proxy 三种用法;proxy 为尚未准备好的同步或异步操作返回统一失败结果,模块就绪后应切换为真实操作。消息函数在调用时读取语言。
548
570
 
549
- // 创建错误实例
550
- const errorInst = core.error.buildHaiErrorInst(
551
- DbError.CONNECTION_FAILED,
552
- 'Unable to connect to PostgreSQL',
553
- originalError, // cause(可选)
554
- 'Check connection string', // suggestion(可选)
555
- )
571
+ ```ts
572
+ import type { HaiResult } from '@h-ai/core'
573
+ import { core, HaiCommonError } from '@h-ai/core'
574
+
575
+ const message = core.i18n.createMessageGetter({
576
+ 'zh-CN': { notReady: '请先初始化模块' },
577
+ 'en-US': { notReady: 'Initialize the module first' },
578
+ 'ja-JP': { notReady: '先にモジュールを初期化してください' },
579
+ })
580
+ // 本例固定使用中文消息,便于观察返回值。
581
+ core.i18n.setGlobalLocale('zh-CN') // 返回 undefined。
582
+ const waiting = core.module.createNotInitializedKit(HaiCommonError.NOT_INITIALIZED, () => message('notReady'))
583
+ // waiting 是带 error/result/proxy 的对象。
584
+ waiting.error() // { code: 'hai:common:001', httpStatus: 500, system: 'hai', module: 'common', message: '请先初始化模块' }。
585
+ waiting.result<string>() // { success: false, error: 上方错误对象 },无 data。
586
+
587
+ // 模块实现可把尚未就绪的操作暂时指向这些代理。
588
+ const asyncOperations = waiting.proxy<{ read: () => Promise<HaiResult<string>> }>()
589
+ const syncOperations = waiting.proxy<{ read: () => HaiResult<string> }>('sync')
590
+ // 两次 proxy 调用分别返回异步和同步操作对象。
591
+ const result = await asyncOperations.read() // { success: false, error: 上方错误对象 }。
592
+ if (!result.success)
593
+ core.logger.warn('Read before initialization', { error: result.error })
594
+ syncOperations.read() // 直接返回 { success: false, error: 上方错误对象 },不返回 Promise。
556
595
  ```
557
596
 
558
- #### 配置错误码
597
+ #### 判断是否需要共享状态
598
+
599
+ `requiresSharedState()` 判断生产进程是否应拒绝只在本机有效的业务状态后端。它只返回判断,不连接或切换数据库;实际环境由服务端传入。
559
600
 
560
- ```typescript
561
- import { HaiConfigError } from '@h-ai/core'
601
+ ```ts
602
+ import { core } from '@h-ai/core'
562
603
 
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 - 配置未加载
604
+ // 此例用显式数据演示;服务端实际使用时传入 process.env。
605
+ core.module.requiresSharedState({ NODE_ENV: 'production' }) // true:应使用跨主机共享后端。
606
+ core.module.requiresSharedState({ NODE_ENV: 'production', HAI_E2E: '1' }) // false:显式 E2E 例外。
568
607
  ```
569
608
 
570
- ### 请求语言隔离
609
+ ### 请求访问检查:`core.access`(Node)
571
610
 
572
- Node 请求与 SSR 使用 `core.i18n.runWithLocale(locale, () => handleRequest())`;SvelteKit 可使用 `kit.i18n.runWithLocale(locale, () => resolve(event))`,serv 自动按请求头建立上下文。异步子任务继承请求语言,优先级为显式 `MessageOptions.locale`、请求语言、进程启动默认语言。`setGlobalLocale` 在请求上下文中只更新当前请求;上下文外仅用于启动默认值,不能在请求 hook 中调用它修改默认值。
611
+ HTTP 请求入口可用这些工具解析可信客户端 IP,再检查地址、路径、方法和允许时段。Core 只提供检查结果;Serv 的 http.access 或 Kit 的 createHandle({ access }) 负责把检查接到请求流程。
573
612
 
574
- 浏览器继续使用应用级 `setGlobalLocale` / `createLocaleStore`。SSR 的 UI store 初始化和 setter 只修改自身状态,不写全局语言;UI 消息从请求上下文读取。`runWithLocale` 仅支持 Node 入口,浏览器通过显式 locale 或应用状态翻译。
613
+ #### 解析与检查客户端地址
575
614
 
576
- ## 可信地址与请求访问策略(Node)
615
+ ```ts
616
+ import { AccessConditionSchema, core } from '@h-ai/core'
617
+
618
+ core.access.normalizeClientIp('::ffff:198.51.100.23') // 198.51.100.23。
619
+ const resolve = core.access.createClientIpResolver(['192.0.2.10']) // 返回 IP 解析函数,只信任这个代理。
620
+ const request = new Request('https://example.test/api/orders', { headers: { 'x-forwarded-for': '198.51.100.23' } })
621
+ // 第二参数必须来自真实 socket/可信平台;这里用测试地址模拟代理对端。
622
+ const clientIp = resolve(request, '192.0.2.10') // '198.51.100.23',读取受信代理的转发头。
623
+ const check = core.access.createConditionCheck(AccessConditionSchema.parse({ allowIps: ['198.51.100.0/24'] }))
624
+ // createConditionCheck 返回检查函数;check(clientIp) 得到 { allowed: true }。
625
+ core.logger.info('Address checked', { allowed: check(clientIp).allowed })
626
+ ```
577
627
 
578
- `core.access` 为 HTTP 宿主提供共享的 IP、时间与路径检查,不自动安装 HTTP 拦截器。Serv 使用 `http.access`,SvelteKit 使用 `kit.createHandle({ access })`;IAM 的 `loginAccess` 只检查登录,不能替代每次请求检查。
628
+ #### 读取策略并检查请求
579
629
 
580
630
  ```ts
581
631
  import { core } from '@h-ai/core'
582
632
 
583
- const check = core.access.createRequestCheck({
584
- rules: [{ id: 'external-api', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'], encryption: 'disabled' }],
633
+ // 可改为 loadPolicy({ file: './config/security/request-access.yml' }) 在启动时读独立文件。
634
+ const policy = core.access.loadPolicy({
635
+ rules: [{ id: 'external-api', paths: ['/api/external/**'], methods: ['POST'], allowIps: ['192.0.2.0/24'], encryption: 'disabled' }],
585
636
  })
586
- const decision = check('/api/v1/external/sync', 'POST', '192.0.2.10')
587
- const mode = check.encryption('/api/v1/external/sync', 'POST') // disabled
637
+ // policy.rules[0] 保留上方规则,并补 denyIps: []、windows: []、timezone: 'UTC'。
638
+ const check = core.access.createRequestCheck(policy) // 返回请求检查函数,另带 encryption 方法和 requiresEncryption 属性。
639
+ const decision = check('/api/external/sync', 'POST', '192.0.2.10') // { allowed: true }。
640
+ check.encryption('/api/external/sync', 'POST') // disabled,供 HTTP 框架选择传输模式。
641
+ check.requiresEncryption // false;本例没有 required 规则。
642
+ core.logger.info('Request access checked', { allowed: decision.allowed })
588
643
  ```
589
644
 
590
- `createRequestCheck` 在创建时验证并编译规则;`loadPolicy({ file: './config/security/request-access.yml' })` 只在启动时读取独立 YAML,最大 1 MiB,错误配置抛出异常并阻止宿主启动。全局条件与所有命中规则取交集,拒绝地址优先;默认空地址、时间列表不限制对应维度,时区为 UTC。路径是实际 URL pathname,包含部署前缀;支持精确路径和末尾 `/**`,GET 不隐含 HEAD。
645
+ - 创建检查器时验证并编译规则;`loadPolicy({ file })` 启动时读取独立 YAML,最大 1 MiB,配置错误抛出并阻止宿主启动。内联规则与文件不能混用。
646
+ - 全局条件与所有命中规则取交集,拒绝地址优先;空 IP/时间列表不限制对应维度,时区默认 UTC。路径包含部署前缀,支持精确路径和末尾 `/**`,GET 不隐含 HEAD。
647
+ - `createClientIpResolver()` 默认 `auto` 在普通宿主忽略转发头,仅受管私有 Compose 采用容器网段。`[]` 关闭代理信任,自管入口明确填写代理 IP/CIDR;对端地址必须来自 socket 或可信平台。
648
+ - 路径 `encryption` 可为 `required` / `disabled`,省略沿用宿主;重叠路径和方法的相反声明在创建时失败。`check.encryption()` / `requiresEncryption` 供宿主装配传输模式,Core 不持有密钥或执行加解密,仍需 HTTPS 和身份鉴别。
591
649
 
592
- 可信地址通过 `core.access.createClientIpResolver()` 解析。默认 `auto` 在普通宿主忽略转发头,仅受管 Compose 的私有代理拓扑采用容器网段;`[]` 显式关闭代理信任,自管入口填入真实代理 IP/CIDR。不得把任意请求头当作可信 socket 地址。检查器不在请求中读文件、解析 CIDR 或扫描全部路径规则;同一时区的分钟结果复用,跨分钟仍重新判定。`core.access` 仅在 Node 入口存在,浏览器不提供伪实现。公开配置 Schema 由 `core-config` 入口转出,类型集中于 `core-types`,函数通过 `core-main` 注册的 `core.access` 调用。
650
+ 完整默认值、独立文件、代理拓扑及部署步骤见[请求访问控制手册](../../docs/usage/configuration/request-access.md)。
593
651
 
594
- 路径规则的 `encryption` 可选 `required` 或 `disabled`,省略时沿用宿主。选择器复用路径索引,不计算 IP/时间。重叠路径与方法若声明相反模式,创建检查器时失败,与声明顺序无关;同模式或不同方法可以组合。`check.requiresEncryption` 告知宿主是否必须装配传输加密,Core 本身不持有密钥或执行加解密。关闭应用层加密仍需 HTTPS、身份鉴别和访问检查。
652
+ ### 生成与校验标识:`core.id`
595
653
 
596
- 完整默认值、代理条件、独立文件、部署和验证步骤见[请求访问控制手册](../../docs/usage/configuration/request-access.md)。
654
+ 生成业务标识与日志关联编号,或检查输入是否符合 UUID v4 / nanoid 格式。
597
655
 
598
- ## 配置的实际消费边界
656
+ ```ts
657
+ import { core } from '@h-ai/core'
599
658
 
600
- `_core.yml` 仅接受 name/version/env/defaultLocale 元数据与 logging;未知字段加载失败。Core 不提供全局 debug、ID 长度/前缀、业务 features 或语言注册列表;日志级别用 logging.level,ID 选项在每次 core.id 调用传入,业务配置放 app.yml 并由应用 Schema 校验。defaultLocale 是可读取的元数据,不会替应用注册翻译或切换 Paraglide。导入 Logger 不改写 LANG、LC_ALL、PYTHONIOENCODING,包括 Windows TTY。全部模块的配置与环境消费链见[配置对应表](../../.agents/skills/hai-module-create/configuration.md)。
659
+ // 以下随机值仅展示格式,每次执行都不同。
660
+ core.id.generate() // 如 'V1StGXR8_Z5jdHi6B-myT',默认 21 字符;generate(8) 返回 8 字符。
661
+ const shortId = core.id.short() // 如 'V1StGXR8_Z',10 字符。
662
+ core.id.withPrefix('order_') // 如 'order_V1StGXR8_Z5jdHi6B-myT',前缀后接 21 字符。
663
+ core.id.trace() // 如 'trace-V1StGXR8_Z5jdHi6B-myT'。
664
+ core.id.request() // 如 'req-V1StGXR8_Z5jdHi6B-myT'。
665
+ const uuid = core.id.uuid() // 如 '550e8400-e29b-41d4-a716-446655440000',UUID v4。
666
+ core.id.isValidUUID(uuid) // true。
667
+ core.id.isValidNanoId(shortId, 10) // true。
668
+ ```
669
+
670
+ ID 不能作为密钥。UUID 只使用密码学安全随机源,不可用时抛错;_core.yml 没有全局 ID 长度或前缀配置,按每次调用传参。
671
+
672
+ ### 判断值的类型:`core.typeUtils`
601
673
 
602
- ## 请求关联与日志观察
674
+ 对边界数据做类型判断,并帮助 TypeScript 在分支中缩小类型;这些判断不替代业务 Schema 校验。
675
+
676
+ ```ts
677
+ import { core } from '@h-ai/core'
603
678
 
604
- 采集器内部的持久化/上传路径可使用 `core.context.run({ suppressLogObservers: true }, operation)` 暂停观察回调,避免异步采集自身日志;stdout 等常规日志输出保持生效,退出作用域后恢复。
679
+ core.typeUtils.isDefined(null) // false,同时排除 null 和 undefined。
680
+ core.typeUtils.isObject({ name: 'orders' }) // true;排除 null/数组,但也接受 Date 和类实例。
681
+ core.typeUtils.isFunction(() => {}) // true。
682
+ core.typeUtils.isPromise(Promise.resolve('ready')) // true。
683
+ core.typeUtils.isString('orders') // true。
684
+ core.typeUtils.isNumber(3000) // true;传 NaN 返回 false。
685
+ core.typeUtils.isBoolean(false) // true。
686
+ core.typeUtils.isArray([1, 2]) // true。
687
+ ```
605
688
 
606
- `core.logger.observe` 观察当前级别允许且已执行默认/自定义字段脱敏的记录,返回取消函数。订阅者应只做有界内存收集,再异步持久化;日志观察本身不代表已经保存。回调异常和同步重入不影响业务。自由文本与图像仍需采集侧的专门隐私策略。
689
+ ### 处理对象:`core.object`
607
690
 
608
- `core.context.run` 在 Node 中用 AsyncLocalStorage 隔离并发请求和子任务,现有模块 logger 自动附带当前 requestId/userId/scopeId/jobId。身份范围只来自可信服务端代码;关联 ID 不是授权。浏览器保持同一 API,但 run 只建立同步作用域,不能用于跨 await 的身份隔离。
691
+ 复制、合并或选取对象字段,也提供带键类型的 keys/values/entries 与 fromEntries。
609
692
 
610
693
  ```ts
611
- import type { LogRecord } from '@h-ai/core'
612
694
  import { core } from '@h-ai/core'
613
695
 
614
- const 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()
696
+ const defaults = { name: 'orders', port: 3000 }
697
+ const copy = core.object.deepClone(defaults) // { name: 'orders', port: 3000 },新副本。
698
+ // 合并局部覆盖时,显式保留包含 name/port 的完整配置类型。
699
+ const settings = core.object.deepMerge<typeof defaults>(copy, { port: 4000 }) // { name: 'orders', port: 4000 }。
700
+ core.object.pick(settings, ['name']) // { name: 'orders' }。
701
+ core.object.omit(settings, ['port']) // { name: 'orders' }。
702
+ core.object.keys(settings) // ['name', 'port'],元素类型为 'name' | 'port'。
703
+ core.object.values(settings) // ['orders', 4000]。
704
+ core.object.entries(settings) // [['name', 'orders'], ['port', 4000]]。
705
+ core.object.fromEntries([['mode', 'local']]) // { mode: 'local' }。
625
706
  ```
626
707
 
627
- ## 应用生命周期编排
708
+ deepClone 使用 structuredClone,支持 Date/Map/Set/循环等可克隆值;不可克隆的函数等会抛错。deepMerge 只递归普通对象,数组、Date、集合和类实例由后值覆盖,不修改输入;不把它当作实例克隆。
628
709
 
629
- `core.module.createRuntime([{ name, enabled?, start, close }])` 按声明启动、反序关闭,合并并发调用。启动前登记部分资源的清理义务;失败回滚。关闭失败停在该模块并保留依赖,下次 close/start 重试,避免先关闭仍被使用的数据库。错误仅保留模块、阶段、错误码和配置字段路径;不转发配置值或 SDK 原始异常。base/API/fullstack 和管理台均使用此入口。
710
+ ### 处理字符串:`core.string`
630
711
 
631
- 启动错误包含配置原因和处理建议:提供 `ext.fields` 时,`HaiConfigError.CONFIG_ENV_VAR_MISSING` 提示环境变量未设置,`CONFIG_VALIDATION_ERROR` 或 Schema 校验异常提示校验未通过;只有字段路径时提示“配置项缺失或无效”,不猜测具体原因。即使启动边界只打印 `error.message`,也能看到字段、处理建议和底层错误码。自定义初始化钩子可在缺少必需环境变量时抛出带 `code: HaiConfigError.CONFIG_ENV_VAR_MISSING.code` 和 `ext: { fields: ['变量名'] }` 的异常;fullstack 模板的初始管理员密码检查采用此方式。
712
+ 统一命名格式、处理显示文字、判断空白以及比较字符串。
632
713
 
633
- ## 错误提示与关联日志
714
+ ```ts
715
+ import { core } from '@h-ai/core'
716
+
717
+ core.string.capitalize('hello') // 'Hello'。
718
+ core.string.kebabCase('helloWorld') // 'hello-world'。
719
+ core.string.camelCase('hello-world') // 'helloWorld'。
720
+ core.string.snakeCase('helloWorld') // 'hello_world'。
721
+ core.string.pascalCase('hello-world') // 'HelloWorld'。
722
+ core.string.truncate('abcdef', 3) // 'abc...'。
723
+ core.string.trim(' hello ') // 'hello'。
724
+ core.string.isBlank(' ') // true。
725
+ core.string.isNotBlank('hello') // true。
726
+ core.string.padStart('5', 3, '0') // '005'。
727
+ core.string.padEnd('5', 3, '0') // '500'。
728
+ core.string.constantTimeEqual('same', 'same') // true;安全边界见下方说明。
729
+ ```
634
730
 
635
- HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐藏编号,由排查信息图标展开和复制。响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
731
+ Node 的 constantTimeEqual 对等长 UTF-16 字节使用原生 timingSafeEqual,不同长度直接返回 false;浏览器不能提供引擎级恒时保证,不用于服务端凭据校验。
636
732
 
637
- `core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
733
+ ### 处理数组:`core.array`
638
734
 
639
- ### 公开业务进度
735
+ 按任务需要对数组去重、分块、分组、筛除空值或计算交集/差集;返回结果,保留输入数组。
640
736
 
641
- 内部 ext 默认不公开。只有明确批准且经过 Schema 校验的业务进度才使用 publicExt,由边界转换为 ext;不能放入内部异常。
737
+ ```ts
738
+ import { core } from '@h-ai/core'
739
+
740
+ core.array.unique([1, 1, 2]) // [1, 2]。
741
+ core.array.chunk([1, 2, 3], 2) // [[1, 2], [3]]。
742
+ core.array.groupBy([{ role: 'admin' }, { role: 'user' }], item => item.role)
743
+ // => { admin: [{ role: 'admin' }], user: [{ role: 'user' }] }
744
+ core.array.first([1, 2, 3]) // 1;空数组为 undefined。
745
+ core.array.last([1, 2, 3]) // 3;空数组为 undefined。
746
+ core.array.flatten([[1], [2, 3]]) // [1, 2, 3],只展开一层。
747
+ core.array.compact([0, null, 1, undefined]) // [0, 1],保留 0/false/空字符串。
748
+ core.array.shuffle([1, 2, 3]) // 如 [2, 3, 1],次序随机;新数组不修改输入。
749
+ core.array.intersection([1, 2], [2, 3]) // [2]。
750
+ core.array.difference([1, 2], [2, 3]) // [1]。
751
+ ```
752
+
753
+ groupBy 支持 `constructor`、`__proto__` 等原型同名键;chunk 的块大小应为正整数,size<=0 返回空数组。
754
+
755
+ ### 组织异步操作:`core.async`
756
+
757
+ 安排延迟、超时、失败重试、限并发与串行处理,或为输入/滚动等事件添加防抖与节流。
758
+
759
+ ```ts
760
+ import { core } from '@h-ai/core'
761
+
762
+ await core.async.delay(1) // 等待约 1ms 后得到 undefined;计时以运行环境为准。
763
+ await core.async.withTimeout(Promise.resolve('ready'), 1000) // 'ready';超时则 reject 为 Error。
764
+ await core.async.retry(async () => 'ready', { maxRetries: 3, delay: 10 }) // 'ready',本例首次即成功。
765
+ await core.async.parallel([1, 2, 3], async value => value * 2, 2) // [2, 4, 6],最多同时执行两项。
766
+ await core.async.serial([1, 2, 3], async value => value * 2) // [2, 4, 6],按输入顺序逐项执行。
767
+
768
+ // 两种包装保留回调参数类型;调用 search/scroll 时传入同样类型的参数。
769
+ const search = core.async.debounce((term: string) => core.logger.debug('Search requested', { term }), 100)
770
+ const scroll = core.async.throttle((position: number) => core.logger.debug('Scroll position', { position }), 100)
771
+ // debounce/throttle 均返回包装函数,调用时返回 undefined。
772
+ core.logger.setLevel('debug') // 返回 undefined;确保下面的 debug 日志可见。
773
+ search('orders') // 约 100ms 后回调收到 'orders';100ms 内连续调用只处理最后一次。
774
+ scroll(100) // 本例第一次调用立即回调,position 为 100;100ms 内最多执行一次。
775
+ ```
776
+
777
+ withTimeout 只限制等待时间,不取消底层任务。retry 仅处理抛错/reject,失败 HaiResult 不会自动重试;当前 maxRetries 包含第一次尝试,例如 3 表示最多执行三次。parallel 的并发数须为大于 0 的安全整数,非法值使 Promise 以 RangeError 失败且不执行任务;结果顺序与输入一致。
778
+
779
+ ### 处理日期与时间:`core.time`
780
+
781
+ 在毫秒/秒时间戳、Date 和显示文本之间转换,并计算当地日期边界与相对时间。
782
+
783
+ ```ts
784
+ import { core } from '@h-ai/core'
785
+
786
+ // date 与以下日期运算使用本机时区;parseDate 中的 Z 表示 UTC。
787
+ const date = new Date(2026, 0, 1, 12, 0, 0)
788
+ core.time.formatDate(date, 'YYYY-MM-DD HH:mm:ss') // '2026-01-01 12:00:00'。
789
+ core.i18n.setGlobalLocale('zh-CN') // 返回 undefined;相对时间使用中文。
790
+ core.time.timeAgo(new Date(Date.now() - 60_000)) // '1 分钟前'(立即执行时)。
791
+ core.time.now() // 当前毫秒时间戳,如 1767268800000;每次执行随时间变化。
792
+ core.time.nowSeconds() // 当前秒时间戳,如 1767268800;相当于当前毫秒时间戳除以 1000 后向下取整。
793
+ const parsed = core.time.parseDate('2026-01-01T00:00:00Z') // Date 对象,toISOString() 为 '2026-01-01T00:00:00.000Z'。
794
+ core.time.isValidDate(parsed) // true;Invalid Date 返回 false。
795
+ core.time.addDays(date, 7) // 新 Date,本机时间 2026-01-08 12:00:00。
796
+ core.time.addHours(date, 2) // 新 Date,本机时间 2026-01-01 14:00:00。
797
+ core.time.startOfDay(date) // 新 Date,本机时间 2026-01-01 00:00:00.000。
798
+ core.time.endOfDay(date) // 新 Date,本机时间 2026-01-01 23:59:59.999。
799
+ ```
800
+
801
+ formatDate 支持 YYYY/MM/DD/HH/mm/ss 占位符,默认 YYYY-MM-DD;日期运算返回新 Date。parseDate 不会因无效文本自动抛错,应使用 isValidDate 检查。
802
+
803
+ 更完整的参数与少见选项见[Core 使用参考](../cli/templates/skills/hai-core/reference.md);公开契约以当前包声明为准。
642
804
 
643
805
  ## 开发测试
644
806
 
807
+ 在仓库根目录执行(PowerShell 使用 `pnpm.cmd`):
808
+
645
809
  ```bash
810
+ pnpm --filter @h-ai/core typecheck
811
+ pnpm --filter @h-ai/core lint
812
+ pnpm --filter @h-ai/core build
646
813
  pnpm --filter @h-ai/core test
647
814
  ```
648
815
 
649
- 公开声明和内部结构的注释及可编译示例见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。Node core.init 同步返回 void;单项 config.load 返回失败时记录错误并继续,必需配置须 validate 并检查 success。目录扫描等异常仍可能抛出;模块业务 HaiResult 则先检查 success。
816
+ 公开声明与示例检查见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。
650
817
 
651
818
  ## 许可证
652
819