@tacoreai/web-sdk 1.18.0 → 1.19.0
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,6 +1,117 @@
|
|
|
1
|
-
import { isBrowser
|
|
1
|
+
import { isBrowser } from "../../../utils/index.js";
|
|
2
2
|
import { BaseAppsClient } from "./BaseAppsClient.js";
|
|
3
3
|
|
|
4
|
+
const REFRESH_TOKEN_STORAGE_KEY = "tacoreai_apps_refresh_token";
|
|
5
|
+
const ACCESS_TOKEN_EXPIRES_AT_STORAGE_KEY = "tacoreai_apps_access_token_expires_at";
|
|
6
|
+
const LEGACY_EXPIRES_AT_STORAGE_KEY = "tacoreai_apps_expires_at";
|
|
7
|
+
const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
|
8
|
+
const REFRESH_RETRY_DELAY_MS = 60 * 1000;
|
|
9
|
+
const MIN_TIMER_DELAY_MS = 1000;
|
|
10
|
+
|
|
11
|
+
const normalizeText = (value) => {
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed || null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const readStorageValue = (key) => {
|
|
21
|
+
if (!isBrowser) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return normalizeText(localStorage.getItem(key));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(`[AppsAuthManager] Failed to read ${key} from localStorage:`, error);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const writeStorageValue = (key, value) => {
|
|
34
|
+
if (!isBrowser) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (value) {
|
|
40
|
+
localStorage.setItem(key, value);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
localStorage.removeItem(key);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`[AppsAuthManager] Failed to persist ${key}:`, error);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const parseExpiresAtMs = (value) => {
|
|
50
|
+
if (value === null || value === undefined || value === "") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const numericValue = Number(value);
|
|
55
|
+
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (numericValue >= 1e12) {
|
|
60
|
+
return Math.trunc(numericValue);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Math.trunc(numericValue * 1000);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const decodeJwtPayload = (token) => {
|
|
67
|
+
const normalizedToken = normalizeText(token);
|
|
68
|
+
if (!normalizedToken) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parts = normalizedToken.split(".");
|
|
73
|
+
if (parts.length < 2) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
79
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
80
|
+
|
|
81
|
+
if (typeof atob === "function") {
|
|
82
|
+
return JSON.parse(atob(padded));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof Buffer !== "undefined") {
|
|
86
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("[AppsAuthManager] Failed to decode JWT payload:", error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const resolveExpiresAtMs = ({ accessToken, expiresAt, expiresIn }) => {
|
|
96
|
+
const normalizedExpiresAt = parseExpiresAtMs(expiresAt);
|
|
97
|
+
if (normalizedExpiresAt) {
|
|
98
|
+
return normalizedExpiresAt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const numericExpiresIn = Number(expiresIn);
|
|
102
|
+
if (Number.isFinite(numericExpiresIn) && numericExpiresIn > 0) {
|
|
103
|
+
return Date.now() + Math.trunc(numericExpiresIn * 1000);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const payload = decodeJwtPayload(accessToken);
|
|
107
|
+
return parseExpiresAtMs(payload?.exp);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const resolveTokenSubject = (token) => {
|
|
111
|
+
const payload = decodeJwtPayload(token);
|
|
112
|
+
return normalizeText(payload?.sub);
|
|
113
|
+
};
|
|
114
|
+
|
|
4
115
|
/**
|
|
5
116
|
* AI应用认证管理器
|
|
6
117
|
* 负责AI应用的用户认证、会话管理
|
|
@@ -9,15 +120,15 @@ import { BaseAppsClient } from "./BaseAppsClient.js";
|
|
|
9
120
|
export class AppsAuthManager extends BaseAppsClient {
|
|
10
121
|
constructor(appId, config = {}) {
|
|
11
122
|
super(appId, config);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
123
|
+
this.refreshToken = normalizeText(config.refreshToken);
|
|
124
|
+
this.expiresAt = parseExpiresAtMs(config.expiresAt);
|
|
125
|
+
this._refreshPromise = null;
|
|
126
|
+
this._refreshTimer = null;
|
|
17
127
|
|
|
18
128
|
// 自动检测 URL 中的 token (用于小程序 WebView 静默登录)
|
|
19
129
|
if (isBrowser) {
|
|
20
130
|
this._checkUrlToken();
|
|
131
|
+
this._ensureRefreshTimer();
|
|
21
132
|
}
|
|
22
133
|
}
|
|
23
134
|
|
|
@@ -46,25 +157,133 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
46
157
|
_checkUrlToken() {
|
|
47
158
|
try {
|
|
48
159
|
const urlParams = new URLSearchParams(window.location.search);
|
|
49
|
-
|
|
50
|
-
const
|
|
160
|
+
const token = normalizeText(urlParams.get("tacoreai_mp_access_token"));
|
|
161
|
+
const refreshToken = normalizeText(urlParams.get("tacoreai_mp_refresh_token"));
|
|
162
|
+
const expiresAt = parseExpiresAtMs(urlParams.get("tacoreai_mp_expires_at"));
|
|
51
163
|
|
|
52
164
|
if (token) {
|
|
53
|
-
|
|
54
|
-
this.
|
|
165
|
+
const currentAccessToken = this.getAccessToken({ fallbackToStorage: true });
|
|
166
|
+
const currentRefreshToken = this.getRefreshToken({ fallbackToStorage: true });
|
|
167
|
+
const currentExpiresAt =
|
|
168
|
+
this.getExpiresAt({ fallbackToStorage: true }) ||
|
|
169
|
+
resolveExpiresAtMs({
|
|
170
|
+
accessToken: currentAccessToken,
|
|
171
|
+
});
|
|
172
|
+
const isDifferentUser =
|
|
173
|
+
Boolean(currentAccessToken) &&
|
|
174
|
+
Boolean(resolveTokenSubject(currentAccessToken)) &&
|
|
175
|
+
Boolean(resolveTokenSubject(token)) &&
|
|
176
|
+
resolveTokenSubject(currentAccessToken) !== resolveTokenSubject(token);
|
|
177
|
+
const shouldReplaceExistingSession =
|
|
178
|
+
!currentAccessToken ||
|
|
179
|
+
!currentExpiresAt ||
|
|
180
|
+
!currentRefreshToken ||
|
|
181
|
+
isDifferentUser ||
|
|
182
|
+
Boolean(refreshToken && expiresAt && expiresAt >= currentExpiresAt);
|
|
183
|
+
|
|
184
|
+
if (shouldReplaceExistingSession) {
|
|
185
|
+
console.log("[AppsAuthManager] Found token in URL, performing silent login...");
|
|
186
|
+
|
|
187
|
+
if (refreshToken) {
|
|
188
|
+
this.setSession({
|
|
189
|
+
accessToken: token,
|
|
190
|
+
refreshToken,
|
|
191
|
+
expiresAt,
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
this.setAccessToken(token);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
55
197
|
|
|
56
198
|
// 清除 URL 中的 token 参数,避免分享泄露
|
|
57
199
|
const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
|
58
200
|
// 保留其他参数
|
|
59
|
-
urlParams.delete(
|
|
201
|
+
urlParams.delete("tacoreai_mp_access_token");
|
|
202
|
+
urlParams.delete("tacoreai_mp_refresh_token");
|
|
203
|
+
urlParams.delete("tacoreai_mp_expires_at");
|
|
60
204
|
const remainingParams = urlParams.toString();
|
|
61
205
|
const finalUrl = remainingParams ? `${newUrl}?${remainingParams}` : newUrl;
|
|
62
206
|
|
|
63
|
-
window.history.replaceState({ path: finalUrl },
|
|
207
|
+
window.history.replaceState({ path: finalUrl }, "", finalUrl);
|
|
64
208
|
}
|
|
65
209
|
} catch (e) {
|
|
66
|
-
console.error(
|
|
210
|
+
console.error("[AppsAuthManager] Failed to parse URL token:", e);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_clearRefreshTimer() {
|
|
215
|
+
if (this._refreshTimer) {
|
|
216
|
+
clearTimeout(this._refreshTimer);
|
|
217
|
+
this._refreshTimer = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_scheduleRefreshRetry() {
|
|
222
|
+
if (!isBrowser || !this.getRefreshToken({ fallbackToStorage: true })) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this._clearRefreshTimer();
|
|
227
|
+
this._refreshTimer = setTimeout(() => {
|
|
228
|
+
this.refreshSessionIfNeeded({ force: true }).catch((error) => {
|
|
229
|
+
console.error("[AppsAuthManager] Retry session refresh failed:", error);
|
|
230
|
+
});
|
|
231
|
+
}, REFRESH_RETRY_DELAY_MS);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_ensureRefreshTimer() {
|
|
235
|
+
if (!isBrowser) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this._clearRefreshTimer();
|
|
240
|
+
|
|
241
|
+
const refreshToken = this.getRefreshToken({ fallbackToStorage: true });
|
|
242
|
+
const accessToken = this.getAccessToken({ fallbackToStorage: true });
|
|
243
|
+
if (!refreshToken || !accessToken) {
|
|
244
|
+
return;
|
|
67
245
|
}
|
|
246
|
+
|
|
247
|
+
let expiresAt = this.getExpiresAt({ fallbackToStorage: true });
|
|
248
|
+
if (!expiresAt) {
|
|
249
|
+
expiresAt = resolveExpiresAtMs({ accessToken });
|
|
250
|
+
if (expiresAt) {
|
|
251
|
+
this.setExpiresAt(expiresAt, { persist: true });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!expiresAt) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const delayMs = Math.max(MIN_TIMER_DELAY_MS, expiresAt - Date.now() - REFRESH_THRESHOLD_MS);
|
|
260
|
+
this._refreshTimer = setTimeout(() => {
|
|
261
|
+
this.refreshSessionIfNeeded({ force: true }).catch((error) => {
|
|
262
|
+
console.error("[AppsAuthManager] Automatic session refresh failed:", error);
|
|
263
|
+
});
|
|
264
|
+
}, delayMs);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_shouldRefreshSession(thresholdMs = REFRESH_THRESHOLD_MS) {
|
|
268
|
+
const accessToken = this.getAccessToken({ fallbackToStorage: true });
|
|
269
|
+
const refreshToken = this.getRefreshToken({ fallbackToStorage: true });
|
|
270
|
+
if (!accessToken || !refreshToken) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let expiresAt = this.getExpiresAt({ fallbackToStorage: true });
|
|
275
|
+
if (!expiresAt) {
|
|
276
|
+
expiresAt = resolveExpiresAtMs({ accessToken });
|
|
277
|
+
if (expiresAt) {
|
|
278
|
+
this.setExpiresAt(expiresAt, { persist: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!expiresAt) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return expiresAt - Date.now() <= thresholdMs;
|
|
68
287
|
}
|
|
69
288
|
|
|
70
289
|
/**
|
|
@@ -103,9 +322,8 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
103
322
|
body: JSON.stringify({ email, token, appId: this.appId }),
|
|
104
323
|
});
|
|
105
324
|
|
|
106
|
-
// 成功验证后,后端会返回会话信息
|
|
107
325
|
if (result.accessToken) {
|
|
108
|
-
this.
|
|
326
|
+
this.setSession(result);
|
|
109
327
|
}
|
|
110
328
|
|
|
111
329
|
return result;
|
|
@@ -125,7 +343,7 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
125
343
|
});
|
|
126
344
|
|
|
127
345
|
if (result.accessToken) {
|
|
128
|
-
this.
|
|
346
|
+
this.setSession(result);
|
|
129
347
|
}
|
|
130
348
|
|
|
131
349
|
return result;
|
|
@@ -144,12 +362,75 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
144
362
|
});
|
|
145
363
|
|
|
146
364
|
if (result.accessToken) {
|
|
147
|
-
this.
|
|
365
|
+
this.setSession(result);
|
|
148
366
|
}
|
|
149
367
|
|
|
150
368
|
return result;
|
|
151
369
|
}
|
|
152
370
|
|
|
371
|
+
async refreshSession(options = {}) {
|
|
372
|
+
const { force = false } = options;
|
|
373
|
+
const refreshToken = this.getRefreshToken({ fallbackToStorage: isBrowser });
|
|
374
|
+
|
|
375
|
+
if (!refreshToken) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!force && !this._shouldRefreshSession()) {
|
|
380
|
+
this._ensureRefreshTimer();
|
|
381
|
+
return this.getSessionSnapshot();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (this._refreshPromise) {
|
|
385
|
+
return this._refreshPromise;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this._clearRefreshTimer();
|
|
389
|
+
this._refreshPromise = (async () => {
|
|
390
|
+
try {
|
|
391
|
+
const result = await this._apiRequest("/apps/auth/refresh", {
|
|
392
|
+
method: "POST",
|
|
393
|
+
body: JSON.stringify({ refreshToken, appId: this.appId }),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (result?.accessToken) {
|
|
397
|
+
this.setSession(result);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return result;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error?.status === 401) {
|
|
403
|
+
this.clearSession();
|
|
404
|
+
} else {
|
|
405
|
+
this._scheduleRefreshRetry();
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
408
|
+
} finally {
|
|
409
|
+
this._refreshPromise = null;
|
|
410
|
+
}
|
|
411
|
+
})();
|
|
412
|
+
|
|
413
|
+
return this._refreshPromise;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async refreshSessionIfNeeded(options = {}) {
|
|
417
|
+
const { force = false, thresholdMs = REFRESH_THRESHOLD_MS } = options;
|
|
418
|
+
const accessToken = this.getAccessToken({ fallbackToStorage: isBrowser });
|
|
419
|
+
const refreshToken = this.getRefreshToken({ fallbackToStorage: isBrowser });
|
|
420
|
+
|
|
421
|
+
if (!accessToken || !refreshToken) {
|
|
422
|
+
this._ensureRefreshTimer();
|
|
423
|
+
return this.getSessionSnapshot();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!force && !this._shouldRefreshSession(thresholdMs)) {
|
|
427
|
+
this._ensureRefreshTimer();
|
|
428
|
+
return this.getSessionSnapshot();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return this.refreshSession({ force: true });
|
|
432
|
+
}
|
|
433
|
+
|
|
153
434
|
/**
|
|
154
435
|
* 请求发送密码重置邮件
|
|
155
436
|
* @param {string} email - 用户的邮箱
|
|
@@ -192,7 +473,7 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
192
473
|
method: "POST",
|
|
193
474
|
});
|
|
194
475
|
} finally {
|
|
195
|
-
this.
|
|
476
|
+
this.clearSession();
|
|
196
477
|
}
|
|
197
478
|
}
|
|
198
479
|
|
|
@@ -202,9 +483,13 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
202
483
|
*/
|
|
203
484
|
async getCurrentUser() {
|
|
204
485
|
try {
|
|
486
|
+
await this.refreshSessionIfNeeded();
|
|
205
487
|
const result = await this._apiRequest(`/apps/auth/profile?appId=${this.appId}`);
|
|
206
488
|
return result.user;
|
|
207
489
|
} catch (error) {
|
|
490
|
+
if (error?.status === 401) {
|
|
491
|
+
this.clearSession();
|
|
492
|
+
}
|
|
208
493
|
return null;
|
|
209
494
|
}
|
|
210
495
|
}
|
|
@@ -214,6 +499,8 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
214
499
|
* @returns {Promise<Object|null>}
|
|
215
500
|
*/
|
|
216
501
|
async getSession() {
|
|
502
|
+
await this.refreshSessionIfNeeded();
|
|
503
|
+
|
|
217
504
|
const token = this.getAccessToken({ fallbackToStorage: isBrowser });
|
|
218
505
|
if (!token) {
|
|
219
506
|
return null;
|
|
@@ -223,9 +510,14 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
223
510
|
const result = await this._apiRequest(`/apps/auth/profile?appId=${this.appId}`);
|
|
224
511
|
return {
|
|
225
512
|
access_token: token,
|
|
513
|
+
refresh_token: this.getRefreshToken({ fallbackToStorage: isBrowser }),
|
|
514
|
+
expires_at: this.getExpiresAt({ fallbackToStorage: isBrowser }),
|
|
226
515
|
user: result.user,
|
|
227
516
|
};
|
|
228
517
|
} catch (error) {
|
|
518
|
+
if (error?.status === 401) {
|
|
519
|
+
this.clearSession();
|
|
520
|
+
}
|
|
229
521
|
return null;
|
|
230
522
|
}
|
|
231
523
|
}
|
|
@@ -239,10 +531,13 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
239
531
|
if (!token) return false;
|
|
240
532
|
|
|
241
533
|
try {
|
|
242
|
-
await this.getCurrentUser();
|
|
534
|
+
const user = await this.getCurrentUser();
|
|
535
|
+
if (!user) {
|
|
536
|
+
this.clearSession();
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
243
539
|
return true;
|
|
244
540
|
} catch (error) {
|
|
245
|
-
this.clearAccessToken();
|
|
246
541
|
return false;
|
|
247
542
|
}
|
|
248
543
|
}
|
|
@@ -257,6 +552,8 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
257
552
|
this.isAuthenticated().then((isAuth) => {
|
|
258
553
|
callback(isAuth ? "SIGNED_IN" : "SIGNED_OUT", {
|
|
259
554
|
access_token: this.getAccessToken({ fallbackToStorage: isBrowser }),
|
|
555
|
+
refresh_token: this.getRefreshToken({ fallbackToStorage: isBrowser }),
|
|
556
|
+
expires_at: this.getExpiresAt({ fallbackToStorage: isBrowser }),
|
|
260
557
|
});
|
|
261
558
|
});
|
|
262
559
|
|
|
@@ -270,6 +567,7 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
270
567
|
*/
|
|
271
568
|
async autoAuth() {
|
|
272
569
|
try {
|
|
570
|
+
await this.refreshSessionIfNeeded();
|
|
273
571
|
const isAuth = await this.isAuthenticated();
|
|
274
572
|
if (isAuth) {
|
|
275
573
|
const user = await this.getCurrentUser();
|
|
@@ -307,10 +605,124 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
307
605
|
});
|
|
308
606
|
}
|
|
309
607
|
|
|
608
|
+
setRefreshToken(token, options = {}) {
|
|
609
|
+
const { persist = isBrowser } = options;
|
|
610
|
+
const normalizedToken = normalizeText(token);
|
|
611
|
+
this.refreshToken = normalizedToken;
|
|
612
|
+
|
|
613
|
+
if (persist) {
|
|
614
|
+
writeStorageValue(REFRESH_TOKEN_STORAGE_KEY, normalizedToken);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return normalizedToken;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
getRefreshToken(options = {}) {
|
|
621
|
+
const { fallbackToStorage = isBrowser } = options;
|
|
622
|
+
if (this.refreshToken) {
|
|
623
|
+
return this.refreshToken;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!fallbackToStorage) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return readStorageValue(REFRESH_TOKEN_STORAGE_KEY);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
clearRefreshToken(options = {}) {
|
|
634
|
+
const { persist = isBrowser } = options;
|
|
635
|
+
this.refreshToken = null;
|
|
636
|
+
|
|
637
|
+
if (persist) {
|
|
638
|
+
writeStorageValue(REFRESH_TOKEN_STORAGE_KEY, null);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
setExpiresAt(value, options = {}) {
|
|
643
|
+
const { persist = isBrowser } = options;
|
|
644
|
+
const normalizedExpiresAt = parseExpiresAtMs(value);
|
|
645
|
+
this.expiresAt = normalizedExpiresAt;
|
|
646
|
+
|
|
647
|
+
if (persist) {
|
|
648
|
+
writeStorageValue(
|
|
649
|
+
ACCESS_TOKEN_EXPIRES_AT_STORAGE_KEY,
|
|
650
|
+
normalizedExpiresAt ? String(normalizedExpiresAt) : null
|
|
651
|
+
);
|
|
652
|
+
writeStorageValue(LEGACY_EXPIRES_AT_STORAGE_KEY, null);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return normalizedExpiresAt;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
getExpiresAt(options = {}) {
|
|
659
|
+
const { fallbackToStorage = isBrowser } = options;
|
|
660
|
+
if (this.expiresAt) {
|
|
661
|
+
return this.expiresAt;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!fallbackToStorage) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return parseExpiresAtMs(readStorageValue(ACCESS_TOKEN_EXPIRES_AT_STORAGE_KEY))
|
|
669
|
+
|| parseExpiresAtMs(readStorageValue(LEGACY_EXPIRES_AT_STORAGE_KEY));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
clearExpiresAt(options = {}) {
|
|
673
|
+
const { persist = isBrowser } = options;
|
|
674
|
+
this.expiresAt = null;
|
|
675
|
+
|
|
676
|
+
if (persist) {
|
|
677
|
+
writeStorageValue(ACCESS_TOKEN_EXPIRES_AT_STORAGE_KEY, null);
|
|
678
|
+
writeStorageValue(LEGACY_EXPIRES_AT_STORAGE_KEY, null);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
setSession(session = {}, options = {}) {
|
|
683
|
+
const { persist = isBrowser, syncConfig = true } = options;
|
|
684
|
+
const accessToken = super.setAccessToken(session.accessToken, {
|
|
685
|
+
persist,
|
|
686
|
+
syncConfig,
|
|
687
|
+
});
|
|
688
|
+
const refreshToken = this.setRefreshToken(session.refreshToken, { persist });
|
|
689
|
+
const expiresAt = this.setExpiresAt(
|
|
690
|
+
resolveExpiresAtMs({
|
|
691
|
+
accessToken,
|
|
692
|
+
expiresAt: session.expiresAt,
|
|
693
|
+
expiresIn: session.expiresIn,
|
|
694
|
+
}),
|
|
695
|
+
{ persist }
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
this._ensureRefreshTimer();
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
accessToken,
|
|
702
|
+
refreshToken,
|
|
703
|
+
expiresAt,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
getSessionSnapshot() {
|
|
708
|
+
return {
|
|
709
|
+
accessToken: this.getAccessToken({ fallbackToStorage: isBrowser }),
|
|
710
|
+
refreshToken: this.getRefreshToken({ fallbackToStorage: isBrowser }),
|
|
711
|
+
expiresAt: this.getExpiresAt({ fallbackToStorage: isBrowser }),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
clearSession(options = {}) {
|
|
716
|
+
this._clearRefreshTimer();
|
|
717
|
+
super.clearAccessToken(options);
|
|
718
|
+
this.clearRefreshToken(options);
|
|
719
|
+
this.clearExpiresAt(options);
|
|
720
|
+
}
|
|
721
|
+
|
|
310
722
|
/**
|
|
311
723
|
* 清除访问令牌
|
|
312
724
|
*/
|
|
313
725
|
clearAccessToken(options = {}) {
|
|
314
|
-
|
|
726
|
+
this.clearSession(options);
|
|
315
727
|
}
|
|
316
728
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isBrowser
|
|
1
|
+
import { isBrowser } from "../../../utils/index.js";
|
|
2
2
|
import { BaseAppsClient } from "./BaseAppsClient.js";
|
|
3
3
|
import { VolcengineImpl } from "./AppsClient.Volcengine.js";
|
|
4
4
|
import { appsClientAppServerMethods } from "./AppsClient.AppServer.js";
|
|
@@ -41,14 +41,6 @@ class AppsClient extends BaseAppsClient {
|
|
|
41
41
|
|
|
42
42
|
constructor(appId, config = {}) {
|
|
43
43
|
super(appId, config);
|
|
44
|
-
const isPreviewMode = isBackend && process.env.TACORE_APPSERVER_PREVIEW_MODE === "true";
|
|
45
|
-
|
|
46
|
-
if (isBackend && !isPreviewMode && !this.config.tacoreServerInteropAppServerApiKey) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
"tacoreServerInteropAppServerApiKey is required for backend environment. Provide it in config or via TACORE_SERVER_INTEROP_APP_SERVER_API_KEY env var."
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
44
|
instanceMap.set(appId, this);
|
|
53
45
|
|
|
54
46
|
// 用于本地实时数据模式的订阅者管理
|
|
@@ -221,4 +213,4 @@ Object.assign(
|
|
|
221
213
|
appsClientCrawlerMethods
|
|
222
214
|
);
|
|
223
215
|
|
|
224
|
-
export { AppsClient };
|
|
216
|
+
export { AppsClient };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { getAppsApiBaseUrl, isBrowser, isBackend, getGlobalOptions } from "../../../utils/index.js";
|
|
1
|
+
import { getAppsApiBaseUrl, isBrowser, isBackend, isSSR, getGlobalOptions } from "../../../utils/index.js";
|
|
2
2
|
|
|
3
3
|
const ACCESS_TOKEN_STORAGE_KEY = "tacoreai_apps_access_token";
|
|
4
4
|
const PREVIEW_APPSERVER_API_KEY_HEADER = "x-tacore-preview-app-server-api-key";
|
|
5
|
+
const INTEROP_APP_SERVER_API_KEY_REQUIRED_ERROR =
|
|
6
|
+
"tacoreServerInteropAppServerApiKey is required for backend environment. Provide it in config or via TACORE_SERVER_INTEROP_APP_SERVER_API_KEY env var.";
|
|
5
7
|
|
|
6
8
|
const normalizeAccessToken = (value) => {
|
|
7
9
|
if (typeof value !== "string") {
|
|
@@ -46,6 +48,7 @@ const writePersistedAccessToken = (token) => {
|
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
const isBackendPreviewMode = () => isBackend && process.env.TACORE_APPSERVER_PREVIEW_MODE === "true";
|
|
51
|
+
const shouldRequireInteropAppServerApiKey = () => isBackend && !isSSR && !isBackendPreviewMode();
|
|
49
52
|
|
|
50
53
|
/**
|
|
51
54
|
* Apps SDK 基类
|
|
@@ -79,9 +82,13 @@ export class BaseAppsClient {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
// 后端环境通用逻辑:尝试从环境变量加载 API Key
|
|
82
|
-
if (isBackend && !this.config.tacoreServerInteropAppServerApiKey) {
|
|
85
|
+
if (isBackend && !isSSR && !this.config.tacoreServerInteropAppServerApiKey) {
|
|
83
86
|
this.config.tacoreServerInteropAppServerApiKey = process.env.TACORE_SERVER_INTEROP_APP_SERVER_API_KEY;
|
|
84
87
|
}
|
|
88
|
+
|
|
89
|
+
if (shouldRequireInteropAppServerApiKey() && !this.config.tacoreServerInteropAppServerApiKey) {
|
|
90
|
+
throw new Error(INTEROP_APP_SERVER_API_KEY_REQUIRED_ERROR);
|
|
91
|
+
}
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
/**
|
|
@@ -179,7 +186,7 @@ export class BaseAppsClient {
|
|
|
179
186
|
headers["Authorization"] = `Bearer ${token}`;
|
|
180
187
|
}
|
|
181
188
|
|
|
182
|
-
if (isBackend && this.config.tacoreServerInteropAppServerApiKey) {
|
|
189
|
+
if (isBackend && !isSSR && this.config.tacoreServerInteropAppServerApiKey) {
|
|
183
190
|
headers["x-tacore-server-interop-app-server-api-key"] = this.config.tacoreServerInteropAppServerApiKey;
|
|
184
191
|
} else if (
|
|
185
192
|
isBackendPreviewMode() &&
|
|
@@ -216,7 +223,10 @@ export class BaseAppsClient {
|
|
|
216
223
|
if (!response.ok) {
|
|
217
224
|
const errorData = await response.json().catch(() => ({ error: "Network error" }));
|
|
218
225
|
console.error(`[SDK][${this.appId}] API request failed: ${response.status} ${response.statusText}`, errorData);
|
|
219
|
-
|
|
226
|
+
const error = new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
|
227
|
+
error.status = response.status;
|
|
228
|
+
error.responseData = errorData;
|
|
229
|
+
throw error;
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
const result = await response.json();
|
package/package.json
CHANGED
package/utils/index.js
CHANGED
|
@@ -15,6 +15,8 @@ export function getGlobalOptions() {
|
|
|
15
15
|
|
|
16
16
|
// 新增:环境判断
|
|
17
17
|
export const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
18
|
+
// 仅将 Vite SSR 识别为 SSR,避免把普通 Node 后端和 SSR 混在一起。
|
|
19
|
+
export const isSSR = !isBrowser && typeof import.meta !== "undefined" && Boolean(import.meta.env?.SSR);
|
|
18
20
|
// 后端环境 (Node.js) 为 'backend', 浏览器环境根据全局变量判断,默认为 'development'
|
|
19
21
|
export const env = isBrowser ? (window.__TACORE_APP_ENV__ || 'development') : 'backend';
|
|
20
22
|
|
|
@@ -47,11 +49,6 @@ export function getRouterBasename() {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function getAppsApiBaseUrl() {
|
|
50
|
-
const isSSR = !!(
|
|
51
|
-
(typeof import.meta !== 'undefined' && import.meta.env?.SSR) ||
|
|
52
|
-
(typeof window === 'undefined')
|
|
53
|
-
);
|
|
54
|
-
// stone手动: 标准构建 ssr 环境,直接返回生产环境地址
|
|
55
52
|
if (isSSR) {
|
|
56
53
|
return `https://api.tacore.chat`;
|
|
57
54
|
}
|