@djvlc/openapi-client-core 1.2.0 → 1.2.2

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
@@ -2,41 +2,523 @@
2
2
 
3
3
  OpenAPI Client 公共运行时库,为 `@djvlc/openapi-*-client` 系列包提供核心功能。
4
4
 
5
- ## 功能
6
-
7
- - **结构化错误类**:`DjvApiError`、`NetworkError`、`TimeoutError`、`AbortError`
8
- - **增强版 fetch**:超时、重试(支持 Retry-After)、取消支持
9
- - **Middleware 系统**:请求/响应拦截
10
- - **拦截器系统**:更灵活的请求/响应/错误处理
11
- - **请求去重**:避免相同 GET 请求重复发送
12
- - **指标收集**:请求级别的性能指标和统计
13
- - **日志系统**:可配置的请求日志
14
- - **客户端工厂**:简化客户端创建
15
- - **自动版本注入**:构建时自动从 package.json 注入版本号
5
+ ## 特性
16
6
 
17
- ## 安装
7
+ - 🔌 **模块化架构**:按功能拆分,每个模块职责单一
8
+ - 🔗 **拦截器系统**:请求/响应/错误拦截器,支持顺序控制
9
+ - 🔐 **多种认证**:Bearer/API Key/Basic/自定义认证
10
+ - 🔄 **智能重试**:支持 Retry-After、指数退避、自定义策略
11
+ - 🎯 **请求去重**:避免相同请求并发执行
12
+ - 📊 **指标收集**:请求级别的性能指标和统计
13
+ - 📝 **日志系统**:可配置的请求日志
14
+ - 🛡️ **类型安全**:完整的 TypeScript 类型定义
18
15
 
19
- 通常你不需要直接安装这个包,它会作为 client 包的依赖自动安装。
16
+ ## 安装
20
17
 
21
18
  ```bash
22
19
  pnpm add @djvlc/openapi-client-core
23
20
  ```
24
21
 
