@h-ai/api-client 0.1.0-alpha.13 → 0.1.0-alpha.15

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
@@ -1,340 +1,118 @@
1
1
  # @h-ai/api-client
2
2
 
3
- 统一 HTTP 客户端模块,提供 Bearer Token 管理、契约调用与流式请求能力。
3
+ > hai-framework 的跨端 oRPC/OpenAPI typed client,面向 Web、App、小程序等运行时提供统一 API 调用与 Token 管理。
4
4
 
5
- ## 支持的能力
5
+ ## 能力概览
6
6
 
7
- - 单例模式:`api.init()` → `api.get()` / `api.call()` → `api.close()`
8
- - 通用 HTTP 方法:`get` / `post` / `put` / `patch` / `delete`
9
- - 契约调用:`api.call(endpoint, input)`(入参/出参 Zod 双向校验,端到端类型安全)
10
- - 自动附加 Bearer Token,401 自动刷新重试(并发去重)
11
- - 可插拔 Token 存储(默认 localStorage,可切换 memory / Capacitor / 自定义)
12
- - 文件上传(支持附加字段)
13
- - SSE 流式响应(`api.stream()`)
14
- - 请求/响应拦截器
15
- - 统一返回 `HaiResult<T>`
7
+ - `apiClient`:默认绑定 iam/storage/ai 领域的统一 typed client 入口。
8
+ - `apiClient.create(contract)`:为自定义 contract 创建 typed client。
9
+ - `apiClient.tokenStorage.{memory,localStorage,httpOnlyCookie}()`:Token 存储工厂。
10
+ - 支持自定义 `fetch`,适配浏览器、Node、Capacitor、小程序桥接层。
11
+ - 支持 Bearer Token 自动注入、401 后刷新并重试一次。
12
+ - 网络错误统一转换为 `HaiResult` 错误。
13
+ - 可选传输加密:`apiClient.init({ transport: { crypto } })` 自动使用 `crypto.transport.createClient()`。
14
+ - 传输加密与自动刷新可同时启用:401 后的 `/auth/refresh` 会继续走 encrypted fetch,不降级明文。
16
15
 
17
16
  ## 快速开始
18
17
 
19
18
  ```ts
20
- import { api } from '@h-ai/api-client'
21
- import { iamEndpoints } from '@h-ai/iam/api'
19
+ import { apiClient } from '@h-ai/api-client'
22
20
 
23
- // 1. 初始化
24
- await api.init({
21
+ await apiClient.init({
25
22
  baseUrl: 'https://api.example.com/api/v1',
26
- auth: { refreshUrl: '/auth/refresh' },
23
+ auth: {}, // 默认使用 httpOnly cookie 存储(推荐);SSR 测试请显式传入 apiClient.tokenStorage.memory()
27
24
  })
28
25
 
29
- // 2. 登录(契约调用)
30
- const loginResult = await api.call(iamEndpoints.login, {
26
+ const login = await apiClient.iam.auth.login({
31
27
  identifier: 'alice',
32
- password: 'xxx',
28
+ password: 'secret',
33
29
  })
34
30
 
35
- // 3. 登录成功后保存 Token(后续请求自动附加 Bearer)
36
- if (loginResult.success) {
37
- await api.auth.setTokens(loginResult.data.tokens)
31
+ if (login.success) {
32
+ // httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
33
+ // api-client 默认存储只需要把 access token 写入内存。
34
+ await apiClient.auth.setTokens(login.data.tokens)
38
35
  }
39
36
 
40
- // 4. 后续请求自动携带 Token
41
- const me = await api.call(iamEndpoints.currentUser, {})
37
+ const me = await apiClient.iam.auth.currentUser()
42
38
 
43
- // 5. 关闭
44
- await api.close()
39
+ await apiClient.close()
45
40
  ```
46
41
 
47
- > `auth.storage` 未传时默认使用 `createLocalStorageTokenStorage()`;在 SSR/测试环境可显式传入 `createMemoryTokenStorage()`。
42
+ ### 启用传输加密
48
43
 
49
- ## 与 @h-ai/iam 集成
50
-
51
- `@h-ai/api-client` 与 `@h-ai/iam` 通过 **API 契约(`EndpointDef`)** 实现端到端类型安全。客户端和服务端共享同一份契约定义,编译时保证一致性。
52
-
53
- ### 架构总览
54
-
55
- ```
56
- ┌─────────────────────────────────────────────────┐
57
- │ @h-ai/iam/api │
58
- │ iam-api-schemas.ts ← Zod Schema(唯一真相源) │
59
- │ iam-api-contract.ts ← iamEndpoints │
60
- └────────┬────────────────────────┬────────────────┘
61
- │ │
62
- ┌──────▼──────┐ ┌───────▼────────┐
63
- │ 客户端 │ │ 服务端 │
64
- │ api.call() │ HTTP │ kit.fromContract│
65
- │ @h-ai/ │ ◄────► │ @h-ai/kit │
66
- │ api-client │ │ │
67
- └─────────────┘ └─────────────────┘
68
- ```
69
-
70
- ### 完整登录 → 使用 → 登出流程
44
+ 服务端需先启用 `serv.createApp({ transport: { crypto } })`;客户端只注入同一个 `@h-ai/crypto` 服务实例,无需手写密钥协商代码。
71
45
 
