@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.
@@ -1,6 +1,117 @@
1
- import { isBrowser, isBackend } from "../../../utils/index.js";
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
- const isPreviewMode = isBackend && process.env.TACORE_APPSERVER_PREVIEW_MODE === "true";
13
-
14
- if (isBackend && !isPreviewMode && !this.config.tacoreServerInteropAppServerApiKey) {
15
- throw new Error("tacoreServerInteropAppServerApiKey is required for backend environment. Provide it in config or via TACORE_SERVER_INTEROP_APP_SERVER_API_KEY env var.");
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
- 'forgotPassword': ({ email, redirectTo }) => this.forgotPassword(email, redirectTo),
35
- 'updatePassword': ({ accessToken, newPassword }) => this.updatePassword(accessToken, newPassword),
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
- // 更新:从小程序传递的特定参数名获取 Token
50
- const token = urlParams.get('tacoreai_mp_access_token');
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
- console.log('[AppsAuthManager] Found token in URL, performing silent login...');
54
- this.setAccessToken(token);
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('tacoreai_mp_access_token');
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 }, '', finalUrl);
207
+ window.history.replaceState({ path: finalUrl }, "", finalUrl);
64
208
  }
65
209
  } catch (e) {
66
- console.error('[AppsAuthManager] Failed to parse URL token:', e);
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.setAccessToken(result.accessToken);
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.setAccessToken(result.accessToken);
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.setAccessToken(result.accessToken);
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, redirectTo) {
160
- if (!email || !redirectTo) {
161
- throw new Error("Email and redirectTo URL are required.");
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, redirectTo }),
445
+ body: JSON.stringify({ email }),
166
446
  });
167
447
  }
168
448
 
169
449
  /**
170
- * 使用令牌更新用户密码
171
- * @param {string} accessToken - 从重定向URL中获取的访问令牌
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 updatePassword(accessToken, newPassword) {
176
- if (!accessToken || !newPassword) {
177
- throw new Error("Access token and new password are required.");
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/update-password", {
460
+ return this._apiRequest("/apps/auth/reset-password", {
180
461
  method: "POST",
181
- body: JSON.stringify({ accessToken, newPassword }),
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.clearAccessToken();
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
- super.clearAccessToken(options);
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, isBackend } from "../../../utils/index.js";
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
- throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tacoreai/web-sdk",
3
3
  "description": "This file is for app server package, not the real npm package",
4
- "version": "1.18.0",
4
+ "version": "1.20.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public",
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
  }