22
+ ## 模块结构
23
+
24
+ ```
25
+ src/
26
+ ├── types/ # 类型定义
27
+ ├── errors/ # 错误处理
28
+ ├── auth/ # 认证模块
29
+ ├── interceptors/ # 拦截器
30
+ │ ├── request/ # 请求拦截器
31
+ │ ├── response/ # 响应拦截器
32
+ │ └── error/ # 错误拦截器
33
+ ├── plugins/ # 插件
34
+ ├── clients/ # HTTP 客户端
35
+ ├── utils/ # 工具函数
36
+ └── index.ts # 主入口
37
+ ```
38
+
39
+ ## 快速开始
40
+
41
+ ### 使用 Fetch 客户端
42
+
43
+ ```typescript
44
+ import { createFetchClient } from '@djvlc/openapi-client-core';
45
+
46
+ const client = createFetchClient({
47
+ baseUrl: 'https://api.example.com',
48
+ timeout: 30000,
49
+ auth: {
50
+ type: 'bearer',
51
+ getToken: () => localStorage.getItem('token'),
52
+ },
53
+ });
54
+
55
+ // 发起请求
56
+ const data = await client.get<User>('/users/me');
57
+ ```
58
+
59
+ ### 使用 Axios 客户端(推荐用于 openapi-generator)
60
+
61
+ ```typescript
62
+ import { createAxiosInstance } from '@djvlc/openapi-client-core';
63
+ import { PagesApi, Configuration } from './generated';
64
+
65
+ const axiosInstance = createAxiosInstance({
66
+ baseUrl: '/api/admin',
67
+ auth: {
68
+ type: 'bearer',
69
+ getToken: () => localStorage.getItem('token'),
70
+ },
71
+ retry: {
72
+ maxRetries: 2,
73
+ initialDelayMs: 1000,
74
+ maxDelayMs: 10000,
75
+ backoff: 'exponential',
76
+ },
77
+ });
78
+
79
+ const config = new Configuration();
80
+ const pagesApi = new PagesApi(config, undefined, axiosInstance);
81
+ ```
82
+
83
+ ## HTTP 客户端详解
84
+
85
+ 本库提供两种 HTTP 客户端实现,共享相同的配置接口,功能完全对等:
86
+
87
+ ### 客户端对比
88
+
89
+ | 特性 | FetchClient | Axios Client |
90
+ |------|-------------|--------------|
91
+ | **依赖** | 无(原生 fetch) | 需要 axios |
92
+ | **使用场景** | 独立使用、轻量场景 | OpenAPI 生成代码集成 |
93
+ | **Bundle 大小** | 更小 | 需包含 axios |
94
+ | **请求拦截器** | ✅ 支持 | ✅ 支持 |
95
+ | **响应拦截器** | ✅ 支持 | ✅ 支持 |
96
+ | **错误拦截器** | ✅ 支持 | ✅ 支持 |
97
+ | **认证** | ✅ 全类型 | ✅ 全类型 |
98
+ | **自动重试** | ✅ 支持 | ✅ 支持 |
99
+ | **超时控制** | ✅ AbortController | ✅ 原生 timeout |
100
+ | **请求取消** | ✅ AbortSignal | ✅ CancelToken |
101
+ | **调试日志** | ✅ 支持 | ✅ 支持 |
102
+ | **Retry-After** | ✅ 尊重 | ✅ 尊重 |
103
+
104
+ ### 如何选择?
105
+
106
+ ```
107
+ ┌─────────────────────────────────────────────────────────────┐
108
+ │ 你的使用场景是什么? │
109
+ └─────────────────────────────────────────────────────────────┘
110
+
111
+ ┌───────────────┴───────────────┐
112
+ ▼ ▼
113
+ 使用 OpenAPI 生成的代码? 独立发起 HTTP 请求?
114
+ │ │
115
+ ▼ ▼
116
+ ┌─────────────────┐ ┌─────────────────┐
117
+ │ Axios Client │ │ FetchClient │
118
+ │ (推荐) │ │ (推荐) │
119
+ └─────────────────┘ └─────────────────┘
120
+ ```
121
+
122
+ ### createEnhancedFetch(推荐用于 OpenAPI 生成代码)
123
+
124
+ `createEnhancedFetch` 创建一个增强的 fetch 函数,专为与 `typescript-fetch` 生成的 API 类集成设计:
125
+
126
+ ```typescript
127
+ import { createEnhancedFetch, createConsoleLogger } from '@djvlc/openapi-client-core';
128
+ import { PagesApi, Configuration } from '@djvlc/openapi-admin-client';
129
+
130
+ // 创建增强的 fetch 函数
131
+ const enhancedFetch = createEnhancedFetch({
132
+ baseUrl: '/api/admin',
133
+ timeout: 30000,
134
+
135
+ // 认证
136
+ auth: {
137
+ type: 'bearer',
138
+ getToken: () => localStorage.getItem('token') ?? '',
139
+ },
140
+
141
+ // 重试
142
+ retry: {
143
+ maxRetries: 3,
144
+ backoffStrategy: 'exponential',
145
+ },
146
+
147
+ // 日志
148
+ logger: createConsoleLogger({ prefix: '[API]' }),
149
+ debug: true,
150
+ });
151
+
152
+ // 与生成的 API 类集成
153
+ const config = new Configuration({ basePath: '/api/admin' });
154
+ const pagesApi = new PagesApi(config, '/api/admin', enhancedFetch);
155
+
156
+ // 使用
157
+ const pages = await pagesApi.listPages();
158
+ ```
159
+
160
+ **增强功能包括:**
161
+ - ✅ 自动认证(Bearer/API Key/Basic/自定义)
162
+ - ✅ 智能重试(指数退避 + Retry-After)
163
+ - ✅ 超时控制(AbortController)
164
+ - ✅ 请求追踪(X-Request-ID, X-Trace-ID)
165
+ - ✅ 错误转换(ApiError, NetworkError, TimeoutError)
166
+ - ✅ 调试日志
167
+
168
+ ### FetchClient 完整配置
169
+
170
+ `FetchClient` 基于原生 fetch API,适合独立使用(不依赖 OpenAPI 生成代码):
171
+
172
+ ```typescript
173
+ import {
174
+ createFetchClient,
175
+ createConsoleLogger,
176
+ type FetchClientConfig,
177
+ type RequestInterceptor,
178
+ type ResponseInterceptor,
179
+ type ErrorInterceptor,
180
+ } from '@djvlc/openapi-client-core';
181
+
182
+ const config: FetchClientConfig = {
183
+ // ============ 基础配置 ============
184
+
185
+ /** 必填:API 基础 URL */
186
+ baseUrl: 'https://api.example.com',
187
+
188
+ /** 请求超时时间(毫秒),默认 30000 */
189
+ timeout: 30000,
190
+
191
+ /** 默认请求头 */
192
+ headers: {
193
+ 'X-Custom-Header': 'value',
194
+ },
195
+
196
+ // ============ 认证配置 ============
197
+
198
+ /** 认证配置 */
199
+ auth: {
200
+ type: 'bearer',
201
+ getToken: async () => await tokenService.getAccessToken(),
202
+ // 其他选项见 "认证" 章节
203
+ },
204
+
205
+ // ============ 重试配置 ============
206
+
207
+ /** 是否启用自动重试,默认 true */
208
+ enableRetry: true,
209
+
210
+ /** 重试策略 */
211
+ retry: {
212
+ maxRetries: 3,
213
+ initialDelayMs: 1000,
214
+ maxDelayMs: 30000,
215
+ backoffStrategy: 'exponential',
216
+ retryableStatusCodes: [429, 500, 502, 503, 504],
217
+ retryOnNetworkError: true,
218
+ retryOnTimeout: true,
219
+ respectRetryAfter: true,
220
+ jitterFactor: 0.1,
221
+ onRetry: (info) => {
222
+ console.log(`重试 ${info.attempt}/${info.maxRetries}`);
223
+ },
224
+ },
225
+
226
+ // ============ 拦截器 ============
227
+
228
+ /** 请求拦截器列表 */
229
+ requestInterceptors: [
230
+ {
231
+ name: 'custom-header',
232
+ order: 0,
233
+ intercept(context) {
234
+ context.options.headers['X-Request-Time'] = Date.now().toString();
235
+ return context.options;
236
+ },
237
+ },
238
+ ],
239
+
240
+ /** 响应拦截器列表 */
241
+ responseInterceptors: [
242
+ {
243
+ name: 'response-logger',
244
+ order: 0,
245
+ intercept(response, context) {
246
+ console.log(`${context.options.method} ${context.options.path} -> ${response.status}`);
247
+ return response;
248
+ },
249
+ },
250
+ ],
251
+
252
+ /** 错误拦截器列表 */
253
+ errorInterceptors: [
254
+ {
255
+ name: 'error-logger',
256
+ order: 0,
257
+ intercept(error, context) {
258
+ console.error(`请求失败: ${error.message}`);
259
+ return undefined; // 不处理,继续传播
260
+ },
261
+ },
262
+ ],
263
+
264
+ // ============ 调试配置 ============
265
+
266
+ /** 日志器 */
267
+ logger: createConsoleLogger({ prefix: '[API]', level: 'debug' }),
268
+
269
+ /** 是否启用调试日志,默认 false */
270
+ debug: true,
271
+ };
272
+
273
+ const client = createFetchClient(config);
274
+
275
+ // 使用客户端
276
+ const user = await client.get<User>('/users/me');
277
+ const created = await client.post<User>('/users', { name: 'Alice' });
278
+ const updated = await client.put<User>('/users/1', { name: 'Bob' });
279
+ const patched = await client.patch<User>('/users/1', { name: 'Charlie' });
280
+ await client.delete('/users/1');
281
+ ```
282
+
283
+ ### Axios Client 完整配置
284
+
285
+ `createAxiosInstance` 返回一个配置好的 Axios 实例,适合与 OpenAPI 生成代码集成:
286
+
287
+ ```typescript
288
+ import {
289
+ createAxiosInstance,
290
+ createConsoleLogger,
291
+ type AxiosClientConfig,
292
+ } from '@djvlc/openapi-client-core';
293
+ import { PagesApi, ComponentsApi, Configuration } from '@djvlc/openapi-admin-client';
294
+
295
+ const config: AxiosClientConfig = {
296
+ // ============ 基础配置 ============
297
+
298
+ /** 必填:API 基础 URL */
299
+ baseUrl: '/api/admin',
300
+
301
+ /** 请求超时时间(毫秒),默认 30000 */
302
+ timeout: 30000,
303
+
304
+ /** 默认请求头 */
305
+ headers: {
306
+ 'X-Client-Version': '1.0.0',
307
+ },
308
+
309
+ // ============ 认证配置 ============
310
+
311
+ auth: {
312
+ type: 'bearer',
313
+ getToken: () => localStorage.getItem('accessToken') ?? '',
314
+ },
315
+
316
+ // ============ 重试配置 ============
317
+
318
+ enableRetry: true,
319
+
320
+ retry: {
321
+ maxRetries: 3,
322
+ initialDelayMs: 1000,
323
+ maxDelayMs: 30000,
324
+ backoffStrategy: 'exponential',
325
+ retryableStatusCodes: [429, 500, 502, 503, 504],
326
+ onRetry: (info) => {
327
+ console.log(`[Retry] ${info.method} ${info.url} - attempt ${info.attempt}`);
328
+ },
329
+ },
330
+
331
+ // ============ 拦截器(与 FetchClient 相同) ============
332
+
333
+ requestInterceptors: [...],
334
+ responseInterceptors: [...],
335
+ errorInterceptors: [...],
336
+
337
+ // ============ 调试配置 ============
338
+
339
+ logger: createConsoleLogger({ prefix: '[Admin API]' }),
340
+ debug: process.env.NODE_ENV === 'development',
341
+ };
342
+
343
+ // 创建 Axios 实例
344
+ const axiosInstance = createAxiosInstance(config);
345
+
346
+ // 与 OpenAPI 生成代码集成
347
+ const apiConfig = new Configuration();
348
+ const pagesApi = new PagesApi(apiConfig, undefined, axiosInstance);
349
+ const componentsApi = new ComponentsApi(apiConfig, undefined, axiosInstance);
350
+
351
+ // 使用生成的 API 客户端
352
+ const pages = await pagesApi.listPages({ limit: 10 });
353
+ const page = await pagesApi.getPage({ id: 'page-123' });
354
+ ```
355
+
356
+ ### 配置选项速查表
357
+
358
+ | 配置项 | 类型 | 默认值 | 说明 |
359
+ |--------|------|--------|------|
360
+ | `baseUrl` | `string` | - | **必填**,API 基础 URL |
361
+ | `timeout` | `number` | `30000` | 请求超时(毫秒) |
362
+ | `headers` | `Record<string, string>` | `{}` | 默认请求头 |
363
+ | `auth` | `AuthConfig` | - | 认证配置 |
364
+ | `retry` | `RetryConfig` | - | 重试配置 |
365
+ | `enableRetry` | `boolean` | `true` | 是否启用重试 |
366
+ | `requestInterceptors` | `RequestInterceptor[]` | `[]` | 请求拦截器 |
367
+ | `responseInterceptors` | `ResponseInterceptor[]` | `[]` | 响应拦截器 |
368
+ | `errorInterceptors` | `ErrorInterceptor[]` | `[]` | 错误拦截器 |
369
+ | `logger` | `Logger` | - | 日志器实例 |
370
+ | `debug` | `boolean` | `false` | 是否启用调试日志 |
371
+
372
+ ### 完整示例:生产环境配置
373
+
374
+ ```typescript
375
+ import {
376
+ createAxiosInstance,
377
+ createConsoleLogger,
378
+ createMetricsCollector,
379
+ createMetricsInterceptors,
380
+ createLoggingInterceptor,
381
+ createReportingInterceptor,
382
+ ApiError,
383
+ } from '@djvlc/openapi-client-core';
384
+
385
+ // ============ 创建日志器 ============
386
+ const logger = createConsoleLogger({
387
+ prefix: '[DJV-API]',
388
+ level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
389
+ timestamp: true,
390
+ });
391
+
392
+ // ============ 创建指标收集器 ============
393
+ const metricsCollector = createMetricsCollector({
394
+ maxMetrics: 1000,
395
+ ttlMs: 300000, // 5 分钟
396
+ onMetrics: (m) => {
397
+ // 发送到监控系统
398
+ if (m.durationMs > 3000) {
399
+ logger.warn(`慢请求: ${m.method} ${m.path} - ${m.durationMs}ms`);
400
+ }
401
+ },
402
+ });
403
+
404
+ const metricsInterceptors = createMetricsInterceptors(metricsCollector);
405
+
406
+ // ============ 创建错误上报拦截器 ============
407
+ const errorReporter = createReportingInterceptor({
408
+ reporter: {
409
+ report: (error, context) => {
410
+ // 只上报服务端错误
411
+ if (ApiError.is(error) && error.isServerError()) {
412
+ Sentry.captureException(error, {
413
+ extra: {
414
+ requestId: context.requestId,
415
+ traceId: context.traceId,
416
+ url: context.url,
417
+ },
418
+ });
419
+ }
420
+ },
421
+ },
422
+ sampleRate: 1.0, // 生产环境 100% 采样
423
+ });
424
+
425
+ // ============ 创建客户端 ============
426
+ const axiosInstance = createAxiosInstance({
427
+ baseUrl: import.meta.env.VITE_API_BASE_URL,
428
+ timeout: 30000,
429
+
430
+ auth: {
431
+ type: 'bearer',
432
+ getToken: () => authStore.getAccessToken(),
433
+ },
434
+
435
+ retry: {
436
+ maxRetries: 3,
437
+ initialDelayMs: 1000,
438
+ maxDelayMs: 30000,
439
+ backoffStrategy: 'exponential',
440
+ jitterFactor: 0.2,
441
+ retryableStatusCodes: [429, 500, 502, 503, 504],
442
+ respectRetryAfter: true,
443
+ onRetry: (info) => {
444
+ logger.info(`重试请求: ${info.method} ${info.url} (${info.attempt}/${info.maxRetries})`);
445
+ },
446
+ },
447
+
448
+ requestInterceptors: [
449
+ metricsInterceptors.request,
450
+ {
451
+ name: 'app-version',
452
+ order: 100,
453
+ intercept(ctx) {
454
+ ctx.options.headers['X-App-Version'] = APP_VERSION;
455
+ ctx.options.headers['X-Platform'] = 'web';
456
+ return ctx.options;
457
+ },
458
+ },
459
+ ],
460
+
461
+ responseInterceptors: [
462
+ metricsInterceptors.response,
463
+ ],
464
+
465
+ errorInterceptors: [
466
+ metricsInterceptors.error,
467
+ errorReporter,
468
+ {
469
+ name: 'auth-redirect',
470
+ order: 1000,
471
+ intercept(error, context) {
472
+ if (ApiError.is(error) && error.isUnauthorized()) {
473
+ // Token 过期,跳转登录
474
+ authStore.logout();
475
+ router.push('/login');
476
+ }
477
+ return undefined;
478
+ },
479
+ },
480
+ ],
481
+
482
+ logger,
483
+ debug: import.meta.env.DEV,
484
+ });
485
+
486
+ // ============ 定期上报指标 ============
487
+ setInterval(() => {
488
+ const summary = metricsCollector.summary();
489
+ analytics.track('api_metrics', {
490
+ totalRequests: summary.totalRequests,
491
+ successRate: summary.successRate,
492
+ p50: summary.p50Ms,
493
+ p95: summary.p95Ms,
494
+ p99: summary.p99Ms,
495
+ });
496
+ }, 60000);
497
+
498
+ export { axiosInstance, metricsCollector };
499
+ ```
500
+
25
501
  ## 错误处理
