@tacoreai/web-sdk 1.8.0 → 1.9.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.
@@ -0,0 +1,351 @@
1
+ import { AppsClient } from "./AppsClient.js";
2
+
3
+ export const APPSERVER_REQUEST_DATA_FIELD = "requestData";
4
+
5
+ const DEFAULT_JSON_LIMIT = "50mb";
6
+ const DEFAULT_FILE_SIZE_LIMIT = 100 * 1024 * 1024;
7
+ const DEFAULT_MAX_FILES = 20;
8
+
9
+ const isPreviewMode = () => process.env.TACORE_APPSERVER_PREVIEW_MODE === "true";
10
+
11
+ const isValidInternalToken = (incomingValue, expectedValue) => {
12
+ return Boolean(expectedValue) && Boolean(incomingValue) && incomingValue === expectedValue;
13
+ };
14
+
15
+ const decodeUploadedFileName = (value) => {
16
+ if (typeof value !== "string" || !value) {
17
+ return "";
18
+ }
19
+
20
+ try {
21
+ return Buffer.from(value, "latin1").toString("utf8");
22
+ } catch {
23
+ return value;
24
+ }
25
+ };
26
+
27
+ const getRequestSource = (isPlatformProxyRequest, isSchedulerRequest) => {
28
+ if (isPlatformProxyRequest) return "platform-proxy";
29
+ if (isSchedulerRequest) return "scheduler";
30
+ return isPreviewMode() ? "preview-direct" : "external-direct";
31
+ };
32
+
33
+ const isPreviewApiKeyShapeValid = (apiKey) => {
34
+ if (typeof apiKey !== "string") return false;
35
+ const value = apiKey.trim();
36
+ if (value.length < 8 || value.length > 128) return false;
37
+ return /^[A-Za-z0-9._-]+$/.test(value);
38
+ };
39
+
40
+ const validateExternalApiKey = async (appsClient, appId, apiKey) => {
41
+ const result = await appsClient._post("/apps/application/validate-app-server-api-key", {
42
+ appId,
43
+ apiKey,
44
+ });
45
+ return Boolean(result?.success && result?.data?.valid);
46
+ };
47
+
48
+ const getPreviewGuidePayload = (req) => {
49
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
50
+ const invokeUrl = `${baseUrl}/invokeAppServerAPI?apiName=YOUR_API_NAME`;
51
+
52
+ return {
53
+ success: true,
54
+ code: "SUCCESS",
55
+ data: {
56
+ previewMode: isPreviewMode(),
57
+ notes: [
58
+ "浏览器内的开发预览会通过 CLI 预览桥自动转发到本地应用服务,无需前端手动配置 X-API-Key。",
59
+ "无文件请求建议继续使用 application/json;需要同时上传文件与 JSON 时,请改用 multipart/form-data,并把 JSON 放在 requestData 字段中。",
60
+ ],
61
+ examples: {
62
+ curlJson: `curl -X POST '${invokeUrl}' -H 'Content-Type: application/json' -H 'X-API-Key: YOUR_APP_SERVER_API_KEY' -d '{\"hello\":\"world\"}'`,
63
+ curlMultipart: `curl -X POST '${invokeUrl}' -H 'X-API-Key: YOUR_APP_SERVER_API_KEY' -F '${APPSERVER_REQUEST_DATA_FIELD}={\"hello\":\"world\"}' -F 'file=@/path/to/file.pdf'`,
64
+ },
65
+ },
66
+ };
67
+ };
68
+
69
+ const createPreviewCorsMiddleware = () => {
70
+ return (req, res, next) => {
71
+ if (isPreviewMode()) {
72
+ res.setHeader("Access-Control-Allow-Origin", "*");
73
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
74
+ res.setHeader(
75
+ "Access-Control-Allow-Headers",
76
+ "Authorization, Content-Type, X-API-Key, X-Tacore-Server-Interop-App-Server-API-Key, X-Cloudflare-Worker-Interop-App-Server-Token"
77
+ );
78
+ if (req.method === "OPTIONS") {
79
+ return res.status(204).end();
80
+ }
81
+ }
82
+ next();
83
+ };
84
+ };
85
+
86
+ const normalizeUploadedFiles = (files = []) => {
87
+ return files.map((file) => {
88
+ const fileName = decodeUploadedFileName(file.originalname || "");
89
+ return {
90
+ fieldName: file.fieldname,
91
+ fileName: fileName || file.originalname || "file",
92
+ mimeType: file.mimetype || "application/octet-stream",
93
+ size: Number(file.size) || 0,
94
+ buffer: file.buffer,
95
+ encoding: file.encoding || "7bit",
96
+ };
97
+ });
98
+ };
99
+
100
+ const isMultipartRequest = (req) => {
101
+ const contentType = req.get("content-type") || "";
102
+ return contentType.includes("multipart/form-data");
103
+ };
104
+
105
+ const parseRequestPayload = (req) => {
106
+ if (!isMultipartRequest(req)) {
107
+ return typeof req.body === "undefined" ? {} : req.body;
108
+ }
109
+
110
+ const rawRequestData = typeof req.body?.[APPSERVER_REQUEST_DATA_FIELD] === "string"
111
+ ? req.body[APPSERVER_REQUEST_DATA_FIELD]
112
+ : "";
113
+
114
+ if (!rawRequestData.trim()) {
115
+ return {};
116
+ }
117
+
118
+ try {
119
+ return JSON.parse(rawRequestData);
120
+ } catch (error) {
121
+ const parseError = new Error(`${APPSERVER_REQUEST_DATA_FIELD} must be valid JSON`);
122
+ parseError.statusCode = 400;
123
+ parseError.code = "INVALID_REQUEST_DATA";
124
+ throw parseError;
125
+ }
126
+ };
127
+
128
+ const createMultipartMiddleware = async (options = {}) => {
129
+ const multerModule = await import("multer");
130
+ const multer = multerModule.default || multerModule;
131
+ const upload = multer({
132
+ storage: multer.memoryStorage(),
133
+ limits: {
134
+ fileSize: Number(options.fileSizeLimit) > 0 ? Number(options.fileSizeLimit) : DEFAULT_FILE_SIZE_LIMIT,
135
+ files: Number(options.maxFiles) > 0 ? Number(options.maxFiles) : DEFAULT_MAX_FILES,
136
+ },
137
+ });
138
+
139
+ return (req, res, next) => {
140
+ if (!isMultipartRequest(req)) {
141
+ next();
142
+ return;
143
+ }
144
+
145
+ upload.any()(req, res, (error) => {
146
+ if (!error) {
147
+ next();
148
+ return;
149
+ }
150
+
151
+ if (error.code === "LIMIT_FILE_SIZE") {
152
+ res.status(413).json({
153
+ success: false,
154
+ code: "FILE_TOO_LARGE",
155
+ error: `File too large (max ${Number(options.fileSizeLimit) > 0 ? Number(options.fileSizeLimit) : DEFAULT_FILE_SIZE_LIMIT} bytes)`,
156
+ });
157
+ return;
158
+ }
159
+
160
+ if (error.code === "LIMIT_FILE_COUNT") {
161
+ res.status(413).json({
162
+ success: false,
163
+ code: "TOO_MANY_FILES",
164
+ error: `Too many files (max ${Number(options.maxFiles) > 0 ? Number(options.maxFiles) : DEFAULT_MAX_FILES})`,
165
+ });
166
+ return;
167
+ }
168
+
169
+ res.status(400).json({
170
+ success: false,
171
+ code: "INVALID_MULTIPART_REQUEST",
172
+ error: error.message,
173
+ });
174
+ });
175
+ };
176
+ };
177
+
178
+ export const createAppServerRuntime = async (options = {}) => {
179
+ const expressModule = await import("express");
180
+ const express = expressModule.default || expressModule;
181
+ const {
182
+ env,
183
+ jsonLimit = DEFAULT_JSON_LIMIT,
184
+ fileSizeLimit = DEFAULT_FILE_SIZE_LIMIT,
185
+ maxFiles = DEFAULT_MAX_FILES,
186
+ } = options;
187
+
188
+ if (!env || typeof env !== "object") {
189
+ throw new Error("env is required for createAppServerRuntime");
190
+ }
191
+
192
+ const app = express();
193
+ const multipartMiddleware = await createMultipartMiddleware({ fileSizeLimit, maxFiles });
194
+
195
+ app.use(express.json({ limit: jsonLimit }));
196
+ app.use(express.urlencoded({ extended: true, limit: jsonLimit }));
197
+ if (isPreviewMode()) {
198
+ app.use(createPreviewCorsMiddleware());
199
+
200
+ app.get("/__preview/health", (req, res) => {
201
+ res.json({
202
+ success: true,
203
+ code: "SUCCESS",
204
+ data: {
205
+ status: "ok",
206
+ previewMode: true,
207
+ timestamp: Date.now(),
208
+ },
209
+ });
210
+ });
211
+
212
+ app.get("/__preview/guide", (req, res) => {
213
+ res.json(getPreviewGuidePayload(req));
214
+ });
215
+ }
216
+
217
+ app.post("/invokeAppServerAPI", multipartMiddleware, async (req, res) => {
218
+ const apiName = req.query.apiName;
219
+ const platformInteropHeader = req.get("x-tacore-server-interop-app-server-api-key");
220
+ const schedulerInteropHeader = req.get("x-cloudflare-worker-interop-app-server-token");
221
+ const requestApiKey = req.get("x-api-key");
222
+ const requestAccessToken = req.get("authorization") || "";
223
+
224
+ if (!apiName) {
225
+ return res.status(400).json({
226
+ success: false,
227
+ code: "INVALID_API_NAME",
228
+ error: "apiName is required",
229
+ });
230
+ }
231
+ if (!env.appId) {
232
+ return res.status(400).json({
233
+ success: false,
234
+ code: "MISSING_APP_ID",
235
+ error: "env.appId is required",
236
+ });
237
+ }
238
+
239
+ let payload = {};
240
+ try {
241
+ payload = parseRequestPayload(req);
242
+ } catch (error) {
243
+ return res.status(Number(error.statusCode) || 400).json({
244
+ success: false,
245
+ code: error.code || "INVALID_REQUEST_PAYLOAD",
246
+ error: error.message,
247
+ });
248
+ }
249
+
250
+ const appsClient = AppsClient.getInstance(env.appId, { ...env, accessToken: requestAccessToken || null });
251
+ const previewBridgeInteropToken =
252
+ process.env.TACORE_APPSERVER_PREVIEW_BRIDGE_INTEROP_TOKEN ||
253
+ process.env.TACORE_SERVER_INTEROP_APP_SERVER_API_KEY;
254
+ const isPlatformProxyRequest = isValidInternalToken(
255
+ platformInteropHeader,
256
+ previewBridgeInteropToken
257
+ );
258
+ const isSchedulerRequest = isValidInternalToken(
259
+ schedulerInteropHeader,
260
+ process.env.CLOUDFLARE_WORKER_INTEROP_APP_SERVER_TOKEN
261
+ );
262
+ const requestSource = getRequestSource(isPlatformProxyRequest, isSchedulerRequest);
263
+
264
+ console.log(`[AppServer] invoke api="${apiName}" source="${requestSource}"`);
265
+
266
+ if (!isPlatformProxyRequest && !isSchedulerRequest) {
267
+ if (!requestApiKey) {
268
+ console.warn("[AppServer Auth] Missing X-API-Key. This request is treated as external call.");
269
+ return res.status(401).json({
270
+ success: false,
271
+ code: "MISSING_API_KEY",
272
+ error: "X-API-Key header is required",
273
+ });
274
+ }
275
+
276
+ if (!isPreviewApiKeyShapeValid(requestApiKey)) {
277
+ return res.status(400).json({
278
+ success: false,
279
+ code: "INVALID_API_KEY_FORMAT",
280
+ error: "X-API-Key format is invalid. Expected 8-128 chars: [A-Za-z0-9._-]",
281
+ });
282
+ }
283
+
284
+ let isValidApiKey = false;
285
+ try {
286
+ isValidApiKey = await validateExternalApiKey(appsClient, env.appId, requestApiKey);
287
+ } catch (error) {
288
+ console.error("[AppServer] Failed to validate external API key:", error);
289
+ return res.status(500).json({
290
+ success: false,
291
+ code: "API_KEY_VALIDATION_FAILED",
292
+ error: error.message,
293
+ });
294
+ }
295
+
296
+ if (!isValidApiKey) {
297
+ return res.status(401).json({
298
+ success: false,
299
+ code: "INVALID_API_KEY",
300
+ error: "Invalid X-API-Key",
301
+ });
302
+ }
303
+ }
304
+
305
+ try {
306
+ const data = await appsClient.invokeAppServerAPI(apiName, payload, {
307
+ appsClient,
308
+ req,
309
+ res,
310
+ files: normalizeUploadedFiles(req.files || []),
311
+ requestSource,
312
+ });
313
+
314
+ if (data && typeof data === "object" && !Buffer.isBuffer(data)) {
315
+ return res.json({ success: true, code: "SUCCESS", data });
316
+ }
317
+ return res.send(data);
318
+ } catch (error) {
319
+ console.error(`[AppServer] invoke failed api="${apiName}" source="${requestSource}"`, error);
320
+ return res.status(500).json({
321
+ success: false,
322
+ code: "INVOKE_APP_SERVER_API_FAILED",
323
+ error: error.message,
324
+ });
325
+ }
326
+ });
327
+
328
+ const listen = (listenOptions = {}) => {
329
+ const port = listenOptions.port || process.env.PORT || 9000;
330
+ const host = listenOptions.host;
331
+ const callback = typeof listenOptions.onListen === "function"
332
+ ? listenOptions.onListen
333
+ : () => {
334
+ console.log(`[AppServer] listening on http://localhost:${port} (previewMode=${isPreviewMode()})`);
335
+ };
336
+
337
+ if (host) {
338
+ return app.listen(port, host, callback);
339
+ }
340
+ return app.listen(port, callback);
341
+ };
342
+
343
+ return {
344
+ app,
345
+ env,
346
+ listen,
347
+ constants: {
348
+ requestDataField: APPSERVER_REQUEST_DATA_FIELD,
349
+ },
350
+ };
351
+ };
@@ -1,30 +1,6 @@
1
1
  import { isBrowser, isBackend } from "../../../utils/index.js";
