@h-ai/api-client 0.1.0-alpha9 → 1.0.0-beta.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
@@ -1,340 +1,210 @@
1
1
  # @h-ai/api-client
2
2
 
3
- 统一 HTTP 客户端模块,提供 Bearer Token 管理、契约调用与流式请求能力。
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
4
 
5
- ## 支持的能力
5
+ 普通请求与 Token 刷新默认使用当前 `core.i18n` 语言作为 `Accept-Language`,服务端错误随界面语言切换。可通过 `headers` 显式覆盖该请求头。
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
+ [框架首页](../../README.md) · 相关模块:[@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md)
16
8
 
17
- ## 快速开始
9
+ ## 简介
10
+
11
+ hai-framework 的跨端 oRPC/OpenAPI typed client,面向 Web、App、小程序等运行时提供统一 API 调用与 Token 管理。
12
+
13
+ ## 核心功能清单
14
+
15
+ - `apiClient`:默认绑定 iam/storage/ai 领域的统一 typed client 入口。
16
+ - `apiClient.create(contract)`:为自定义 contract 创建 typed client。
17
+ - `apiClient.tokenStorage.{memory,httpOnlyCookie}()`:Token 存储工厂。浏览器使用内存 access token + 服务端 httpOnly refresh Cookie;原生端注入平台安全存储,不将凭据写入 Web Storage。
18
+ - 支持自定义 `fetch`,适配浏览器、Node、Capacitor、小程序桥接层。
19
+ - 支持 Bearer Token 自动注入、401 后刷新并重试一次。
20
+ - 网络错误统一转换为 `HaiResult` 错误。
21
+ - 可选传输加密:`apiClient.init({ transport: { crypto } })` 自动使用 `crypto.transport.createClient()`。
22
+ - 传输加密与自动刷新可同时启用:401 后的 `/auth/refresh` 会继续走 encrypted fetch,不降级明文。
23
+
24
+ ## 使用入门
25
+
26
+ ### 快速开始
18
27
 
19
28
  ```ts
20
- import { api } from '@h-ai/api-client'
21
- import { iamEndpoints } from '@h-ai/iam/api'
29
+ import { apiClient } from '@h-ai/api-client'
22
30
 
23
- // 1. 初始化
24
- await api.init({
31
+ await apiClient.init({
25
32
  baseUrl: 'https://api.example.com/api/v1',
26
- auth: { refreshUrl: '/auth/refresh' },
33
+ auth: {}, // 默认使用 httpOnly cookie 存储(推荐);SSR 测试请显式传入 apiClient.tokenStorage.memory()
27
34
  })
28
35
 
29
- // 2. 登录(契约调用)
30
- const loginResult = await api.call(iamEndpoints.login, {
36
+ const login = await apiClient.iam.auth.login({
31
37
  identifier: 'alice',
32
- password: 'xxx',
38
+ password: 'secret',
33
39
  })
34
40
 
35
- // 3. 登录成功后保存 Token(后续请求自动附加 Bearer)
36
- if (loginResult.success) {
37
- await api.auth.setTokens(loginResult.data.tokens)
41
+ if (login.success) {
42
+ // httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
43
+ // api-client 默认存储只需要把 access token 写入内存。
44
+ await apiClient.auth.setTokens(login.data.tokens)
38
45
  }
39
46
 
40
- // 4. 后续请求自动携带 Token
41
- const me = await api.call(iamEndpoints.currentUser, {})
47
+ const me = await apiClient.iam.auth.currentUser()
48
+ if (me.success) {
49
+ // 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
50
+ const { roles, permissions, ...user } = me.data
51
+ }
42
52
 
43
- // 5. 关闭
44
- await api.close()
53
+ await apiClient.close()
45
54
  ```
46
55
 
