@tacoreai/web-sdk 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/database/core/apps/AppServerRuntime.js +351 -0
- package/database/core/apps/AppsAuthManager.js +17 -38
- package/database/core/apps/AppsClient.AI.js +29 -3
- package/database/core/apps/AppsClient.AppServer.js +369 -84
- package/database/core/apps/AppsClient.Debug.js +19 -10
- package/database/core/apps/AppsClient.js +4 -4
- package/database/core/apps/BaseAppsClient.js +120 -12
- package/package.json +3 -2
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
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();
|
|
@@ -75,6 +88,19 @@ export const appsClientAiMethods = {
|
|
|
75
88
|
try {
|
|
76
89
|
const parsed = JSON.parse(data);
|
|
77
90
|
|
|
91
|
+
if (parsed?.error) {
|
|
92
|
+
const streamError = new Error(
|
|
93
|
+
parsed.error?.message ||
|
|
94
|
+
parsed.message ||
|
|
95
|
+
"AI service error"
|
|
96
|
+
);
|
|
97
|
+
if (parsed.error?.code) {
|
|
98
|
+
streamError.code = parsed.error.code;
|
|
99
|
+
}
|
|
100
|
+
options.onError?.(streamError);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
if (parsed.choices && parsed.choices.length > 0) {
|
|
79
105
|
const choice = parsed.choices[0];
|
|
80
106
|
|
|
@@ -226,4 +252,4 @@ export const appsClientAiMethods = {
|
|
|
226
252
|
throw error;
|
|
227
253
|
}
|
|
228
254
|
},
|
|
229
|
-
};
|
|
255
|
+
};
|
|
@@ -1,146 +1,431 @@
|
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
92
|
+
const clearPreviewSessionToken = (appId) => {
|
|
93
|
+
previewSessionTokenCache.delete(appId);
|
|
94
|
+
previewSessionPromiseCache.delete(appId);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getPreviewSessionToken = async function(options = {}) {
|
|
98
|
+
const { forceRefresh = false } = options;
|
|
99
|
+
if (forceRefresh) {
|
|
100
|
+
clearPreviewSessionToken(this.appId);
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
})();
|
|
140
|
+
|
|
141
|
+
previewSessionPromiseCache.set(this.appId, requestPromise);
|
|
142
|
+
try {
|
|
143
|
+
return await requestPromise;
|
|
144
|
+
} finally {
|
|
145
|
+
previewSessionPromiseCache.delete(this.appId);
|
|
39
146
|
}
|
|
40
|
-
throw new Error(res.error || res.message || `AppServer invocation failed: ${res.code || "UNKNOWN_ERROR"}`);
|
|
41
147
|
};
|
|
42
148
|
|
|
43
|
-
const
|
|
44
|
-
const
|
|
149
|
+
const isRefreshablePreviewTokenError = (error) => {
|
|
150
|
+
const response = error?.response;
|
|
151
|
+
const responseData = error?.responseData;
|
|
152
|
+
if (response?.status !== 401) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
45
155
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
body: JSON.stringify(payload || {}),
|
|
52
|
-
});
|
|
156
|
+
return [
|
|
157
|
+
"INVALID_PREVIEW_TOKEN",
|
|
158
|
+
"MISSING_PREVIEW_TOKEN",
|
|
159
|
+
].includes(String(responseData?.code || "").trim());
|
|
160
|
+
};
|
|
53
161
|
|
|
162
|
+
const parseAppServerResponsePayload = async (response) => {
|
|
54
163
|
const contentType = response.headers.get("content-type") || "";
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
164
|
+
if (contentType.includes("application/json")) {
|
|
165
|
+
return await response.json();
|
|
166
|
+
}
|
|
167
|
+
return await response.text();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const createAppServerError = (label, response, responseData) => {
|
|
171
|
+
const message = typeof responseData === "object"
|
|
172
|
+
? (responseData?.error || responseData?.message || JSON.stringify(responseData))
|
|
173
|
+
: String(responseData || response.statusText || "");
|
|
174
|
+
|
|
175
|
+
const error = new Error(`${label} status: ${response.status}, message: ${message}`);
|
|
176
|
+
error.response = response;
|
|
177
|
+
error.responseData = responseData;
|
|
178
|
+
error.statusCode = response.status;
|
|
179
|
+
if (typeof responseData === "object" && responseData?.code) {
|
|
180
|
+
error.code = responseData.code;
|
|
181
|
+
}
|
|
182
|
+
return error;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const unwrapAppServerResponse = async (response, label) => {
|
|
186
|
+
const responseData = await parseAppServerResponsePayload(response);
|
|
58
187
|
|
|
59
188
|
if (!response.ok) {
|
|
60
|
-
|
|
61
|
-
? (responseData?.error || responseData?.message || JSON.stringify(responseData))
|
|
62
|
-
: String(responseData || "");
|
|
63
|
-
throw new Error(`[AppServer-DevBridge] status: ${response.status}, message: ${errorMessage}`);
|
|
189
|
+
throw createAppServerError(label, response, responseData);
|
|
64
190
|
}
|
|
65
191
|
|
|
66
|
-
if (responseData && typeof responseData === "object" &&
|
|
192
|
+
if (responseData && typeof responseData === "object" && hasOwn(responseData, "success")) {
|
|
67
193
|
if (responseData.success) {
|
|
68
194
|
return responseData.data;
|
|
69
195
|
}
|
|
70
|
-
|
|
196
|
+
|
|
197
|
+
const error = new Error(responseData.error || responseData.message || `${label} invocation failed`);
|
|
198
|
+
error.response = response;
|
|
199
|
+
error.responseData = responseData;
|
|
200
|
+
error.statusCode = response.status;
|
|
201
|
+
error.code = responseData.code;
|
|
202
|
+
throw error;
|
|
71
203
|
}
|
|
72
204
|
|
|
73
205
|
return responseData;
|
|
74
206
|
};
|
|
75
207
|
|
|
208
|
+
const toBlob = (rawValue, contentType = "application/octet-stream") => {
|
|
209
|
+
if (isBlobLike(rawValue)) {
|
|
210
|
+
return rawValue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isBufferLike(rawValue)) {
|
|
214
|
+
return new Blob([rawValue], { type: contentType });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (rawValue instanceof ArrayBuffer) {
|
|
218
|
+
return new Blob([rawValue], { type: contentType });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (ArrayBuffer.isView(rawValue)) {
|
|
222
|
+
return new Blob([rawValue], { type: contentType });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new Error("Unsupported binary payload for appServer file upload");
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const normalizeSingleFileInput = (value, defaultFieldName) => {
|
|
229
|
+
if (!value) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
return value.flatMap((item) => normalizeSingleFileInput(item, defaultFieldName));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (isFileLike(value) || isBlobLike(value) || isBinaryLike(value)) {
|
|
238
|
+
return [{
|
|
239
|
+
fieldName: defaultFieldName,
|
|
240
|
+
rawValue: value,
|
|
241
|
+
fileName: value?.name || "file",
|
|
242
|
+
contentType: value?.type || "application/octet-stream",
|
|
243
|
+
}];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isPlainObject(value) && hasOwn(value, "file")) {
|
|
247
|
+
const rawValue = value.file;
|
|
248
|
+
return [{
|
|
249
|
+
fieldName: value.fieldName || defaultFieldName,
|
|
250
|
+
rawValue,
|
|
251
|
+
fileName: value.fileName || value.name || rawValue?.name || "file",
|
|
252
|
+
contentType: value.contentType || value.mimeType || value.type || rawValue?.type || "application/octet-stream",
|
|
253
|
+
}];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (isPlainObject(value) && isBinaryLike(value.buffer)) {
|
|
257
|
+
return [{
|
|
258
|
+
fieldName: value.fieldName || defaultFieldName,
|
|
259
|
+
rawValue: value.buffer,
|
|
260
|
+
fileName: value.fileName || value.name || "file",
|
|
261
|
+
contentType: value.contentType || value.mimeType || value.type || "application/octet-stream",
|
|
262
|
+
}];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
isPlainObject(value) &&
|
|
267
|
+
typeof value.data === "string" &&
|
|
268
|
+
typeof value.encoding === "string" &&
|
|
269
|
+
value.encoding.toLowerCase() === "base64"
|
|
270
|
+
) {
|
|
271
|
+
return [{
|
|
272
|
+
fieldName: value.fieldName || defaultFieldName,
|
|
273
|
+
rawValue: decodeBase64Binary(value.data),
|
|
274
|
+
fileName: value.fileName || value.name || "file",
|
|
275
|
+
contentType: value.contentType || value.mimeType || value.type || "application/octet-stream",
|
|
276
|
+
}];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isPlainObject(value) && isBlobLike(value.blob)) {
|
|
280
|
+
return [{
|
|
281
|
+
fieldName: value.fieldName || defaultFieldName,
|
|
282
|
+
rawValue: value.blob,
|
|
283
|
+
fileName: value.fileName || value.name || value.blob?.name || "file",
|
|
284
|
+
contentType: value.contentType || value.mimeType || value.type || value.blob?.type || "application/octet-stream",
|
|
285
|
+
}];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error(`Unsupported appServer file input for field "${defaultFieldName}"`);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const normalizeAppServerFiles = (files) => {
|
|
292
|
+
if (!files) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (Array.isArray(files)) {
|
|
297
|
+
return files.flatMap((item) => normalizeSingleFileInput(item, item?.fieldName || DEFAULT_FILE_FIELD_NAME));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isPlainObject(files) && !hasOwn(files, "file") && !hasOwn(files, "buffer") && !hasOwn(files, "data")) {
|
|
301
|
+
return Object.entries(files).flatMap(([fieldName, value]) => normalizeSingleFileInput(value, fieldName));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return normalizeSingleFileInput(files, DEFAULT_FILE_FIELD_NAME);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const buildRemoteRequestInit = function(payload, options = {}, extraHeaders = {}) {
|
|
308
|
+
const requestPayload = typeof payload === "undefined" ? {} : payload;
|
|
309
|
+
const normalizedFiles = normalizeAppServerFiles(options.files);
|
|
310
|
+
const headers = { ...extraHeaders };
|
|
311
|
+
const authorizationHeader = getAccessTokenHeaderValue.call(this);
|
|
312
|
+
if (authorizationHeader && !headers.Authorization && !headers.authorization) {
|
|
313
|
+
headers.Authorization = authorizationHeader;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (normalizedFiles.length === 0) {
|
|
317
|
+
headers["Content-Type"] = "application/json";
|
|
318
|
+
return {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers,
|
|
321
|
+
body: JSON.stringify(requestPayload),
|
|
322
|
+
signal: options.signal,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
delete headers["Content-Type"];
|
|
327
|
+
delete headers["content-type"];
|
|
328
|
+
|
|
329
|
+
const formData = new FormData();
|
|
330
|
+
formData.append(APPSERVER_REQUEST_DATA_FIELD, JSON.stringify(requestPayload));
|
|
331
|
+
for (const fileDescriptor of normalizedFiles) {
|
|
332
|
+
const fileValue = isFileLike(fileDescriptor.rawValue)
|
|
333
|
+
? fileDescriptor.rawValue
|
|
334
|
+
: toBlob(fileDescriptor.rawValue, fileDescriptor.contentType);
|
|
335
|
+
formData.append(fileDescriptor.fieldName, fileValue, fileDescriptor.fileName || "file");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers,
|
|
341
|
+
body: formData,
|
|
342
|
+
signal: options.signal,
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const invokeByPlatformProxy = async function(appServerAPIName, payload, options = {}) {
|
|
347
|
+
const endpoint = `/apps/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
|
|
348
|
+
const response = await fetch(`${this.config.apiBaseUrl}${endpoint}`, buildRemoteRequestInit.call(this, payload, options));
|
|
349
|
+
return await unwrapAppServerResponse(response, "[AppServer-PlatformProxy]");
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const invokeByPreviewProxyOnce = async function(appServerAPIName, payload, options = {}, previewToken) {
|
|
353
|
+
const endpoint = `/appserver-preview/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
|
|
354
|
+
const response = await fetch(
|
|
355
|
+
endpoint,
|
|
356
|
+
buildRemoteRequestInit.call(this, payload, options, {
|
|
357
|
+
[PREVIEW_TOKEN_HEADER]: previewToken,
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
return await unwrapAppServerResponse(response, "[AppServer-DevBridge]");
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const invokeByPreviewProxy = async function(appServerAPIName, payload, options = {}) {
|
|
365
|
+
let previewToken = await getPreviewSessionToken.call(this);
|
|
366
|
+
try {
|
|
367
|
+
return await invokeByPreviewProxyOnce.call(this, appServerAPIName, payload, options, previewToken);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (!isRefreshablePreviewTokenError(error)) {
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
clearPreviewSessionToken(this.appId);
|
|
374
|
+
previewToken = await getPreviewSessionToken.call(this, { forceRefresh: true });
|
|
375
|
+
return await invokeByPreviewProxyOnce.call(this, appServerAPIName, payload, options, previewToken);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const invokeByCrossAppDirect = async function(appServerAPIName, payload, options = {}) {
|
|
380
|
+
const endpoint = `${this.config.appServerBaseUrl}/invokeAppServerAPI?apiName=${encodeURIComponent(appServerAPIName)}`;
|
|
381
|
+
const requestInit = buildRemoteRequestInit.call(this, payload, options, {
|
|
382
|
+
"X-API-Key": this.config.appServerApiKey,
|
|
383
|
+
});
|
|
384
|
+
const response = await fetch(endpoint, requestInit);
|
|
385
|
+
return await unwrapAppServerResponse(response, "[AppServer-CrossApp]");
|
|
386
|
+
};
|
|
387
|
+
|
|
76
388
|
/**
|
|
77
389
|
* 调用一个 App Server API。
|
|
78
|
-
* -
|
|
79
|
-
* -
|
|
80
|
-
* -
|
|
81
|
-
* -
|
|
82
|
-
*
|
|
83
|
-
* @param {
|
|
84
|
-
* @
|
|
390
|
+
* - 开发态浏览器:预览页走 CLI preview bridge,其他页面走平台转发。
|
|
391
|
+
* - 生产态浏览器:统一走平台转发。
|
|
392
|
+
* - 后端跨应用:允许使用 appServerBaseUrl + appServerApiKey 直连目标 AppServer。
|
|
393
|
+
* - 后端当前应用:从本地注册表执行。
|
|
394
|
+
*
|
|
395
|
+
* @param {string} appServerAPIName
|
|
396
|
+
* @param {any} payload
|
|
397
|
+
* @param {Object} options
|
|
398
|
+
* @param {Object|Array} [options.files] 仅在存在文件时传入,SDK 会自动切换到 multipart/form-data,并将 JSON 放到 requestData 字段。
|
|
85
399
|
*/
|
|
86
|
-
const invokeAppServerAPI = async function(appServerAPIName, payload) {
|
|
87
|
-
|
|
88
|
-
|
|
400
|
+
const invokeAppServerAPI = async function(appServerAPIName, payload, options = {}) {
|
|
401
|
+
if (!appServerAPIName || typeof appServerAPIName !== "string") {
|
|
402
|
+
throw new Error("appServerAPIName is required");
|
|
403
|
+
}
|
|
404
|
+
|
|
89
405
|
if (isBackend && this.config.appServerBaseUrl && this.config.appServerApiKey) {
|
|
90
|
-
|
|
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
|
-
}
|
|
406
|
+
return await invokeByCrossAppDirect.call(this, appServerAPIName, payload, options);
|
|
118
407
|
}
|
|
119
408
|
|
|
120
|
-
// 场景一:开发环境浏览器
|
|
121
409
|
if (isDevelopmentBrowser) {
|
|
122
410
|
if (isBrowserPreviewRuntime()) {
|
|
123
|
-
return await invokeByPreviewProxy.call(this, appServerAPIName, payload);
|
|
411
|
+
return await invokeByPreviewProxy.call(this, appServerAPIName, payload, options);
|
|
124
412
|
}
|
|
125
|
-
return await invokeByPlatformProxy.call(this, appServerAPIName, payload);
|
|
413
|
+
return await invokeByPlatformProxy.call(this, appServerAPIName, payload, options);
|
|
126
414
|
}
|
|
127
415
|
|
|
128
|
-
// 场景二:生产环境的浏览器,统一走平台转发
|
|
129
416
|
if (isProductionBrowser) {
|
|
130
|
-
|
|
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
|
-
}
|
|
417
|
+
return await invokeByPlatformProxy.call(this, appServerAPIName, payload, options);
|
|
136
418
|
}
|
|
137
419
|
|
|
138
|
-
// 场景三:后端云函数环境,从本地 Map 执行
|
|
139
420
|
const appServerAPIHandler = appServerAPIMap.get(appServerAPIName);
|
|
140
421
|
if (!appServerAPIHandler) {
|
|
141
422
|
throw new Error(`[AppServer-${env}] AppServer "${appServerAPIName}" not found or not registered.`);
|
|
142
423
|
}
|
|
143
|
-
|
|
424
|
+
|
|
425
|
+
return await appServerAPIHandler(payload, {
|
|
426
|
+
appsClient: this,
|
|
427
|
+
...options,
|
|
428
|
+
});
|
|
144
429
|
};
|
|
145
430
|
|
|
146
431
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "1.10.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"
|