2
2
  import { BaseAppsClient } from "./BaseAppsClient.js";
3
3
 
4
- /**
5
- * AI应用 Token 管理工具类
6
- */
7
- class AppsTokenManager {
8
- static setAccessToken(token) {
9
- if (isBrowser) {
10
- localStorage.setItem("tacoreai_apps_access_token", token);
11
- }
12
- }
13
-
14
- static getAccessToken() {
15
- if (isBrowser) {
16
- return localStorage.getItem("tacoreai_apps_access_token");
17
- }
18
- return null;
19
- }
20
-
21
- static clearAccessToken() {
22
- if (isBrowser) {
23
- localStorage.removeItem("tacoreai_apps_access_token");
24
- }
25
- }
26
- }
27
-
28
4
  /**
29
5
  * AI应用认证管理器
30
6
  * 负责AI应用的用户认证、会话管理
@@ -75,7 +51,7 @@ export class AppsAuthManager extends BaseAppsClient {
75
51
 
76
52
  if (token) {
77
53
  console.log('[AppsAuthManager] Found token in URL, performing silent login...');
78
- AppsTokenManager.setAccessToken(token);
54
+ this.setAccessToken(token);
79
55
 
80
56
  // 清除 URL 中的 token 参数,避免分享泄露
81
57
  const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
@@ -95,7 +71,7 @@ export class AppsAuthManager extends BaseAppsClient {
95
71
  * 重写基类方法,提供 Token
96
72
  */