47
- > `auth.storage` 未传时默认使用 `createLocalStorageTokenStorage()`;在 SSR/测试环境可显式传入 `createMemoryTokenStorage()`。
48
-
49
- ## 与 @h-ai/iam 集成
50
-
51
- `@h-ai/api-client` 与 `@h-ai/iam` 通过 **API 契约(`EndpointDef`)** 实现端到端类型安全。客户端和服务端共享同一份契约定义,编译时保证一致性。
56
+ #### 启用传输加密
52
57
 
53
- ### 架构总览
58
+ 服务端需先启用 `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })`;客户端只注入同一个 `@h-ai/crypto` 服务实例,无需手写密钥协商代码。
54
59
 
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
- ### 完整登录 → 使用 → 登出流程
60
+ 服务端必须注入受管 serverKeyRing 和共享 registry;客户端只接收 serverKeyId/公钥,并在密文请求带回版本。密钥环/私钥不进入客户端配置。旧版本撤销后,销毁 transport 会话并重新协商;不要自动重放非幂等业务。
71
61
 
72
62
  ```ts
73
- import { api } from '@h-ai/api-client'
74
- import { iamEndpoints } from '@h-ai/iam/api'
63
+ import { apiClient } from '@h-ai/api-client'
64
+ import { crypto } from '@h-ai/crypto'
75
65
 
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
- })
66
+ await crypto.init()
91
67
 
92
- // ── 登录 ──
93
- const loginResult = await api.call(iamEndpoints.login, {
94
- identifier: 'alice',
95
- password: 'StrongPassword123',
68
+ await apiClient.init({
69
+ baseUrl: 'https://api.example.com/api/v1',
70
+ auth: {},
71
+ transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
96
72
  })
97
73
 
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
74
+ await apiClient.close()
75
+ await crypto.close()
76
+ ```
108
77
 
109
- // 保存 Token — 后续所有请求自动附加 Authorization: Bearer <accessToken>
110
- await api.auth.setTokens(tokens)
111
- }
78
+ 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
112
79
 
113
- // ── 获取当前用户 ──(自动携带 Bearer Token)
114
- const meResult = await api.call(iamEndpoints.currentUser, {})
115
- if (meResult.success) {
116
- // meResult.data 类型推导为 { user, roles, permissions }
117
- }
80
+ 空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
118
81
 
119
- // ── 修改密码 ──
120
- await api.call(iamEndpoints.changePassword, {
121
- oldPassword: 'OldPass123',
122
- newPassword: 'NewPass456',
123
- })
82
+ ### API 契约
124
83
 
125
- // ── 登出 ──
126
- const accessToken = '...' // 从 Token 存储获取
127
- await api.call(iamEndpoints.logout, { accessToken })
128
- await api.auth.clear() // 清空本地 Token 存储
129
- ```
84
+ 默认 `apiClient.ai.chats.createCompletion({ messages, tools, tool_choice })` 使用非流式 JSON 契约,完整传递 function 工具的嵌套 JSON Schema 与 `strict`。先判断 `success`,再读取 `data.choices[].message.tool_calls`;业务方授权执行后,把助手工具调用和带对应 `tool_call_id` 的工具结果放入下一次 `messages`。输入不支持 `stream`,即使绕过 TypeScript 传入也会被服务端拒绝;流式接口须绑定应用自己的流契约。
130
85
 
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)
86
+ 自定义应用可以绑定自己的 oRPC contract:
174
87
 
175
88
  ```ts
176
- import { api } from '@h-ai/api-client'
89
+ import { apiClient } from '@h-ai/api-client'
90
+ import { apiContract } from '@h-ai/api-contract'
177
91
 
