@h-ai/core 0.1.0-alpha.67 → 0.1.0-alpha.69
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 +55 -8
- package/README.ja.md +55 -8
- package/README.md +55 -8
- package/dist/browser.d.ts +9 -5
- package/dist/browser.js +21 -9
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-RXNSRXYF.js → chunk-MUQFCAQ5.js} +552 -354
- package/dist/{chunk-RXNSRXYF.js.map → chunk-MUQFCAQ5.js.map} +1 -1
- package/dist/{core-function-config.node-7KVlEiFk.d.ts → core-function-config.node-CGXWsohk.d.ts} +568 -175
- package/dist/node.d.ts +23 -7
- package/dist/node.js +41 -12
- package/dist/node.js.map +1 -1
- package/package.json +6 -3
package/README.en.md
CHANGED
|
@@ -158,7 +158,9 @@ After `core.init()` loads `_core.yml.logging`, or after `configure` / `setLevel`
|
|
|
158
158
|
|
|
159
159
|
Browser logs still use the DevTools console: `pretty` prints readable messages with context, while `json` prints a JSON string containing `level` / `msg`. Default credential masking and configured path redaction both apply, including nested dot/bracket paths and `*`. Callers remain responsible for redacting message text.
|
|
160
160
|
|
|
161
|
-
`
|
|
161
|
+
Native `Error` values in log context are copied into diagnostics containing `name`, `message`, `stack`, `cause`, `AggregateError.errors`, and enumerable own context before default field/URL masking and configured `redact` paths run. Both backends use the same rules and leave the original error unchanged. Error-subgraph cycles become `[Circular]` and the 32-level limit becomes `[MaxDepth]`; custom replacements in these subgraphs follow the same normalization, while ordinary-object replacement behavior is unchanged. Ordinary-object cycles without an Error retain their original structure and are not guaranteed to be directly JSON-serializable. `name/message/stack` are free text: secrets embedded in them are not automatically parsed. Clean them before logging or mask whole paths such as `error.message` / `error.stack`.
|
|
162
|
+
|
|
163
|
+
`core.logger` redacts log context by default: credentials such as `password`, `token`, `secret`, `privateKey`, and `apiV3Key` become `[REDACTED]`. Fields such as `url` / `baseUrl` / `endpoint` mask embedded usernames/passwords, sensitive query parameters, and OAuth key/value fragments; malformed URLs return a fixed placeholder. Email addresses and phone numbers are not masked by default.
|
|
162
164
|
|
|
163
165
|
##### Log level conventions
|
|
164
166
|
|
|
@@ -173,6 +175,8 @@ Browser logs still use the DevTools console: `pretty` prints readable messages w
|
|
|
173
175
|
|
|
174
176
|
#### Internationalization (i18n)
|
|
175
177
|
|
|
178
|
+
Built-in locales are `zh-CN`, `en-US` and `ja-JP`. The `ja` alias normalizes to `ja-JP`; all modules share this setting.
|
|
179
|
+
|
|
176
180
|
```typescript
|
|
177
181
|
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
178
182
|
core.i18n.setGlobalLocale('en-US')
|
|
@@ -191,13 +195,13 @@ getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
|
191
195
|
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
192
196
|
|
|
193
197
|
// 常量
|
|
194
|
-
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }]
|
|
198
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]
|
|
195
199
|
core.i18n.DEFAULT_LOCALE // 'zh-CN'
|
|
196
200
|
```
|
|
197
201
|
|
|
198
202
|
#### Redaction utilities
|
|
199
203
|
|
|
200
|
-
`core.sanitize.sanitizeSensitiveFields()` recursively redacts objects and arrays. By default it handles password, token, secret, privateKey, apiV3Key, and similar credentials, and masks embedded credentials in `url` / `baseUrl` / `endpoint`.
|
|
204
|
+
`core.sanitize.sanitizeSensitiveFields()` recursively redacts objects and arrays. By default it handles password, token, secret, privateKey, apiV3Key, and similar credentials, and masks embedded credentials in `url` / `baseUrl` / `endpoint`. `core.sanitize.sanitizeUrl(url, options)` also handles individual URLs and returns `(invalid url)` for malformed input. Custom matchers apply to parameter names as well.
|
|
201
205
|
|
|
202
206
|
```typescript
|
|
203
207
|
core.sanitize.sanitizeSensitiveFields({
|
|
@@ -280,13 +284,15 @@ if (!result.success) {
|
|
|
280
284
|
|
|
281
285
|
> Browsers have no filesystem access. `core.config` offers placeholder APIs only: file-related methods return `SERVICE_UNAVAILABLE` and do not load YAML.
|
|
282
286
|
|
|
287
|
+
Node automatically loads and watches all `.yml` / `.yaml` files in the configuration directory. The CLI reads `build` / `deploy` from the project root `hai.json`; no YAML filename needs special handling. A failed per-file load result is logged and loading continues; applications must validate required configuration and check `success`. Directory scanning, logger setup and other exceptions may still propagate.
|
|
288
|
+
|
|
283
289
|
```typescript
|
|
284
290
|
import { core, HaiConfigError } from '@h-ai/core'
|
|
285
291
|
import { z } from 'zod'
|
|
286
292
|
|
|
287
|
-
// 方式 1:init
|
|
293
|
+
// 方式 1:init 时自动加载运行时配置
|
|
288
294
|
core.init({
|
|
289
|
-
configDir: './config', //
|
|
295
|
+
configDir: './config', // 扫描配置目录内所有 .yml / .yaml
|
|
290
296
|
watchConfig: true, // 监听变更并自动重载
|
|
291
297
|
})
|
|
292
298
|
|
|
@@ -308,8 +314,8 @@ const validResult = core.config.validate('app', AppSchema)
|
|
|
308
314
|
|
|
309
315
|
// 查询和获取
|
|
310
316
|
if (core.config.has('app')) {
|
|
311
|
-
const cfg = core.config.get<typeof AppSchema
|
|
312
|
-
const cfg2 = core.config.getOrThrow<typeof AppSchema
|
|
317
|
+
const cfg = core.config.get<z.infer<typeof AppSchema>>('app') // undefined 或配置
|
|
318
|
+
const cfg2 = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // 未加载时抛异常
|
|
313
319
|
}
|
|
314
320
|
core.config.keys() // ['core', 'app', 'db', ...]
|
|
315
321
|
|
|
@@ -402,6 +408,8 @@ core.id.isValidUUID('f47ac10b-...') // true/false
|
|
|
402
408
|
core.id.isValidNanoId('abc123', 6) // true/false
|
|
403
409
|
```
|
|
404
410
|
|
|
411
|
+
UUID generation requires cryptographic randomUUID/getRandomValues and throws if neither is available; it never falls back to Math.random.
|
|
412
|
+
|
|
405
413
|
#### Utilities
|
|
406
414
|
|
|
407
415
|
```typescript
|
|
@@ -438,7 +446,7 @@ core.string.isBlank('') // true
|
|
|
438
446
|
core.string.isNotBlank('x') // true
|
|
439
447
|
core.string.padStart('5', 3, '0') // '005'
|
|
440
448
|
core.string.padEnd('5', 3, '0') // '500'
|
|
441
|
-
core.string.constantTimeEqual(a, b) //
|
|
449
|
+
core.string.constantTimeEqual(a, b) // Node 使用原生等长比较;浏览器不保证恒时
|
|
442
450
|
|
|
443
451
|
// 数组操作
|
|
444
452
|
core.array.unique([1, 1, 2]) // [1, 2]
|
|
@@ -474,6 +482,8 @@ core.time.startOfDay(date)
|
|
|
474
482
|
core.time.endOfDay(date)
|
|
475
483
|
```
|
|
476
484
|
|
|
485
|
+
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.
|
|
486
|
+
|
|
477
487
|
#### Module utilities (module)
|
|
478
488
|
|
|
479
489
|
Shared initialization error handling for modules:
|
|
@@ -555,12 +565,49 @@ HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
|
555
565
|
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
556
566
|
```
|
|
557
567
|
|
|
568
|
+
### Request locale isolation
|
|
569
|
+
|
|
570
|
+
Wrap Node requests and SSR in `core.i18n.runWithLocale(locale, () => handleRequest())`. SvelteKit can use `kit.i18n.runWithLocale(locale, () => resolve(event))`; serv creates the scope automatically from request headers. Async descendants inherit it. Explicit `MessageOptions.locale` takes precedence over the request locale and the startup default. Within a scope, `setGlobalLocale` only changes that request; outside a scope it configures the startup default and must not be called by request hooks.
|
|
571
|
+
|
|
572
|
+
Browsers keep application-level `setGlobalLocale` / `createLocaleStore`. During SSR, UI store initialization and setters only update the store; messages read the request scope. `runWithLocale` requires the Node entry; browser translations use explicit locale options or application state.
|
|
573
|
+
|
|
574
|
+
## Configuration consumption
|
|
575
|
+
|
|
576
|
+
`_core.yml` accepts only name/version/env/defaultLocale metadata and logging; unknown fields fail loading. Core has no global debug, ID length/prefix, business features or locale registration list. Use logging.level, per-call core.id arguments, and application-owned app.yml with its own schema. defaultLocale is metadata, not automatic translation registration or a Paraglide switch. Importing Logger preserves LANG, LC_ALL and PYTHONIOENCODING, including Windows TTY. See the [configuration-to-consumer map (Chinese)](../../.agents/skills/hai-module-create/configuration.md).
|
|
577
|
+
|
|
558
578
|
## Development and testing
|
|
559
579
|
|
|
560
580
|
```bash
|
|
561
581
|
pnpm --filter @h-ai/core test
|
|
562
582
|
```
|
|
563
583
|
|
|
584
|
+
See the [module JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) for declaration comments and compilable examples. Node core.init is synchronous and returns void. A failed per-file config.load result is logged and loading continues; validate required configuration and check success. Directory scanning and other exceptions may still throw. Business HaiResult APIs require a success check.
|
|
585
|
+
|
|
586
|
+
## Request correlation and log observation
|
|
587
|
+
|
|
588
|
+
Collection persistence and upload paths can use `core.context.run({ suppressLogObservers: true }, operation)` to prevent observing their own asynchronous logs. Normal output such as stdout remains enabled; observation resumes outside the scope.
|
|
589
|
+
|
|
590
|
+
`core.logger.observe` receives records permitted by the current log level after default and custom field redaction, and returns an unsubscribe function. Subscribers should collect into bounded memory and persist asynchronously; observation does not imply persistence. Observer failures and synchronous reentry are isolated. Free text and images still require capture-specific privacy rules.
|
|
591
|
+
|
|
592
|
+
On Node, `core.context.run` uses AsyncLocalStorage to isolate concurrent requests and child tasks. Existing module loggers inherit requestId/userId/scopeId/jobId. Only trusted server code supplies identities; correlation IDs do not authorize access. The browser exposes the same API but run only establishes a synchronous scope, without isolation across await.
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
import type { LogRecord } from '@h-ai/core'
|
|
596
|
+
import { core } from '@h-ai/core'
|
|
597
|
+
|
|
598
|
+
const records: LogRecord[] = []
|
|
599
|
+
const unsubscribe = core.logger.observe((record) => {
|
|
600
|
+
if (records.length === 100)
|
|
601
|
+
records.shift()
|
|
602
|
+
records.push(record)
|
|
603
|
+
})
|
|
604
|
+
await core.context.run({ requestId: 'request-1', scopeId: 'tenant-1', userId: 'user-1' }, async () => {
|
|
605
|
+
await Promise.resolve()
|
|
606
|
+
core.logger.warn('Business operation failed')
|
|
607
|
+
})
|
|
608
|
+
unsubscribe()
|
|
609
|
+
```
|
|
610
|
+
|
|
564
611
|
## License
|
|
565
612
|
|
|
566
613
|
Apache-2.0
|
package/README.ja.md
CHANGED
|
@@ -158,7 +158,9 @@ const level = core.logger.getLevel() // 'debug'
|
|
|
158
158
|
|
|
159
159
|
ブラウザーでは引き続き DevTools console に出力します。`pretty` は読みやすいメッセージと context、`json` は `level` / `msg` を含む JSON 文字列です。既定の認証情報マスキングと設定したパスのマスキングを併用し、ネストしたドット・括弧パスと `*` に対応します。メッセージ本文の機密情報は呼び出し側で除去してください。
|
|
160
160
|
|
|
161
|
-
`
|
|
161
|
+
ログ文脈のネイティブ `Error` は、`name`、`message`、`stack`、`cause`、`AggregateError.errors` と列挙可能な自身の文脈を持つコピーに変換してから、既定のフィールド/URL マスクと設定済み `redact` を適用します。両環境で同じ規則を使い、元の例外は変更しません。例外サブグラフの循環は `[Circular]`、32 階層の上限は `[MaxDepth]` になり、この範囲の独自置換値にも同じ正規化を適用します。通常オブジェクトの置換動作は変えません。Error を含まない通常オブジェクトの循環は元の構造を保持し、直接 JSON 化できる保証はありません。`name/message/stack` は自由文で、含まれる秘密を自動解析しません。記録前に除去するか、`error.message` / `error.stack` などのパス全体を隠してください。
|
|
162
|
+
|
|
163
|
+
`core.logger` は既定でログの機密情報をマスクします。`password`、`token`、`secret`、`privateKey`、`apiV3Key` などは `[REDACTED]` に置換されます。`url` / `baseUrl` / `endpoint` は URL 内のユーザー名・パスワード、機密クエリ値、OAuth 形式の fragment をマスクします。不正な URL は固定値を返します。メールアドレス・電話番号は既定では隠しません。
|
|
162
164
|
|
|
163
165
|
##### ログレベルの規約
|
|
164
166
|
|
|
@@ -173,6 +175,8 @@ const level = core.logger.getLevel() // 'debug'
|
|
|
173
175
|
|
|
174
176
|
#### 国際化(i18n)
|
|
175
177
|
|
|
178
|
+
組み込みの言語は `zh-CN`、`en-US`、`ja-JP` です。略記 `ja` は `ja-JP` に正規化され、すべてのモジュールで設定を共有します。
|
|
179
|
+
|
|
176
180
|
```typescript
|
|
177
181
|
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
178
182
|
core.i18n.setGlobalLocale('en-US')
|
|
@@ -191,13 +195,13 @@ getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
|
191
195
|
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
192
196
|
|
|
193
197
|
// 常量
|
|
194
|
-
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }]
|
|
198
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]
|
|
195
199
|
core.i18n.DEFAULT_LOCALE // 'zh-CN'
|
|
196
200
|
```
|
|
197
201
|
|
|
198
202
|
#### 機密情報のマスク
|
|
199
203
|
|
|
200
|
-
`core.sanitize.sanitizeSensitiveFields()` はオブジェクト・配列内の機密情報を再帰的にマスクします。既定では password、token、secret、privateKey、apiV3Key などを処理し、`url` / `baseUrl` / `endpoint`
|
|
204
|
+
`core.sanitize.sanitizeSensitiveFields()` はオブジェクト・配列内の機密情報を再帰的にマスクします。既定では password、token、secret、privateKey、apiV3Key などを処理し、`url` / `baseUrl` / `endpoint` の認証情報もマスクします。`core.sanitize.sanitizeUrl(url, options)` は単独の URL にも使用でき、不正な URL は `(invalid url)` を返します。独自の matcher はクエリ名にも適用されます。
|
|
201
205
|
|
|
202
206
|
```typescript
|
|
203
207
|
core.sanitize.sanitizeSensitiveFields({
|
|
@@ -280,13 +284,15 @@ if (!result.success) {
|
|
|
280
284
|
|
|
281
285
|
> ブラウザーにはファイルシステムがないため、`core.config` は代替 API の形状のみを提供します。ファイル関連メソッドは `SERVICE_UNAVAILABLE` を返し、YAML を読み込みません。
|
|
282
286
|
|
|
287
|
+
Node は設定ディレクトリー内のすべての `.yml` / `.yaml` を自動読み込み・監視します。CLI はプロジェクトルートの `hai.json` から `build` / `deploy` を読み取り、特別扱いする YAML ファイル名はありません。単一ファイルの失敗結果はログに記録して続行するため、アプリは必須設定を validate して `success` を確認してください。ディレクトリー走査や Logger 設定などの例外は伝播する場合があります。
|
|
288
|
+
|
|
283
289
|
```typescript
|
|
284
290
|
import { core, HaiConfigError } from '@h-ai/core'
|
|
285
291
|
import { z } from 'zod'
|
|
286
292
|
|
|
287
|
-
// 方式 1:init
|
|
293
|
+
// 方式 1:init 时自动加载运行时配置
|
|
288
294
|
core.init({
|
|
289
|
-
configDir: './config', //
|
|
295
|
+
configDir: './config', // 扫描配置目录内所有 .yml / .yaml
|
|
290
296
|
watchConfig: true, // 监听变更并自动重载
|
|
291
297
|
})
|
|
292
298
|
|
|
@@ -308,8 +314,8 @@ const validResult = core.config.validate('app', AppSchema)
|
|
|
308
314
|
|
|
309
315
|
// 查询和获取
|
|
310
316
|
if (core.config.has('app')) {
|
|
311
|
-
const cfg = core.config.get<typeof AppSchema
|
|
312
|
-
const cfg2 = core.config.getOrThrow<typeof AppSchema
|
|
317
|
+
const cfg = core.config.get<z.infer<typeof AppSchema>>('app') // undefined 或配置
|
|
318
|
+
const cfg2 = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // 未加载时抛异常
|
|
313
319
|
}
|
|
314
320
|
core.config.keys() // ['core', 'app', 'db', ...]
|
|
315
321
|
|
|
@@ -402,6 +408,8 @@ core.id.isValidUUID('f47ac10b-...') // true/false
|
|
|
402
408
|
core.id.isValidNanoId('abc123', 6) // true/false
|
|
403
409
|
```
|
|
404
410
|
|
|
411
|
+
UUID は暗号学的乱数の randomUUID/getRandomValues のみを使用し、どちらも利用できない場合は例外を送出します。Math.random にはフォールバックしません。
|
|
412
|
+
|
|
405
413
|
#### ツール関数
|
|
406
414
|
|
|
407
415
|
```typescript
|
|
@@ -438,7 +446,7 @@ core.string.isBlank('') // true
|
|
|
438
446
|
core.string.isNotBlank('x') // true
|
|
439
447
|
core.string.padStart('5', 3, '0') // '005'
|
|
440
448
|
core.string.padEnd('5', 3, '0') // '500'
|
|
441
|
-
core.string.constantTimeEqual(a, b) //
|
|
449
|
+
core.string.constantTimeEqual(a, b) // Node 使用原生等长比较;浏览器不保证恒时
|
|
442
450
|
|
|
443
451
|
// 数组操作
|
|
444
452
|
core.array.unique([1, 1, 2]) // [1, 2]
|
|
@@ -474,6 +482,8 @@ core.time.startOfDay(date)
|
|
|
474
482
|
core.time.endOfDay(date)
|
|
475
483
|
```
|
|
476
484
|
|
|
485
|
+
Node 入口は元の UTF-16 コード単位を UTF-16LE に変換し、同じバイト長を node:crypto.timingSafeEqual で比較します。長さが違えば false となり、長さ自体は隠しません。ブラウザー実装には JavaScript エンジンの定時間保証がなく、サーバーの認証境界には使用しません。周辺の分岐・変換・リクエスト処理まで定時間になるわけではありません。
|
|
486
|
+
|
|
477
487
|
#### モジュール共通ツール(module)
|
|
478
488
|
|
|
479
489
|
モジュール共通の初期化エラー処理:
|
|
@@ -555,12 +565,49 @@ HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
|
555
565
|
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
556
566
|
```
|
|
557
567
|
|
|
568
|
+
### リクエスト言語の分離
|
|
569
|
+
|
|
570
|
+
Node リクエストと SSR は `core.i18n.runWithLocale(locale, () => handleRequest())` で囲みます。SvelteKit は `kit.i18n.runWithLocale(locale, () => resolve(event))` を使えます。serv はリクエストヘッダーから自動設定します。非同期の子処理も言語を継承し、明示的な `MessageOptions.locale`、リクエスト言語、起動時の既定値の順で優先します。コンテキスト内の `setGlobalLocale` はそのリクエストだけを変更します。コンテキスト外の設定は起動時に限定し、リクエスト hook から呼びません。
|
|
571
|
+
|
|
572
|
+
ブラウザーはアプリ全体の `setGlobalLocale` / `createLocaleStore` を使います。SSR の UI store 初期化と setter は自身の状態のみ変更し、メッセージはリクエストコンテキストを参照します。`runWithLocale` は Node 入口専用です。
|
|
573
|
+
|
|
574
|
+
## 設定の消費範囲
|
|
575
|
+
|
|
576
|
+
`_core.yml` は name/version/env/defaultLocale のメタデータと logging のみを受け付け、不明なフィールドでは読み込みに失敗します。グローバルな debug、ID 長/接頭辞、業務 features、言語登録リストは提供しません。logging.level、core.id の呼び出し引数、独自 Schema で検証する app.yml を使用してください。defaultLocale はメタデータであり、翻訳登録や Paraglide の切り替えを自動実行しません。Logger の import は Windows TTY でも LANG、LC_ALL、PYTHONIOENCODING を変更しません。[設定と消費点の対応表(中国語)](../../.agents/skills/hai-module-create/configuration.md)を参照してください。
|
|
577
|
+
|
|
558
578
|
## 開発とテスト
|
|
559
579
|
|
|
560
580
|
```bash
|
|
561
581
|
pnpm --filter @h-ai/core test
|
|
562
582
|
```
|
|
563
583
|
|
|
584
|
+
宣言の注釈と型検査できる例は [JSDoc 規約(中国語)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) を参照してください。Node core.init は同期的に void を返します。単一ファイルの config.load 失敗結果は記録して続行するため、必須設定は validate して success を確認してください。ディレクトリー走査などの例外は伝播する場合があります。業務 HaiResult API は success を先に確認します。
|
|
585
|
+
|
|
586
|
+
## リクエスト関連付けとログの監視
|
|
587
|
+
|
|
588
|
+
収集側の保存・アップロード処理は `core.context.run({ suppressLogObservers: true }, operation)` で自身の非同期ログ監視を一時停止できます。stdout など通常の出力は維持し、スコープ外では監視を再開します。
|
|
589
|
+
|
|
590
|
+
`core.logger.observe` は現在のレベルと既定・追加フィールドのマスキングを通過したレコードを受け取り、購読解除関数を返します。購読側は上限付きメモリに収集して非同期で保存してください。監視は永続化の完了を意味しません。コールバックの失敗と同期再入は業務処理から隔離されます。自由文や画像には収集側の追加プライバシー処理が必要です。
|
|
591
|
+
|
|
592
|
+
Node の `core.context.run` は AsyncLocalStorage で並行リクエストと子タスクを隔離し、既存 logger に requestId/userId/scopeId/jobId を付加します。認証範囲は信頼できるサーバーからのみ設定し、関連 ID を認可に使用しません。ブラウザーは同じ API を公開しますが、run は同期スコープのみで await をまたぐ隔離を提供しません。
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
import type { LogRecord } from '@h-ai/core'
|
|
596
|
+
import { core } from '@h-ai/core'
|
|
597
|
+
|
|
598
|
+
const records: LogRecord[] = []
|
|
599
|
+
const unsubscribe = core.logger.observe((record) => {
|
|
600
|
+
if (records.length === 100)
|
|
601
|
+
records.shift()
|
|
602
|
+
records.push(record)
|
|
603
|
+
})
|
|
604
|
+
await core.context.run({ requestId: 'request-1', scopeId: 'tenant-1', userId: 'user-1' }, async () => {
|
|
605
|
+
await Promise.resolve()
|
|
606
|
+
core.logger.warn('Business operation failed')
|
|
607
|
+
})
|
|
608
|
+
unsubscribe()
|
|
609
|
+
```
|
|
610
|
+
|
|
564
611
|
## ライセンス
|
|
565
612
|
|
|
566
613
|
Apache-2.0
|
package/README.md
CHANGED
|
@@ -158,7 +158,9 @@ const level = core.logger.getLevel() // 'debug'
|
|
|
158
158
|
|
|
159
159
|
浏览器仍输出到 DevTools console:`pretty` 使用可读消息与上下文,`json` 输出包含 `level` / `msg` 的 JSON 字符串。默认凭据脱敏与配置的字段路径脱敏同时生效,支持嵌套点/括号路径和 `*`;消息文本本身需调用方脱敏。
|
|
160
160
|
|
|
161
|
-
`
|
|
161
|
+
日志上下文中的原生 `Error` 会先转为包含 `name`、`message`、`stack`、`cause`、`AggregateError.errors` 与可枚举自有上下文的副本,再执行默认字段/URL 脱敏与配置的 `redact`;两端规则一致,原异常不变。异常子图循环返回 `[Circular]`,32 层上限返回 `[MaxDepth]`;其中的自定义替换值也经同一规范化处理,普通对象的替换语义不变。无 Error 的普通对象循环仍保留原结构,不保证可直接 JSON 序列化。`name/message/stack` 是自由文本,不自动解析其中的秘密;调用方应先清理,或用 `error.message` / `error.stack` 等路径整体遮蔽。
|
|
162
|
+
|
|
163
|
+
`core.logger` 默认会对日志上下文执行安全脱敏:`password`、`token`、`secret`、`privateKey`、`apiV3Key` 等凭证类字段会替换为 `[REDACTED]`,`url` / `baseUrl` / `endpoint` 这类字段会掩码 URL 中的用户名、密码和敏感查询参数(含 OAuth 键值 fragment);非法 URL 返回固定占位符;邮箱、手机号默认不会被遮盖。
|
|
162
164
|
|
|
163
165
|
##### 日志级别规范
|
|
164
166
|
|
|
@@ -173,6 +175,8 @@ const level = core.logger.getLevel() // 'debug'
|
|
|
173
175
|
|
|
174
176
|
#### 国际化(i18n)
|
|
175
177
|
|
|
178
|
+
内置语言为 `zh-CN`、`en-US`、`ja-JP`;`ja` 简写会规范化为 `ja-JP`,所有模块共用此语言设置。
|
|
179
|
+
|
|
176
180
|
```typescript
|
|
177
181
|
// 设置全局语言(所有 getMessage 函数自动响应)
|
|
178
182
|
core.i18n.setGlobalLocale('en-US')
|
|
@@ -191,13 +195,13 @@ getMessage('welcome', { params: { name: 'Alice' } }) // 'Welcome Alice'
|
|
|
191
195
|
getMessage('welcome', { locale: 'zh-CN', params: { name: 'Alice' } }) // '欢迎 Alice'
|
|
192
196
|
|
|
193
197
|
// 常量
|
|
194
|
-
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }]
|
|
198
|
+
core.i18n.DEFAULT_LOCALES // [{ code: 'zh-CN', label: '简体中文' }, { code: 'en-US', label: 'English' }, { code: 'ja-JP', label: '日本語' }]
|
|
195
199
|
core.i18n.DEFAULT_LOCALE // 'zh-CN'
|
|
196
200
|
```
|
|
197
201
|
|
|
198
202
|
#### 脱敏工具
|
|
199
203
|
|
|
200
|
-
`core.sanitize.sanitizeSensitiveFields()` 可递归脱敏对象/数组中的敏感字段;默认会处理密码、token、secret、privateKey、apiV3Key 等凭证类字段,并自动掩码 `url` / `baseUrl` / `endpoint`
|
|
204
|
+
`core.sanitize.sanitizeSensitiveFields()` 可递归脱敏对象/数组中的敏感字段;默认会处理密码、token、secret、privateKey、apiV3Key 等凭证类字段,并自动掩码 `url` / `baseUrl` / `endpoint` 字段中的内嵌凭证、敏感查询参数和 OAuth 键值 fragment。`core.sanitize.sanitizeUrl(url, options)` 可单独处理 URL,非法 URL 返回 `(invalid url)`;自定义 matcher 同样适用于参数名。
|
|
201
205
|
|
|
202
206
|
```typescript
|
|
203
207
|
core.sanitize.sanitizeSensitiveFields({
|
|
@@ -280,13 +284,15 @@ if (!result.success) {
|
|
|
280
284
|
|
|
281
285
|
> 浏览器端没有文件系统,`core.config` 仅提供占位 API:文件相关方法返回 `SERVICE_UNAVAILABLE`,不会加载 YAML。
|
|
282
286
|
|
|
287
|
+
Node 自动加载和监听配置目录内所有 `.yml` / `.yaml`,没有保留文件名例外。项目根 `hai.json` 中的 `build` / `deploy` 由 CLI 读取,不进入运行时 YAML 配置。单项加载返回失败时记录错误并继续,必需配置由应用调用 `core.config.validate` 并检查 `success`;目录扫描、日志设置等异常仍可能传播。
|
|
288
|
+
|
|
283
289
|
```typescript
|
|
284
290
|
import { core, HaiConfigError } from '@h-ai/core'
|
|
285
291
|
import { z } from 'zod'
|
|
286
292
|
|
|
287
|
-
// 方式 1:init
|
|
293
|
+
// 方式 1:init 时自动加载运行时配置
|
|
288
294
|
core.init({
|
|
289
|
-
configDir: './config', //
|
|
295
|
+
configDir: './config', // 扫描配置目录内所有 .yml / .yaml
|
|
290
296
|
watchConfig: true, // 监听变更并自动重载
|
|
291
297
|
})
|
|
292
298
|
|
|
@@ -308,8 +314,8 @@ const validResult = core.config.validate('app', AppSchema)
|
|
|
308
314
|
|
|
309
315
|
// 查询和获取
|
|
310
316
|
if (core.config.has('app')) {
|
|
311
|
-
const cfg = core.config.get<typeof AppSchema
|
|
312
|
-
const cfg2 = core.config.getOrThrow<typeof AppSchema
|
|
317
|
+
const cfg = core.config.get<z.infer<typeof AppSchema>>('app') // undefined 或配置
|
|
318
|
+
const cfg2 = core.config.getOrThrow<z.infer<typeof AppSchema>>('app') // 未加载时抛异常
|
|
313
319
|
}
|
|
314
320
|
core.config.keys() // ['core', 'app', 'db', ...]
|
|
315
321
|
|
|
@@ -402,6 +408,8 @@ core.id.isValidUUID('f47ac10b-...') // true/false
|
|
|
402
408
|
core.id.isValidNanoId('abc123', 6) // true/false
|
|
403
409
|
```
|
|
404
410
|
|
|
411
|
+
UUID 只使用密码学安全的 randomUUID/getRandomValues;均不可用时明确抛错,不生成 Math.random 标识。
|
|
412
|
+
|
|
405
413
|
#### 工具函数
|
|
406
414
|
|
|
407
415
|
```typescript
|
|
@@ -438,7 +446,7 @@ core.string.isBlank('') // true
|
|
|
438
446
|
core.string.isNotBlank('x') // true
|
|
439
447
|
core.string.padStart('5', 3, '0') // '005'
|
|
440
448
|
core.string.padEnd('5', 3, '0') // '500'
|
|
441
|
-
core.string.constantTimeEqual(a, b) //
|
|
449
|
+
core.string.constantTimeEqual(a, b) // Node 使用原生等长比较;浏览器不保证恒时
|
|
442
450
|
|
|
443
451
|
// 数组操作
|
|
444
452
|
core.array.unique([1, 1, 2]) // [1, 2]
|
|
@@ -474,6 +482,8 @@ core.time.startOfDay(date)
|
|
|
474
482
|
core.time.endOfDay(date)
|
|
475
483
|
```
|
|
476
484
|
|
|
485
|
+
Node 入口的 core.string.constantTimeEqual 将原始 UTF-16 单元编码后使用 node:crypto.timingSafeEqual 比较等长字节,长度不相同时直接 false,长度本身不受保护。浏览器实现仅避免按内容提前返回,不提供 JavaScript 引擎级恒时保证,不用于服务端凭证校验。外围分支、编码和请求处理不因此自动成为恒时。
|
|
486
|
+
|
|
477
487
|
#### 模块基础工具(module)
|
|
478
488
|
|
|
479
489
|
提供各模块共用的初始化错误处理:
|
|
@@ -555,12 +565,49 @@ HaiConfigError.CONFIG_ENV_VAR_MISSING // hai:core:013 - 环境变量缺失
|
|
|
555
565
|
HaiConfigError.CONFIG_NOT_LOADED // hai:core:014 - 配置未加载
|
|
556
566
|
```
|
|
557
567
|
|
|
568
|
+
### 请求语言隔离
|
|
569
|
+
|
|
570
|
+
Node 请求与 SSR 使用 `core.i18n.runWithLocale(locale, () => handleRequest())`;SvelteKit 可使用 `kit.i18n.runWithLocale(locale, () => resolve(event))`,serv 自动按请求头建立上下文。异步子任务继承请求语言,优先级为显式 `MessageOptions.locale`、请求语言、进程启动默认语言。`setGlobalLocale` 在请求上下文中只更新当前请求;上下文外仅用于启动默认值,不能在请求 hook 中调用它修改默认值。
|
|
571
|
+
|
|
572
|
+
浏览器继续使用应用级 `setGlobalLocale` / `createLocaleStore`。SSR 的 UI store 初始化和 setter 只修改自身状态,不写全局语言;UI 消息从请求上下文读取。`runWithLocale` 仅支持 Node 入口,浏览器通过显式 locale 或应用状态翻译。
|
|
573
|
+
|
|
574
|
+
## 配置的实际消费边界
|
|
575
|
+
|
|
576
|
+
`_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)。
|
|
577
|
+
|
|
558
578
|
## 开发测试
|
|
559
579
|
|
|
560
580
|
```bash
|
|
561
581
|
pnpm --filter @h-ai/core test
|
|
562
582
|
```
|
|
563
583
|
|
|
584
|
+
公开声明和内部结构的注释及可编译示例见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。Node core.init 同步返回 void;单项 config.load 返回失败时记录错误并继续,必需配置须 validate 并检查 success。目录扫描等异常仍可能抛出;模块业务 HaiResult 则先检查 success。
|
|
585
|
+
|
|
586
|
+
## 请求关联与日志观察
|
|
587
|
+
|
|
588
|
+
采集器内部的持久化/上传路径可使用 `core.context.run({ suppressLogObservers: true }, operation)` 暂停观察回调,避免异步采集自身日志;stdout 等常规日志输出保持生效,退出作用域后恢复。
|
|
589
|
+
|
|
590
|
+
`core.logger.observe` 观察当前级别允许且已执行默认/自定义字段脱敏的记录,返回取消函数。订阅者应只做有界内存收集,再异步持久化;日志观察本身不代表已经保存。回调异常和同步重入不影响业务。自由文本与图像仍需采集侧的专门隐私策略。
|
|
591
|
+
|
|
592
|
+
`core.context.run` 在 Node 中用 AsyncLocalStorage 隔离并发请求和子任务,现有模块 logger 自动附带当前 requestId/userId/scopeId/jobId。身份范围只来自可信服务端代码;关联 ID 不是授权。浏览器保持同一 API,但 run 只建立同步作用域,不能用于跨 await 的身份隔离。
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
import type { LogRecord } from '@h-ai/core'
|
|
596
|
+
import { core } from '@h-ai/core'
|
|
597
|
+
|
|
598
|
+
const records: LogRecord[] = []
|
|
599
|
+
const unsubscribe = core.logger.observe((record) => {
|
|
600
|
+
if (records.length === 100)
|
|
601
|
+
records.shift()
|
|
602
|
+
records.push(record)
|
|
603
|
+
})
|
|
604
|
+
await core.context.run({ requestId: 'request-1', scopeId: 'tenant-1', userId: 'user-1' }, async () => {
|
|
605
|
+
await Promise.resolve()
|
|
606
|
+
core.logger.warn('Business operation failed')
|
|
607
|
+
})
|
|
608
|
+
unsubscribe()
|
|
609
|
+
```
|
|
610
|
+
|
|
564
611
|
## 许可证
|
|
565
612
|
|
|
566
613
|
Apache-2.0
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { H as HaiResult, W as WatchCallback, C as CoreOptions, a as CoreLogger, I as I18nFn, z as zodValidation, b as IdFn, T as TypeUtilFn, O as ObjectFn, S as SanitizeFn, c as StringFn, A as ArrayFn, d as AsyncFn, e as TimeFn, E as ErrorFn, M as ModuleFn } from './core-function-config.node-
|
|
2
|
-
export { f as CoreConfig, g as CoreConfigSchema, h as CoreFunctions, i as Env, j as EnvSchema, k as ErrorInfo, l as ErrorInfoValue, m as HaiCommonError, n as HaiConfigError, o as HaiError, p as HaiErrorDef, q as
|
|
1
|
+
import { H as HaiResult, W as WatchCallback, C as CoreOptions, R as RequestContext, a as CoreLogger, I as I18nFn, z as zodValidation, b as IdFn, T as TypeUtilFn, O as ObjectFn, S as SanitizeFn, c as StringFn, A as ArrayFn, d as AsyncFn, e as TimeFn, E as ErrorFn, M as ModuleFn } from './core-function-config.node-CGXWsohk.js';
|
|
2
|
+
export { f as CoreConfig, g as CoreConfigSchema, h as CoreFunctions, i as Env, j as EnvSchema, k as ErrorInfo, l as ErrorInfoValue, m as HaiCommonError, n as HaiConfigError, o as HaiError, p as HaiErrorDef, q as InterpolationParams, L as Locale, r as LocaleInfo, s as LocaleMessages, t as LogContext, u as LogFormat, v as LogFormatSchema, w as LogLevel, x as LogLevelSchema, y as LogObserver, B as LogRecord, D as Logger, F as LoggerFunctions, G as LoggerOptions, J as LoggingConfig, K as LoggingConfigSchema, N as MatchHandlers, P as MessageDictionary, Q as MessageOptions, U as PaginatedResult, V as PaginationOptions, X as PaginationOptionsInput, Y as ValidationFormError, Z as ZodMessageGetter, _ as ZodValidationFn, $ as ZodValidationMessageKey, a0 as err, a1 as ok } from './core-function-config.node-CGXWsohk.js';
|
|
3
3
|
import * as zod from 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Core 服务对象 - 聚合常用功能(浏览器)。
|
|
7
|
-
*
|
|
8
7
|
* @example
|
|
9
8
|
* ```ts
|
|
10
9
|
* import { core } from '@h-ai/core'
|
|
11
|
-
* core.
|
|
10
|
+
* core.init({ logging: { level: 'info' } })
|
|
11
|
+
* core.logger.info('Browser ready')
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
14
|
declare const core: {
|
|
@@ -32,6 +32,10 @@ declare const core: {
|
|
|
32
32
|
};
|
|
33
33
|
/** 初始化 Core */
|
|
34
34
|
init: typeof initCore;
|
|
35
|
+
context: {
|
|
36
|
+
readonly get: () => Readonly<RequestContext>;
|
|
37
|
+
readonly run: <T>(context: RequestContext, operation: () => T) => T;
|
|
38
|
+
};
|
|
35
39
|
logger: CoreLogger;
|
|
36
40
|
i18n: I18nFn;
|
|
37
41
|
zodValidation: typeof zodValidation;
|
|
@@ -60,4 +64,4 @@ declare const core: {
|
|
|
60
64
|
*/
|
|
61
65
|
declare function initCore(options?: CoreOptions): void;
|
|
62
66
|
|
|
63
|
-
export { CoreLogger, CoreOptions, HaiResult, core };
|
|
67
|
+
export { CoreLogger, CoreOptions, HaiResult, RequestContext, core };
|
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { __commonJS, __toESM, createCore, i18n, err, HaiCommonError, sanitize } from './chunk-
|
|
2
|
-
export { CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError,
|
|
1
|
+
import { __commonJS, __toESM, createCore, i18n, err, HaiCommonError, requestContext, emitLogRecord, sanitize } from './chunk-MUQFCAQ5.js';
|
|
2
|
+
export { CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, err, ok } from './chunk-MUQFCAQ5.js';
|
|
3
3
|
|
|
4
4
|
// ../../node_modules/loglevel/lib/loglevel.js
|
|
5
5
|
var require_loglevel = __commonJS({
|
|
@@ -373,25 +373,37 @@ function formatMessage(message, context, format = globalFormat, level = "info")
|
|
|
373
373
|
return `${message} ${JSON.stringify(sanitizedContext)}`;
|
|
374
374
|
}
|
|
375
375
|
function wrapLoglevel(resolveLogger, resolveContext, resolveFormat) {
|
|
376
|
+
const write = (level, message, ctx) => {
|
|
377
|
+
const output = resolveLogger();
|
|
378
|
+
const priorities = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 4 };
|
|
379
|
+
if (priorities[level] < output.getLevel())
|
|
380
|
+
return;
|
|
381
|
+
const context = sanitizeLogContext({ ...resolveContext(), ...ctx, ...requestContext.get() }) ?? {};
|
|
382
|
+
const text = formatMessage(message, context, resolveFormat(), level);
|
|
383
|
+
if (level === "fatal")
|
|
384
|
+
output.error(resolveFormat() === "json" ? text : `[FATAL] ${text}`);
|
|
385
|
+
else output[level](text);
|
|
386
|
+
if (!requestContext.get().suppressLogObservers)
|
|
387
|
+
emitLogRecord({ timestamp: Date.now(), level, message, context });
|
|
388
|
+
};
|
|
376
389
|
return {
|
|
377
390
|
trace(message, ctx) {
|
|
378
|
-
|
|
391
|
+
write("trace", message, ctx);
|
|
379
392
|
},
|
|
380
393
|
debug(message, ctx) {
|
|
381
|
-
|
|
394
|
+
write("debug", message, ctx);
|
|
382
395
|
},
|
|
383
396
|
info(message, ctx) {
|
|
384
|
-
|
|
397
|
+
write("info", message, ctx);
|
|
385
398
|
},
|
|
386
399
|
warn(message, ctx) {
|
|
387
|
-
|
|
400
|
+
write("warn", message, ctx);
|
|
388
401
|
},
|
|
389
402
|
error(message, ctx) {
|
|
390
|
-
|
|
403
|
+
write("error", message, ctx);
|
|
391
404
|
},
|
|
392
405
|
fatal(message, ctx) {
|
|
393
|
-
|
|
394
|
-
resolveLogger().error(resolveFormat() === "json" ? text : `[FATAL] ${text}`);
|
|
406
|
+
write("fatal", message, ctx);
|
|
395
407
|
},
|
|
396
408
|
child(childContext) {
|
|
397
409
|
return wrapLoglevel(resolveLogger, () => ({ ...resolveContext(), ...childContext }), resolveFormat);
|