97
73
  _getAccessToken() {
98
- return AppsTokenManager.getAccessToken();
74
+ return this.getAccessToken({ fallbackToStorage: isBrowser });
99
75
  }
100
76
 
101
77
  /**
@@ -129,7 +105,7 @@ export class AppsAuthManager extends BaseAppsClient {
129
105
 
130
106
  // 成功验证后,后端会返回会话信息
131
107
  if (result.accessToken) {
132
- AppsTokenManager.setAccessToken(result.accessToken);
108
+ this.setAccessToken(result.accessToken);
133
109
  }
134
110
 
135
111
  return result;
@@ -149,7 +125,7 @@ export class AppsAuthManager extends BaseAppsClient {
149
125
  });
150
126
 
151
127
  if (result.accessToken) {
152
- AppsTokenManager.setAccessToken(result.accessToken);
128
+ this.setAccessToken(result.accessToken);
153
129
  }
154
130
 
155
131
  return result;
@@ -168,7 +144,7 @@ export class AppsAuthManager extends BaseAppsClient {
168
144
  });
169
145
 
170
146
  if (result.accessToken) {
171
- AppsTokenManager.setAccessToken(result.accessToken);
147
+ this.setAccessToken(result.accessToken);
172
148
  }
173
149
 
174
150
  return result;
@@ -216,7 +192,7 @@ export class AppsAuthManager extends BaseAppsClient {
216
192
  method: "POST",
217
193
  });
218
194
  } finally {
219
- AppsTokenManager.clearAccessToken();
195
+ this.clearAccessToken();
220
196
  }
221
197
  }
222
198
 
@@ -238,7 +214,7 @@ export class AppsAuthManager extends BaseAppsClient {
238
214
  * @returns {Promise<Object|null>}
239
215
  */