26
502
 
27
503
  ```typescript
28
- import { DjvApiError, NetworkError, TimeoutError, AbortError } from '@djvlc/openapi-client-core';
504
+ import {
505
+ ApiError,
506
+ NetworkError,
507
+ TimeoutError,
508
+ AbortError,
509
+ isRetryableError,
510
+ } from '@djvlc/openapi-client-core';
29
511
 
30
512
  try {
31
- await client.PageApi.resolvePage({ pageUid: 'xxx' });
513
+ await client.get('/users/me');
32
514
  } catch (e) {
33
- if (DjvApiError.is(e)) {
515
+ if (ApiError.is(e)) {
34
516
  // 业务错误(服务器返回的错误响应)
35
517
  console.log('业务错误:', e.code, e.message);
36
- console.log('HTTP 状态码:', e.status);
518
+ console.log('HTTP 状态码:', e.statusCode);
37
519
  console.log('追踪 ID:', e.traceId);
38
520
 
39
- // 增强方法
521
+ // 状态码判断
40
522
  if (e.isAuthError()) {
41
523
  // 认证错误 (401/403)
42
524
  redirectToLogin();
@@ -55,10 +537,15 @@ try {
55
537
  } else if (NetworkError.is(e)) {
56
538
  console.log('网络错误:', e.message);
57
539
  }
540
+
541
+ // 判断是否可重试
542
+ if (isRetryableError(e)) {
543
+ console.log('此错误可以重试');
544
+ }
58
545
  }
59
546
  ```
