@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.
- package/README.md +404 -0
- package/database/core/apps/AppsAuthManager.js +336 -0
- package/database/core/apps/AppsClient.AI.js +229 -0
- package/database/core/apps/AppsClient.AppServer.js +92 -0
- package/database/core/apps/AppsClient.Crawler.js +71 -0
- package/database/core/apps/AppsClient.Data.js +187 -0
- package/database/core/apps/AppsClient.Debug.js +41 -0
- package/database/core/apps/AppsClient.Deploy.js +66 -0
- package/database/core/apps/AppsClient.Ecommerce.js +151 -0
- package/database/core/apps/AppsClient.KV.js +72 -0
- package/database/core/apps/AppsClient.KnowledgeBase.js +130 -0
- package/database/core/apps/AppsClient.Message.js +27 -0
- package/database/core/apps/AppsClient.Realtime.js +46 -0
- package/database/core/apps/AppsClient.Storage.js +275 -0
- package/database/core/apps/AppsClient.TextIn.js +341 -0
- package/database/core/apps/AppsClient.Tools.js +36 -0
- package/database/core/apps/AppsClient.Users.js +49 -0
- package/database/core/apps/AppsClient.VoiceVideo.js +106 -0
- package/database/core/apps/AppsClient.Volcengine.js +169 -0
- package/database/core/apps/AppsClient.js +215 -0
- package/database/core/apps/AppsPublicClient.js +107 -0
- package/database/core/apps/BaseAppsClient.js +216 -0
- package/index.js +9 -0
- package/package.json +11 -23
- package/utils/errorLogger.js +28 -0
- package/utils/index.js +114 -0
- package/dist/index.umd.js +0 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export const appsClientKnowledgeBaseMethods = {
|
|
2
|
+
// ==================== Knowledge Base (RAG) ====================
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 创建 Knowledge Base Data Store
|
|
6
|
+
* @param {Object} params
|
|
7
|
+
* @param {string} params.storeId - Data Store ID
|
|
8
|
+
* @param {string} params.displayName - 显示名称
|
|
9
|
+
* @param {string} [params.provider='google'] - 提供商 (google, aliyun)
|
|
10
|
+
* @returns {Promise<Object>} 创建结果
|
|
11
|
+
*/
|
|
12
|
+
async createKnowledgeBaseDataStore({ storeId, displayName, provider = "google" }) {
|
|
13
|
+
if (!storeId || !displayName) {
|
|
14
|
+
throw new Error("storeId and displayName are required.");
|
|
15
|
+
}
|
|
16
|
+
return this._post("/apps/knowledge-base/dataStore", { storeId, displayName, provider });
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 删除 Knowledge Base Data Store
|
|
21
|
+
* @param {string} storeId
|
|
22
|
+
* @param {string} [provider='google']
|
|
23
|
+
* @returns {Promise<Object>} 删除结果
|
|
24
|
+
*/
|
|
25
|
+
async deleteKnowledgeBaseDataStore(storeId, provider = "google") {
|
|
26
|
+
if (!storeId) {
|
|
27
|
+
throw new Error("storeId is required.");
|
|
28
|
+
}
|
|
29
|
+
return this._delete(`/apps/knowledge-base/dataStore/${storeId}`, { provider });
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 获取 Knowledge Base Data Store 详情
|
|
34
|
+
* @param {string} storeId
|
|
35
|
+
* @param {string} [provider='google']
|
|
36
|
+
* @returns {Promise<Object>} Data Store 详情
|
|
37
|
+
*/
|
|
38
|
+
async getKnowledgeBaseDataStore(storeId, provider = "google") {
|
|
39
|
+
if (!storeId) {
|
|
40
|
+
throw new Error("storeId is required.");
|
|
41
|
+
}
|
|
42
|
+
return this._get(`/apps/knowledge-base/dataStore/${storeId}`, { provider });
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 列出所有 Knowledge Base Data Stores
|
|
47
|
+
* @param {string} [provider='google']
|
|
48
|
+
* @returns {Promise<Object>} Data Store 列表
|
|
49
|
+
*/
|
|
50
|
+
async listKnowledgeBaseDataStores(provider = "google") {
|
|
51
|
+
return this._get("/apps/knowledge-base/dataStore", { provider });
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 导入文档到 Knowledge Base Data Store
|
|
56
|
+
* @param {Object} params
|
|
57
|
+
* @param {string} params.storeId - 目标 Data Store ID
|
|
58
|
+
* @param {string} params.sourceUri - 源 URI
|
|
59
|
+
* @param {string} [params.provider='google']
|
|
60
|
+
* @returns {Promise<Object>} 导入操作信息
|
|
61
|
+
*/
|
|
62
|
+
async importKnowledgeBaseDocuments({ storeId, sourceUri, provider = "google" }) {
|
|
63
|
+
if (!storeId || !sourceUri) {
|
|
64
|
+
throw new Error("storeId and sourceUri are required.");
|
|
65
|
+
}
|
|
66
|
+
return this._post("/apps/knowledge-base/dataStore/import", { storeId, sourceUri, provider });
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 获取 Knowledge Base 文档列表
|
|
71
|
+
* @param {string} storeId
|
|
72
|
+
* @param {string} pageToken
|
|
73
|
+
* @param {number} pageSize
|
|
74
|
+
* @param {string} provider
|
|
75
|
+
*/
|
|
76
|
+
async listKnowledgeBaseDocuments(storeId, pageToken = "", pageSize = 10, provider = "google") {
|
|
77
|
+
return this._get(`/apps/knowledge-base/dataStore/${storeId}/documents`, {
|
|
78
|
+
pageToken,
|
|
79
|
+
pageSize,
|
|
80
|
+
provider,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 删除 Knowledge Base 文档
|
|
86
|
+
* @param {string} storeId
|
|
87
|
+
* @param {string} documentId
|
|
88
|
+
* @param {string} provider
|
|
89
|
+
*/
|
|
90
|
+
async deleteKnowledgeBaseDocument(storeId, documentId, provider = "google") {
|
|
91
|
+
return this._delete(`/apps/knowledge-base/dataStore/${storeId}/documents/${documentId}`, { provider });
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// ==================== Deprecated Vertex AI Aliases ====================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @deprecated Use createKnowledgeBaseDataStore
|
|
98
|
+
*/
|
|
99
|
+
async createVertexDataStore({ dataStoreId, displayName }) {
|
|
100
|
+
return this.createKnowledgeBaseDataStore({ storeId: dataStoreId, displayName, provider: "google" });
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Use deleteKnowledgeBaseDataStore
|
|
105
|
+
*/
|
|
106
|
+
async deleteVertexDataStore(dataStoreId) {
|
|
107
|
+
return this.deleteKnowledgeBaseDataStore(dataStoreId, "google");
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @deprecated Use getKnowledgeBaseDataStore
|
|
112
|
+
*/
|
|
113
|
+
async getVertexDataStore(dataStoreId) {
|
|
114
|
+
return this.getKnowledgeBaseDataStore(dataStoreId, "google");
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @deprecated Use listKnowledgeBaseDataStores
|
|
119
|
+
*/
|
|
120
|
+
async listVertexDataStores() {
|
|
121
|
+
return this.listKnowledgeBaseDataStores("google");
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @deprecated Use importKnowledgeBaseDocuments
|
|
126
|
+
*/
|
|
127
|
+
async importVertexDocuments({ dataStoreId, gcsUri }) {
|
|
128
|
+
return this.importKnowledgeBaseDocuments({ storeId: dataStoreId, sourceUri: gcsUri, provider: "google" });
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const appsClientMessageMethods = {
|
|
2
|
+
// ==================== [新增] 统一消息服务 ====================
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 发送消息(统一接口)
|
|
6
|
+
* 支持多种 Provider (email, sms, wechat 等)
|
|
7
|
+
*
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {string} options.provider - 渠道标识: 'email' | 'sms' | 'wechat' ...
|
|
10
|
+
* @param {object} options.data - 消息数据,根据 provider 不同而不同
|
|
11
|
+
* @returns {Promise<{success: boolean, data: any}>}
|
|
12
|
+
*/
|
|
13
|
+
async sendMessage({ provider, data }) {
|
|
14
|
+
if (!provider) {
|
|
15
|
+
throw new Error("provider is required for sendMessage.");
|
|
16
|
+
}
|
|
17
|
+
if (!data || typeof data !== "object") {
|
|
18
|
+
throw new Error("data object is required for sendMessage.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return this._post("/apps/message/send", {
|
|
22
|
+
appId: this.appId,
|
|
23
|
+
provider,
|
|
24
|
+
data,
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const appsClientRealtimeMethods = {
|
|
2
|
+
// ==================== 实时数据接口 (高频/本地) ====================
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 发布(写入)一条新的实时数据。
|
|
6
|
+
* 强制规则:此方法仅在客户端本地通过内部事件总线工作,永远不会将数据发送到云端。
|
|
7
|
+
* 这是为了保护云端数据库免受高频写入的影响。
|
|
8
|
+
* @param {string} modelName - 数据模型的名称。
|
|
9
|
+
* @param {Object} data - 要发布的数据。
|
|
10
|
+
*/
|
|
11
|
+
publishRealtimeData(modelName, data) {
|
|
12
|
+
const listeners = this.subscribers.get(modelName);
|
|
13
|
+
if (listeners) {
|
|
14
|
+
listeners.forEach(callback => {
|
|
15
|
+
try {
|
|
16
|
+
callback(data);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error("Error in realtime data subscriber:", e);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 订阅指定模型的数据流。
|
|
26
|
+
* 强制规则:此方法仅订阅来自客户端本地的数据流。
|
|
27
|
+
* @param {string} modelName - 要订阅的数据模型的名称。
|
|
28
|
+
* @param {Function} callback - 接收新数据的回调函数。
|
|
29
|
+
* @returns {Function} 一个 unsubscribe 函数,用于取消订阅。
|
|
30
|
+
*/
|
|
31
|
+
subscribeToRealtimeData(modelName, callback) {
|
|
32
|
+
if (!this.subscribers.has(modelName)) {
|
|
33
|
+
this.subscribers.set(modelName, []);
|
|
34
|
+
}
|
|
35
|
+
const listeners = this.subscribers.get(modelName);
|
|
36
|
+
listeners.push(callback);
|
|
37
|
+
|
|
38
|
+
// 返回取消订阅函数
|
|
39
|
+
return () => {
|
|
40
|
+
const index = listeners.indexOf(callback);
|
|
41
|
+
if (index > -1) {
|
|
42
|
+
listeners.splice(index, 1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { isBrowser } from "../../../utils/index.js";
|
|
2
|
+
|
|
3
|
+
export const appsClientStorageMethods = {
|
|
4
|
+
// ==================== 文件存储 API (R2 & GCS & OSS) ====================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 上传文件到 Cloudflare R2 (用于前端展示)
|
|
8
|
+
* @param {File | {data: string, encoding: 'base64', name: string}} file - 文件对象
|
|
9
|
+
* @returns {Promise<{success: boolean, data: {url: string, key: string}}>}
|
|
10
|
+
*/
|
|
11
|
+
async uploadFile(file) {
|
|
12
|
+
// 统一使用 R2 作为默认存储
|
|
13
|
+
return this.uploadToR2(file);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* [原子接口] 获取 R2 上传预签名 URL
|
|
18
|
+
* @param {string} fileName - 文件名 (包含扩展名)
|
|
19
|
+
* @param {string} contentType - 文件 MIME 类型
|
|
20
|
+
* @returns {Promise<{uploadUrl: string, url: string, key: string, headers: object}>}
|
|
21
|
+
*/
|
|
22
|
+
async getR2UploadUrl(fileName, contentType) {
|
|
23
|
+
if (!fileName) throw new Error("fileName is required");
|
|
24
|
+
|
|
25
|
+
const result = await this._post("/apps/upload-r2-presigned", {
|
|
26
|
+
appId: this.appId,
|
|
27
|
+
fileName,
|
|
28
|
+
contentType: contentType || "application/octet-stream",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!result.success || !result.data?.uploadUrl) {
|
|
32
|
+
throw new Error(result.message || "Failed to get upload URL");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result.data;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 上传文件到 Cloudflare R2 (使用 Presigned URL 直传,支持代理转发以解决 CORS)
|
|
40
|
+
* @param {File | {data: string, encoding: 'base64', name: string}} file - 文件对象
|
|
41
|
+
* @returns {Promise<{success: boolean, data: {url: string, key: string}}>}
|
|
42
|
+
*/
|
|
43
|
+
async uploadToR2(file) {
|
|
44
|
+
if (!file) throw new Error("file is required");
|
|
45
|
+
|
|
46
|
+
let fileName;
|
|
47
|
+
let contentType;
|
|
48
|
+
let fileBody;
|
|
49
|
+
|
|
50
|
+
if (isBrowser) {
|
|
51
|
+
if (!(file instanceof File)) {
|
|
52
|
+
throw new Error("Invalid file input in browser. Provide a File object.");
|
|
53
|
+
}
|
|
54
|
+
fileName = file.name;
|
|
55
|
+
contentType = file.type || "application/octet-stream";
|
|
56
|
+
fileBody = file;
|
|
57
|
+
} else {
|
|
58
|
+
// isBackend
|
|
59
|
+
if (typeof file === "object" && file.data && file.name && file.encoding === "base64") {
|
|
60
|
+
fileName = file.name;
|
|
61
|
+
contentType = file.type || "application/octet-stream"; // 默认类型
|
|
62
|
+
fileBody = Buffer.from(file.data, "base64");
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string }."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 1. 获取上传地址 (使用原子接口)
|
|
71
|
+
const { uploadUrl, url: publicUrl, key, headers: uploadHeaders } = await this.getR2UploadUrl(fileName, contentType);
|
|
72
|
+
|
|
73
|
+
// 2. 执行上传 (直接使用后端返回的代理 URL 和 headers)
|
|
74
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
75
|
+
method: "PUT",
|
|
76
|
+
headers: uploadHeaders || { "Content-Type": contentType },
|
|
77
|
+
body: fileBody,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!uploadResponse.ok) {
|
|
81
|
+
throw new Error(`Failed to upload to R2: ${uploadResponse.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
url: publicUrl,
|
|
87
|
+
key: key,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 下载 R2 文件 (流式/Blob)
|
|
93
|
+
* @param {string} key - 文件 Key
|
|
94
|
+
* @returns {Promise<Response>}
|
|
95
|
+
*/
|
|
96
|
+
async downloadFromR2(key) {
|
|
97
|
+
if (!key) throw new Error("key is required");
|
|
98
|
+
const url = `${this.config.apiBaseUrl}/apps/download-r2?key=${encodeURIComponent(key)}`;
|
|
99
|
+
const headers = this._getRequestHeaders();
|
|
100
|
+
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorData = await response.json().catch(() => ({ error: "Network error" }));
|
|
108
|
+
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return response;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取 R2 文件的公开访问链接
|
|
116
|
+
* @param {string} key - 文件 Key
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
getR2PublicUrl(key) {
|
|
120
|
+
if (!key) throw new Error("key is required");
|
|
121
|
+
return `https://storage-r2.tacore.ai/${key}`;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 删除 R2 文件
|
|
126
|
+
* @param {string} key - 文件 Key
|
|
127
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
128
|
+
*/
|
|
129
|
+
async deleteFromR2(key) {
|
|
130
|
+
if (!key) throw new Error("key is required");
|
|
131
|
+
const url = `${this.config.apiBaseUrl}/apps/delete-r2`;
|
|
132
|
+
const headers = this._getRequestHeaders();
|
|
133
|
+
headers["Content-Type"] = "application/json";
|
|
134
|
+
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method: "DELETE",
|
|
137
|
+
headers,
|
|
138
|
+
body: JSON.stringify({ key }),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorData = await response.json().catch(() => ({ error: "Network error" }));
|
|
143
|
+
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return response.json();
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 上传文件到 Aliyun OSS
|
|
151
|
+
* @param {File | {data: string, encoding: 'base64', name: string}} file - 文件对象
|
|
152
|
+
* @returns {Promise<{success: boolean, data: {url: string, name: string}}>}
|
|
153
|
+
*/
|
|
154
|
+
async uploadToOSS(file) {
|
|
155
|
+
if (!file) throw new Error("file is required");
|
|
156
|
+
|
|
157
|
+
const url = `${this.config.apiBaseUrl}/apps/oss/upload`;
|
|
158
|
+
const form = new FormData();
|
|
159
|
+
form.append("appId", this.appId);
|
|
160
|
+
|
|
161
|
+
if (isBrowser) {
|
|
162
|
+
if (!(file instanceof File)) {
|
|
163
|
+
throw new Error("Invalid file input in browser. Provide a File object.");
|
|
164
|
+
}
|
|
165
|
+
form.append("file", file, file.name);
|
|
166
|
+
} else {
|
|
167
|
+
// isBackend
|
|
168
|
+
if (typeof file === "object" && file.data && file.name && file.encoding === "base64") {
|
|
169
|
+
const buffer = Buffer.from(file.data, "base64");
|
|
170
|
+
const fileBlob = new Blob([buffer]);
|
|
171
|
+
form.append("file", fileBlob, file.name);
|
|
172
|
+
} else {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string }."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const headers = this._getRequestHeaders();
|
|
180
|
+
delete headers["Content-Type"]; // Let browser set boundary
|
|
181
|
+
|
|
182
|
+
const resp = await fetch(url, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers,
|
|
185
|
+
body: form,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!resp.ok) {
|
|
189
|
+
const err = await resp.json().catch(() => ({}));
|
|
190
|
+
throw new Error(err.message || `HTTP ${resp.status}: ${resp.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
return resp.json();
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 上传文件到 Google Cloud Storage (GCS) - 用于 AI 多模态理解
|
|
197
|
+
* @param {File | {data: string, encoding: 'base64', name: string, type: string}} file - 文件对象
|
|
198
|
+
* @returns {Promise<{signedUrl: string, fileUrl: string, fileUri: string, accessUrl: string}>}
|
|
199
|
+
*/
|
|
200
|
+
async uploadToGCS(file) {
|
|
201
|
+
if (!file) throw new Error("file is required");
|
|
202
|
+
|
|
203
|
+
let fileName;
|
|
204
|
+
let contentType;
|
|
205
|
+
let fileBody;
|
|
206
|
+
|
|
207
|
+
if (isBrowser) {
|
|
208
|
+
if (!(file instanceof File)) {
|
|
209
|
+
throw new Error("Invalid file input in browser. Provide a File object.");
|
|
210
|
+
}
|
|
211
|
+
fileName = file.name;
|
|
212
|
+
contentType = file.type || "application/octet-stream";
|
|
213
|
+
fileBody = file;
|
|
214
|
+
} else {
|
|
215
|
+
// isBackend
|
|
216
|
+
if (typeof file === "object" && file.data && file.name && file.encoding === "base64") {
|
|
217
|
+
fileName = file.name;
|
|
218
|
+
contentType = file.type || "application/octet-stream";
|
|
219
|
+
fileBody = Buffer.from(file.data, "base64");
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string, type: string }."
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// [DEBUG] 打印上传参数
|
|
228
|
+
console.log("[AppsClient] uploadToGCS - Requesting signed URL for:", { fileName, contentType });
|
|
229
|
+
|
|
230
|
+
// 1. 获取签名 URL
|
|
231
|
+
const signResult = await this._post("/apps/storage/gcs-signed-url/generate", {
|
|
232
|
+
appId: this.appId,
|
|
233
|
+
fileName,
|
|
234
|
+
contentType,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!signResult.success || !signResult.data?.signedUrl) {
|
|
238
|
+
throw new Error(signResult.error || "Failed to get GCS signed URL");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const { signedUrl, fileUrl, fileUri, accessUrl } = signResult.data;
|
|
242
|
+
|
|
243
|
+
// [DEBUG] 打印签名 URL 和上传头
|
|
244
|
+
console.log("[AppsClient] uploadToGCS - Got signed URL:", signedUrl);
|
|
245
|
+
console.log("[AppsClient] uploadToGCS - Uploading with headers:", {
|
|
246
|
+
"Content-Type": contentType,
|
|
247
|
+
"x-target-hostname": "storage.googleapis.com",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// 2. 使用签名 URL 上传文件
|
|
251
|
+
// 注意:这里直接使用 fetch 上传到 GCS (通过代理),不经过 tacore-server
|
|
252
|
+
// 后端已经将 signedUrl 替换为代理域名,直接使用即可
|
|
253
|
+
try {
|
|
254
|
+
const uploadResponse = await fetch(signedUrl, {
|
|
255
|
+
method: "PUT",
|
|
256
|
+
headers: {
|
|
257
|
+
"Content-Type": contentType,
|
|
258
|
+
"x-target-hostname": "storage.googleapis.com",
|
|
259
|
+
},
|
|
260
|
+
body: fileBody,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!uploadResponse.ok) {
|
|
264
|
+
const errorText = await uploadResponse.text().catch(() => "No error details");
|
|
265
|
+
console.error("[AppsClient] uploadToGCS - Upload failed:", uploadResponse.status, errorText);
|
|
266
|
+
throw new Error(`Failed to upload to GCS: ${uploadResponse.statusText} (${uploadResponse.status})`);
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error("[AppsClient] uploadToGCS - Network error:", error);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { signedUrl, fileUrl, fileUri, accessUrl };
|
|
274
|
+
},
|
|
275
|
+
};
|