240
216
  async getSession() {
241
- const token = AppsTokenManager.getAccessToken();
217
+ const token = this.getAccessToken({ fallbackToStorage: isBrowser });
242
218
  if (!token) {
243
219
  return null;
244
220
  }
@@ -259,14 +235,14 @@ export class AppsAuthManager extends BaseAppsClient {
259
235
  * @returns {Promise<boolean>}
260
236
  */
261
237
  async isAuthenticated() {
262
- const token = AppsTokenManager.getAccessToken();
238
+ const token = this.getAccessToken({ fallbackToStorage: isBrowser });
263
239
  if (!token) return false;
264
240
 
265
241
  try {
266
242
  await this.getCurrentUser();
267
243
  return true;
268
244
  } catch (error) {
269
- AppsTokenManager.clearAccessToken();
245
+ this.clearAccessToken();
270
246
  return false;
271
247
  }
272
248
  }
@@ -280,7 +256,7 @@ export class AppsAuthManager extends BaseAppsClient {
280
256
  // 简化版本:立即检查当前状态
281
257
  this.isAuthenticated().then((isAuth) => {
282
258
  callback(isAuth ? "SIGNED_IN" : "SIGNED_OUT", {
283
- access_token: AppsTokenManager.getAccessToken(),
259
+ access_token: this.getAccessToken({ fallbackToStorage: isBrowser }),
284
260
  });
285
261
  });
286
262
 
@@ -324,14 +300,17 @@ export class AppsAuthManager extends BaseAppsClient {
324
300
  * 获取访问令牌
325
301
  * @returns {string|null}
326
302
  */
327
- getAccessToken() {
328
- return AppsTokenManager.getAccessToken();
303
+ getAccessToken(options = {}) {
304
+ return super.getAccessToken({
305
+ fallbackToStorage: isBrowser,
306
+ ...options,
307
+ });
329
308
  }
330
309
 
331
310
  /**
332
311
  * 清除访问令牌
333
312
  */
334
- clearAccessToken() {
335
- AppsTokenManager.clearAccessToken();
313
+ clearAccessToken(options = {}) {
314
+ super.clearAccessToken(options);
336
315
  }
337
316
  }
@@ -36,8 +36,21 @@ export const appsClientAiMethods = {
36
36
  });
37
37
 
38
38
  if (!response.ok) {
39
- const errorData = await response.json().catch(() => ({ error: "Network error" }));
40
- throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
39
+ const errorText = await response.text().catch(() => "");
40
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
41
+
42
+ if (errorText) {
43
+ try {
44
+ const errorData = JSON.parse(errorText);
45
+ errorMessage = [errorData?.error, errorData?.message]
46
+ .filter((item) => typeof item === "string" && item.trim())
47
+ .join(": ") || errorText;
48
+ } catch {
49
+ errorMessage = errorText;
50
+ }
51
+ }
52
+
53
+ throw new Error(errorMessage);
41
54
  }
42
55
 
43
56
  const reader = response.body?.getReader();
@@ -226,4 +239,4 @@ export const appsClientAiMethods = {
226
239
  throw error;
227
240
  }
228
241
  },
229
- };
242
+ };
@@ -1,146 +1,424 @@
1
1
  import { env, isDevelopmentBrowser, isProductionBrowser, isBackend } from "../../../utils/index.js";
2
2
 
3
3
  const appServerAPIMap = new Map();
4
+ const previewSessionTokenCache = new Map();
5
+ const previewSessionPromiseCache = new Map();
6
+
7
+ const PREVIEW_TOKEN_HEADER = "X-Tacore-AppServer-Preview-Token";
8
+ const APPSERVER_REQUEST_DATA_FIELD = "requestData";
9
+ const DEFAULT_FILE_FIELD_NAME = "file";
4
10
 
5
11
  const isBrowserPreviewRuntime = () => {
6
12
  if (typeof window === "undefined") {
7
13
  return false;
8
14
  }
9
15
 
10
- const explicitFlag = window.__TACORE_PREVIEW_MODE__;
11
- if (explicitFlag === true || explicitFlag === "true") {
16
+ const pathname = typeof window.location?.pathname === "string" ? window.location.pathname : "";
17
+ return pathname.startsWith("/app-run/");
18
+ };
19
+
20
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
21
+
22
+ const isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
23
+
24
+ const isFileLike = (value) => typeof File !== "undefined" && value instanceof File;
25
+
26
+ const isBlobLike = (value) => typeof Blob !== "undefined" && value instanceof Blob;
27
+
28
+ const isBufferLike = (value) => typeof Buffer !== "undefined" && Buffer.isBuffer(value);
29
+
30
+ const isBinaryLike = (value) => {
31
+ if (!value) {
32
+ return false;
33
+ }
34
+
35
+ if (isFileLike(value) || isBlobLike(value) || isBufferLike(value)) {
12
36
  return true;
13
37
  }
14
38
 
15
- const pathname = typeof window.location?.pathname === "string" ? window.location.pathname : "";
16
- return pathname.startsWith("/app-run/");
39
+ if (value instanceof ArrayBuffer) {
40
+ return true;
41
+ }
42
+
43
+ return ArrayBuffer.isView(value);
44
+ };
45
+
46
+ const decodeBase64Binary = (value) => {
47
+ if (typeof Buffer !== "undefined") {
48
+ return Buffer.from(value, "base64");
49
+ }
50
+
51
+ if (typeof atob === "function") {
52
+ const decoded = atob(value);
53
+ const bytes = new Uint8Array(decoded.length);
54
+ for (let i = 0; i < decoded.length; i += 1) {
55
+ bytes[i] = decoded.charCodeAt(i);
56
+ }
57
+ return bytes;
58
+ }
59
+
60
+ throw new Error("Base64 appServer file input is not supported in the current runtime");
61
+ };
62
+
63
+ const buildAuthorizationHeaderValue = (token) => {
64
+ if (typeof token !== "string") {
65
+ return "";
66
+ }
67
+
68
+ const trimmed = token.trim();
69
+ if (!trimmed) {
70
+ return "";
71
+ }
72
+
73
+ return /^Bearer\s+/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
74
+ };
75
+
76
+ const getAccessTokenHeaderValue = function() {
77
+ return buildAuthorizationHeaderValue(typeof this._getAccessToken === "function" ? this._getAccessToken() : "");
17
78
  };
18
79
 
19
80
  /**
20
81
  * 注册一个 App Server API。
21
- * - 在开发态浏览器中:有效,用于本地调用。
22
- * - 在生产态浏览器中:无效,为空操作。
23
- * - 在后端云函数中:有效,用于加载服务。
24
- * @param {string} appServerAPIName - API名称。
25
- * @param {Function} appServerAPIHandler - 服务的处理函数,接收一个 payload 参数。
82
+ * - 开发态浏览器:用于兼容旧的本地执行模式。
83
+ * - 后端环境:用于实际注册 AppServer API。
84
+ * - 生产态浏览器:无效,为空操作。
26
85
  */
27
86
  const registerAppServerAPI = (appServerAPIName, appServerAPIHandler) => {
28
87
  if (isDevelopmentBrowser || isBackend) {
29
88
  appServerAPIMap.set(appServerAPIName, appServerAPIHandler);
30
89
  }
31
- // 在生产态浏览器中,这是一个空操作。
32
90
  };
33
91
 
34
- const invokeByPlatformProxy = async function(appServerAPIName, payload) {
35
- const endpoint = `/apps/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
36
- const res = await this._post(endpoint, payload);
37
- if (res.success) {
38
- return res.data;
39
- }
40
- throw new Error(res.error || res.message || `AppServer invocation failed: ${res.code || "UNKNOWN_ERROR"}`);
92
+ const clearPreviewSessionToken = (appId) => {
93
+ previewSessionTokenCache.delete(appId);
94
+ previewSessionPromiseCache.delete(appId);
41
95
  };
42
96
 
43
- const invokeByPreviewProxy = async function(appServerAPIName, payload) {
44
- const endpoint = `/appserver-preview/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
97
+ const getPreviewSessionToken = async function(options = {}) {
98
+ const { forceRefresh = false } = options;
99
+ if (forceRefresh) {
100
+ clearPreviewSessionToken(this.appId);
101
+ }
45
102
 
46
- const response = await fetch(endpoint, {
47
- method: "POST",
48
- headers: {
49
- "Content-Type": "application/json",
50
- },
51
- body: JSON.stringify(payload || {}),
52
- });
103
+ const cachedToken = previewSessionTokenCache.get(this.appId);
104
+ if (cachedToken) {
105
+ return cachedToken;
106
+ }
107
+
108
+ const pendingRequest = previewSessionPromiseCache.get(this.appId);
109
+ if (pendingRequest) {
110
+ return pendingRequest;
111
+ }
112
+
113
+ const requestPromise = (async () => {
114
+ const endpoint = `/appserver-preview/session?appId=${encodeURIComponent(this.appId)}`;
115
+ const response = await fetch(endpoint, {
116
+ method: "GET",
117
+ headers: {
118
+ Accept: "application/json",
119
+ },
120
+ cache: "no-store",
121
+ });
122
+
123
+ const responseData = await parseAppServerResponsePayload(response);
124
+
125
+ if (!response.ok) {
126
+ throw createAppServerError("[AppServer-PreviewSession]", response, responseData);
127
+ }
128
+
129
+ const previewToken = typeof responseData?.data?.previewToken === "string"
130
+ ? responseData.data.previewToken.trim()
131
+ : "";
132
+
133
+ if (!previewToken) {
134
+ throw new Error("[AppServer-PreviewSession] Missing preview token in session response.");
135
+ }
136
+
137
+ previewSessionTokenCache.set(this.appId, previewToken);
138
+ return previewToken;
139
+ })();
53
140
 
141
+ previewSessionPromiseCache.set(this.appId, requestPromise);
142
+ try {
143
+ return await requestPromise;
144
+ } finally {
145
+ previewSessionPromiseCache.delete(this.appId);
146
+ }
147
+ };
148
+
149
+ const isInvalidPreviewTokenError = (error) => {
150
+ const response = error?.response;
151
+ const responseData = error?.responseData;
152
+ return response?.status === 401 && responseData?.code === "INVALID_PREVIEW_TOKEN";
153
+ };
154
+
155
+ const parseAppServerResponsePayload = async (response) => {
54
156
  const contentType = response.headers.get("content-type") || "";
55
- const responseData = contentType.includes("application/json")
56
- ? await response.json()
57
- : await response.text();
157
+ if (contentType.includes("application/json")) {
158
+ return await response.json();
159
+ }
160
+ return await response.text();
161
+ };
162
+
163
+ const createAppServerError = (label, response, responseData) => {
164
+ const message = typeof responseData === "object"
165
+ ? (responseData?.error || responseData?.message || JSON.stringify(responseData))
166
+ : String(responseData || response.statusText || "");
167
+
168
+ const error = new Error(`${label} status: ${response.status}, message: ${message}`);
169
+ error.response = response;
170
+ error.responseData = responseData;
171
+ error.statusCode = response.status;
172
+ if (typeof responseData === "object" && responseData?.code) {
173
+ error.code = responseData.code;
174
+ }
175
+ return error;
176
+ };
177
+
178
+ const unwrapAppServerResponse = async (response, label) => {
179
+ const responseData = await parseAppServerResponsePayload(response);
58
180
 
59
181
  if (!response.ok) {
60
- const errorMessage = typeof responseData === "object"
61
- ? (responseData?.error || responseData?.message || JSON.stringify(responseData))
62
- : String(responseData || "");
63
- throw new Error(`[AppServer-DevBridge] status: ${response.status}, message: ${errorMessage}`);
182
+ throw createAppServerError(label, response, responseData);
64
183
  }
65
184
 
66
- if (responseData && typeof responseData === "object" && Object.prototype.hasOwnProperty.call(responseData, "success")) {
185
+ if (responseData && typeof responseData === "object" && hasOwn(responseData, "success")) {
67
186
  if (responseData.success) {
68
187
  return responseData.data;
69
188
  }
70
- throw new Error(responseData.error || "AppServer invocation failed in dev bridge");
189
+
190
+ const error = new Error(responseData.error || responseData.message || `${label} invocation failed`);
191
+ error.response = response;
192
+ error.responseData = responseData;
193
+ error.statusCode = response.status;
194
+ error.code = responseData.code;
195
+ throw error;
71
196
  }
72
197
 
73
198
  return responseData;
74
199
  };
75
200
 
201
+ const toBlob = (rawValue, contentType = "application/octet-stream") => {
202
+ if (isBlobLike(rawValue)) {
203
+ return rawValue;
204
+ }
205
+
206
+ if (isBufferLike(rawValue)) {
207
+ return new Blob([rawValue], { type: contentType });
208
+ }
209
+
210
+ if (rawValue instanceof ArrayBuffer) {
211
+ return new Blob([rawValue], { type: contentType });
212
+ }
213
+
214
+ if (ArrayBuffer.isView(rawValue)) {
215
+ return new Blob([rawValue], { type: contentType });
216
+ }
217
+
218
+ throw new Error("Unsupported binary payload for appServer file upload");
219
+ };
220
+
221
+ const normalizeSingleFileInput = (value, defaultFieldName) => {
222
+ if (!value) {
223
+ return [];
224
+ }
225
+
226
+ if (Array.isArray(value)) {
227
+ return value.flatMap((item) => normalizeSingleFileInput(item, defaultFieldName));
228
+ }
229
+
230
+ if (isFileLike(value) || isBlobLike(value) || isBinaryLike(value)) {
231
+ return [{
232
+ fieldName: defaultFieldName,
233
+ rawValue: value,
234
+ fileName: value?.name || "file",
235
+ contentType: value?.type || "application/octet-stream",
236
+ }];
237
+ }
238
+
239
+ if (isPlainObject(value) && hasOwn(value, "file")) {
240
+ const rawValue = value.file;
241
+ return [{
242
+ fieldName: value.fieldName || defaultFieldName,
243
+ rawValue,
244
+ fileName: value.fileName || value.name || rawValue?.name || "file",
245
+ contentType: value.contentType || value.mimeType || value.type || rawValue?.type || "application/octet-stream",
246
+ }];
247
+ }
248
+
249
+ if (isPlainObject(value) && isBinaryLike(value.buffer)) {
250
+ return [{
251
+ fieldName: value.fieldName || defaultFieldName,
252
+ rawValue: value.buffer,
253
+ fileName: value.fileName || value.name || "file",
254
+ contentType: value.contentType || value.mimeType || value.type || "application/octet-stream",
255
+ }];
256
+ }
257
+
258
+ if (
259
+ isPlainObject(value) &&
260
+ typeof value.data === "string" &&
261
+ typeof value.encoding === "string" &&
262
+ value.encoding.toLowerCase() === "base64"
263
+ ) {
264
+ return [{
265
+ fieldName: value.fieldName || defaultFieldName,
266
+ rawValue: decodeBase64Binary(value.data),
267
+ fileName: value.fileName || value.name || "file",
268
+ contentType: value.contentType || value.mimeType || value.type || "application/octet-stream",
269
+ }];
270
+ }
271
+
272
+ if (isPlainObject(value) && isBlobLike(value.blob)) {
273
+ return [{
274
+ fieldName: value.fieldName || defaultFieldName,
275
+ rawValue: value.blob,
276
+ fileName: value.fileName || value.name || value.blob?.name || "file",
277
+ contentType: value.contentType || value.mimeType || value.type || value.blob?.type || "application/octet-stream",
278
+ }];
279
+ }
280
+
281
+ throw new Error(`Unsupported appServer file input for field "${defaultFieldName}"`);
282
+ };
283
+
284
+ const normalizeAppServerFiles = (files) => {
285
+ if (!files) {
286
+ return [];
287
+ }
288
+
289
+ if (Array.isArray(files)) {
290
+ return files.flatMap((item) => normalizeSingleFileInput(item, item?.fieldName || DEFAULT_FILE_FIELD_NAME));
291
+ }
292
+
293
+ if (isPlainObject(files) && !hasOwn(files, "file") && !hasOwn(files, "buffer") && !hasOwn(files, "data")) {
294
+ return Object.entries(files).flatMap(([fieldName, value]) => normalizeSingleFileInput(value, fieldName));
295
+ }
296
+
297
+ return normalizeSingleFileInput(files, DEFAULT_FILE_FIELD_NAME);
298
+ };
299
+
300
+ const buildRemoteRequestInit = function(payload, options = {}, extraHeaders = {}) {
301
+ const requestPayload = typeof payload === "undefined" ? {} : payload;
302
+ const normalizedFiles = normalizeAppServerFiles(options.files);
303
+ const headers = { ...extraHeaders };
304
+ const authorizationHeader = getAccessTokenHeaderValue.call(this);
305
+ if (authorizationHeader && !headers.Authorization && !headers.authorization) {
306
+ headers.Authorization = authorizationHeader;
307
+ }
308
+
309
+ if (normalizedFiles.length === 0) {
310
+ headers["Content-Type"] = "application/json";
311
+ return {
312
+ method: "POST",
313
+ headers,
314
+ body: JSON.stringify(requestPayload),
315
+ signal: options.signal,
316
+ };
317
+ }
318
+
319
+ delete headers["Content-Type"];
320
+ delete headers["content-type"];
321
+
322
+ const formData = new FormData();
323
+ formData.append(APPSERVER_REQUEST_DATA_FIELD, JSON.stringify(requestPayload));
324
+ for (const fileDescriptor of normalizedFiles) {
325
+ const fileValue = isFileLike(fileDescriptor.rawValue)
326
+ ? fileDescriptor.rawValue
327
+ : toBlob(fileDescriptor.rawValue, fileDescriptor.contentType);
328
+ formData.append(fileDescriptor.fieldName, fileValue, fileDescriptor.fileName || "file");
329
+ }
330
+
331
+ return {
332
+ method: "POST",
333
+ headers,
334
+ body: formData,
335
+ signal: options.signal,
336
+ };
337
+ };
338
+
339
+ const invokeByPlatformProxy = async function(appServerAPIName, payload, options = {}) {
340
+ const endpoint = `/apps/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
341
+ const response = await fetch(`${this.config.apiBaseUrl}${endpoint}`, buildRemoteRequestInit.call(this, payload, options));
342
+ return await unwrapAppServerResponse(response, "[AppServer-PlatformProxy]");
343
+ };
344
+
345
+ const invokeByPreviewProxyOnce = async function(appServerAPIName, payload, options = {}, previewToken) {
346
+ const endpoint = `/appserver-preview/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
347
+ const response = await fetch(
348
+ endpoint,
349
+ buildRemoteRequestInit.call(this, payload, options, {
350
+ [PREVIEW_TOKEN_HEADER]: previewToken,
351
+ })
352
+ );
353
+
354
+ return await unwrapAppServerResponse(response, "[AppServer-DevBridge]");
355
+ };
356
+
357
+ const invokeByPreviewProxy = async function(appServerAPIName, payload, options = {}) {
358
+ let previewToken = await getPreviewSessionToken.call(this);
359
+ try {
360
+ return await invokeByPreviewProxyOnce.call(this, appServerAPIName, payload, options, previewToken);
361
+ } catch (error) {
362
+ if (!isInvalidPreviewTokenError(error)) {
363
+ throw error;
364
+ }
365
+
366
+ clearPreviewSessionToken(this.appId);
367
+ previewToken = await getPreviewSessionToken.call(this, { forceRefresh: true });
368
+ return await invokeByPreviewProxyOnce.call(this, appServerAPIName, payload, options, previewToken);
369
+ }
370
+ };
371
+
372
+ const invokeByCrossAppDirect = async function(appServerAPIName, payload, options = {}) {
373
+ const endpoint = `${this.config.appServerBaseUrl}/invokeAppServerAPI?apiName=${encodeURIComponent(appServerAPIName)}`;
374
+ const requestInit = buildRemoteRequestInit.call(this, payload, options, {
375
+ "X-API-Key": this.config.appServerApiKey,
376
+ });
377
+ const response = await fetch(endpoint, requestInit);
378
+ return await unwrapAppServerResponse(response, "[AppServer-CrossApp]");
379
+ };
380
+
76
381
  /**
77
382
  * 调用一个 App Server API。
78
- * - 在开发态浏览器中:预览页(/app-run/*)走本地预览代理,其余开发页直接走平台转发。
79
- * - 在生产态浏览器中:统一走平台转发(/apps/invokeAppServerAPI)。
80
- * - 在后端云函数中:本地执行 (由云函数入口调用)
81
- * - 跨应用调用:仅后端场景可直连目标 AppServer(需 appServerBaseUrl + appServerApiKey)。
82
- * @param {string} appServerAPIName - 要调用的服务名称。
83
- * @param {Object} payload - 传递给服务的负载数据。
84
- * @returns {Promise<any>} 服务处理后的结果。
383
+ * - 开发态浏览器:预览页走 CLI preview bridge,其他页面走平台转发。
384
+ * - 生产态浏览器:统一走平台转发。
385
+ * - 后端跨应用:允许使用 appServerBaseUrl + appServerApiKey 直连目标 AppServer
386
+ * - 后端当前应用:从本地注册表执行。
387
+ *
388
+ * @param {string} appServerAPIName
389
+ * @param {any} payload
390
+ * @param {Object} options
391
+ * @param {Object|Array} [options.files] 仅在存在文件时传入,SDK 会自动切换到 multipart/form-data,并将 JSON 放到 requestData 字段。
85
392
  */
86
- const invokeAppServerAPI = async function(appServerAPIName, payload) {
87
- console.log(`[AppServer][${this.appId}] Invoking "${appServerAPIName}" with payload:`, payload);
88
- // 场景零:跨应用调用,在源应用的 appserver 云函数调用目标应用的 appserver 云函数
393
+ const invokeAppServerAPI = async function(appServerAPIName, payload, options = {}) {
394
+ if (!appServerAPIName || typeof appServerAPIName !== "string") {
395
+ throw new Error("appServerAPIName is required");
396
+ }
397
+
89
398
  if (isBackend && this.config.appServerBaseUrl && this.config.appServerApiKey) {
90
- const endpoint = `${this.config.appServerBaseUrl}/invokeAppServerAPI?apiName=${appServerAPIName}`;
91
- try {
92
- const response = await fetch(endpoint, {
93
- method: 'POST',
94
- headers: {
95
- 'Content-Type': 'application/json',
96
- 'X-API-Key': this.config.appServerApiKey,
97
- },
98
- body: JSON.stringify(payload),
99
- });
100
-
101
- console.log(`App server request id: ${response.headers.get('x-scf-request-id')}`)
102
-
103
- const res = await response.json();
104
- if (!response.ok) {
105
- // 抛出网络层面的错误
106
- throw new Error(`[AppServer-CrossApp] error! status: ${response.status}, statusText: ${response.statusText}, requestId: ${response.headers.get('x-scf-request-id')}, body: ${JSON.stringify(res)}`);
107
- }
108
-
109
- if (res.success) {
110
- return res.data;
111
- }
112
- // 抛出业务层面的错误
113
- throw new Error(res.error || 'Cross-application server invocation failed at the business level.');
114
- } catch (error) {
115
- console.error(`[AppServer-CrossApp] Failed to invoke "${appServerAPIName}" via cross-app call:`, error);
116
- throw error;
117
- }
399
+ return await invokeByCrossAppDirect.call(this, appServerAPIName, payload, options);
118
400
  }
119
401
 
120
- // 场景一:开发环境浏览器
121
402
  if (isDevelopmentBrowser) {
122
403
  if (isBrowserPreviewRuntime()) {
123
- return await invokeByPreviewProxy.call(this, appServerAPIName, payload);
404
+ return await invokeByPreviewProxy.call(this, appServerAPIName, payload, options);
124
405
  }
125
- return await invokeByPlatformProxy.call(this, appServerAPIName, payload);
406
+ return await invokeByPlatformProxy.call(this, appServerAPIName, payload, options);
126
407
  }
127
408
 
128
- // 场景二:生产环境的浏览器,统一走平台转发
129
409
  if (isProductionBrowser) {
130
- try {
131
- return await invokeByPlatformProxy.call(this, appServerAPIName, payload);
132
- } catch (error) {
133
- console.error(`[AppServer-Prod] Failed to invoke "${appServerAPIName}" via appsClient:`, error);
134
- throw error;
135
- }
410
+ return await invokeByPlatformProxy.call(this, appServerAPIName, payload, options);
136
411
  }
137
412
 
138
- // 场景三:后端云函数环境,从本地 Map 执行
139
413
  const appServerAPIHandler = appServerAPIMap.get(appServerAPIName);
140
414
  if (!appServerAPIHandler) {
141
415
  throw new Error(`[AppServer-${env}] AppServer "${appServerAPIName}" not found or not registered.`);
142
416
  }
143
- return await appServerAPIHandler(payload, { appsClient: this });
417
+
418
+ return await appServerAPIHandler(payload, {
419
+ appsClient: this,
420
+ ...options,
421
+ });
144
422
  };
145
423
 
146
424
  export const appsClientAppServerMethods = {
@@ -1,14 +1,27 @@
1
- import { isBrowser } from "../../../utils/index.js";
2
-
3
1
  export const appsClientDebugMethods = {
4
2
  // ==================== 便捷方法 ====================
5
3
 
6
4
  updateConfig(newConfig) {
7
- this.config = { ...this.config, ...newConfig };
5
+ const nextConfig = { ...newConfig };
6
+ const hasAccessToken = Object.prototype.hasOwnProperty.call(nextConfig, "accessToken");
7
+
8
+ if (hasAccessToken && typeof this.setAccessToken === "function") {
9
+ this.setAccessToken(nextConfig.accessToken, {
10
+ persist: false,
11
+ syncConfig: false,
12
+ });
13
+ }
14
+
15
+ this.config = { ...this.config, ...nextConfig };
16
+ if (hasAccessToken) {
17
+ this.config.accessToken = this.accessToken;
18
+ }
8
19
  },
9
20
 
10
21
  getConfig() {
11
- return { ...this.config };
22
+ const config = { ...this.config };
23
+ delete config.accessToken;
24
+ return config;
12
25
  },
13
26
 
14
27
  // ==================== 调试和监控 ====================
@@ -20,9 +33,7 @@ export const appsClientDebugMethods = {
20
33
  config: this.getConfig(),
21
34
  clientType: "AppsClient (Multi-Model with AI)",
22
35
  };
23
- if (isBrowser) {
24
- status.hasAccessToken = !!localStorage.getItem("tacoreai_apps_access_token");
25
- }
36
+ status.hasAccessToken = !!this.getAccessToken();
26
37
  return status;
27
38
  } catch (error) {
28
39
  console.error("获取AI应用客户端状态失败:", error);
@@ -32,9 +43,7 @@ export const appsClientDebugMethods = {
32
43
  error: error.message,
33
44
  clientType: "AppsClient (Multi-Model with AI)",
34
45
  };
35
- if (isBrowser) {
36
- status.hasAccessToken = !!localStorage.getItem("tacoreai_apps_access_token");
37
- }
46
+ status.hasAccessToken = !!this.getAccessToken();
38
47
  return status;
39
48
  }
40
49
  },
@@ -31,6 +31,9 @@ class AppsClient extends BaseAppsClient {
31
31
  static getInstance(tagetAppId, config = {}) {
32
32
  const instance = instanceMap.get(tagetAppId);
33
33
  if (instance) {
34
+ if (config && Object.keys(config).length > 0) {
35
+ instance.updateConfig(config);
36
+ }
34
37
  return instance;
35
38
  }
36
39
  return new AppsClient(tagetAppId, config);
@@ -176,10 +179,7 @@ class AppsClient extends BaseAppsClient {
176
179
  * 重写基类方法,提供 Token
177
180
  */
178
181
  _getAccessToken() {
179
- if (isBrowser) {
180
- return localStorage.getItem("tacoreai_apps_access_token");
181
- }
182
- return null;
182
+ return this.getAccessToken({ fallbackToStorage: isBrowser });
183
183
  }
184
184
 
185
185
  /**
@@ -1,5 +1,49 @@
1
1
  import { getAppsApiBaseUrl, isBrowser, isBackend, getGlobalOptions } from "../../../utils/index.js";
2
2
 
3
+ const ACCESS_TOKEN_STORAGE_KEY = "tacoreai_apps_access_token";
4
+
5
+ const normalizeAccessToken = (value) => {
6
+ if (typeof value !== "string") {
7
+ return null;
8
+ }
9
+
10
+ const trimmed = value.trim();
11
+ if (!trimmed) {
12
+ return null;
13
+ }
14
+
15
+ return trimmed.replace(/^Bearer\s+/i, "").trim() || null;
16
+ };
17
+
18
+ const readPersistedAccessToken = () => {
19
+ if (!isBrowser) {
20
+ return null;
21
+ }
22
+
23
+ try {
24
+ return normalizeAccessToken(localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY));
25
+ } catch (error) {
26
+ console.error("[BaseAppsClient] Failed to read access token from localStorage:", error);
27
+ return null;
28
+ }
29
+ };
30
+
31
+ const writePersistedAccessToken = (token) => {
32
+ if (!isBrowser) {
33
+ return;
34
+ }
35
+
36
+ try {
37
+ if (token) {
38
+ localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, token);
39
+ return;
40
+ }
41
+ localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
42
+ } catch (error) {
43
+ console.error("[BaseAppsClient] Failed to persist access token:", error);
44
+ }
45
+ };
46
+
3
47
  /**
4
48
  * Apps SDK 基类
5
49
  * 封装通用的配置管理、Header构建、HTTP请求逻辑
@@ -22,6 +66,14 @@ export class BaseAppsClient {
22
66
  ...defaultConfig,
23
67
  ...config,
24
68
  };
69
+ this.accessToken = null;
70
+
71
+ if (Object.prototype.hasOwnProperty.call(this.config, "accessToken")) {
72
+ this.setAccessToken(this.config.accessToken, {
73
+ persist: false,
74
+ syncConfig: true,
75
+ });
76
+ }
25
77
 
26
78
  // 后端环境通用逻辑:尝试从环境变量加载 API Key
27
79
  if (isBackend && !this.config.tacoreServerInteropAppServerApiKey) {
@@ -73,7 +125,7 @@ export class BaseAppsClient {
73
125
  * @returns {string|null}
74
126
  */
75
127
  _getAccessToken() {
76
- return null;
128
+ return this.getAccessToken();
77
129
  }
78
130
 
79
131
  /**
@@ -119,15 +171,13 @@ export class BaseAppsClient {
119
171
  headers["x-app-id"] = this.appId;
120
172
  }
121
173
 
122
- if (isBrowser) {
123
- const token = this._getAccessToken();
124
- if (token) {
125
- headers["Authorization"] = `Bearer ${token}`;
126
- }
127
- } else { // isBackend
128
- if (this.config.tacoreServerInteropAppServerApiKey) {
129
- headers["x-tacore-server-interop-app-server-api-key"] = this.config.tacoreServerInteropAppServerApiKey;
130
- }
174
+ const token = this._getAccessToken();
175
+ if (token) {
176
+ headers["Authorization"] = `Bearer ${token}`;
177
+ }
178
+
179
+ if (isBackend && this.config.tacoreServerInteropAppServerApiKey) {
180
+ headers["x-tacore-server-interop-app-server-api-key"] = this.config.tacoreServerInteropAppServerApiKey;
131
181
  }
132
182
 
133
183
  return headers;
@@ -204,13 +254,71 @@ export class BaseAppsClient {
204
254
  * 更新配置
205
255
  */
206
256
  updateConfig(newConfig) {
207
- this.config = { ...this.config, ...newConfig };
257
+ const nextConfig = { ...newConfig };
258
+ const hasAccessToken = Object.prototype.hasOwnProperty.call(nextConfig, "accessToken");
259
+
260
+ if (hasAccessToken) {
261
+ this.setAccessToken(nextConfig.accessToken, {
262
+ persist: false,
263
+ syncConfig: false,
264
+ });
265
+ }
266
+
267
+ this.config = { ...this.config, ...nextConfig };
268
+ if (hasAccessToken) {
269
+ this.config.accessToken = this.accessToken;
270
+ }
208
271
  }
209
272
 
210
273
  /**
211
274
  * 获取当前配置
212
275
  */
213
276
  getConfig() {
214
- return { ...this.config };
277
+ const config = { ...this.config };
278
+ delete config.accessToken;
279
+ return config;
280
+ }
281
+
282
+ setAccessToken(token, options = {}) {
283
+ const { persist = isBrowser, syncConfig = true } = options;
284
+ const normalizedToken = normalizeAccessToken(token);
285
+
286
+ this.accessToken = normalizedToken;
287
+
288
+ if (syncConfig) {
289
+ this.config.accessToken = normalizedToken;
290
+ }
291
+
292
+ if (persist) {
293
+ writePersistedAccessToken(normalizedToken);
294
+ }
295
+
296
+ return normalizedToken;
297
+ }
298
+
299
+ getAccessToken(options = {}) {
300
+ const { fallbackToStorage = isBrowser } = options;
301
+ if (this.accessToken) {
302
+ return this.accessToken;
303
+ }
304
+
305
+ if (!fallbackToStorage) {
306
+ return null;
307
+ }
308
+
309
+ return readPersistedAccessToken();
310
+ }
311
+
312
+ clearAccessToken(options = {}) {
313
+ const { persist = isBrowser, syncConfig = true } = options;
314
+ this.accessToken = null;
315
+
316
+ if (syncConfig) {
317
+ this.config.accessToken = null;
318
+ }
319
+
320
+ if (persist) {
321
+ writePersistedAccessToken(null);
322
+ }
215
323
  }
216
324
  }
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.8.0",
4
+ "version": "1.9.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "exports": {
11
11
  ".": "./index.js",
12
- "./utils": "./utils/index.js"
12
+ "./utils": "./utils/index.js",
13
+ "./app-server-runtime": "./database/core/apps/AppServerRuntime.js"
13
14
  },
14
15
  "scripts": {
15
16
  "release": "npm version minor && git commit -am \"web-sdk update version\" && npm publish"