@h-ai/api-client 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +340 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +748 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import { core, ok, err } from '@h-ai/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// src/api-client-auth.ts
|
|
5
|
+
var LS_ACCESS_KEY = "hai_access_token";
|
|
6
|
+
var LS_REFRESH_KEY = "hai_refresh_token";
|
|
7
|
+
function createLocalStorageTokenStorage() {
|
|
8
|
+
return {
|
|
9
|
+
async getAccessToken() {
|
|
10
|
+
return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null;
|
|
11
|
+
},
|
|
12
|
+
async setAccessToken(token) {
|
|
13
|
+
globalThis.localStorage?.setItem(LS_ACCESS_KEY, token);
|
|
14
|
+
},
|
|
15
|
+
async getRefreshToken() {
|
|
16
|
+
return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null;
|
|
17
|
+
},
|
|
18
|
+
async setRefreshToken(token) {
|
|
19
|
+
globalThis.localStorage?.setItem(LS_REFRESH_KEY, token);
|
|
20
|
+
},
|
|
21
|
+
async clear() {
|
|
22
|
+
globalThis.localStorage?.removeItem(LS_ACCESS_KEY);
|
|
23
|
+
globalThis.localStorage?.removeItem(LS_REFRESH_KEY);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createMemoryTokenStorage() {
|
|
28
|
+
let accessToken = null;
|
|
29
|
+
let refreshToken = null;
|
|
30
|
+
return {
|
|
31
|
+
async getAccessToken() {
|
|
32
|
+
return accessToken;
|
|
33
|
+
},
|
|
34
|
+
async setAccessToken(token) {
|
|
35
|
+
accessToken = token;
|
|
36
|
+
},
|
|
37
|
+
async getRefreshToken() {
|
|
38
|
+
return refreshToken;
|
|
39
|
+
},
|
|
40
|
+
async setRefreshToken(token) {
|
|
41
|
+
refreshToken = token;
|
|
42
|
+
},
|
|
43
|
+
async clear() {
|
|
44
|
+
accessToken = null;
|
|
45
|
+
refreshToken = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// messages/en-US.json
|
|
51
|
+
var en_US_default = {
|
|
52
|
+
apiClient_networkError: "Network request failed",
|
|
53
|
+
apiClient_timeout: "Request timeout",
|
|
54
|
+
apiClient_serverError: "Server error ({status})",
|
|
55
|
+
apiClient_unauthorized: "Unauthorized, please log in again",
|
|
56
|
+
apiClient_forbidden: "Access forbidden",
|
|
57
|
+
apiClient_notFound: "Resource not found",
|
|
58
|
+
apiClient_validationFailed: "Request validation failed",
|
|
59
|
+
apiClient_tokenRefreshFailed: "Token refresh failed, please log in again",
|
|
60
|
+
apiClient_notInitialized: "API client not initialized, please call api.init() first",
|
|
61
|
+
apiClient_configError: "API client config error: {error}",
|
|
62
|
+
apiClient_responseValidationFailed: "Response validation failed",
|
|
63
|
+
apiClient_unknown: "Unknown error"
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// messages/zh-CN.json
|
|
67
|
+
var zh_CN_default = {
|
|
68
|
+
apiClient_networkError: "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25",
|
|
69
|
+
apiClient_timeout: "\u8BF7\u6C42\u8D85\u65F6",
|
|
70
|
+
apiClient_serverError: "\u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09",
|
|
71
|
+
apiClient_unauthorized: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
72
|
+
apiClient_forbidden: "\u65E0\u6743\u9650\u8BBF\u95EE",
|
|
73
|
+
apiClient_notFound: "\u8BF7\u6C42\u7684\u8D44\u6E90\u4E0D\u5B58\u5728",
|
|
74
|
+
apiClient_validationFailed: "\u8BF7\u6C42\u53C2\u6570\u6821\u9A8C\u5931\u8D25",
|
|
75
|
+
apiClient_tokenRefreshFailed: "Token\u5237\u65B0\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
76
|
+
apiClient_notInitialized: "API \u5BA2\u6237\u7AEF\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 api.init()",
|
|
77
|
+
apiClient_configError: "API \u5BA2\u6237\u7AEF\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
|
|
78
|
+
apiClient_responseValidationFailed: "\u54CD\u5E94\u6570\u636E\u6821\u9A8C\u5931\u8D25",
|
|
79
|
+
apiClient_unknown: "\u672A\u77E5\u9519\u8BEF"
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/api-client-i18n.ts
|
|
83
|
+
var apiClientM = core.i18n.createMessageGetter({
|
|
84
|
+
"zh-CN": zh_CN_default,
|
|
85
|
+
"en-US": en_US_default
|
|
86
|
+
});
|
|
87
|
+
var ApiClientErrorInfo = {
|
|
88
|
+
NETWORK_ERROR: "001:502",
|
|
89
|
+
TIMEOUT: "002:504",
|
|
90
|
+
SERVER_ERROR: "003:502",
|
|
91
|
+
UNAUTHORIZED: "004:401",
|
|
92
|
+
FORBIDDEN: "005:403",
|
|
93
|
+
NOT_FOUND: "006:404",
|
|
94
|
+
VALIDATION_FAILED: "007:400",
|
|
95
|
+
TOKEN_REFRESH_FAILED: "008:401",
|
|
96
|
+
NOT_INITIALIZED: "010:500",
|
|
97
|
+
CONFIG_ERROR: "011:500",
|
|
98
|
+
UNKNOWN: "099:500"
|
|
99
|
+
};
|
|
100
|
+
var HaiApiClientError = core.error.buildHaiErrorsDef("api-client", ApiClientErrorInfo);
|
|
101
|
+
function defineEndpoint(def) {
|
|
102
|
+
return def;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/api-client-contract.ts
|
|
106
|
+
function createContractCaller(fetchClient) {
|
|
107
|
+
async function call(endpoint, input) {
|
|
108
|
+
const parsed = endpoint.input.safeParse(input);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
return err(
|
|
111
|
+
HaiApiClientError.VALIDATION_FAILED,
|
|
112
|
+
apiClientM("apiClient_validationFailed"),
|
|
113
|
+
parsed.error.issues
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const validInput = parsed.data;
|
|
117
|
+
let result;
|
|
118
|
+
switch (endpoint.method) {
|
|
119
|
+
case "GET":
|
|
120
|
+
result = await fetchClient.get(endpoint.path, validInput);
|
|
121
|
+
break;
|
|
122
|
+
case "POST":
|
|
123
|
+
result = await fetchClient.post(endpoint.path, validInput);
|
|
124
|
+
break;
|
|
125
|
+
case "PUT":
|
|
126
|
+
result = await fetchClient.put(endpoint.path, validInput);
|
|
127
|
+
break;
|
|
128
|
+
case "PATCH":
|
|
129
|
+
result = await fetchClient.patch(endpoint.path, validInput);
|
|
130
|
+
break;
|
|
131
|
+
case "DELETE":
|
|
132
|
+
result = await fetchClient.delete(endpoint.path, validInput);
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
return err(
|
|
136
|
+
HaiApiClientError.UNKNOWN,
|
|
137
|
+
apiClientM("apiClient_unknown")
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
const outputParsed = endpoint.output.safeParse(result.data);
|
|
144
|
+
if (!outputParsed.success) {
|
|
145
|
+
return err(
|
|
146
|
+
HaiApiClientError.VALIDATION_FAILED,
|
|
147
|
+
apiClientM("apiClient_responseValidationFailed"),
|
|
148
|
+
outputParsed.error.issues
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return ok(outputParsed.data);
|
|
152
|
+
}
|
|
153
|
+
return call;
|
|
154
|
+
}
|
|
155
|
+
async function responseToError(response) {
|
|
156
|
+
let details;
|
|
157
|
+
try {
|
|
158
|
+
const body = await response.json();
|
|
159
|
+
details = body;
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
const status = response.status;
|
|
163
|
+
if (status === 401) {
|
|
164
|
+
return err(
|
|
165
|
+
HaiApiClientError.UNAUTHORIZED,
|
|
166
|
+
apiClientM("apiClient_unauthorized"),
|
|
167
|
+
details
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (status === 403) {
|
|
171
|
+
return err(
|
|
172
|
+
HaiApiClientError.FORBIDDEN,
|
|
173
|
+
apiClientM("apiClient_forbidden"),
|
|
174
|
+
details
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (status === 404) {
|
|
178
|
+
return err(
|
|
179
|
+
HaiApiClientError.NOT_FOUND,
|
|
180
|
+
apiClientM("apiClient_notFound"),
|
|
181
|
+
details
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (status === 400 || status === 422) {
|
|
185
|
+
return err(
|
|
186
|
+
HaiApiClientError.VALIDATION_FAILED,
|
|
187
|
+
apiClientM("apiClient_validationFailed"),
|
|
188
|
+
details
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (status >= 500) {
|
|
192
|
+
return err(
|
|
193
|
+
HaiApiClientError.SERVER_ERROR,
|
|
194
|
+
apiClientM("apiClient_serverError", { params: { status: String(status) } }),
|
|
195
|
+
details
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return err(
|
|
199
|
+
HaiApiClientError.UNKNOWN,
|
|
200
|
+
apiClientM("apiClient_unknown"),
|
|
201
|
+
details
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
function networkErrorToResult(cause) {
|
|
205
|
+
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
206
|
+
return err(
|
|
207
|
+
HaiApiClientError.TIMEOUT,
|
|
208
|
+
apiClientM("apiClient_timeout"),
|
|
209
|
+
cause
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return err(
|
|
213
|
+
HaiApiClientError.NETWORK_ERROR,
|
|
214
|
+
apiClientM("apiClient_networkError"),
|
|
215
|
+
cause
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/api-client-fetch.ts
|
|
220
|
+
var logger = core.logger.child({ module: "api-client", scope: "fetch" });
|
|
221
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
222
|
+
function createFetchClient(config, tokenManager) {
|
|
223
|
+
const fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
224
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
225
|
+
const requestInterceptors = config.interceptors?.request ?? [];
|
|
226
|
+
const responseInterceptors = config.interceptors?.response ?? [];
|
|
227
|
+
function buildUrl(path, params) {
|
|
228
|
+
const base = config.baseUrl.replace(/\/+$/, "");
|
|
229
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
230
|
+
const url = new URL(`${base}${normalizedPath}`);
|
|
231
|
+
if (params) {
|
|
232
|
+
for (const [key, value] of Object.entries(params)) {
|
|
233
|
+
if (value !== void 0 && value !== null) {
|
|
234
|
+
url.searchParams.set(key, String(value));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return url.toString();
|
|
239
|
+
}
|
|
240
|
+
async function execute(requestConfig, retryOnUnauthorized = true) {
|
|
241
|
+
logger.trace("Executing request", { method: requestConfig.method, url: requestConfig.url });
|
|
242
|
+
if (tokenManager) {
|
|
243
|
+
const accessToken = await tokenManager.storage.getAccessToken();
|
|
244
|
+
if (accessToken) {
|
|
245
|
+
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let finalConfig = requestConfig;
|
|
249
|
+
for (const interceptor of requestInterceptors) {
|
|
250
|
+
finalConfig = await interceptor(finalConfig);
|
|
251
|
+
}
|
|
252
|
+
const controller = new AbortController();
|
|
253
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
254
|
+
finalConfig.signal = controller.signal;
|
|
255
|
+
try {
|
|
256
|
+
let response = await fetchFn(finalConfig.url, {
|
|
257
|
+
method: finalConfig.method,
|
|
258
|
+
headers: finalConfig.headers,
|
|
259
|
+
body: finalConfig.body,
|
|
260
|
+
signal: finalConfig.signal
|
|
261
|
+
});
|
|
262
|
+
clearTimeout(timeoutId);
|
|
263
|
+
for (const interceptor of responseInterceptors) {
|
|
264
|
+
response = await interceptor(response);
|
|
265
|
+
}
|
|
266
|
+
if (response.status === 401 && retryOnUnauthorized && tokenManager) {
|
|
267
|
+
logger.trace("Received 401, attempting token refresh", { url: requestConfig.url });
|
|
268
|
+
const tokens = await tokenManager.refresh();
|
|
269
|
+
if (tokens) {
|
|
270
|
+
logger.trace("Token refreshed, retrying request", { url: requestConfig.url });
|
|
271
|
+
requestConfig.headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
272
|
+
return execute(requestConfig, false);
|
|
273
|
+
}
|
|
274
|
+
logger.warn("Token refresh failed, returning 401 error", { url: requestConfig.url });
|
|
275
|
+
}
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
return responseToError(response);
|
|
278
|
+
}
|
|
279
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
280
|
+
if (contentType.includes("application/json")) {
|
|
281
|
+
const body = await response.json();
|
|
282
|
+
if (body && typeof body === "object" && "data" in body) {
|
|
283
|
+
return ok(body.data);
|
|
284
|
+
}
|
|
285
|
+
return ok(body);
|
|
286
|
+
}
|
|
287
|
+
return ok(void 0);
|
|
288
|
+
} catch (cause) {
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
logger.warn("Request failed", { url: requestConfig.url, error: cause });
|
|
291
|
+
return networkErrorToResult(cause);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function get(path, params) {
|
|
295
|
+
return execute({
|
|
296
|
+
url: buildUrl(path, params),
|
|
297
|
+
method: "GET",
|
|
298
|
+
headers: { Accept: "application/json" }
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async function post(path, body) {
|
|
302
|
+
return execute({
|
|
303
|
+
url: buildUrl(path),
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
306
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function put(path, body) {
|
|
310
|
+
return execute({
|
|
311
|
+
url: buildUrl(path),
|
|
312
|
+
method: "PUT",
|
|
313
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
314
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async function patch(path, body) {
|
|
318
|
+
return execute({
|
|
319
|
+
url: buildUrl(path),
|
|
320
|
+
method: "PATCH",
|
|
321
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
322
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async function del(path, params) {
|
|
326
|
+
return execute({
|
|
327
|
+
url: buildUrl(path, params),
|
|
328
|
+
method: "DELETE",
|
|
329
|
+
headers: { Accept: "application/json" }
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async function upload(path, file, options) {
|
|
333
|
+
const formData = new FormData();
|
|
334
|
+
formData.append(options?.fieldName ?? "file", file);
|
|
335
|
+
if (options?.extraFields) {
|
|
336
|
+
for (const [key, value] of Object.entries(options.extraFields)) {
|
|
337
|
+
formData.append(key, value);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return execute({
|
|
341
|
+
url: buildUrl(path),
|
|
342
|
+
method: "POST",
|
|
343
|
+
headers: { Accept: "application/json" },
|
|
344
|
+
body: formData
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async function* stream(path, body, options) {
|
|
348
|
+
const requestConfig = {
|
|
349
|
+
url: buildUrl(path),
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "Content-Type": "application/json", "Accept": "text/event-stream" },
|
|
352
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
353
|
+
};
|
|
354
|
+
if (tokenManager) {
|
|
355
|
+
const accessToken = await tokenManager.storage.getAccessToken();
|
|
356
|
+
if (accessToken) {
|
|
357
|
+
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
let finalConfig = requestConfig;
|
|
361
|
+
for (const interceptor of requestInterceptors) {
|
|
362
|
+
finalConfig = await interceptor(finalConfig);
|
|
363
|
+
}
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
let timeoutId = null;
|
|
366
|
+
let timeoutTriggered = false;
|
|
367
|
+
const clearStreamTimeout = () => {
|
|
368
|
+
if (timeoutId) {
|
|
369
|
+
clearTimeout(timeoutId);
|
|
370
|
+
timeoutId = null;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const armStreamTimeout = () => {
|
|
374
|
+
clearStreamTimeout();
|
|
375
|
+
timeoutTriggered = false;
|
|
376
|
+
timeoutId = setTimeout(() => {
|
|
377
|
+
timeoutTriggered = true;
|
|
378
|
+
controller.abort();
|
|
379
|
+
}, timeout);
|
|
380
|
+
};
|
|
381
|
+
const externalSignal = options?.signal;
|
|
382
|
+
const onExternalAbort = () => {
|
|
383
|
+
controller.abort();
|
|
384
|
+
};
|
|
385
|
+
if (externalSignal) {
|
|
386
|
+
if (externalSignal.aborted) {
|
|
387
|
+
controller.abort();
|
|
388
|
+
} else {
|
|
389
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function doStreamFetch() {
|
|
393
|
+
armStreamTimeout();
|
|
394
|
+
try {
|
|
395
|
+
return await fetchFn(finalConfig.url, {
|
|
396
|
+
method: finalConfig.method,
|
|
397
|
+
headers: finalConfig.headers,
|
|
398
|
+
body: finalConfig.body,
|
|
399
|
+
signal: controller.signal
|
|
400
|
+
});
|
|
401
|
+
} catch (cause) {
|
|
402
|
+
clearStreamTimeout();
|
|
403
|
+
throw cause;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
let reader = null;
|
|
407
|
+
try {
|
|
408
|
+
let response = await doStreamFetch();
|
|
409
|
+
if (response.status === 401 && tokenManager) {
|
|
410
|
+
logger.info("Stream received 401, attempting token refresh", { url: requestConfig.url });
|
|
411
|
+
const tokens = await tokenManager.refresh();
|
|
412
|
+
if (tokens) {
|
|
413
|
+
finalConfig.headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
414
|
+
response = await doStreamFetch();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
const errorResult = await responseToError(response);
|
|
419
|
+
if (!errorResult.success) {
|
|
420
|
+
throw new Error(errorResult.error.message, { cause: errorResult.error });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (!response.body) {
|
|
424
|
+
throw new Error(apiClientM("apiClient_networkError"));
|
|
425
|
+
}
|
|
426
|
+
reader = response.body.getReader();
|
|
427
|
+
const decoder = new TextDecoder();
|
|
428
|
+
let lineBuffer = "";
|
|
429
|
+
while (true) {
|
|
430
|
+
armStreamTimeout();
|
|
431
|
+
const { done, value } = await reader.read();
|
|
432
|
+
clearStreamTimeout();
|
|
433
|
+
if (done)
|
|
434
|
+
break;
|
|
435
|
+
lineBuffer += decoder.decode(value, { stream: true });
|
|
436
|
+
const parts = lineBuffer.split("\n");
|
|
437
|
+
lineBuffer = parts.pop() ?? "";
|
|
438
|
+
for (const line of parts) {
|
|
439
|
+
if (line.startsWith("data: ")) {
|
|
440
|
+
const data = line.slice(6);
|
|
441
|
+
if (data !== "[DONE]") {
|
|
442
|
+
yield data;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (lineBuffer.startsWith("data: ")) {
|
|
448
|
+
const data = lineBuffer.slice(6);
|
|
449
|
+
if (data !== "[DONE]") {
|
|
450
|
+
yield data;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch (cause) {
|
|
454
|
+
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
455
|
+
if (externalSignal?.aborted && !timeoutTriggered) {
|
|
456
|
+
throw cause;
|
|
457
|
+
}
|
|
458
|
+
throw new Error(apiClientM("apiClient_timeout"), { cause });
|
|
459
|
+
}
|
|
460
|
+
throw cause;
|
|
461
|
+
} finally {
|
|
462
|
+
clearStreamTimeout();
|
|
463
|
+
if (externalSignal) {
|
|
464
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
465
|
+
}
|
|
466
|
+
if (reader) {
|
|
467
|
+
reader.releaseLock();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { get, post, put, patch, delete: del, upload, stream, execute, buildUrl };
|
|
472
|
+
}
|
|
473
|
+
var logger2 = core.logger.child({ module: "api-client", scope: "auth" });
|
|
474
|
+
var TokenPairSchema = z.object({
|
|
475
|
+
accessToken: z.string().min(1),
|
|
476
|
+
refreshToken: z.string().min(1),
|
|
477
|
+
expiresIn: z.number().optional(),
|
|
478
|
+
tokenType: z.string().optional()
|
|
479
|
+
});
|
|
480
|
+
function createTokenManager(storage, refreshUrl, fetchFn, onRefreshFailed) {
|
|
481
|
+
const callbacks = [];
|
|
482
|
+
let refreshPromise = null;
|
|
483
|
+
async function refresh() {
|
|
484
|
+
if (refreshPromise) {
|
|
485
|
+
return refreshPromise;
|
|
486
|
+
}
|
|
487
|
+
refreshPromise = doRefresh();
|
|
488
|
+
try {
|
|
489
|
+
return await refreshPromise;
|
|
490
|
+
} finally {
|
|
491
|
+
refreshPromise = null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function doRefresh() {
|
|
495
|
+
const refreshToken = await storage.getRefreshToken();
|
|
496
|
+
if (!refreshToken) {
|
|
497
|
+
logger2.warn("Token refresh skipped, no refresh token available");
|
|
498
|
+
onRefreshFailed?.();
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
logger2.debug("Refreshing token", { url: refreshUrl });
|
|
502
|
+
try {
|
|
503
|
+
const response = await fetchFn(refreshUrl, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: { "Content-Type": "application/json" },
|
|
506
|
+
body: JSON.stringify({ refreshToken })
|
|
507
|
+
});
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
await storage.clear();
|
|
510
|
+
onRefreshFailed?.();
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const body = await response.json();
|
|
514
|
+
const parsed = TokenPairSchema.safeParse(body.data);
|
|
515
|
+
if (!parsed.success) {
|
|
516
|
+
logger2.warn("Token refresh returned invalid data", { issues: parsed.error.issues });
|
|
517
|
+
await storage.clear();
|
|
518
|
+
onRefreshFailed?.();
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const tokens = {
|
|
522
|
+
accessToken: parsed.data.accessToken,
|
|
523
|
+
refreshToken: parsed.data.refreshToken,
|
|
524
|
+
expiresIn: parsed.data.expiresIn ?? 3600,
|
|
525
|
+
tokenType: "Bearer"
|
|
526
|
+
};
|
|
527
|
+
await storage.setAccessToken(tokens.accessToken);
|
|
528
|
+
await storage.setRefreshToken(tokens.refreshToken);
|
|
529
|
+
logger2.info("Token refreshed successfully");
|
|
530
|
+
for (const cb of callbacks) {
|
|
531
|
+
cb(tokens);
|
|
532
|
+
}
|
|
533
|
+
return tokens;
|
|
534
|
+
} catch (error) {
|
|
535
|
+
logger2.error("Token refresh failed", { error });
|
|
536
|
+
await storage.clear();
|
|
537
|
+
onRefreshFailed?.();
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
storage,
|
|
543
|
+
refresh,
|
|
544
|
+
async setTokens(tokens) {
|
|
545
|
+
await storage.setAccessToken(tokens.accessToken);
|
|
546
|
+
await storage.setRefreshToken(tokens.refreshToken);
|
|
547
|
+
},
|
|
548
|
+
async clear() {
|
|
549
|
+
await storage.clear();
|
|
550
|
+
},
|
|
551
|
+
onTokenRefreshed(callback) {
|
|
552
|
+
callbacks.push(callback);
|
|
553
|
+
return () => {
|
|
554
|
+
const idx = callbacks.indexOf(callback);
|
|
555
|
+
if (idx >= 0)
|
|
556
|
+
callbacks.splice(idx, 1);
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/api-client-main.ts
|
|
563
|
+
var logger3 = core.logger.child({ module: "api-client", scope: "main" });
|
|
564
|
+
var currentClient = null;
|
|
565
|
+
var currentConfig = null;
|
|
566
|
+
var initInProgress = false;
|
|
567
|
+
function createClient(config) {
|
|
568
|
+
const fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
569
|
+
const tokenStorage = config.auth?.storage ?? createLocalStorageTokenStorage();
|
|
570
|
+
const tokenManager = config.auth ? createTokenManager(
|
|
571
|
+
tokenStorage,
|
|
572
|
+
`${config.baseUrl.replace(/\/+$/, "")}${config.auth.refreshUrl}`,
|
|
573
|
+
fetchFn,
|
|
574
|
+
config.auth.onRefreshFailed
|
|
575
|
+
) : void 0;
|
|
576
|
+
if (config.auth?.onTokenRefreshed && tokenManager) {
|
|
577
|
+
tokenManager.onTokenRefreshed(config.auth.onTokenRefreshed);
|
|
578
|
+
}
|
|
579
|
+
const fetchClient = createFetchClient(config, tokenManager);
|
|
580
|
+
const call = createContractCaller(fetchClient);
|
|
581
|
+
const auth = {
|
|
582
|
+
async setTokens(tokens) {
|
|
583
|
+
if (tokenManager) {
|
|
584
|
+
await tokenManager.setTokens(tokens);
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
async clear() {
|
|
588
|
+
if (tokenManager) {
|
|
589
|
+
await tokenManager.clear();
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
onTokenRefreshed(callback) {
|
|
593
|
+
if (tokenManager) {
|
|
594
|
+
return tokenManager.onTokenRefreshed(callback);
|
|
595
|
+
}
|
|
596
|
+
return () => {
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
return {
|
|
601
|
+
get: fetchClient.get,
|
|
602
|
+
post: fetchClient.post,
|
|
603
|
+
put: fetchClient.put,
|
|
604
|
+
patch: fetchClient.patch,
|
|
605
|
+
delete: fetchClient.delete,
|
|
606
|
+
upload: fetchClient.upload,
|
|
607
|
+
stream: fetchClient.stream,
|
|
608
|
+
call,
|
|
609
|
+
auth
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
613
|
+
HaiApiClientError.NOT_INITIALIZED,
|
|
614
|
+
() => apiClientM("apiClient_notInitialized")
|
|
615
|
+
);
|
|
616
|
+
var notInitializedOps = notInitialized.proxy();
|
|
617
|
+
var notInitializedAuth = {
|
|
618
|
+
async setTokens() {
|
|
619
|
+
logger3.warn("api.auth.setTokens called before initialization, ignored");
|
|
620
|
+
},
|
|
621
|
+
async clear() {
|
|
622
|
+
logger3.warn("api.auth.clear called before initialization, ignored");
|
|
623
|
+
},
|
|
624
|
+
onTokenRefreshed() {
|
|
625
|
+
logger3.warn("api.auth.onTokenRefreshed called before initialization, ignored");
|
|
626
|
+
return () => {
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
function notInitializedStreamFn(_path, _body, _options) {
|
|
631
|
+
return {
|
|
632
|
+
[Symbol.asyncIterator]() {
|
|
633
|
+
return {
|
|
634
|
+
async next() {
|
|
635
|
+
throw new Error(apiClientM("apiClient_notInitialized"));
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
var api = {
|
|
642
|
+
/**
|
|
643
|
+
* 初始化 API 客户端
|
|
644
|
+
*
|
|
645
|
+
* 如果当前已有活跃实例,会先自动 close 再重新初始化。
|
|
646
|
+
* 创建异常时返回 CONFIG_ERROR 错误。
|
|
647
|
+
*
|
|
648
|
+
* @param config 客户端配置(baseUrl、auth 等)
|
|
649
|
+
* @returns 成功时返回 ok(undefined);失败时返回包含错误码和消息的 err。
|
|
650
|
+
*/
|
|
651
|
+
async init(config) {
|
|
652
|
+
if (initInProgress) {
|
|
653
|
+
return err(
|
|
654
|
+
HaiApiClientError.CONFIG_ERROR,
|
|
655
|
+
apiClientM("apiClient_configError", { params: { error: "initialization already in progress" } })
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
initInProgress = true;
|
|
659
|
+
try {
|
|
660
|
+
if (currentClient) {
|
|
661
|
+
logger3.warn("Api-client module is already initialized, reinitializing");
|
|
662
|
+
await api.close();
|
|
663
|
+
}
|
|
664
|
+
logger3.info("Initializing api-client module");
|
|
665
|
+
currentClient = createClient(config);
|
|
666
|
+
currentConfig = config;
|
|
667
|
+
logger3.info("Api-client module initialized");
|
|
668
|
+
return ok(void 0);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
logger3.error("Api-client module initialization failed", { error });
|
|
671
|
+
return err(
|
|
672
|
+
HaiApiClientError.CONFIG_ERROR,
|
|
673
|
+
apiClientM("apiClient_configError", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
674
|
+
error
|
|
675
|
+
);
|
|
676
|
+
} finally {
|
|
677
|
+
initInProgress = false;
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
/** GET 请求。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
681
|
+
get get() {
|
|
682
|
+
return currentClient?.get ?? notInitializedOps.get;
|
|
683
|
+
},
|
|
684
|
+
/** POST 请求。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
685
|
+
get post() {
|
|
686
|
+
return currentClient?.post ?? notInitializedOps.post;
|
|
687
|
+
},
|
|
688
|
+
/** PUT 请求。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
689
|
+
get put() {
|
|
690
|
+
return currentClient?.put ?? notInitializedOps.put;
|
|
691
|
+
},
|
|
692
|
+
/** PATCH 请求。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
693
|
+
get patch() {
|
|
694
|
+
return currentClient?.patch ?? notInitializedOps.patch;
|
|
695
|
+
},
|
|
696
|
+
/** DELETE 请求。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
697
|
+
get delete() {
|
|
698
|
+
return currentClient?.delete ?? notInitializedOps.delete;
|
|
699
|
+
},
|
|
700
|
+
/** 文件上传。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
701
|
+
get upload() {
|
|
702
|
+
return currentClient?.upload ?? notInitializedOps.upload;
|
|
703
|
+
},
|
|
704
|
+
/** 契约调用。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
705
|
+
get call() {
|
|
706
|
+
return currentClient?.call ?? notInitializedOps.call;
|
|
707
|
+
},
|
|
708
|
+
/**
|
|
709
|
+
* 流式请求。未初始化时迭代抛出异常。
|
|
710
|
+
*
|
|
711
|
+
* @throws 未初始化时抛出 Error
|
|
712
|
+
*/
|
|
713
|
+
get stream() {
|
|
714
|
+
return currentClient?.stream ?? notInitializedStreamFn;
|
|
715
|
+
},
|
|
716
|
+
/** Token 管理接口。未初始化时为空操作 */
|
|
717
|
+
get auth() {
|
|
718
|
+
return currentClient?.auth ?? notInitializedAuth;
|
|
719
|
+
},
|
|
720
|
+
/** 当前客户端配置;未初始化或已关闭时为 null */
|
|
721
|
+
get config() {
|
|
722
|
+
return currentConfig;
|
|
723
|
+
},
|
|
724
|
+
/** 是否已完成初始化 */
|
|
725
|
+
get isInitialized() {
|
|
726
|
+
return currentClient !== null;
|
|
727
|
+
},
|
|
728
|
+
/**
|
|
729
|
+
* 关闭 API 客户端并释放资源
|
|
730
|
+
*
|
|
731
|
+
* 关闭后所有 HTTP 操作将返回 NOT_INITIALIZED 错误。
|
|
732
|
+
* 重复调用不会报错。
|
|
733
|
+
*/
|
|
734
|
+
async close() {
|
|
735
|
+
if (!currentClient) {
|
|
736
|
+
currentConfig = null;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
logger3.info("Closing api-client module");
|
|
740
|
+
currentClient = null;
|
|
741
|
+
currentConfig = null;
|
|
742
|
+
logger3.info("Api-client module closed");
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
export { HaiApiClientError, api, createLocalStorageTokenStorage, createMemoryTokenStorage, defineEndpoint };
|
|
747
|
+
//# sourceMappingURL=index.js.map
|
|
748
|
+
//# sourceMappingURL=index.js.map
|