72
46
  ```ts
73
- import { api } from '@h-ai/api-client'
74
- import { iamEndpoints } from '@h-ai/iam/api'
75
-
76
- // ── 初始化 ──
77
- await api.init({
78
- baseUrl: 'https://api.example.com/api/v1',
79
- auth: {
80
- refreshUrl: '/auth/refresh',
81
- // Token 刷新成功后的回调(可选,适合更新全局状态)
82
- onTokenRefreshed: (tokens) => {
83
- // tokens 包含新的 accessToken / refreshToken / expiresIn
84
- },
85
- // Token 刷新失败后的回调(可选,常用于跳转登录页)
86
- onRefreshFailed: () => {
87
- window.location.href = '/login'
88
- },
89
- },
90
- })
91
-
92
- // ── 登录 ──
93
- const loginResult = await api.call(iamEndpoints.login, {
94
- identifier: 'alice',
95
- password: 'StrongPassword123',
96
- })
97
-
98
- if (!loginResult.success) {
99
- // loginResult.error.code 可能是:
100
- // 1203 (UNAUTHORIZED) — 凭证错误
101
- // 1206 (VALIDATION_FAILED) — 入参不合法
102
- // 1202 (SERVER_ERROR) — 服务端异常
103
- console.error('Login failed:', loginResult.error.message)
104
- }
105
- else {
106
- // loginResult.data 类型自动推导为 { user, tokens, agreements? }
107
- const { user, tokens } = loginResult.data
108
-
109
- // 保存 Token — 后续所有请求自动附加 Authorization: Bearer <accessToken>
110
- await api.auth.setTokens(tokens)
111
- }
112
-
113
- // ── 获取当前用户 ──(自动携带 Bearer Token)
114
- const meResult = await api.call(iamEndpoints.currentUser, {})
115
- if (meResult.success) {
116
- // meResult.data 类型推导为 { user, roles, permissions }
117
- }
118
-
119
- // ── 修改密码 ──
120
- await api.call(iamEndpoints.changePassword, {
121
- oldPassword: 'OldPass123',
122
- newPassword: 'NewPass456',
123
- })
124
-
125
- // ── 登出 ──
126
- const accessToken = '...' // 从 Token 存储获取
127
- await api.call(iamEndpoints.logout, { accessToken })
128
- await api.auth.clear() // 清空本地 Token 存储
129
- ```
47
+ import { apiClient } from '@h-ai/api-client'
48
+ import { crypto } from '@h-ai/crypto'
130
49
 
131
- ### 401 自动刷新机制
132
-
133
- 当请求收到 401 响应时,`api-client` 自动执行以下流程:
134
-
135
- 1. 使用存储的 `refreshToken` 调用 `auth.refreshUrl`(POST)
136
- 2. 刷新成功 → 更新存储 → 用新 Token 重试原请求(仅重试一次)
137
- 3. 刷新失败 → 清空 Token → 触发 `onRefreshFailed` 回调
138
- 4. 多个并发 401 请求 → **自动去重**,只发一次刷新请求
139
-
140
- > 流式请求(`api.stream()`)同样支持 401 自动刷新 + 重试。
141
-
142
- ### 可用的 IAM 端点
143
-
144
- | 分类 | 端点 | 方法 | 路径 | 认证 |
145
- | ------------ | --------------------------------- | ------ | ----------------------------- | ---- |
146
- | **认证** | `iamEndpoints.login` | POST | /auth/login | 否 |
147
- | | `iamEndpoints.loginWithOtp` | POST | /auth/login/otp | 否 |
148
- | | `iamEndpoints.logout` | POST | /auth/logout | 是 |
149
- | | `iamEndpoints.currentUser` | GET | /auth/me | 是 |
150
- | | `iamEndpoints.refreshToken` | POST | /auth/refresh | 否 |
151
- | | `iamEndpoints.sendOtp` | POST | /auth/otp/send | 否 |
152
- | | `iamEndpoints.register` | POST | /auth/register | 否 |
153
- | | `iamEndpoints.changePassword` | POST | /auth/change-password | 是 |
154
- | | `iamEndpoints.updateCurrentUser` | PUT | /auth/me | 是 |
155
- | **用户管理** | `iamEndpoints.listUsers` | GET | /iam/users | 是 |
156
- | | `iamEndpoints.getUser` | GET | /iam/users/:id | 是 |
157
- | | `iamEndpoints.createUser` | POST | /iam/users | 是 |
158
- | | `iamEndpoints.updateUser` | PUT | /iam/users/:id | 是 |
159
- | | `iamEndpoints.deleteUser` | DELETE | /iam/users/:id | 是 |
160
- | | `iamEndpoints.adminResetPassword` | POST | /iam/users/:id/reset-password | 是 |
161
- | **角色管理** | `iamEndpoints.listRoles` | GET | /iam/roles | 是 |
162
- | | `iamEndpoints.getRole` | GET | /iam/roles/:id | 是 |
163
- | | `iamEndpoints.createRole` | POST | /iam/roles | 是 |
164
- | | `iamEndpoints.updateRole` | PUT | /iam/roles/:id | 是 |
165
- | | `iamEndpoints.deleteRole` | DELETE | /iam/roles/:id | 是 |
166
- | **权限管理** | `iamEndpoints.listPermissions` | GET | /iam/permissions | 是 |
167
- | | `iamEndpoints.getPermission` | GET | /iam/permissions/:id | 是 |
168
- | | `iamEndpoints.createPermission` | POST | /iam/permissions | 是 |
169
- | | `iamEndpoints.deletePermission` | DELETE | /iam/permissions/:id | 是 |
170
-
171
- ## 常见配置场景
172
-
173
- ### 浏览器端(默认 localStorage)
50
+ await crypto.init()
174
51
 
