@tacoreai/web-sdk 1.16.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, 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
 
@@ -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,12 +362,75 @@ 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
  * 请求发送密码重置邮件
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.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
  }
@@ -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";
@@ -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 };
@@ -20,8 +20,16 @@ export class AppsPublicClient extends BaseAppsClient {
20
20
  get adapters() {
21
21
  return {
22
22
  'submitPublicData': ({ modelName, data }) => this.submitPublicData(modelName, data),
23
- 'readConfiguredPublicData': (params) => {
24
- const { modelName, ...rest } = params;
23
+ 'readConfiguredPublicData': (params = {}) => {
24
+ const { modelName, query, ...rest } = params;
25
+
26
+ // 新版约定:invoke('readConfiguredPublicData', { modelName, query })
27
+ // 其中 query 可以是 wyID 字符串,也可以是分页/过滤对象。
28
+ if (query !== undefined) {
29
+ return this.readConfiguredPublicData(modelName, query);
30
+ }
31
+
32
+ // 兼容旧版平铺参数:invoke('readConfiguredPublicData', { modelName, wyID })
25
33
  if (rest.wyID && Object.keys(rest).length === 1) {
26
34
  return this.readConfiguredPublicData(modelName, rest.wyID);
27
35
  }
@@ -104,4 +112,4 @@ export class AppsPublicClient extends BaseAppsClient {
104
112
  throw error;
105
113
  }
106
114
  }
107
- }
115
+ }
@@ -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() &&
@@ -200,13 +207,13 @@ export class BaseAppsClient {
200
207
  const url = `${this.config.apiBaseUrl}${endpoint}`;
201
208
  const headers = this._getRequestHeaders(options.headers);
202
209
 
203
- if (isBackendPreviewMode()) {
204
- console.log(`[BaseAppsClient Debug][${this.appId}] endpoint="${endpoint}"`, {
205
- hasAuthorization: Boolean(headers.Authorization || headers.authorization),
206
- hasInteropHeader: Boolean(headers["x-tacore-server-interop-app-server-api-key"]),
207
- hasPreviewAppServerApiKeyHeader: Boolean(headers[PREVIEW_APPSERVER_API_KEY_HEADER]),
208
- });
209
- }
210
+ // if (isBackendPreviewMode()) {
211
+ // console.log(`[BaseAppsClient Debug][${this.appId}] endpoint="${endpoint}"`, {
212
+ // hasAuthorization: Boolean(headers.Authorization || headers.authorization),
213
+ // hasInteropHeader: Boolean(headers["x-tacore-server-interop-app-server-api-key"]),
214
+ // hasPreviewAppServerApiKeyHeader: Boolean(headers[PREVIEW_APPSERVER_API_KEY_HEADER]),
215
+ // });
216
+ // }
210
217
 
211
218
  const response = await fetch(url, {
212
219
  ...options,
@@ -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.16.0",
4
+ "version": "1.19.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,8 +49,7 @@ export function getRouterBasename() {
47
49
  }
48
50
 
49
51
  export function getAppsApiBaseUrl() {
50
- // stone手动: 标准构建 ssr 环境,直接返回生产环境地址
51
- if (process.env.MODE === 'production' && process.env.SSR === 'true') {
52
+ if (isSSR) {
52
53
  return `https://api.tacore.chat`;
53
54
  }
54
55
  // 后端环境逻辑