@h-ai/cli 0.1.0-alpha.12 → 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.
@@ -1,355 +1,154 @@
1
1
  ---
2
2
  name: hai-api-client
3
- description: 使用 @h-ai/api-client 单例模式构建多端共用的纯 TypeScript HTTP 客户端,通过 api.init() 初始化,支持 Bearer Token 自动管理、401 刷新重试、契约调用(EndpointDef)与流式响应;当需求涉及客户端 API 请求、Token 管理、契约调用或多端数据层时使用。
3
+ description: 使用 @h-ai/api-client 构建多端共用的 oRPC/OpenAPI typed client,通过 apiClient.init() 初始化,支持 Bearer Token 自动管理、401 刷新重试与 contract 嵌套方法调用;当需求涉及客户端 API 请求、Token 管理或多端数据层时使用。
4
4
  ---
5
5
 
6
6
  # hai-api-client
7
7
 
8
- > `@h-ai/api-client` 是 hai-framework 的纯 TypeScript HTTP 客户端,多端共用(Web/H5/Android),基于 fetch API 实现。以单例模式暴露 `api` 对象,通过 `api.init()` 初始化。核心能力:Token 自动附加、401 自动刷新重试、基于 EndpointDef 的类型安全契约调用。
8
+ > `@h-ai/api-client` 是跨端 typed API client。公共 HTTP API 由 `@h-ai/api-contract` 定义,服务端由 `@h-ai/serv` 挂载,客户端直接调用 `apiClient.<domain>.<group>.<operation>()`。
9
9
 
10
- ---
11
-
12
- ## 运行环境
13
-
14
- **浏览器端专用。** 服务端 `+page.server.ts` / `+server.ts` 应直接调用模块 API(如 `iam.auth.login()`),不经过 HTTP 自环。
15
-
16
- ### 与 kit.client 的分工
17
-
18
- | 场景 | 推荐 | 原因 |
19
- |------|------|------|
20
- | SSR SvelteKit 应用(同源请求) | `kit.client.create().apiFetch`(见 hai-kit SKILL) | 自动 CSRF、传输加密、同源 Cookie 透传 |
21
- | SPA / 原生 App(跨域请求) | **本模块** `api.init()` + `api.call()` | 完整 Token 管理、401 自动刷新、独立 baseUrl |
22
- | 契约调用(类型安全,跨域) | **本模块** `api.call(endpoint, input)` | EndpointDef 双向 Zod 校验 |
23
-
24
- ---
25
-
26
- ## 适用场景
10
+ ## 使用步骤
27
11
 
28
- - 浏览器端/App 端统一 API 请求
29
- - Bearer Token 自动管理(存储、附加、刷新)
30
- - 基于 EndpointDef 契约的类型安全调用
31
- - 流式响应(SSE / ReadableStream)
32
- - 拦截器(请求/响应前后处理)
12
+ ### 1. 初始化
33
13
 
34
- ---
14
+ 浏览器默认使用 httpOnly cookie 存储(推荐):
35
15
 