175
- ```ts
176
- import { api } from '@h-ai/api-client'
177
-
178
- await api.init({
52
+ await apiClient.init({
179
53
  baseUrl: 'https://api.example.com/api/v1',
180
- auth: { refreshUrl: '/auth/refresh' },
181
- timeout: 15000,
54
+ auth: {},
55
+ transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
182
56
  })
183
- ```
184
57
 
185
- ### Android / iOS App(Capacitor 安全存储)
186
-
187
- ```ts
188
- import { api } from '@h-ai/api-client'
189
- import { createCapacitorTokenStorage } from '@h-ai/capacitor'
190
-
191
- await api.init({
192
- baseUrl: `${PUBLIC_API_BASE}/api/v1`,
193
- auth: {
194
- storage: createCapacitorTokenStorage(),
195
- refreshUrl: '/auth/refresh',
196
- },
197
- })
58
+ await apiClient.close()
59
+ await crypto.close()
198
60
  ```
199
61
 
200
- ### SSR / 测试环境(内存存储)
62
+ ## API 契约
201
63
 
202
- ```ts
203
- import { api, createMemoryTokenStorage } from '@h-ai/api-client'
204
-
205
- await api.init({
206
- baseUrl: 'https://api.example.com/api/v1',
207
- auth: {
208
- storage: createMemoryTokenStorage(),
209
- refreshUrl: '/auth/refresh',
210
- },
211
- })
212
- ```
213
-
214
- ## 错误处理
215
-
216
- 所有通用 HTTP / 契约调用均返回 `HaiResult<T>`:
64
+ 自定义应用可以绑定自己的 oRPC contract:
217
65
 
218
66
  ```ts
219
- const result = await api.call(iamEndpoints.currentUser, {})
67
+ import { apiClient } from '@h-ai/api-client'
68
+ import { apiContract } from '@h-ai/api-contract'
220
69
 
221
- if (!result.success) {
222
- switch (result.error.code) {
223
- case 1203: // UNAUTHORIZED — Token 失效且刷新也失败
224
- redirectToLogin()
225
- break
226
- case 1201: // TIMEOUT — 请求超时
227
- showRetryDialog()
228
- break
229
- case 1206: // VALIDATION_FAILED — 入参或出参校验失败
230
- showValidationError(result.error.details)
231
- break
232
- default:
233
- showGenericError(result.error.message)
234
- }
235
- return
236
- }
70
+ const contract = apiContract.create({ iam: apiContract.iam })
71
+ const client = apiClient.create(contract)
237
72
 
238
- // result.data 类型安全
239
- renderUserProfile(result.data.user)
73
+ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
74
+ const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
240
75
  ```
241
76
 
242
- ### 错误码速查
77
+ ## API 概览
243
78
 