178
- await api.init({
179
- baseUrl: 'https://api.example.com/api/v1',
180
- auth: { refreshUrl: '/auth/refresh' },
181
- timeout: 15000,
182
- })
92
+ const contract = apiContract.create({ iam: apiContract.iam })
93
+ const client = apiClient.create(contract)
94
+
95
+ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
96
+ const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
183
97
  ```
184
98
 
185
- ### Android / iOS App(Capacitor 安全存储)
99
+ ### API 概览
186
100
 
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
- })
198
- ```
101
+ - `apiClient.init(config)`:初始化默认 client。
102
+ - `apiClient.close()`:清理 client 状态。
103
+ - `apiClient.auth.setTokens(tokens)`:替换凭据,写入 access token;非 httpOnly 存储写入提供的 refresh token,缺省时清除旧值。
104
+ - `apiClient.auth.clear()`:清理 token。
105
+ - `apiClient.create(contract)`:创建自定义 typed client。
199
106
 
200
- ### SSR / 测试环境(内存存储)
107
+ `auth.clear()` 和 `auth.setTokens()` 会立即取消并作废当前客户端的旧刷新请求。迟到的成功或 401 响应不会恢复已退出会话、覆盖新账号或触发旧刷新回调;存储写入串行提交,方法完成后不会再被之前已开始的异步写入覆盖。退出和切换账号须通过这些方法更新凭据,不直接修改 storage。httpOnly Cookie 和服务端会话仍由服务端退出接口撤销。
201
108
 
202
- ```ts
203
- import { api, createMemoryTokenStorage } from '@h-ai/api-client'
109
+ 已初始化后再次调用 `init()` 会返回 `CONFIG_ERROR`,不会覆盖现有 client;如需切换 `baseUrl` / `fetch` / `transport`,请先 `await apiClient.close()` 再重新初始化。
204
110
 
205
- await api.init({
206
- baseUrl: 'https://api.example.com/api/v1',
207
- auth: {
208
- storage: createMemoryTokenStorage(),
209
- refreshUrl: '/auth/refresh',
210
- },
211
- })
212
- ```
111
+ ### 配置
213
112
 
214
- ## 错误处理
113
+ - `baseUrl`:API 基础地址,通常包含 `/api/v1`。
114
+ - `auth.storage`:Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(浏览器推荐);SSR / 测试场景请显式传入 `apiClient.tokenStorage.memory()`。
115
+ - `auth.refreshPath`:刷新 token 路径,默认 `/auth/refresh`。
116
+ - `timeout`:请求超时,默认 30000ms。
117
+ - `headers`:静态或动态公共请求头。
118
+ - `fetch`:自定义 fetch 实现。
119
+ - `transport.crypto`:启用透明请求/响应加解密,必须传入已初始化的 `@h-ai/crypto` 实例。
120
+ - `transport.keyExchangePath`:密钥协商子路径,默认 `/_hai/key-exchange`;会自动拼接到 `baseUrl` 后。
121
+ - `apiClient.config`:返回**脱敏后的运行配置快照**;`baseUrl` 的内嵌凭证、`headers.authorization` 等敏感值会自动隐藏。
215
122
 
216
- 所有通用 HTTP / 契约调用均返回 `HaiResult<T>`:
123
+ ### 配置驱动认证页面
217
124
 
218
- ```ts
219
- const result = await api.call(iamEndpoints.currentUser, {})
125
+ 密码登录只提交 `identifier` 和 `password`;`AuthPanel` 将 `LoginForm` 的 `username` 映射为 `identifier`。会话有效期由服务端 `session.maxAge` / `session.refreshTokenMaxAge` 配置决定,登录表单不提供按次登录的持久会话选项。
126
+
127
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
128
+
129
+ `apiClient.createAuthActions(client)` 为含标准 iam contract 的客户端提供 publicConfig、login、register、sendOtp、requestPasswordReset、confirmPasswordReset。login 输入使用 identifier/password/method/code,method 为 password、ldap、email、sms。成功认证自动调用既有安全 TokenStorage;应用只需把动作传给 AuthPanel,并在 onauthenticated 中刷新自己的用户状态或导航。
220
130
 
131
+ [完整 IAM 配置 / IAM configuration](../iam/README.md)
132
+
133
+ ### 错误处理
134
+
135
+ 业务 API 返回 `HaiResult<T>`:
136
+
137
+ ```ts
138
+ const result = await apiClient.iam.auth.currentUser()
221
139
  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
