@tacoreai/web-sdk 1.18.0 → 1.20.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.
- package/database/core/apps/AppsAuthManager.js +448 -36
- package/database/core/apps/AppsClient.Users.js +16 -0
- package/database/core/apps/AppsClient.Wechat.js +155 -0
- package/database/core/apps/AppsClient.js +6 -11
- package/database/core/apps/BaseAppsClient.js +14 -4
- package/package.json +1 -1
- package/utils/index.js +2 -5
|
@@ -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
|
|
|
@@ -31,8 +142,8 @@ export class AppsAuthManager extends BaseAppsClient {
|
|
|
31
142
|
'verifyRegistration': ({ email, token }) => this.verifyRegistration(email, token),
|
|
32
143
|
'loginWithPassword': ({ email, password }) => this.loginWithPassword(email, password),
|
|
33
144
|
'loginWithWechatH5': ({ code, scope }) => this.loginWithWechatH5(code, scope),
|
|
34
|
-
'
|
|
35
|
-
'
|
|
145
|
+
'auth/forgot-password': ({ email }) => this.forgotPassword(email),
|
|
146
|
+
'auth/reset-password': ({ email, token, newPassword }) => this.resetPassword(email, token, newPassword),
|
|
36
147
|
'logout': () => this.logout(),
|
|
37
148
|
'getCurrentUser': () => this.getCurrentUser(),
|
|
38
149
|
'getSession': () => this.getSession(),
|
|
@@ -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,41 +362,104 @@ 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
|
+
* 请求发送密码重置邮件(包含 recovery 验证码)
|
|
155
436
|
* @param {string} email - 用户的邮箱
|
|
156
|
-
* @param {string} redirectTo - 用户点击邮件链接后重定向到的前端页面URL
|
|
157
437
|
* @returns {Promise<Object>}
|
|
158
438
|
*/
|
|
159
|
-
async forgotPassword(email
|
|
160
|
-
if (!email
|
|
161
|
-
throw new Error("Email
|
|
439
|
+
async forgotPassword(email) {
|
|
440
|
+
if (!email) {
|
|
441
|
+
throw new Error("Email is required.");
|
|
162
442
|
}
|
|
163
443
|
return this._apiRequest("/apps/auth/forgot-password", {
|
|
164
444
|
method: "POST",
|
|
165
|
-
body: JSON.stringify({ email
|
|
445
|
+
body: JSON.stringify({ email }),
|
|
166
446
|
});
|
|
167
447
|
}
|
|
168
448
|
|
|
169
449
|
/**
|
|
170
|
-
*
|
|
171
|
-
* @param {string}
|
|
450
|
+
* 使用邮箱验证码更新用户密码
|
|
451
|
+
* @param {string} email - 用户邮箱
|
|
452
|
+
* @param {string} token - 邮箱收到的 recovery OTP
|
|
172
453
|
* @param {string} newPassword - 用户输入的新密码
|
|
173
454
|
* @returns {Promise<Object>}
|
|
174
455
|
*/
|
|
175
|
-
async
|
|
176
|
-
if (!
|
|
177
|
-
throw new Error("
|
|
456
|
+
async resetPassword(email, token, newPassword) {
|
|
457
|
+
if (!email || !token || !newPassword) {
|
|
458
|
+
throw new Error("Email, token and new password are required.");
|
|
178
459
|
}
|
|
179
|
-
return this._apiRequest("/apps/auth/
|
|
460
|
+
return this._apiRequest("/apps/auth/reset-password", {
|
|
180
461
|
method: "POST",
|
|
181
|
-
body: JSON.stringify({
|
|
462
|
+
body: JSON.stringify({ email, token, newPassword }),
|
|
182
463
|
});
|
|
183
464
|
}
|
|
184
465
|
|
|
@@ -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
|
}
|
|
@@ -46,4 +46,20 @@ export const appsClientUserMethods = {
|
|
|
46
46
|
role: role,
|
|
47
47
|
});
|
|
48
48
|
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* [新增] 更新或重置一个用户在当前应用中的登录密码
|
|
52
|
+
* @param {Object} params { userId: string, password: string }
|
|
53
|
+
* @returns {Promise<{success: boolean, data: {userId: string, appId: string}}>}
|
|
54
|
+
*/
|
|
55
|
+
async updateUserPassword({ userId, password }) {
|
|
56
|
+
if (!userId || !password) {
|
|
57
|
+
throw new Error("userId and password are required.");
|
|
58
|
+
}
|
|
59
|
+
return this._post("/apps/users/update-password", {
|
|
60
|
+
appId: this.appId,
|
|
61
|
+
targetUserId: userId,
|
|
62
|
+
password,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
49
65
|
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isBrowser } from "../../../utils/index.js";
|
|
2
|
+
|
|
3
|
+
const appendWechatFileToForm = (form, file) => {
|
|
4
|
+
if (!file) {
|
|
5
|
+
throw new Error("file is required");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (isBrowser) {
|
|
9
|
+
if (file instanceof File || file instanceof Blob) {
|
|
10
|
+
form.append("file", file, file.name || "file");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
throw new Error("Invalid file input in browser. Provide a File or Blob object.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof file === "object" && file?.data && file?.encoding === "base64" && file?.name) {
|
|
18
|
+
const buffer = Buffer.from(file.data, "base64");
|
|
19
|
+
const blob = new Blob([buffer], {
|
|
20
|
+
type: file.type || file.mimeType || "application/octet-stream",
|
|
21
|
+
});
|
|
22
|
+
form.append("file", blob, file.name);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (file instanceof Blob) {
|
|
27
|
+
form.append("file", file, file.name || "file");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw new Error(
|
|
32
|
+
"Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or a Blob."
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const postWechatMultipart = async function(endpoint, form) {
|
|
37
|
+
const headers = this._getRequestHeaders();
|
|
38
|
+
delete headers["Content-Type"];
|
|
39
|
+
delete headers["content-type"];
|
|
40
|
+
|
|
41
|
+
const response = await fetch(`${this.config.apiBaseUrl}${endpoint}`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers,
|
|
44
|
+
body: form,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const result = await response.json().catch(() => ({}));
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(result.message || result.error || `HTTP ${response.status}: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof this._validateResponse === "function") {
|
|
53
|
+
this._validateResponse(result);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const appsClientWechatMethods = {
|
|
60
|
+
async wechatGetCapabilities(options = {}) {
|
|
61
|
+
return this._post("/apps/wechat/capabilities", options);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async wechatUploadArticleImage({ file }) {
|
|
65
|
+
const form = new FormData();
|
|
66
|
+
form.append("appId", this.appId);
|
|
67
|
+
appendWechatFileToForm(form, file);
|
|
68
|
+
return await postWechatMultipart.call(this, "/apps/wechat/official-account/materials/upload-article-image", form);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async wechatUploadTemporaryMaterial({ file, type }) {
|
|
72
|
+
if (!type) {
|
|
73
|
+
throw new Error("type is required for wechatUploadTemporaryMaterial");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const form = new FormData();
|
|
77
|
+
form.append("appId", this.appId);
|
|
78
|
+
form.append("type", type);
|
|
79
|
+
appendWechatFileToForm(form, file);
|
|
80
|
+
return await postWechatMultipart.call(this, "/apps/wechat/official-account/materials/upload-temporary", form);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async wechatUploadPermanentMaterial({ file, type, title, introduction }) {
|
|
84
|
+
if (!type) {
|
|
85
|
+
throw new Error("type is required for wechatUploadPermanentMaterial");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const form = new FormData();
|
|
89
|
+
form.append("appId", this.appId);
|
|
90
|
+
form.append("type", type);
|
|
91
|
+
if (title) form.append("title", title);
|
|
92
|
+
if (introduction) form.append("introduction", introduction);
|
|
93
|
+
appendWechatFileToForm(form, file);
|
|
94
|
+
return await postWechatMultipart.call(this, "/apps/wechat/official-account/materials/upload-permanent", form);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async wechatGetPermanentMaterialCount() {
|
|
98
|
+
return this._post("/apps/wechat/official-account/materials/count", {});
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async wechatListPermanentMaterials(options = {}) {
|
|
102
|
+
return this._post("/apps/wechat/official-account/materials/list", options);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async wechatDeletePermanentMaterial({ mediaId }) {
|
|
106
|
+
return this._post("/apps/wechat/official-account/materials/delete", { mediaId });
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async wechatCreateDraft({ articles }) {
|
|
110
|
+
return this._post("/apps/wechat/official-account/drafts/add", { articles });
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async wechatUpdateDraft({ mediaId, index = 0, article }) {
|
|
114
|
+
return this._post("/apps/wechat/official-account/drafts/update", {
|
|
115
|
+
mediaId,
|
|
116
|
+
index,
|
|
117
|
+
article,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async wechatGetDraft({ mediaId }) {
|
|
122
|
+
return this._post("/apps/wechat/official-account/drafts/get", { mediaId });
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async wechatListDrafts(options = {}) {
|
|
126
|
+
return this._post("/apps/wechat/official-account/drafts/list", options);
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async wechatDeleteDraft({ mediaId }) {
|
|
130
|
+
return this._post("/apps/wechat/official-account/drafts/delete", { mediaId });
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async wechatPublishDraft({ mediaId }) {
|
|
134
|
+
return this._post("/apps/wechat/official-account/publish/submit", { mediaId });
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async wechatGetPublishStatus({ publishId }) {
|
|
138
|
+
return this._post("/apps/wechat/official-account/publish/status", { publishId });
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async wechatGetPublishedArticle({ articleId }) {
|
|
142
|
+
return this._post("/apps/wechat/official-account/publish/get-article", { articleId });
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
async wechatListPublishedArticles(options = {}) {
|
|
146
|
+
return this._post("/apps/wechat/official-account/publish/list", options);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async wechatDeletePublishedArticle({ articleId, index }) {
|
|
150
|
+
return this._post("/apps/wechat/official-account/publish/delete-article", {
|
|
151
|
+
articleId,
|
|
152
|
+
index,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
};
|
|
@@ -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";
|
|
@@ -17,6 +17,7 @@ import { appsClientTextInMethods } from "./AppsClient.TextIn.js";
|
|
|
17
17
|
import { appsClientToolsMethods } from "./AppsClient.Tools.js";
|
|
18
18
|
import { appsClientDebugMethods } from "./AppsClient.Debug.js";
|
|
19
19
|
import { appsClientCrawlerMethods } from "./AppsClient.Crawler.js";
|
|
20
|
+
import { appsClientWechatMethods } from "./AppsClient.Wechat.js";
|
|
20
21
|
|
|
21
22
|
const instanceMap = new Map();
|
|
22
23
|
/**
|
|
@@ -41,14 +42,6 @@ class AppsClient extends BaseAppsClient {
|
|
|
41
42
|
|
|
42
43
|
constructor(appId, config = {}) {
|
|
43
44
|
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
45
|
instanceMap.set(appId, this);
|
|
53
46
|
|
|
54
47
|
// 用于本地实时数据模式的订阅者管理
|
|
@@ -109,6 +102,7 @@ class AppsClient extends BaseAppsClient {
|
|
|
109
102
|
// === 用户与角色 ===
|
|
110
103
|
"createRole": params => this.createRole(params),
|
|
111
104
|
"updateUserRole": params => this.updateUserRole(params),
|
|
105
|
+
"users/update-password": params => this.updateUserPassword(params),
|
|
112
106
|
"listAppUsers": params => this.listAppUsers(params),
|
|
113
107
|
|
|
114
108
|
// === 文件存储 ===
|
|
@@ -218,7 +212,8 @@ Object.assign(
|
|
|
218
212
|
appsClientTextInMethods,
|
|
219
213
|
appsClientToolsMethods,
|
|
220
214
|
appsClientDebugMethods,
|
|
221
|
-
appsClientCrawlerMethods
|
|
215
|
+
appsClientCrawlerMethods,
|
|
216
|
+
appsClientWechatMethods
|
|
222
217
|
);
|
|
223
218
|
|
|
224
|
-
export { AppsClient };
|
|
219
|
+
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
|
}
|