244
- | 错误码 | code | 说明 |
245
- | ---------------------------------------- | -------------------- | ---------------- |
246
- | `HaiApiClientError.NETWORK_ERROR` | `hai:api-client:001` | 网络不可达 |
247
- | `HaiApiClientError.TIMEOUT` | `hai:api-client:002` | 请求超时 |
248
- | `HaiApiClientError.SERVER_ERROR` | `hai:api-client:003` | 5xx 服务端错误 |
249
- | `HaiApiClientError.UNAUTHORIZED` | `hai:api-client:004` | 401 未认证 |
250
- | `HaiApiClientError.FORBIDDEN` | `hai:api-client:005` | 403 无权限 |
251
- | `HaiApiClientError.NOT_FOUND` | `hai:api-client:006` | 404 资源不存在 |
252
- | `HaiApiClientError.VALIDATION_FAILED` | `hai:api-client:007` | 请求参数校验失败 |
253
- | `HaiApiClientError.TOKEN_REFRESH_FAILED` | `hai:api-client:008` | Token 刷新失败 |
254
- | `HaiApiClientError.NOT_INITIALIZED` | `hai:api-client:010` | 未初始化 |
255
- | `HaiApiClientError.CONFIG_ERROR` | `hai:api-client:011` | 配置错误 |
256
- | `HaiApiClientError.UNKNOWN` | `hai:api-client:099` | 未知错误 |
79
+ - `apiClient.init(config)`:初始化默认 client。
80
+ - `apiClient.close()`:清理 client 状态。
81
+ - `apiClient.auth.setTokens(tokens)`:写入 access token;非 httpOnly 存储会同时写入 refresh token。
82
+ - `apiClient.auth.clear()`:清理 token。
83
+ - `apiClient.create(contract)`:创建自定义 typed client。
257
84
 
258
- ## Token 管理
85
+ ## 配置
259
86
 
260
- ```ts
261
- // 设置 Token(通常在登录成功后)
262
- await api.auth.setTokens({
263
- accessToken: 'xxx',
264
- refreshToken: 'yyy',
265
- expiresIn: 3600,
266
- tokenType: 'Bearer',
267
- })
87
+ - `baseUrl`:API 基础地址,通常包含 `/api/v1`。
88
+ - `auth.storage`:Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(浏览器推荐);SSR / 测试场景请显式传入 `apiClient.tokenStorage.memory()`。
89
+ - `auth.refreshPath`:刷新 token 路径,默认 `/auth/refresh`。
90
+ - `timeout`:请求超时,默认 30000ms。
91
+ - `headers`:静态或动态公共请求头。
92
+ - `fetch`:自定义 fetch 实现。
93
+ - `transport.crypto`:启用透明请求/响应加解密,必须传入已初始化的 `@h-ai/crypto` 实例。
94
+ - `transport.keyExchangePath`:密钥协商子路径,默认 `/_hai/key-exchange`;会自动拼接到 `baseUrl` 后。
268
95
 
269
- // 清空 Token(登出时)
270
- await api.auth.clear()
271
-
272
- // 监听自动刷新事件
273
- const unsubscribe = api.auth.onTokenRefreshed((tokens) => {
274
- // Token 自动刷新成功时触发
275
- })
276
-
277
- // 取消监听
278
- unsubscribe()
279
- ```
96
+ ## 错误处理
280
97
 
281
- ## 流式响应(SSE)
98
+ 业务 API 返回 `HaiResult<T>`:
282
99
 
283
100
  ```ts
284
- const controller = new AbortController()
285
-
286
- for await (const chunk of api.stream('/ai/chat/stream', { message: 'hello' }, { signal: controller.signal })) {
287
- // chunk 对应 SSE 的 data: 内容
288
- process.stdout.write(chunk)
101
+ const result = await apiClient.iam.auth.currentUser()
102
+ if (!result.success) {
103
+ // result.error.code / result.error.message
289
104
  }
290
-
291
- // 主动停止流式响应
292
- controller.abort()
293
105
  ```
294
106
 
295
- 说明:
296
-
297
- - `stream()` 内置超时控制(连接阶段与流式读取阶段均生效)
298
- - 401 会尝试自动刷新 Token 后重试一次
299
- - 解析器支持跨 chunk 的 SSE 行缓冲
300
- - 支持外部传入 `AbortSignal` 主动取消
301
-
302
- ## 拦截器(高级用法)
303
-
304
- ```ts
305
- await api.init({
306
- baseUrl: 'https://api.example.com/api/v1',
307
- auth: { refreshUrl: '/auth/refresh' },
308
- interceptors: {
309
- request: [
310
- async config => ({
311
- ...config,
312
- headers: {
313
- ...config.headers,
314
- 'X-App-Version': '1.0.0',
315
- },
316
- }),
317
- ],
318
- response: [
319
- async (response) => {
320
- // 可统一记录埋点、限流处理等
321
- return response
322
- },
323
- ],
324
- },
325
- })
326
- ```
107
+ 未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
327
108
 
328
109
  ## 测试
329
110
 
330
111
  ```bash
331
112
  pnpm --filter @h-ai/api-client test
113
+ pnpm --filter @h-ai/api-client typecheck
332
114
  ```
333
115
 
334
116
  ## License
335
117
 
336
118
  Apache-2.0
337
-
338
- ## License
339
-
340
- Apache-2.0