140
+ // result.error.code / result.error.message
141
+ }
142
+ else {
143
+ // result.data = User & { roles: string[], permissions: string[] }
236
144
  }
237
-
238
- // result.data 类型安全
239
- renderUserProfile(result.data.user)
240
145
  ```
241
146
 
242
- ### 错误码速查
147
+ 未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
243
148
 
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` | 未知错误 |
149
+ 服务端以非 2xx HTTP 状态返回合法的失败 `HaiResult` 时,客户端保留业务错误码、消息和 `traceId`,例如 409 冲突仍可提示刷新后重试。`httpStatus` 以真实 HTTP 响应为准;5xx 的内部诊断仍被隐藏,正文不能通过声称 400 绕过脱敏。非框架响应继续使用客户端错误映射。
257
150
 
258
- ## Token 管理
151
+ 自动刷新在真实 HTTP 401(`Response.status`)或 HTTP 200 的有效失败 HaiResult(`success=false`、`error.code/message` 为字符串且 `error.httpStatus=401`)后触发,刷新成功后只重试一次原请求。内置登录、注册与配置的 `auth.refreshPath` 不触发自动刷新,403/500 等其它失败也不会触发。serv 普通 procedure 的 HTTP 状态仍为 200;调用方须检查最终结果的 `success` 与 `error.code`,并处理刷新失败后的登录失效。
259
152
 
260
- ```ts
261
- // 设置 Token(通常在登录成功后)
262
- await api.auth.setTokens({
263
- accessToken: 'xxx',
264
- refreshToken: 'yyy',
265
- expiresIn: 3600,
266
- tokenType: 'Bearer',
267
- })
153
+ ## 输入约束与 HTTP 能力
268
154
 
269
- // 清空 Token(登出时)
270
- await api.auth.clear()
155
+ 存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](../storage/README.md#存储输入约束与跨端能力)。
271
156
 
272
- // 监听自动刷新事件
273
- const unsubscribe = api.auth.onTokenRefreshed((tokens) => {
274
- // Token 自动刷新成功时触发
275
- })
157
+ ## 逻辑请求观察
276
158
 
277
- // 取消监听
159
+ `client.observeRequests` 返回可取消的逻辑请求观察:requestId、procedure、耗时、HTTP 状态、最终 success/errorCode 与实际业务请求 attempts。HTTP 200 的失败 HaiResult 也记为失败,自动刷新后的重试保留同一 requestId,整个逻辑调用只产生一次结果。记录不包含正文、Token 或 URL 查询参数。刷新/密钥协商不伪装成用户业务操作;close 清理本实例的订阅,重新 init 后需重新订阅。
160
+
161
+ ```ts
162
+ import { apiClient } from '@h-ai/api-client'
163
+
164
+ const unsubscribe = apiClient.observeRequests((record) => {
165
+ const failed = !record.success
166
+ const requestId = record.requestId
167
+ })
278
168
  unsubscribe()
279
169
  ```
280
170
 
281
- ## 流式响应(SSE)
171
+ ## 错误提示与关联日志
282
172
 
283
- ```ts
284
- const controller = new AbortController()
173
+ HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐藏编号,由排查信息图标展开和复制。响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
285
174
 
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)
289
- }
175
+ `core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
290
176
 
291
- // 主动停止流式响应
292
- controller.abort()
293
- ```
177
+ ## 可选交互挑战
294
178
 
295
- 说明:
179
+ createAuthActions(client) 提供 createCaptcha({operation}),所有登录映射与续期/注册会保留 captcha;sendOtp(identifier,{captcha})、requestPasswordReset(identifier,{captcha}) 接收可选第二参数。框架 AuthPanel 自动求解,自定义表单复用 ALTCHA 协议,证明不进入安全 TokenStorage。
296
180
 