36
- ## 使用步骤
16
+ ```ts
17
+ import { apiClient } from '@h-ai/api-client'
37
18
 
38
- ### 1. 初始化客户端
39
-
40
- ```typescript
41
- import { api } from '@h-ai/api-client'
42
-
43
- // 应用启动时初始化
44
- await api.init({
45
- baseUrl: 'http://localhost:3000',
46
- auth: {
47
- refreshUrl: '/auth/refresh',
48
- // Token 刷新失败回调(可选,常用于跳转登录页)
49
- onRefreshFailed: () => {
50
- window.location.href = '/login'
51
- },
52
- },
53
- timeout: 15000,
19
+ await apiClient.init({
20
+ baseUrl: 'https://api.example.com/api/v1',
21
+ auth: {},
54
22
  })
55
23
  ```
56
24
 
57
- > `auth.storage` 可选:默认使用 `createLocalStorageTokenStorage()`;SSR/测试建议显式传 `createMemoryTokenStorage()`,App 端建议传 `createCapacitorTokenStorage()`。
25
+ 启用传输加密时,只传顶层 `crypto` 句柄;api-client 内部自动调用 `crypto.transport.createClient()`:
58
26
 
59
- ### 2. 普通请求
27
+ ```ts
28
+ import { apiClient } from '@h-ai/api-client'
29
+ import { crypto } from '@h-ai/crypto'
60
30
 
61
- ```typescript
62
- // GET 请求
63
- const users = await api.get<User[]>('/api/v1/users')
31
+ await crypto.init()
64
32
 
65
- // POST 请求
66
- const result = await api.post<CreateResult>('/api/v1/users', {
67
- username: 'alice',
68
- email: 'alice@example.com',
33
+ await apiClient.init({
34
+ baseUrl: 'https://api.example.com/api/v1',
35
+ auth: {},
36
+ transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
69
37
  })
70
-
71
- // 带查询参数(直接传入 Record<string, unknown>)
72
- const page = await api.get<PageResult>('/api/v1/users', { page: 1, pageSize: 20 })
73
38
  ```
74
39
 
75
- ### 3. 契约调用(推荐)
40
+ App/Capacitor 场景传入自定义 TokenStorage:
76
41
 
77
- ```typescript
78
- import { iamEndpoints } from '@h-ai/iam/api'
42
+ ```ts
43
+ import { createCapacitorTokenStorage } from '@h-ai/capacitor'
79
44
 
80
- // 类型安全:入参和返回值由 EndpointDef 的 Zod schema 推导,入参/出参双向校验
81
- const loginResult = await api.call(iamEndpoints.login, {
82
- identifier: 'admin',
83
- password: 'StrongPassword123',
45
+ await apiClient.init({
46
+ baseUrl: 'https://api.example.com/api/v1',
47
+ auth: { storage: createCapacitorTokenStorage() },
84
48
  })
85
-
86
- if (loginResult.success) {
87
- // loginResult.data 类型自动推导为 { user, tokens, agreements? }
88
- await api.auth.setTokens(loginResult.data.tokens)
89
- }
90
49
  ```
91
50
 
92
- ### 4. Token 管理
93
-
94
- ```typescript
95
- // 手动设置 Token(登录后)
96
- await api.auth.setTokens({
97
- accessToken: 'xxx',
98
- refreshToken: 'yyy',
99
- expiresIn: 3600,
100
- tokenType: 'Bearer',
101
- })
51
+ SSR / Node.js 测试场景请显式传入内存存储:
102
52
 
103
- // 清除 Token(登出)
104
- await api.auth.clear()
53
+ ```ts
54
+ import { apiClient } from '@h-ai/api-client'
105
55
 
106
- // 监听 Token 刷新
107
- api.auth.onTokenRefreshed((tokens) => {
108
- // tokens 包含新的 accessToken / refreshToken
56
+ await apiClient.init({
57
+ baseUrl: 'https://api.example.com/api/v1',
58
+ auth: { storage: apiClient.tokenStorage.memory() },
109
59
  })
110
60
  ```
111
61
 
112
- ### 5. 流式响应
113
-
114
- ```typescript
115
- const controller = new AbortController()
62
+ ### 2. 调用 typed API
116
63
 
117
- // stream(path, body?, options?) — 返回 AsyncIterable<string>,按 SSE data: 行 yield
118
- for await (const chunk of api.stream('/api/v1/ai/chat', { message: 'Hello' }, { signal: controller.signal })) {
119
- // 每个 chunk 是 SSE data: 行的内容
64
+ ```ts
65
+ const login = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })
66
+ if (login.success) {
67
+ // 默认 httpOnly cookie 模式下,refresh token 由服务端 Set-Cookie 管理。
68
+ await apiClient.auth.setTokens(login.data.tokens)
120
69
  }
121
70
 
122
- // 主动停止
123
- controller.abort()
124
- ```
125
-
126
- ### 6. 关闭
127
-
128
- ```typescript
129
- await api.close()
71
+ const me = await apiClient.iam.auth.currentUser()
72
+ const upload = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
73
+ const answer = await apiClient.ai.chats.sendMessage({ message: 'hello' })
130
74
  ```
131
75
 
132
- ---
76
+ ### 3. 自定义 contract
133
77
 
134
- ## 核心 API
78
+ ```ts
79
+ import { apiClient } from '@h-ai/api-client'
80
+ import { apiContract } from '@h-ai/api-contract'
135
81
 
136
- | API | 用途 | 关键点 |
137
- | ---------------------------------- | -------------- | ------------------------------------- |
138
- | `api.init(config)` | 初始化客户端 | 返回 `HaiResult<void>` |
139
- | `api.close()` | 关闭并释放资源 | 重复调用安全 |
140
- | `api.isInitialized` | 初始化状态 | `boolean` |
141
- | `api.config` | 当前配置 | 未初始化时为 `null` |
142
- | `api.get<T>(path, params?)` | GET 请求 | params 附加到 URL query string |
143
- | `api.post<T>(path, body?)` | POST 请求 | body 自动 JSON 序列化 |
144
- | `api.put<T>(path, body?)` | PUT 请求 | — |
145
- | `api.patch<T>(path, body?)` | PATCH 请求 | — |
146
- | `api.delete<T>(path, params?)` | DELETE 请求 | params 附加到 URL query string |
147
- | `api.call(endpoint, input)` | 契约调用 | 入参/出参 Zod 双向校验,路径/方法由契约决定 |
148
- | `api.upload(path, file, options?)` | 文件上传 | FormData,支持附加字段 |
149
- | `api.stream(path, body?)` | 流式请求 | 返回 AsyncIterable<string>(SSE) |
150
- | `api.auth.setTokens(tokens)` | 设置 Token | 存入 TokenStorage |
151
- | `api.auth.clear()` | 清空 Token | 清空存储 |
152
- | `api.auth.onTokenRefreshed(cb)` | 刷新回调 | 返回取消订阅函数,Token 自动刷新成功时通知 |
153
-
154
- ### EndpointDef 契约结构
155
-
156
- ```typescript
157
- interface EndpointDef<TInput, TOutput> {
158
- method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
159
- path: string
160
- input: ZodSchema<TInput>
161
- output: ZodSchema<TOutput>
162
- requireAuth?: boolean
163
- meta?: { summary?: string; tags?: string[] }
164
- }
82
+ const contract = apiContract.create({ iam: apiContract.iam })
83
+ const client = apiClient.create(contract)
84
+ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
165
85
  ```
166
86
 
167
- ### TokenStorage 接口
87
+ ### 4. 关闭
168
88
 
169
- ```typescript
170
- interface TokenStorage {
171
- getAccessToken: () => Promise<string | null>
172
- setAccessToken: (token: string) => Promise<void>
173
- getRefreshToken: () => Promise<string | null>
174
- setRefreshToken: (token: string) => Promise<void>
175
- clear: () => Promise<void>
176
- }
89
+ ```ts
90
+ await apiClient.close()
177
91
  ```
178
92
 
179
- 内置实现:`createLocalStorageTokenStorage()`(Web)、`createMemoryTokenStorage()`(SSR/测试)。
180
- App 端使用 `@h-ai/capacitor` 的 `createCapacitorTokenStorage()`。
93
+ ## Token Storage 适配器
181
94
 
182
- ---
95
+ | 工厂调用 | 存储位置 | 适用场景 |
96
+ | --- | --- | --- |
97
+ | `apiClient.tokenStorage.httpOnlyCookie()` | httpOnly cookie(服务端管理) | **默认**;浏览器端推荐,refresh token 不暴露给 JS |
98
+ | `apiClient.tokenStorage.memory()` | 内存 | SSR / Node.js 单元测试(需显式传入) |
99
+ | `apiClient.tokenStorage.localStorage()` | localStorage | 非敏感场景(有 XSS 风险,生产不推荐) |
183
100
 
184
- ## 错误码 — `HaiApiClientError`
185
-
186
- | 错误码 | code | 说明 |
187
- |--------|------|------|
188
- | `HaiApiClientError.NETWORK_ERROR` | `hai:api-client:001` | 网络不可达 |
189
- | `HaiApiClientError.TIMEOUT` | `hai:api-client:002` | 请求超时 |
190
- | `HaiApiClientError.SERVER_ERROR` | `hai:api-client:003` | 5xx 服务端错误 |
191
- | `HaiApiClientError.UNAUTHORIZED` | `hai:api-client:004` | 401 未认证 |
192
- | `HaiApiClientError.FORBIDDEN` | `hai:api-client:005` | 403 无权限 |
193
- | `HaiApiClientError.NOT_FOUND` | `hai:api-client:006` | 404 资源不存在 |
194
- | `HaiApiClientError.VALIDATION_FAILED` | `hai:api-client:007` | 请求参数校验失败 |
195
- | `HaiApiClientError.TOKEN_REFRESH_FAILED` | `hai:api-client:008` | Token 刷新失败 |
196
- | `HaiApiClientError.NOT_INITIALIZED` | `hai:api-client:010` | 未初始化 |
197
- | `HaiApiClientError.CONFIG_ERROR` | `hai:api-client:011` | 配置错误 |
198
- | `HaiApiClientError.UNKNOWN` | `hai:api-client:099` | 未知错误 |
101
+ ### httpOnly Cookie 模式
199
102
 
200
- ---
103
+ Access token 存在内存中,refresh token 由服务端通过 `HttpOnly` cookie 管理,浏览器 JS 无法读取,可有效防止 XSS 窃取 token。
201
104
 
202
- ## API 契约范式(端到端类型安全)
105
+ **前提**:服务端需配置 `serv.createApp({ iam, refreshCookie: {} })`,参见 `hai-serv` skill。
203
106
 
204
- > `@h-ai/api-client` 和 `@h-ai/kit` 共同消费模块 `api/` 下的 `EndpointDef` 定义,实现客户端↔服务端的全链路类型安全 + 运行时 Zod 校验。
107
+ 启用 `transport` 时,登录、业务请求以及 401 后的自动刷新都会复用同一个加密 fetch 链路;不要在业务代码中手写明文 `/auth/refresh` 请求。
205
108
 
206
- ### 契约架构
109
+ ```ts
110
+ import { apiClient } from '@h-ai/api-client'
207
111
 
208
- ```
209
- ┌─────────────────────────────────────────────────────────────┐
210
- │ @h-ai/xx/api │
211
- │ xx-api-schemas.ts ← Zod Schema(唯一真相源) │
212
- │ xx-api-contract.ts ← xxEndpoints(method + path + schema)│
213
- └─────────────┬──────────────────────────┬────────────────────┘
214
- │ │
215
- ┌────────▼────────┐ ┌─────────▼─────────┐
216
- │ 客户端(浏览器)│ │ 服务端(SvelteKit)│
217
- │ api.call(ep, i) │ │ kit.fromContract │
218
- │ @h-ai/api-client│ │ @h-ai/kit │
219
- └────────┬────────┘ └─────────┬─────────┘
220
- │ HTTP(类型安全) │
221
- └──────────────────────────┘
222
- ```
223
-
224
- ### 客户端流程(本模块责任)
225
-
226
- 1. 从 `@h-ai/xx/api` 导入 `xxEndpoints`
227
- 2. `api.call(xxEndpoints.xxx, input)` → Zod 校验入参 → 按 method/path 发起 HTTP → Zod 校验出参
228
- 3. 入参或出参校验失败均返回 `VALIDATION_FAILED`(1206)
229
- 4. 返回 `HaiResult<TOutput>`
230
-
231
- ```typescript
232
- import { storageEndpoints } from '@h-ai/storage/api'
233
- import { iamEndpoints } from '@h-ai/iam/api'
234
- import { aiEndpoints } from '@h-ai/ai/api'
235
-
236
- // 存储:获取上传签名 URL
237
- const upload = await api.call(storageEndpoints.presignUpload, {
238
- key: 'avatars/user-1.png',
239
- contentType: 'image/png',
240
- })
241
-
242
- // IAM:登录
243
- const login = await api.call(iamEndpoints.login, {
244
- username: 'admin',
245
- password: 'xxx',
246
- })
247
-
248
- // AI:发送消息
249
- const chat = await api.call(aiEndpoints.sendMessage, {
250
- conversationId: 'conv-1',
251
- content: 'Hello',
112
+ // apiClient.tokenStorage.httpOnlyCookie() 是默认存储,无需显式指定
113
+ await apiClient.init({
114
+ baseUrl: 'https://api.example.com/api/v1',
115
+ auth: {},
252
116
  })
253
- ```
254
-
255
- ### 已有契约模块一览
256
-
257
- | 模块 | 导入路径 | 端点对象 | 典型端点 |
258
- | ---------- | -------------------- | -------------------- | ------------------------------- |
259
- | `storage` | `@h-ai/storage/api` | `storageEndpoints` | presignUpload, listFiles, … |
260
- | `iam` | `@h-ai/iam/api` | `iamEndpoints` | login, logout, currentUser, … |
261
- | `ai` | `@h-ai/ai/api` | `aiEndpoints` | chat, chatStream, sendMessage |
262
- | `payment` | `@h-ai/payment/api` | `paymentEndpoints` | createOrder, queryOrder, … |
263
-
264
117
 
265
- ## 常见模式
266
-
267
- ### 与 @h-ai/iam/api 契约配合
268
-
269
- ```typescript
270
- import { iamEndpoints } from '@h-ai/iam/api'
271
-
272
- // 登录(入参字段为 identifier + password)
273
- const login = await api.call(iamEndpoints.login, { identifier, password })
118
+ // 登录后设置 access token(refresh token 由服务端写入 cookie,前端无需操作)
119
+ const login = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })
274
120
  if (login.success) {
275
- // login.data 类型:{ user, tokens, agreements? }
276
- await api.auth.setTokens(login.data.tokens)
121
+ await apiClient.auth.setTokens(login.data.tokens)
277
122
  }
123
+ ```
278
124
 
279
- // 获取当前用户(自动携带 Bearer Token)
280
- const me = await api.call(iamEndpoints.currentUser, {})
281
- // me.data 类型:{ user, roles, permissions }
125
+ ## 核心 API
282
126
 
283
- // 修改密码
284
- await api.call(iamEndpoints.changePassword, { oldPassword, newPassword })
127
+ | API | 用途 |
128
+ | --- | --- |
129
+ | `apiClient.init(config)` | 初始化默认 API Service client |
130
+ | `apiClient.close()` | 清理 client 状态 |
131
+ | `apiClient.auth.setTokens(tokens)` | 写入 access token;非 httpOnly 存储会同时写入 refresh token |
132
+ | `apiClient.auth.clear()` | 清理 Token |
133
+ | `apiClient.auth.onTokenRefreshed(cb)` | 监听刷新结果 |
134
+ | `apiClient.create(contract)` | 创建自定义 typed client |
135
+ | `apiClient.tokenStorage.{memory,localStorage,httpOnlyCookie}()` | TokenStorage 工厂 |
285
136
 
286
- // 登出
287
- const accessToken = '...' // 从 Token 存储获取
288
- await api.call(iamEndpoints.logout, { accessToken })
289
- await api.auth.clear()
290
- ```
137
+ ### Transport 配置
291
138
 
292
- #### 401 自动刷新机制
293
-
294
- 当请求收到 401 响应时,客户端自动执行:
295
-
296
- 1. 使用存储的 `refreshToken` 调用 `auth.refreshUrl`(POST)
297
- 2. 刷新成功 → 更新存储 → 用新 Token 重试原请求(仅重试一次)
298
- 3. 刷新失败 → 清空 Token → 触发 `onRefreshFailed` 回调
299
- 4. 多个并发 401 → **自动去重**,只发一次刷新请求
300
-
301
- > 流式请求(`api.stream()`)同样支持 401 自动刷新 + 重试。
302
-
303
- #### 可用的 IAM 端点
304
-
305
- | 分类 | 端点 | 方法 | 路径 | 认证 |
306
- |------|------|------|------|------|
307
- | 认证 | `iamEndpoints.login` | POST | /auth/login | 否 |
308
- | | `iamEndpoints.loginWithOtp` | POST | /auth/login/otp | 否 |
309
- | | `iamEndpoints.logout` | POST | /auth/logout | 是 |
310
- | | `iamEndpoints.currentUser` | GET | /auth/me | 是 |
311
- | | `iamEndpoints.refreshToken` | POST | /auth/refresh | 否 |
312
- | | `iamEndpoints.sendOtp` | POST | /auth/otp/send | 否 |
313
- | | `iamEndpoints.register` | POST | /auth/register | 否 |
314
- | | `iamEndpoints.changePassword` | POST | /auth/change-password | 是 |
315
- | | `iamEndpoints.updateCurrentUser` | PUT | /auth/me | 是 |
316
- | 用户管理 | `iamEndpoints.listUsers` | GET | /iam/users | 是 |
317
- | | `iamEndpoints.getUser` | GET | /iam/users/:id | 是 |
318
- | | `iamEndpoints.createUser` | POST | /iam/users | 是 |
319
- | | `iamEndpoints.updateUser` | PUT | /iam/users/:id | 是 |
320
- | | `iamEndpoints.deleteUser` | DELETE | /iam/users/:id | 是 |
321
- | | `iamEndpoints.adminResetPassword` | POST | /iam/users/:id/reset-password | 是 |
322
- | 角色管理 | `iamEndpoints.listRoles` | GET | /iam/roles | 是 |
323
- | | `iamEndpoints.getRole` | GET | /iam/roles/:id | 是 |
324
- | | `iamEndpoints.createRole` | POST | /iam/roles | 是 |
325
- | | `iamEndpoints.updateRole` | PUT | /iam/roles/:id | 是 |
326
- | | `iamEndpoints.deleteRole` | DELETE | /iam/roles/:id | 是 |
327
- | 权限管理 | `iamEndpoints.listPermissions` | GET | /iam/permissions | 是 |
328
- | | `iamEndpoints.getPermission` | GET | /iam/permissions/:id | 是 |
329
- | | `iamEndpoints.createPermission` | POST | /iam/permissions | 是 |
330
- | | `iamEndpoints.deletePermission` | DELETE | /iam/permissions/:id | 是 |
331
-
332
- ### Android App 中使用
333
-
334
- ```typescript
335
- import { api } from '@h-ai/api-client'
336
- import { createCapacitorTokenStorage } from '@h-ai/capacitor'
337
-
338
- // 应用启动时初始化
339
- await api.init({
340
- baseUrl: import.meta.env.PUBLIC_API_BASE,
341
- auth: {
342
- storage: createCapacitorTokenStorage(),
343
- refreshUrl: '/api/v1/auth/refresh',
344
- },
345
- })
346
- ```
139
+ | 字段 | 说明 |
140
+ | --- | --- |
141
+ | `transport.crypto` | 已初始化的 `@h-ai/crypto` 实例 |
142
+ | `transport.keyExchangePath` | 相对于 `baseUrl` 的协商子路径,默认 `/_hai/key-exchange` |
347
143
 
348
- ---
144
+ 服务端必须对应启用 `serv.createApp({ transport: { crypto } })`。
145
+ 自动刷新会沿用 `transport.encryptedFetch`,并在 httpOnly cookie 模式下发送空请求体 + `credentials: 'include'`,由服务端从 cookie 读取 refresh token。
349
146
 
350
- ## 相关 Skills
147
+ ## 常见模式
351
148
 
352
- - `hai-iam`:认证契约定义(iamEndpoints)
353
- - `hai-capacitor`:原生 Token 存储
354
- - `hai-kit`:服务端契约处理(kit.fromContract)
355
- - `hai-core`:HaiResult 类型、错误处理基础
149
+ - `baseUrl` 通常包含服务端 `apiPrefix`,例如 `/api/v1`。
150
+ - 业务错误不 throw,统一判断 `HaiResult.success`。
151
+ - 客户端不导入服务端 `procedures` 或 `@h-ai/serv`。
152
+ - 客户端不导入 `@h-ai/crypto` 的内部 transport 工厂,只通过 `transport: { crypto }` 装配。
153
+ - 前端只依赖 `@h-ai/api-client` 和 contract 类型,不依赖业务模块实现。
154
+ - httpOnly cookie 模式需服务端与客户端同步配置,不支持跨域刷新(`SameSite=Strict`)。