@tacoreai/web-sdk 1.0.12 → 1.3.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,336 @@
1
+ import { isBrowser, isBackend } from "../../../utils/index.js";
2
+ import { BaseAppsClient } from "./BaseAppsClient.js";
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
+ /**
29
+ * AI应用认证管理器
30
+ * 负责AI应用的用户认证、会话管理
31
+ * 所有认证操作通过后端 /apps/auth API 完成
32
+ */
33
+ export class AppsAuthManager extends BaseAppsClient {
34
+ constructor(appId, config = {}) {
35
+ super(appId, config);
36
+
37
+ if (isBackend && !this.config.tacoreServerInteropAppServerApiKey) {
38
+ throw new Error("tacoreServerInteropAppServerApiKey is required for backend environment. Provide it in config or via TACORE_SERVER_INTEROP_APP_SERVER_API_KEY env var.");
39
+ }
40
+
41
+ // 自动检测 URL 中的 token (用于小程序 WebView 静默登录)
42
+ if (isBrowser) {
43
+ this._checkUrlToken();
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 定义策略适配器映射
49
+ * 将 invoke 的对象参数转换为老方法的位置参数
50
+ */
51
+ get adapters() {
52
+ return {
53
+ 'signUp': ({ email, password, profile }) => this.signUp(email, password, profile),
54
+ 'verifyRegistration': ({ email, token }) => this.verifyRegistration(email, token),
55
+ 'loginWithPassword': ({ email, password }) => this.loginWithPassword(email, password),
56
+ 'loginWithWechatH5': ({ code, scope }) => this.loginWithWechatH5(code, scope),
57
+ 'forgotPassword': ({ email, redirectTo }) => this.forgotPassword(email, redirectTo),
58
+ 'updatePassword': ({ accessToken, newPassword }) => this.updatePassword(accessToken, newPassword),
59
+ 'logout': () => this.logout(),
60
+ 'getCurrentUser': () => this.getCurrentUser(),
61
+ 'getSession': () => this.getSession(),
62
+ 'isAuthenticated': () => this.isAuthenticated(),
63
+ };
64
+ }
65
+
66
+ /**
67
+ * 检查 URL 中是否有 token 参数,如果有则保存并清除 URL 参数
68
+ */
69
+ _checkUrlToken() {
70
+ try {
71
+ const urlParams = new URLSearchParams(window.location.search);
72
+ // 更新:从小程序传递的特定参数名获取 Token
73
+ const token = urlParams.get('tacoreai_mp_access_token');
74
+
75
+ if (token) {
76
+ console.log('[AppsAuthManager] Found token in URL, performing silent login...');
77
+ AppsTokenManager.setAccessToken(token);
78
+
79
+ // 清除 URL 中的 token 参数,避免分享泄露
80
+ const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
81
+ // 保留其他参数
82
+ urlParams.delete('tacoreai_mp_access_token');
83
+ const remainingParams = urlParams.toString();
84
+ const finalUrl = remainingParams ? `${newUrl}?${remainingParams}` : newUrl;
85
+
86
+ window.history.replaceState({ path: finalUrl }, '', finalUrl);
87
+ }
88
+ } catch (e) {
89
+ console.error('[AppsAuthManager] Failed to parse URL token:', e);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 重写基类方法,提供 Token
95
+ */
96
+ _getAccessToken() {
97
+ return AppsTokenManager.getAccessToken();
98
+ }
99
+
100
+ /**
101
+ * AI应用用户注册 (第一步: 请求发送验证码)
102
+ * @param {string} email
103
+ * @param {string} password
104
+ * @param {Object} profile - 用户资料
105
+ * @returns {Promise<Object>}
106
+ */
107
+ async signUp(email, password, profile = {}) {
108
+ // 此方法现在只负责启动注册流程并发送验证码
109
+ // 它不再返回会话信息
110
+ const result = await this._apiRequest("/apps/auth/register", {
111
+ method: "POST",
112
+ body: JSON.stringify({ email, password, appId: this.appId, profile }),
113
+ });
114
+ return result; // 应包含成功消息
115
+ }
116
+
117
+ /**
118
+ * AI应用用户注册 (第二步: 验证验证码并完成注册)
119
+ * @param {string} email
120
+ * @param {string} token - 从邮件收到的OTP验证码
121
+ * @returns {Promise<Object>}
122
+ */
123
+ async verifyRegistration(email, token) {
124
+ const result = await this._apiRequest("/apps/auth/verify-registration", {
125
+ method: "POST",
126
+ body: JSON.stringify({ email, token, appId: this.appId }),
127
+ });
128
+
129
+ // 成功验证后,后端会返回会话信息
130
+ if (result.accessToken) {
131
+ AppsTokenManager.setAccessToken(result.accessToken);
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * AI应用用户登录
139
+ * @param {string} email
140
+ * @param {string} password
141
+ * @returns {Promise<Object>}
142
+ */
143
+ async loginWithPassword(email, password) {
144
+ console.log({ email, password, appId: this.appId }, this);
145
+ const result = await this._apiRequest("/apps/auth/login", {
146
+ method: "POST",
147
+ body: JSON.stringify({ email, password, appId: this.appId }),
148
+ });
149
+
150
+ if (result.accessToken) {
151
+ AppsTokenManager.setAccessToken(result.accessToken);
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * 微信 H5 登录
159
+ * @param {string} code - 微信授权回调的 code
160
+ * @param {string} scope - 授权作用域 ('snsapi_base' | 'snsapi_userinfo')
161
+ * @returns {Promise<Object>}
162
+ */
163
+ async loginWithWechatH5(code, scope = 'snsapi_base') {
164
+ const result = await this._apiRequest("/apps/auth/wechat/h5/login", {
165
+ method: "POST",
166
+ body: JSON.stringify({ code, appId: this.appId, scope }),
167
+ });
168
+
169
+ if (result.accessToken) {
170
+ AppsTokenManager.setAccessToken(result.accessToken);
171
+ }
172
+
173
+ return result;
174
+ }
175
+
176
+ /**
177
+ * 请求发送密码重置邮件
178
+ * @param {string} email - 用户的邮箱
179
+ * @param {string} redirectTo - 用户点击邮件链接后重定向到的前端页面URL
180
+ * @returns {Promise<Object>}
181
+ */
182
+ async forgotPassword(email, redirectTo) {
183
+ if (!email || !redirectTo) {
184
+ throw new Error("Email and redirectTo URL are required.");
185
+ }
186
+ return this._apiRequest("/apps/auth/forgot-password", {
187
+ method: "POST",
188
+ body: JSON.stringify({ email, redirectTo }),
189
+ });
190
+ }
191
+
192
+ /**
193
+ * 使用令牌更新用户密码
194
+ * @param {string} accessToken - 从重定向URL中获取的访问令牌
195
+ * @param {string} newPassword - 用户输入的新密码
196
+ * @returns {Promise<Object>}
197
+ */
198
+ async updatePassword(accessToken, newPassword) {
199
+ if (!accessToken || !newPassword) {
200
+ throw new Error("Access token and new password are required.");
201
+ }
202
+ return this._apiRequest("/apps/auth/update-password", {
203
+ method: "POST",
204
+ body: JSON.stringify({ accessToken, newPassword }),
205
+ });
206
+ }
207
+
208
+ /**
209
+ * AI应用用户登出
210
+ * @returns {Promise<void>}
211
+ */
212
+ async logout() {
213
+ try {
214
+ await this._apiRequest("/apps/auth/logout", {
215
+ method: "POST",
216
+ });
217
+ } finally {
218
+ AppsTokenManager.clearAccessToken();
219
+ }
220
+ }
221
+
222
+ /**
223
+ * 获取当前登录的AI应用用户信息
224
+ * @returns {Promise<Object|null>}
225
+ */
226
+ async getCurrentUser() {
227
+ try {
228
+ const result = await this._apiRequest(`/apps/auth/profile?appId=${this.appId}`);
229
+ return result.user;
230
+ } catch (error) {
231
+ return null;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 获取当前AI应用会话信息
237
+ * @returns {Promise<Object|null>}
238
+ */
239
+ async getSession() {
240
+ const token = AppsTokenManager.getAccessToken();
241
+ if (!token) {
242
+ return null;
243
+ }
244
+
245
+ try {
246
+ const result = await this._apiRequest(`/apps/auth/profile?appId=${this.appId}`);
247
+ return {
248
+ access_token: token,
249
+ user: result.user,
250
+ };
251
+ } catch (error) {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 检查AI应用用户是否已认证
258
+ * @returns {Promise<boolean>}
259
+ */
260
+ async isAuthenticated() {
261
+ const token = AppsTokenManager.getAccessToken();
262
+ if (!token) return false;
263
+
264
+ try {
265
+ await this.getCurrentUser();
266
+ return true;
267
+ } catch (error) {
268
+ AppsTokenManager.clearAccessToken();
269
+ return false;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 监听AI应用认证状态变化(简化版本)
275
+ * @param {function} callback
276
+ * @returns {function} unsubscribe function
277
+ */
278
+ onAuthStateChange(callback) {
279
+ // 简化版本:立即检查当前状态
280
+ this.isAuthenticated().then((isAuth) => {
281
+ callback(isAuth ? "SIGNED_IN" : "SIGNED_OUT", {
282
+ access_token: AppsTokenManager.getAccessToken(),
283
+ });
284
+ });
285
+
286
+ // 返回空的取消订阅函数
287
+ return () => {};
288
+ }
289
+
290
+ /**
291
+ * 自动认证(用于AI应用初始化)
292
+ * @returns {Promise<Object>}
293
+ */
294
+ async autoAuth() {
295
+ try {
296
+ const isAuth = await this.isAuthenticated();
297
+ if (isAuth) {
298
+ const user = await this.getCurrentUser();
299
+ return {
300
+ isAuthenticated: true,
301
+ user: user,
302
+ appId: this.appId,
303
+ };
304
+ } else {
305
+ return {
306
+ isAuthenticated: false,
307
+ user: null,
308
+ appId: this.appId,
309
+ };
310
+ }
311
+ } catch (error) {
312
+ console.error("AI应用自动认证失败:", error);
313
+ return {
314
+ isAuthenticated: false,
315
+ user: null,
316
+ appId: this.appId,
317
+ error: error.message,
318
+ };
319
+ }
320
+ }
321
+
322
+ /**
323
+ * 获取访问令牌
324
+ * @returns {string|null}
325
+ */
326
+ getAccessToken() {
327
+ return AppsTokenManager.getAccessToken();
328
+ }
329
+
330
+ /**
331
+ * 清除访问令牌
332
+ */
333
+ clearAccessToken() {
334
+ AppsTokenManager.clearAccessToken();
335
+ }
336
+ }
@@ -0,0 +1,229 @@
1
+ export const appsClientAiMethods = {
2
+ // ==================== AI 能力 ====================
3
+
4
+ async streamCompletion(options = {}) {
5
+ try {
6
+ if (!options.messages || !Array.isArray(options.messages)) {
7
+ throw new Error("messages array is required for AI completion.");
8
+ }
9
+
10
+ if (!options.onChunk || typeof options.onChunk !== "function") {
11
+ throw new Error("onChunk callback is required for streaming.");
12
+ }
13
+
14
+ const requestData = {
15
+ appId: this.appId,
16
+ model: options.model || "agent:tacore-1.2",
17
+ messages: options.messages,
18
+ temperature: options.temperature,
19
+ max_tokens: options.max_tokens,
20
+ stream: true,
21
+ system: options.system,
22
+ // 透传 Vertex AI Search Data Store ID
23
+ vertexAiSearchDatastore: options.vertexAiSearchDatastore,
24
+ // [新增] 透传 tools 配置对象到后端
25
+ tools: options.tools,
26
+ };
27
+
28
+ const url = `${this.config.apiBaseUrl}/apps/agent/completions`;
29
+ const headers = this._getRequestHeaders();
30
+
31
+ const response = await fetch(url, {
32
+ method: "POST",
33
+ headers,
34
+ body: JSON.stringify(requestData),
35
+ signal: options.signal,
36
+ });
37
+
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}`);
41
+ }
42
+
43
+ const reader = response.body?.getReader();
44
+ if (!reader) {
45
+ throw new Error("Response body is not readable");
46
+ }
47
+
48
+ const decoder = new TextDecoder();
49
+ let buffer = "";
50
+
51
+ try {
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+
56
+ buffer += decoder.decode(value, { stream: true });
57
+
58
+ while (true) {
59
+ const lineEnd = buffer.indexOf("\n");
60
+ if (lineEnd === -1) break;
61
+
62
+ const line = buffer.slice(0, lineEnd).trim();
63
+ buffer = buffer.slice(lineEnd + 1);
64
+
65
+ if (!line || line.startsWith(":")) continue;
66
+
67
+ if (line.startsWith("data: ")) {
68
+ const data = line.slice(6);
69
+
70
+ if (data === "[DONE]") {
71
+ options.onComplete?.();
72
+ return;
73
+ }
74
+
75
+ try {
76
+ const parsed = JSON.parse(data);
77
+
78
+ if (parsed.choices && parsed.choices.length > 0) {
79
+ const choice = parsed.choices[0];
80
+
81
+ if (choice.error) {
82
+ const error = new Error(choice.error.message || "AI service error");
83
+ options.onError?.(error);
84
+ return;
85
+ }
86
+
87
+ if (choice.delta && choice.delta.hasOwnProperty("content")) {
88
+ const content = choice.delta.content || "";
89
+ // 修复:直接透传 content,不进行后处理,避免丢失空格
90
+ options.onChunk(content, parsed);
91
+ }
92
+
93
+ if (choice.finish_reason === "stop") {
94
+ options.onComplete?.();
95
+ return;
96
+ }
97
+
98
+ if (choice.finish_reason === "error") {
99
+ const error = new Error("AI generation finished with error");
100
+ options.onError?.(error);
101
+ return;
102
+ }
103
+ }
104
+ } catch (parseError) {
105
+ console.warn("Failed to parse SSE data:", parseError);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ options.onComplete?.();
112
+ } finally {
113
+ reader.cancel();
114
+ }
115
+
116
+ console.log(`✅ [AppsClient] AI streaming completion finished for app: ${this.appId}`);
117
+ } catch (error) {
118
+ console.error(`AI流式对话失败 (app: ${this.appId}):`, error);
119
+
120
+ if (error.name === "AbortError") {
121
+ console.log("AI generation was aborted by user");
122
+ return;
123
+ }
124
+
125
+ options.onError?.(error);
126
+ throw error;
127
+ }
128
+ },
129
+
130
+ async generateImage(options = {}) {
131
+ try {
132
+ if (!options.prompt) {
133
+ throw new Error("prompt is required for image generation.");
134
+ }
135
+
136
+ // 构建与 database 核心库一致的请求体
137
+ const payload = {
138
+ appId: this.appId,
139
+ imageOptions: options,
140
+ };
141
+
142
+ // 关键变更:切换到与 database 核心库相同的接口
143
+ const result = await this._post("/apps/agent/generate-google-image", payload);
144
+
145
+ console.log(`✅ [AppsClient] Image generated successfully via Google Image for app: ${this.appId}`);
146
+
147
+ // 修改:保持传输透明性,直接返回后端响应
148
+ return result;
149
+ } catch (error) {
150
+ console.error(`生成图片失败 (app: ${this.appId}):`, error);
151
+ throw error;
152
+ }
153
+ },
154
+
155
+ /**
156
+ * 新增:图片编辑(单图编辑、双图融合、掩膜修复/扩展)
157
+ * 支持两种调用方式:
158
+ * 1) 直接传 messages(将被透传到后端)
159
+ * 2) 传 prompt + inputs(SDK 自动构造 messages)
160
+ *
161
+ * @param {Object} options
162
+ * - model?: string = 'gemini-3-pro-image-preview'
163
+ * - temperature?: number
164
+ * - size?: string
165
+ * - provider?: string ('google' | 'volcengine')
166
+ * - messages?: Array<{ role: string, content: any[] }>
167
+ * - prompt?: string
168
+ * - inputs?: Array<{ type:'uploaded_file', fileUri?: string, mimeType?: string, uploaded_file?: {fileUri:string, mimeType:string} }>
169
+ * @returns {Promise<Object>} 后端响应对象
170
+ */
171
+ async editImage(options = {}) {
172
+ try {
173
+ if (!options || typeof options !== "object") {
174
+ throw new Error("options are required for image editing.");
175
+ }
176
+
177
+ const model = options.model || "gemini-3-pro-image-preview";
178
+ let messages = options.messages;
179
+
180
+ // 如果没有传 messages,则由 prompt + inputs 构造
181
+ if (!messages) {
182
+ const prompt = options.prompt;
183
+ const inputs = options.inputs || [];
184
+ if (!prompt || inputs.length === 0) {
185
+ throw new Error("Either 'messages' or ('prompt' + 'inputs') is required for image editing.");
186
+ }
187
+
188
+ const content = [{ type: "text", text: prompt }];
189
+
190
+ inputs.forEach(input => {
191
+ if (!input || input.type !== "uploaded_file") return;
192
+ let uploaded_file = null;
193
+ if (input.uploaded_file) {
194
+ uploaded_file = input.uploaded_file;
195
+ } else if (input.fileUri && input.mimeType) {
196
+ uploaded_file = { fileUri: input.fileUri, mimeType: input.mimeType };
197
+ }
198
+ if (uploaded_file?.fileUri && uploaded_file?.mimeType) {
199
+ content.push({
200
+ type: "uploaded_file",
201
+ uploaded_file,
202
+ });
203
+ }
204
+ });
205
+
206
+ messages = [{ role: "user", content }];
207
+ }
208
+
209
+ const payload = {
210
+ appId: this.appId,
211
+ imageEditOptions: {
212
+ model,
213
+ messages,
214
+ temperature: options.temperature,
215
+ size: options.size,
216
+ provider: options.provider, // 透传 provider 参数
217
+ },
218
+ };
219
+ const result = await this._post("/apps/agent/generate-google-image-edit", payload);
220
+ console.log(`✅ [AppsClient] Image edited successfully via Google Image Edit for app: ${this.appId}`);
221
+
222
+ // 修改:保持传输透明性,直接返回后端响应
223
+ return result;
224
+ } catch (error) {
225
+ console.error(`编辑图片失败 (app: ${this.appId}):`, error);
226
+ throw error;
227
+ }
228
+ },
229
+ };
@@ -0,0 +1,92 @@
1
+ import { env, isDevelopmentBrowser, isProductionBrowser, isBackend } from "../../../utils/index.js";
2
+
3
+ const appServerAPIMap = new Map();
4
+
5
+ /**
6
+ * 注册一个 App Server API。
7
+ * - 在开发态浏览器中:有效,用于本地调用。
8
+ * - 在生产态浏览器中:无效,为空操作。
9
+ * - 在后端云函数中:有效,用于加载服务。
10
+ * @param {string} appServerAPIName - API名称。
11
+ * @param {Function} appServerAPIHandler - 服务的处理函数,接收一个 payload 参数。
12
+ */
13
+ const registerAppServerAPI = (appServerAPIName, appServerAPIHandler) => {
14
+ if (isDevelopmentBrowser || isBackend) {
15
+ appServerAPIMap.set(appServerAPIName, appServerAPIHandler);
16
+ }
17
+ // 在生产态浏览器中,这是一个空操作。
18
+ };
19
+
20
+ /**
21
+ * 调用一个 App Server API。
22
+ * - 在开发态浏览器中:本地执行。
23
+ * - 在生产态浏览器中:统一走平台转发(/apps/invokeAppServerAPI)。
24
+ * - 在后端云函数中:本地执行 (由云函数入口调用)。
25
+ * - 跨应用调用:仅后端场景可直连目标 AppServer(需 appServerBaseUrl + appServerApiKey)。
26
+ * @param {string} appServerAPIName - 要调用的服务名称。
27
+ * @param {Object} payload - 传递给服务的负载数据。
28
+ * @returns {Promise<any>} 服务处理后的结果。
29
+ */
30
+ const invokeAppServerAPI = async function(appServerAPIName, payload) {
31
+ console.log(`[AppServer][${this.appId}] Invoking "${appServerAPIName}" with payload:`, payload);
32
+ // 场景零:跨应用调用,在源应用的 appserver 云函数调用目标应用的 appserver 云函数
33
+ if (isBackend && this.config.appServerBaseUrl && this.config.appServerApiKey) {
34
+ const endpoint = `${this.config.appServerBaseUrl}/invokeAppServerAPI?apiName=${appServerAPIName}`;
35
+ try {
36
+ const response = await fetch(endpoint, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'X-API-Key': this.config.appServerApiKey,
41
+ },
42
+ body: JSON.stringify(payload),
43
+ });
44
+
45
+ console.log(`App server request id: ${response.headers.get('x-scf-request-id')}`)
46
+
47
+ const res = await response.json();
48
+ if (!response.ok) {
49
+ // 抛出网络层面的错误
50
+ throw new Error(`[AppServer-CrossApp] error! status: ${response.status}, statusText: ${response.statusText}, requestId: ${response.headers.get('x-scf-request-id')}, body: ${JSON.stringify(res)}`);
51
+ }
52
+
53
+ if (res.success) {
54
+ return res.data;
55
+ }
56
+ // 抛出业务层面的错误
57
+ throw new Error(res.error || 'Cross-application server invocation failed at the business level.');
58
+ } catch (error) {
59
+ console.error(`[AppServer-CrossApp] Failed to invoke "${appServerAPIName}" via cross-app call:`, error);
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ // 场景一:生产环境的浏览器,必须发起网络请求
65
+ if (isProductionBrowser) {
66
+ // 浏览器端统一走平台转发,由平台负责组织/应用上下文与内部鉴权链路
67
+ const endpoint = `/apps/invokeAppServerAPI?appId=${encodeURIComponent(this.appId)}&apiName=${encodeURIComponent(appServerAPIName)}`;
68
+
69
+ try {
70
+ const res = await this._post(endpoint, payload);
71
+ if (res.success) {
72
+ return res.data;
73
+ }
74
+ throw new Error(res.error || res.message || `AppServer invocation failed: ${res.code || "UNKNOWN_ERROR"}`);
75
+ } catch (error) {
76
+ console.error(`[AppServer-Prod] Failed to invoke "${appServerAPIName}" via appsClient:`, error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ // 场景二:开发环境的浏览器 或 后端云函数环境,都从本地 Map 执行
82
+ const appServerAPIHandler = appServerAPIMap.get(appServerAPIName);
83
+ if (!appServerAPIHandler) {
84
+ throw new Error(`[AppServer-${env}] AppServer "${appServerAPIName}" not found or not registered.`);
85
+ }
86
+ return await appServerAPIHandler(payload, { appsClient: this });
87
+ };
88
+
89
+ export const appsClientAppServerMethods = {
90
+ registerAppServerAPI,
91
+ invokeAppServerAPI,
92
+ };