297
- - `stream()` 内置超时控制(连接阶段与流式读取阶段均生效)
298
- - 401 会尝试自动刷新 Token 后重试一次
299
- - 解析器支持跨 chunk 的 SSE 行缓冲
300
- - 支持外部传入 `AbortSignal` 主动取消
181
+ 策略、默认值和恢复边界见 [IAM](../iam/README.md)。
301
182
 
302
- ## 拦截器(高级用法)
183
+ ## 敏感请求再认证
303
184
 
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
- ```
185
+ 客户端单次调用的 `options.context.reauthentication` 携带 `reauthenticate` 返回的用途凭据,由请求管线生成 `X-Hai-Reauthentication`。不要写入客户端全局 header、持久存储或日志。服务端同时检查当前权限、资源范围和凭据用途,并原子消费;并发普通请求不会继承该 header。跨域宿主须允许此 header,默认 Serv/Kit 已包含。
186
+
187
+ ## 原地恢复过期会话
188
+
189
+ 配置 `auth.recoverSession` 回调可在服务端明确拒绝会话时原地重新登录并保留页面输入。使用 `@h-ai/ui` 的 `createSessionRecovery` / `SessionRecoveryDialog`:根布局绑定公开配置与原用户,回调设为 `recovery.recover`,弹窗使用标准认证 actions,销毁时 `cancel()`。登录的 `expectedUserId` 由 IAM 限定原主体;常规验证码、锁定、改密和登录限制照常执行。只对明确未认证拒绝的原请求续接一次;取消、5xx、超时和结果不明的网络写入不重放。敏感操作证明绑定旧会话时可能需要再次确认,不复用到新会话。未传回调保持原处理方式;配置回调后由恢复流程处理,不再立即调用 onRefreshFailed 导航离开。终端分组通过 auth.terminalType(web/app/desktop/api,默认 web)传入;它不是硬件身份认证。
190
+
191
+ ## 页面离开会话策略
327
192
 
328
- ## 测试
193
+ `session.endOnPageHide` 默认 `false`。启用后宿主调用 `actions.bindPageExit({ enabled: config.session.endOnPageHide })` 并在卸载时调用返回的清理函数;标准 admin/H5/fullstack 模板已经接入。非 BFCache 的 `pagehide`(也包括刷新、整页导航)通过 POST `/auth/session/close` 尽力撤销当前 access 及关联 refresh,沿用 CSRF/传输加密,使用 `keepalive` 且不自动刷新会话。该响应不清 Cookie/本地凭据,避免旧响应覆盖下一次登录。显式退出仍使用正常 logout,可靠撤销并清理当前凭据。浏览器可能不发送页面离开请求,因此必须按业务配合服务端 `idleTimeout`/`absoluteTimeout`;不能据此保证关闭即撤销。
194
+
195
+ 敏感调用通过本次 `reauthorize: () => sensitiveActions.authorize(operation)` 重新确认用途:Kit 放 apiFetch 第三参数,typed client 放 options.context。会话刷新/恢复或明确的再认证拒绝后仅续发一次;取消不写入,未提供回调不重放旧证明。
196
+
197
+ ## 开发测试
329
198
 
330
199
  ```bash
331
200
  pnpm --filter @h-ai/api-client test
201
+ pnpm --filter @h-ai/api-client typecheck
332
202
  ```
333
203
 
334
- ## License
204
+ 客户端默认从服务端握手获取 `transport.maxPlaintextBytes`;可显式设置本地同名字段取较小值。容量失败返回 `HaiApiClientError.PAYLOAD_TOO_LARGE`(`hai:api-client:020`),保留方向、阶段和字节诊断。见 [容量配置与升级](../crypto/TRANSPORT-CAPACITY.md)。
335
205
 
336
- Apache-2.0
206
+ serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
337
207
 
338
- ## License
208
+ ## 许可证
339
209
 
340
210
  Apache-2.0