60
547
 
61
- ### DjvApiError 增强方法
548
+ ### ApiError 方法
62
549
 
63
550
  | 方法 | 说明 |
64
551
  |---|---|
@@ -74,37 +561,325 @@ try {
74
561
  | `getRetryAfter()` | 获取 Retry-After 头的值(秒) |
75
562
  | `getRetryDelayMs(default)` | 获取重试延迟(毫秒) |
76
563
 
77
- ## 重试配置(支持 Retry-After)
564
+ ## 认证
565
+
566
+ ### Bearer Token
78
567
 
79
568
  ```typescript
80
- import { createUserClient } from '@djvlc/openapi-user-client';
569
+ import { createFetchClient } from '@djvlc/openapi-client-core';
81
570
 
82
- const client = createUserClient({
571
+ const client = createFetchClient({
83
572
  baseUrl: 'https://api.example.com',
84
- retry: {
85
- maxRetries: 3, // 最大重试次数
86
- retryDelayMs: 1000, // 基础延迟
87
- exponentialBackoff: true, // 指数退避
88
- maxDelayMs: 30000, // 最大延迟
89
- respectRetryAfter: true, // 尊重 Retry-After 响应头(默认 true)
90
- onRetry: (error, attempt, delayMs) => {
91
- console.log('第', attempt, '次重试,延迟', delayMs, 'ms');
573
+ auth: {
574
+ type: 'bearer',
575
+ getToken: async () => {
576
+ // 支持异步获取 Token
577
+ return await tokenService.getAccessToken();
92
578
  },
579
+ headerName: 'Authorization', // 可选,默认 'Authorization'
580
+ prefix: 'Bearer', // 可选,默认 'Bearer'
93
581
  },
94
582
  });
95
583
  ```
96
584
 
97
- 当服务器返回 `429 Too Many Requests` 或 `503 Service Unavailable` 并带有 `Retry-After` 头时,SDK 会优先使用该值作为重试延迟。
585
+ ### API Key
98
586
 
99
- ## 请求去重
587
+ ```typescript
588
+ const client = createFetchClient({
589
+ baseUrl: 'https://api.example.com',
590
+ auth: {
591
+ type: 'api-key',
592
+ apiKey: 'your-api-key',
593
+ headerName: 'X-API-Key', // 可选
594
+ },
595
+ });
596
+ ```
597
+
598
+ ### Basic Auth
599
+
600
+ ```typescript
601
+ const client = createFetchClient({
602
+ baseUrl: 'https://api.example.com',
603
+ auth: {
604
+ type: 'basic',
605
+ username: 'admin',
606
+ password: 'secret',
607
+ },
608
+ });
609
+ ```
610
+
611
+ ### 自定义认证
612
+
613
+ ```typescript
614
+ const client = createFetchClient({
615
+ baseUrl: 'https://api.example.com',
616
+ auth: {
617
+ type: 'custom',
618
+ authenticate: async (headers) => {
619
+ headers['X-Custom-Auth'] = await getCustomToken();
620
+ headers['X-Timestamp'] = Date.now().toString();
621
+ },
622
+ },
623
+ });
624
+ ```
625
+
626
+ ## 拦截器
627
+
628
+ ### 请求拦截器
629
+
630
+ ```typescript
631
+ import {
632
+ createFetchClient,
633
+ createAuthInterceptor,
634
+ createRequestIdInterceptor,
635
+ createTraceInterceptor,
636
+ createBearerAuthenticator,
637
+ } from '@djvlc/openapi-client-core';
638
+
639
+ const client = createFetchClient({
640
+ baseUrl: 'https://api.example.com',
641
+ requestInterceptors: [
642
+ // 请求 ID 拦截器
643
+ createRequestIdInterceptor({
644
+ headerName: 'X-Request-ID',
645
+ }),
646
+
647
+ // 追踪拦截器
648
+ createTraceInterceptor({
649
+ traceIdHeader: 'X-Trace-ID',
650
+ addTraceparent: true,
651
+ getTraceparent: () => otel.getCurrentSpan()?.spanContext(),
652
+ }),
653
+ ],
654
+ });
655
+ ```
656
+
657
+ ### 错误拦截器
658
+
659
+ ```typescript
660
+ import {
661
+ createFetchClient,
662
+ createLoggingInterceptor,
663
+ createReportingInterceptor,
664
+ createConsoleLogger,
665
+ } from '@djvlc/openapi-client-core';
666
+
667
+ const client = createFetchClient({
668
+ baseUrl: 'https://api.example.com',
669
+ errorInterceptors: [
670
+ // 日志拦截器
671
+ createLoggingInterceptor({
672
+ logger: createConsoleLogger({ prefix: '[API]' }),
673
+ verbose: true,
674
+ }),
675
+
676
+ // 错误上报拦截器
677
+ createReportingInterceptor({
678
+ reporter: {
679
+ report: (error, context) => {
680
+ Sentry.captureException(error, { extra: context });
681
+ },
682
+ },
683
+ sampleRate: 0.1, // 10% 采样
684
+ }),
685
+ ],
686
+ });
687
+ ```
688
+
689
+ ### 自定义拦截器
690
+
691
+ ```typescript
692
+ import type { RequestInterceptor, ErrorInterceptor } from '@djvlc/openapi-client-core';
693
+
694
+ // 自定义请求拦截器
695
+ const customRequestInterceptor: RequestInterceptor = {
696
+ name: 'custom-request',
697
+ order: 0,
698
+ intercept(context) {
699
+ // 修改请求
700
+ context.options.headers['X-Custom'] = 'value';
701
+ return context.options;
702
+ },
703
+ };
704
+
705
+ // 自定义错误拦截器
706
+ const customErrorInterceptor: ErrorInterceptor = {
707
+ name: 'custom-error',
708
+ order: 0,
709
+ intercept(error, context) {
710
+ console.log('请求失败:', context.url, error.message);
711
+ return undefined; // 不处理,让错误继续传播
712
+ },
713
+ };
100
714
 
101
- 避免相同的 GET 请求并发执行多次:
715
+ const client = createFetchClient({
716
+ baseUrl: 'https://api.example.com',
717
+ requestInterceptors: [customRequestInterceptor],
718
+ errorInterceptors: [customErrorInterceptor],
719
+ });
720
+ ```
721
+
722
+ ## 重试配置
723
+
724
+ ### 基础用法
725
+
726
+ ```typescript
727
+ const client = createFetchClient({
728
+ baseUrl: 'https://api.example.com',
729
+ enableRetry: true, // 启用重试,默认 true
730
+ retry: {
731
+ maxRetries: 3,
732
+ initialDelayMs: 1000,
733
+ maxDelayMs: 30000,
734
+ backoffStrategy: 'exponential',
735
+ },
736
+ });
737
+ ```
738
+
739
+ ### 完整配置选项
740
+
741
+ ```typescript
742
+ import type { RetryConfig, RetryInfo, RetryContext } from '@djvlc/openapi-client-core';
743
+
744
+ const retryConfig: RetryConfig = {
745
+ // ============ 基础参数 ============
746
+
747
+ /** 最大重试次数,默认 3 */
748
+ maxRetries: 3,
749
+
750
+ /** 初始延迟(毫秒),默认 1000 */
751
+ initialDelayMs: 1000,
752
+
753
+ /** 最大延迟(毫秒),默认 30000 */
754
+ maxDelayMs: 30000,
755
+
756
+ // ============ 退避策略 ============
757
+
758
+ /**
759
+ * 退避策略
760
+ * - 'fixed': 固定延迟,每次重试使用 initialDelayMs
761
+ * - 'linear': 线性增长,delay = initialDelayMs * attempt
762
+ * - 'exponential': 指数增长,delay = initialDelayMs * (2 ^ attempt)
763
+ * 默认 'exponential'
764
+ */
765
+ backoffStrategy: 'exponential',
766
+
767
+ /**
768
+ * 抖动系数(0-1)
769
+ * 在计算的延迟基础上添加随机抖动,防止惊群效应
770
+ * 实际延迟 = delay * (1 + random(-jitterFactor, +jitterFactor))
771
+ * 默认 0.1
772
+ */
773
+ jitterFactor: 0.1,
774
+
775
+ // ============ 重试条件 ============
776
+
777
+ /**
778
+ * 可重试的 HTTP 状态码
779
+ * 默认 [429, 500, 502, 503, 504]
780
+ */
781
+ retryableStatusCodes: [429, 500, 502, 503, 504],
782
+
783
+ /** 网络错误是否重试,默认 true */
784
+ retryOnNetworkError: true,
785
+
786
+ /** 超时是否重试,默认 true */
787
+ retryOnTimeout: true,
788
+
789
+ /** 是否尊重 Retry-After 响应头,默认 true */
790
+ respectRetryAfter: true,
791
+
792
+ /**
793
+ * 自定义重试判断函数
794
+ * 返回 true 表示应该重试
795
+ */
796
+ shouldRetry: (error: Error, attempt: number, context: RetryContext): boolean => {
797
+ // 例:只重试 GET 请求
798
+ if (context.method !== 'GET') return false;
799
+
800
+ // 例:429 错误最多重试 5 次
801
+ if (context.statusCode === 429 && attempt >= 5) return false;
802
+
803
+ return true;
804
+ },
805
+
806
+ // ============ 回调 ============
807
+
808
+ /**
809
+ * 重试时的回调函数
810
+ * 可用于日志、监控等
811
+ */
812
+ onRetry: (info: RetryInfo) => {
813
+ console.log(`重试请求: ${info.method} ${info.url}`);
814
+ console.log(` - 第 ${info.attempt}/${info.maxRetries} 次`);
815
+ console.log(` - 延迟 ${info.delayMs}ms`);
816
+ console.log(` - 原因: ${info.error.message}`);
817
+ },
818
+ };
819
+ ```
820
+
821
+ ### 退避策略对比
822
+
823
+ 假设 `initialDelayMs = 1000`,`maxDelayMs = 30000`:
824
+
825
+ | 重试次数 | fixed | linear | exponential |
826
+ |----------|-------|--------|-------------|
827
+ | 第 1 次 | 1000ms | 1000ms | 1000ms |
828
+ | 第 2 次 | 1000ms | 2000ms | 2000ms |
829
+ | 第 3 次 | 1000ms | 3000ms | 4000ms |
830
+ | 第 4 次 | 1000ms | 4000ms | 8000ms |
831
+ | 第 5 次 | 1000ms | 5000ms | 16000ms |
832
+ | 第 6 次 | 1000ms | 6000ms | 30000ms (capped) |
833
+
834
+ ### Retry-After 响应头
835
+
836
+ 当 `respectRetryAfter: true` 时,客户端会检查响应头:
837
+
838
+ ```http
839
+ HTTP/1.1 429 Too Many Requests
840
+ Retry-After: 60
841
+ ```
842
+
843
+ 客户端会等待 60 秒后重试(但不超过 `maxDelayMs`)。
844
+
845
+ 支持的格式:
846
+ - 秒数:`Retry-After: 120`
847
+ - HTTP 日期:`Retry-After: Wed, 21 Oct 2025 07:28:00 GMT`
848
+
849
+ ### 禁用重试
850
+
851
+ ```typescript
852
+ // 方式 1:全局禁用
853
+ const client = createFetchClient({
854
+ baseUrl: 'https://api.example.com',
855
+ enableRetry: false,
856
+ });
857
+
858
+ // 方式 2:不配置 retry
859
+ const client = createFetchClient({
860
+ baseUrl: 'https://api.example.com',
861
+ // 不传 retry 配置
862
+ });
863
+
864
+ // 方式 3:单次请求禁用(通过自定义 shouldRetry)
865
+ const retryConfig: RetryConfig = {
866
+ maxRetries: 3,
867
+ shouldRetry: (error, attempt, context) => {
868
+ // POST 请求不重试
869
+ if (context.method === 'POST') return false;
870
+ return true;
871
+ },
872
+ };
873
+ ```
874
+
875
+ ## 请求去重
102
876
 
103
877
  ```typescript
104
878
  import { createRequestDeduper } from '@djvlc/openapi-client-core';
105
879
 
106
880
  const deduper = createRequestDeduper({
107
- getOnly: true, // 只对 GET 请求去重(默认)
881
+ getOnly: true, // 只对 GET 请求去重
882
+ maxSize: 100, // 最大缓存数量
108
883
  });
109
884
 
110
885
  // 包装 fetch
@@ -116,6 +891,8 @@ const [r1, r2] = await Promise.all([
116
891
  dedupedFetch('/api/user'),
117
892
  ]);
118
893
  // 只发送了 1 个请求!
894
+
895
+ console.log('当前飞行中请求数:', deduper.inflightCount);
119
896
  ```
120
897
 
121
898
  ## 指标收集
@@ -123,23 +900,27 @@ const [r1, r2] = await Promise.all([
123
900
  ```typescript
124
901
  import {
125
902
  createMetricsCollector,
126
- createMetricsMiddleware
903
+ createMetricsInterceptors,
904
+ createFetchClient,
127
905
  } from '@djvlc/openapi-client-core';
128
906
 
129
907
  // 创建指标收集器
130
908
  const metrics = createMetricsCollector({
131
- maxMetrics: 1000, // 最大保留数量
132
- ttlMs: 3600000, // 1 小时过期
909
+ maxMetrics: 1000,
910
+ ttlMs: 3600000, // 1 小时过期
133
911
  onMetrics: (m) => {
134
- // 每次请求完成时回调
135
- console.log(m.method, m.path, '-', m.durationMs, 'ms');
912
+ console.log(`${m.method} ${m.path} - ${m.durationMs}ms`);
136
913
  },
137
914
  });
138
915
 
139
- // 添加到客户端
140
- const client = createUserClient({
916
+ // 创建拦截器
917
+ const metricsInterceptors = createMetricsInterceptors(metrics);
918
+
919
+ const client = createFetchClient({
141
920
  baseUrl: 'https://api.example.com',
142
- middleware: [createMetricsMiddleware(metrics)],
921
+ requestInterceptors: [metricsInterceptors.request],
922
+ responseInterceptors: [metricsInterceptors.response],
923
+ errorInterceptors: [metricsInterceptors.error],
143
924
  });
144
925
 
145
926
  // 获取指标摘要
@@ -151,7 +932,7 @@ console.log('P95:', summary.p95Ms, 'ms');
151
932
  console.log('P99:', summary.p99Ms, 'ms');
152
933
 
153
934
  // 按路径查看
154
- const pageMetrics = metrics.getByPath('/api/resolve/page');
935
+ const pageMetrics = metrics.getByPath('/api/pages');
155
936
 
156
937
  // 清空并获取所有指标
157
938
  const allMetrics = metrics.flush();
@@ -170,149 +951,218 @@ const allMetrics = metrics.flush();
170
951
  | `durationMs` | number | 耗时(毫秒) |
171
952
  | `status` | number | HTTP 状态码 |
172
953
  | `success` | boolean | 是否成功 (2xx) |
173
- | `retryCount` | number? | 重试次数 |
954
+ | `retryCount` | number | 重试次数 |
174
955
  | `traceId` | string? | 追踪 ID |
956
+ | `error` | string? | 错误信息 |
175
957
 
176
- ## 请求取消
958
+ ## 日志
177
959
 
178
960
  ```typescript
179
- const controller = new AbortController();
180
-
181
- // 在某个时机取消请求
182
- setTimeout(() => controller.abort(), 5000);
961
+ import { createConsoleLogger, createBufferLogger } from '@djvlc/openapi-client-core';
183
962
 
184
- try {
185
- await client.PageApi.resolvePage(
186
- { pageUid: 'xxx' },
187
- { signal: controller.signal }
188
- );
189
- } catch (e) {
190
- if (AbortError.is(e)) {
191
- console.log('请求已取消');
192
- }
193
- }
194
- ```
963
+ // 控制台日志器
964
+ const consoleLogger = createConsoleLogger({
965
+ prefix: '[DJV-API]',
966
+ level: 'debug', // 最低日志级别
967
+ timestamp: true, // 显示时间戳
968
+ });
195
969
 
196
- ## 日志
970
+ // 缓冲日志器(用于测试)
971
+ const bufferLogger = createBufferLogger(1000);
197
972
 
198
- ```typescript
199
- import { createConsoleLogger } from '@djvlc/openapi-client-core';
200
-
201
- const client = createUserClient({
973
+ // 使用
974
+ const client = createFetchClient({
202
975
  baseUrl: 'https://api.example.com',
203
- debug: true, // 开启调试日志
204
- logger: createConsoleLogger({
205
- prefix: '[MyApp-API]',
206
- level: 'debug',
207
- timestamp: true,
208
- }),
976
+ logger: consoleLogger,
977
+ debug: true,
209
978
  });
979
+
980
+ // 获取缓冲的日志
981
+ const logs = bufferLogger.getLogs();
982
+ const errorLogs = bufferLogger.getLogsByLevel('error');
210
983
  ```
211
984
 
212
- ## 拦截器
985
+ ## Token 自动刷新
213
986
 
214
987
  ```typescript
215
- const client = createUserClient({
988
+ import {
989
+ createFetchClient,
990
+ createTokenRefreshInterceptor,
991
+ } from '@djvlc/openapi-client-core';
992
+
993
+ // 创建客户端时配置 Token 刷新
994
+ const client = createFetchClient({
216
995
  baseUrl: 'https://api.example.com',
217
- interceptors: {
218
- request: [
219
- (context) => {
220
- // 修改请求
221
- context.init.headers = new Headers(context.init.headers);
222
- context.init.headers.set('X-Custom', 'value');
223
- return context;
224
- },
225
- ],
226
- response: [
227
- async (response, context) => {
228
- // 处理响应
229
- if (response.status === 401) {
230
- // Token 刷新逻辑
231
- const newToken = await refreshToken();
232
- // 重试请求...
233
- }
234
- return response;
235
- },
236
- ],
237
- error: [
238
- (error, context) => {
239
- // 统一错误上报
240
- reportError(error);
241
- throw error;
242
- },
243
- ],
996
+ auth: {
997
+ type: 'bearer',
998
+ getToken: () => tokenService.getAccessToken(),
244
999
  },
245
1000
  });
1001
+
1002
+ // 添加 Token 刷新拦截器
1003
+ client.interceptors.addErrorInterceptor(
1004
+ createTokenRefreshInterceptor({
1005
+ refreshToken: async () => {
1006
+ const newToken = await authService.refresh();
1007
+ tokenService.setAccessToken(newToken);
1008
+ return newToken;
1009
+ },
1010
+ triggerStatusCodes: [401],
1011
+ maxRetries: 1,
1012
+ onTokenRefreshed: (newToken) => {
1013
+ console.log('Token 已刷新');
1014
+ },
1015
+ onRefreshFailed: (error) => {
1016
+ console.log('Token 刷新失败,跳转登录');
1017
+ redirectToLogin();
1018
+ },
1019
+ executeRequest: (context) => client.request(context.originalOptions),
1020
+ })
1021
+ );
246
1022
  ```
247
1023
 
248
- ## Token 自动刷新
1024
+ ## 工具函数
249
1025
 
250
1026
  ```typescript
251
- import { createTokenRefreshInterceptor } from '@djvlc/openapi-client-core';
1027
+ import {
1028
+ // 请求 ID
1029
+ generateRequestId,
1030
+ generateTraceId,
1031
+ isValidRequestId,
1032
+
1033
+ // 重试延迟
1034
+ calculateRetryDelay,
1035
+ parseRetryAfter,
1036
+
1037
+ // 延迟执行
1038
+ sleep,
1039
+ sleepWithAbort,
1040
+
1041
+ // URL 处理
1042
+ buildUrl,
1043
+ extractPath,
1044
+ parseQueryString,
1045
+ joinPaths,
1046
+
1047
+ // 请求头处理
1048
+ mergeHeaders,
1049
+ getHeader,
1050
+ setHeader,
1051
+ removeHeader,
1052
+ } from '@djvlc/openapi-client-core';
252
1053
 
253
- const client = createUserClient({
254
- baseUrl: 'https://api.example.com',
255
- interceptors: {
256
- response: [
257
- createTokenRefreshInterceptor({
258
- refreshToken: async () => {
259
- const newToken = await authService.refresh();
260
- return newToken;
261
- },
262
- }),
263
- ],
264
- },
265
- });
1054
+ // 生成 ID
1055
+ const requestId = generateRequestId(); // req_m5abc123_k7def456
1056
+ const traceId = generateTraceId(); // trace_m5abc123_k7def456_x8ghi789
1057
+
1058
+ // URL 处理
1059
+ const url = buildUrl('https://api.example.com', '/users', { page: 1, size: 10 });
1060
+ // https://api.example.com/users?page=1&size=10
1061
+
1062
+ // 请求头处理
1063
+ const headers = mergeHeaders(
1064
+ { 'Content-Type': 'application/json' },
1065
+ { 'Authorization': 'Bearer xxx' },
1066
+ );
266
1067
  ```
267
1068
 
268
- ## API
1069
+ ## API 参考
269
1070
 
270
1071
  ### 错误类
271
1072
 
272
1073
  | 类 | 说明 |
273
1074
  |---|---|
274
- | `DjvApiError` | 业务错误(HTTP 非 2xx 响应) |
1075
+ | `BaseClientError` | 所有错误的基类 |
1076
+ | `ApiError` | 业务错误(HTTP 非 2xx 响应) |
275
1077
  | `NetworkError` | 网络层错误 |
276
1078
  | `TimeoutError` | 超时错误 |
277
1079
  | `AbortError` | 请求被取消 |
278
1080
 
279
- ### 工具函数
1081
+ ### 认证器
1082
+
1083
+ | 类 | 说明 |
1084
+ |---|---|
1085
+ | `BearerAuthenticator` | Bearer Token 认证 |
1086
+ | `ApiKeyAuthenticator` | API Key 认证 |
1087
+ | `BasicAuthenticator` | Basic Auth 认证 |
1088
+ | `CustomAuthenticator` | 自定义认证 |
1089
+ | `NoAuthenticator` | 无认证 |
1090
+
1091
+ ### 拦截器
1092
+
1093
+ | 类 | 类型 | 说明 |
1094
+ |---|---|---|
1095
+ | `AuthInterceptor` | 请求 | 自动添加认证信息 |
1096
+ | `RequestIdInterceptor` | 请求 | 生成请求 ID |
1097
+ | `TraceInterceptor` | 请求 | 添加追踪上下文 |
1098
+ | `TimeoutInterceptor` | 请求 | 智能设置超时 |
1099
+ | `ErrorTransformInterceptor` | 响应 | 错误格式转换 |
1100
+ | `RetryInterceptor` | 错误 | 自动重试 |
1101
+ | `TokenRefreshInterceptor` | 错误 | Token 自动刷新 |
1102
+ | `LoggingInterceptor` | 错误 | 错误日志记录 |
1103
+ | `ReportingInterceptor` | 错误 | 错误上报监控 |
1104
+
1105
+ ### 客户端
280
1106
 
281
- | 函数 | 说明 |
1107
+ | 类/函数 | 说明 |
282
1108
  |---|---|
283
- | `isRetryableError(error)` | 判断错误是否可重试 |
284
- | `getRetryDelay(error, defaultMs)` | 获取错误的推荐重试延迟 |
285
- | `createConsoleLogger(opts)` | 创建控制台日志器 |
286
- | `createSilentLogger()` | 创建静默日志器 |
287
- | `createTokenRefreshInterceptor(opts)` | 创建 Token 刷新拦截器 |
288
- | `createRequestDeduper(opts)` | 创建请求去重器 |
289
- | `createMetricsCollector(opts)` | 创建指标收集器 |
290
- | `createMetricsMiddleware(collector)` | 创建指标收集中间件 |
291
-
292
- ### 类型
293
-
294
- | 类型 | 说明 |
1109
+ | `FetchClient` | 基于原生 fetch 的独立客户端 |
1110
+ | `createFetchClient()` | 创建 FetchClient 实例 |
1111
+ | `createEnhancedFetch()` | 创建增强的 fetch 函数(用于 typescript-fetch 生成代码) |
1112
+ | `createAxiosInstance()` | 创建配置好的 Axios 实例(向后兼容) |
1113
+
1114
+ ### 插件
1115
+
1116
+ | 类/函数 | 说明 |
295
1117
  |---|---|
296
- | `BaseClientOptions` | 客户端配置选项 |
297
- | `RetryOptions` | 重试配置 |
298
- | `Logger` | 日志接口 |
299
- | `Middleware` | 中间件接口 |
300
- | `Interceptors` | 拦截器配置 |
301
- | `RequestMetrics` | 请求指标 |
302
- | `MetricsSummary` | 指标摘要 |
303
- | `MetricsCollector` | 指标收集器接口 |
1118
+ | `RequestDeduper` | 请求去重器 |
1119
+ | `DefaultMetricsCollector` | 默认指标收集器 |
1120
+ | `ConsoleLogger` | 控制台日志器 |
1121
+ | `BufferLogger` | 缓冲日志器(测试用) |
304
1122
 
305
1123
  ## 版本信息
306
1124
 
307
- SDK 版本在构建时自动注入:
308
-
309
1125
  ```typescript
310
- import { SDK_VERSION, SDK_NAME, getSdkInfo } from '@djvlc/openapi-client-core';
1126
+ import { VERSION, SDK_NAME, getSdkInfo } from '@djvlc/openapi-client-core';
311
1127
 
312
- console.log(SDK_NAME + '@' + SDK_VERSION);
313
- // 输出: @djvlc/openapi-client-core@1.0.0
1128
+ console.log(`${SDK_NAME}@${VERSION}`);
1129
+ // @djvlc/openapi-client-core@2.0.0
314
1130
 
315
1131
  const info = getSdkInfo();
316
- console.log(info);
317
- // 输出: { name: '@djvlc/openapi-client-core', version: '1.0.0' }
1132
+ // { name: '@djvlc/openapi-client-core', version: '2.0.0' }
318
1133
  ```
1134
+
1135
+ ## 迁移指南(从 1.x 升级)
1136
+
1137
+ ### 破坏性变更
1138
+
1139
+ 1. **模块路径变更**:原来的单文件被拆分为多个模块目录
1140
+ 2. **类型定义变更**:部分类型名称和结构有调整
1141
+ 3. **HttpClient 重命名**:`HttpClient` 改名为 `FetchClient`
1142
+ 4. **createClient 重命名**:`createClient` 改名为 `createFetchClient`
1143
+ 5. **默认使用 Fetch**:OpenAPI 生成的客户端默认使用 `typescript-fetch`,不再依赖 axios
1144
+
1145
+ ### 从 axios 迁移到 fetch
1146
+
1147
+ 如果你之前使用 `createAxiosInstance`,现在推荐使用 `createEnhancedFetch`:
1148
+
1149
+ ```typescript
1150
+ // 旧代码(axios)
1151
+ import { createAxiosInstance } from '@djvlc/openapi-client-core';
1152
+ const axiosInstance = createAxiosInstance({ baseUrl: '/api' });
1153
+ const api = new PagesApi(config, '/api', axiosInstance);
1154
+
1155
+ // 新代码(fetch)
1156
+ import { createEnhancedFetch } from '@djvlc/openapi-client-core';
1157
+ const enhancedFetch = createEnhancedFetch({ baseUrl: '/api' });
1158
+ const api = new PagesApi(config, '/api', enhancedFetch);
1159
+ ```
1160
+
1161
+ ### 向后兼容
1162
+
1163
+ - `createAxiosInstance` 仍然可用,用于需要 axios 的场景
1164
+ - 所有配置选项在 `createEnhancedFetch` 中保持一致
1165
+
1166
+ ## License
1167
+
1168
+ MIT