@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,341 @@
1
+ import { isBrowser } from "../../../utils/index.js";
2
+
3
+ export const appsClientTextInMethods = {
4
+ // ==================== TextIn OCR & File Convert ====================
5
+
6
+ /**
7
+ * TextIn OCR 识别
8
+ * @param {File | {data: string, encoding: 'base64', name: string} | string} input - 文件对象、Base64对象或URL字符串
9
+ * @param {Object} options - TextIn 识别参数 (如 pdf_page_count, page_start 等)
10
+ * @returns {Promise<Object>} 识别结果
11
+ */
12
+ async recognizeText(input, options = {}) {
13
+ if (!input) throw new Error("input (file or url) is required");
14
+
15
+ // 构建查询参数
16
+ const queryParams = new URLSearchParams();
17
+ for (const [key, value] of Object.entries(options)) {
18
+ if (value !== undefined && value !== null) {
19
+ queryParams.append(key, value.toString());
20
+ }
21
+ }
22
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
23
+ const url = `${this.config.apiBaseUrl}/apps/textIn/recognize${queryString}`;
24
+
25
+ // 判断输入类型
26
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
27
+ // URL 模式
28
+ return this._post(`/apps/textIn/recognize${queryString}`, { url: input });
29
+ }
30
+ // 文件模式 (FormData)
31
+ const form = new FormData();
32
+ form.append("appId", this.appId);
33
+
34
+ if (isBrowser) {
35
+ if (!(input instanceof File)) {
36
+ throw new Error("Invalid file input in browser. Provide a File object or URL string.");
37
+ }
38
+ form.append("file", input, input.name);
39
+ } else {
40
+ // isBackend
41
+ if (typeof input === "object" && input.data && input.name && input.encoding === "base64") {
42
+ const buffer = Buffer.from(input.data, "base64");
43
+ const fileBlob = new Blob([buffer]);
44
+ form.append("file", fileBlob, input.name);
45
+ } else {
46
+ throw new Error(
47
+ "Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or URL string."
48
+ );
49
+ }
50
+ }
51
+
52
+ const headers = this._getRequestHeaders();
53
+ delete headers["Content-Type"]; // Let fetch set Content-Type for multipart/form-data
54
+
55
+ const resp = await fetch(url, {
56
+ method: "POST",
57
+ headers,
58
+ body: form,
59
+ });
60
+
61
+ if (!resp.ok) {
62
+ const err = await resp.json().catch(() => ({}));
63
+ throw new Error(err.message || err.error || `HTTP ${resp.status}: ${resp.statusText}`);
64
+ }
65
+ return resp.json();
66
+ },
67
+
68
+ /**
69
+ * Word 转 PDF
70
+ * @param {File | {data: string, encoding: 'base64', name: string} | string} input - 文件对象、Base64对象或URL字符串
71
+ * @returns {Promise<{success: boolean, data: {url: string}}>} 转换结果,包含文件下载链接
72
+ */
73
+ async wordToPdf(input) {
74
+ if (!input) throw new Error("input (file or url) is required");
75
+
76
+ const url = `${this.config.apiBaseUrl}/apps/textIn/word-to-pdf`;
77
+
78
+ // 判断输入类型
79
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
80
+ // URL 模式
81
+ return this._post("/apps/textIn/word-to-pdf", { url: input });
82
+ }
83
+ // 文件模式 (FormData)
84
+ const form = new FormData();
85
+ form.append("appId", this.appId);
86
+
87
+ if (isBrowser) {
88
+ if (!(input instanceof File)) {
89
+ throw new Error("Invalid file input in browser. Provide a File object or URL string.");
90
+ }
91
+ form.append("file", input, input.name);
92
+ } else {
93
+ // isBackend
94
+ if (typeof input === "object" && input.data && input.name && input.encoding === "base64") {
95
+ const buffer = Buffer.from(input.data, "base64");
96
+ const fileBlob = new Blob([buffer]);
97
+ form.append("file", fileBlob, input.name);
98
+ } else {
99
+ throw new Error(
100
+ "Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or URL string."
101
+ );
102
+ }
103
+ }
104
+
105
+ const headers = this._getRequestHeaders();
106
+ delete headers["Content-Type"]; // Let fetch set Content-Type for multipart/form-data
107
+
108
+ const resp = await fetch(url, {
109
+ method: "POST",
110
+ headers,
111
+ body: form,
112
+ });
113
+
114
+ if (!resp.ok) {
115
+ const err = await resp.json().catch(() => ({}));
116
+ throw new Error(err.message || err.error || `HTTP ${resp.status}: ${resp.statusText}`);
117
+ }
118
+ return resp.json();
119
+ },
120
+
121
+ /**
122
+ * PDF 转 Word
123
+ * @param {File | {data: string, encoding: 'base64', name: string} | string} input - 文件对象、Base64对象或URL字符串
124
+ * @returns {Promise<{success: boolean, data: {url: string}}>} 转换结果,包含文件下载链接
125
+ */
126
+ async pdfToWord(input) {
127
+ if (!input) throw new Error("input (file or url) is required");
128
+
129
+ const url = `${this.config.apiBaseUrl}/apps/textIn/pdf-to-word`;
130
+
131
+ // 判断输入类型
132
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
133
+ // URL 模式
134
+ return this._post("/apps/textIn/pdf-to-word", { url: input });
135
+ }
136
+ // 文件模式 (FormData)
137
+ const form = new FormData();
138
+ form.append("appId", this.appId);
139
+
140
+ if (isBrowser) {
141
+ if (!(input instanceof File)) {
142
+ throw new Error("Invalid file input in browser. Provide a File object or URL string.");
143
+ }
144
+ form.append("file", input, input.name);
145
+ } else {
146
+ // isBackend
147
+ if (typeof input === "object" && input.data && input.name && input.encoding === "base64") {
148
+ const buffer = Buffer.from(input.data, "base64");
149
+ const fileBlob = new Blob([buffer]);
150
+ form.append("file", fileBlob, input.name);
151
+ } else {
152
+ throw new Error(
153
+ "Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or URL string."
154
+ );
155
+ }
156
+ }
157
+
158
+ const headers = this._getRequestHeaders();
159
+ delete headers["Content-Type"]; // Let fetch set Content-Type for multipart/form-data
160
+
161
+ const resp = await fetch(url, {
162
+ method: "POST",
163
+ headers,
164
+ body: form,
165
+ });
166
+
167
+ if (!resp.ok) {
168
+ const err = await resp.json().catch(() => ({}));
169
+ throw new Error(err.message || err.error || `HTTP ${resp.status}: ${resp.statusText}`);
170
+ }
171
+ return resp.json();
172
+ },
173
+
174
+ /**
175
+ * Excel 转 PDF
176
+ * @param {File | {data: string, encoding: 'base64', name: string} | string} input - 文件对象、Base64对象或URL字符串
177
+ * @returns {Promise<{success: boolean, data: {url: string}}>} 转换结果,包含文件下载链接
178
+ */
179
+ async excelToPdf(input) {
180
+ if (!input) throw new Error("input (file or url) is required");
181
+
182
+ const url = `${this.config.apiBaseUrl}/apps/textIn/excel-to-pdf`;
183
+
184
+ // 判断输入类型
185
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
186
+ // URL 模式
187
+ return this._post("/apps/textIn/excel-to-pdf", { url: input });
188
+ }
189
+ // 文件模式 (FormData)
190
+ const form = new FormData();
191
+ form.append("appId", this.appId);
192
+
193
+ if (isBrowser) {
194
+ if (!(input instanceof File)) {
195
+ throw new Error("Invalid file input in browser. Provide a File object or URL string.");
196
+ }
197
+ form.append("file", input, input.name);
198
+ } else {
199
+ // isBackend
200
+ if (typeof input === "object" && input.data && input.name && input.encoding === "base64") {
201
+ const buffer = Buffer.from(input.data, "base64");
202
+ const fileBlob = new Blob([buffer]);
203
+ form.append("file", fileBlob, input.name);
204
+ } else {
205
+ throw new Error(
206
+ "Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or URL string."
207
+ );
208
+ }
209
+ }
210
+
211
+ const headers = this._getRequestHeaders();
212
+ delete headers["Content-Type"]; // Let fetch set Content-Type for multipart/form-data
213
+
214
+ const resp = await fetch(url, {
215
+ method: "POST",
216
+ headers,
217
+ body: form,
218
+ });
219
+
220
+ if (!resp.ok) {
221
+ const err = await resp.json().catch(() => ({}));
222
+ throw new Error(err.message || err.error || `HTTP ${resp.status}: ${resp.statusText}`);
223
+ }
224
+ return resp.json();
225
+ },
226
+
227
+ /**
228
+ * PDF 转 Excel
229
+ * @param {File | {data: string, encoding: 'base64', name: string} | string} input - 文件对象、Base64对象或URL字符串
230
+ * @returns {Promise<{success: boolean, data: {url: string}}>} 转换结果,包含文件下载链接
231
+ */
232
+ async pdfToExcel(input) {
233
+ if (!input) throw new Error("input (file or url) is required");
234
+
235
+ const url = `${this.config.apiBaseUrl}/apps/textIn/pdf-to-excel`;
236
+
237
+ // 判断输入类型
238
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
239
+ // URL 模式
240
+ return this._post("/apps/textIn/pdf-to-excel", { url: input });
241
+ }
242
+ // 文件模式 (FormData)
243
+ const form = new FormData();
244
+ form.append("appId", this.appId);
245
+
246
+ if (isBrowser) {
247
+ if (!(input instanceof File)) {
248
+ throw new Error("Invalid file input in browser. Provide a File object or URL string.");
249
+ }
250
+ form.append("file", input, input.name);
251
+ } else {
252
+ // isBackend
253
+ if (typeof input === "object" && input.data && input.name && input.encoding === "base64") {
254
+ const buffer = Buffer.from(input.data, "base64");
255
+ const fileBlob = new Blob([buffer]);
256
+ form.append("file", fileBlob, input.name);
257
+ } else {
258
+ throw new Error(
259
+ "Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or URL string."
260
+ );
261
+ }
262
+ }
263
+
264
+ const headers = this._getRequestHeaders();
265
+ delete headers["Content-Type"]; // Let fetch set Content-Type for multipart/form-data
266
+
267
+ const resp = await fetch(url, {
268
+ method: "POST",
269
+ headers,
270
+ body: form,
271
+ });
272
+
273
+ if (!resp.ok) {
274
+ const err = await resp.json().catch(() => ({}));
275
+ throw new Error(err.message || err.error || `HTTP ${resp.status}: ${resp.statusText}`);
276
+ }
277
+ return resp.json();
278
+ },
279
+
280
+ /**
281
+ * 通用文件转 Markdown (支持 PDF, Word, Excel, PPT, 图片等)
282
+ * @param {File | {data: string, encoding: 'base64', name: string} | string} input - 文件对象、Base64对象或URL字符串
283
+ * @param {Object} options - 转换选项 (如 page_start, page_count 等)
284
+ * @returns {Promise<{success: boolean, data: {url: string}}>} 转换结果,包含 Markdown 文件下载链接
285
+ */
286
+ async fileToMarkdown(input, options = {}) {
287
+ if (!input) throw new Error("input (file or url) is required");
288
+
289
+ // 构建查询参数
290
+ const queryParams = new URLSearchParams();
291
+ for (const [key, value] of Object.entries(options)) {
292
+ if (value !== undefined && value !== null) {
293
+ queryParams.append(key, value.toString());
294
+ }
295
+ }
296
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
297
+ const url = `${this.config.apiBaseUrl}/apps/textIn/file-to-markdown${queryString}`;
298
+
299
+ // 判断输入类型
300
+ if (typeof input === "string" && (input.startsWith("http://") || input.startsWith("https://"))) {
301
+ // URL 模式
302
+ return this._post(`/apps/textIn/file-to-markdown${queryString}`, { url: input });
303
+ }
304
+ // 文件模式 (FormData)
305
+ const form = new FormData();
306
+ form.append("appId", this.appId);
307
+
308
+ if (isBrowser) {
309
+ if (!(input instanceof File)) {
310
+ throw new Error("Invalid file input in browser. Provide a File object or URL string.");
311
+ }
312
+ form.append("file", input, input.name);
313
+ } else {
314
+ // isBackend
315
+ if (typeof input === "object" && input.data && input.name && input.encoding === "base64") {
316
+ const buffer = Buffer.from(input.data, "base64");
317
+ const fileBlob = new Blob([buffer]);
318
+ form.append("file", fileBlob, input.name);
319
+ } else {
320
+ throw new Error(
321
+ "Invalid file input in Node.js. Provide { data: base64_string, encoding: 'base64', name: string } or URL string."
322
+ );
323
+ }
324
+ }
325
+
326
+ const headers = this._getRequestHeaders();
327
+ delete headers["Content-Type"]; // Let fetch set Content-Type for multipart/form-data
328
+
329
+ const resp = await fetch(url, {
330
+ method: "POST",
331
+ headers,
332
+ body: form,
333
+ });
334
+
335
+ if (!resp.ok) {
336
+ const err = await resp.json().catch(() => ({}));
337
+ throw new Error(err.message || err.error || `HTTP ${resp.status}: ${resp.statusText}`);
338
+ }
339
+ return resp.json();
340
+ },
341
+ };
@@ -0,0 +1,36 @@
1
+ export const appsClientToolsMethods = {
2
+ // ==================== PDF 水印 ====================
3
+
4
+ /**
5
+ * 给 PDF 添加水印
6
+ * @param {object} options
7
+ * @param {string} options.url - PDF 文件的 URL
8
+ * @param {string} options.text - 水印文案
9
+ * @param {object} [options.style] - 水印样式 { size, opacity, color, rotate, fontUrl }
10
+ * @returns {Promise<{success: boolean, data: {url: string, fileId: string, size: number}}>}
11
+ */
12
+ async addWatermark(options = {}) {
13
+ if (!options.url || !options.text) {
14
+ throw new Error("url and text are required for addWatermark.");
15
+ }
16
+ return this._post("/apps/watermark/process", {
17
+ appId: this.appId,
18
+ ...options,
19
+ });
20
+ },
21
+
22
+ // ==================== 敏感信息检测 ====================
23
+
24
+ /**
25
+ * 检测内容中的敏感信息
26
+ * @param {string} content - 待检测的内容(文本字符串、图片URL或文档URL)
27
+ * @returns {Promise<{isSensitive: boolean, result: string}>}
28
+ */
29
+ async detectSensitiveInfo(content) {
30
+ if (!content) {
31
+ throw new Error("content is required for sensitive detection.");
32
+ }
33
+ const result = await this._post("/apps/sensitive/detect", { content });
34
+ return result.data;
35
+ },
36
+ };
@@ -0,0 +1,49 @@
1
+ export const appsClientUserMethods = {
2
+ // ==================== 新增:应用用户与角色管理 ====================
3
+
4
+ /**
5
+ * 获取当前 App 下的用户列表(含 appRole)
6
+ * @param {Object} params { page?: number, pageSize?: number }
7
+ * @returns {Promise<{ success: boolean, data: Array, pagination: Object }>}
8
+ */
9
+ async listAppUsers(params = {}) {
10
+ const { page = 1, pageSize = 20 } = params || {};
11
+ return this._get("/apps/users/list", {
12
+ appId: this.appId,
13
+ page,
14
+ pageSize,
15
+ });
16
+ },
17
+
18
+ /**
19
+ * [新增] 为当前应用创建一个新的自定义角色
20
+ * @param {Object} params { roleName: string, description?: string }
21
+ * @returns {Promise<{success: boolean, data: {name: string, description: string}}>}
22
+ */
23
+ async createRole({ roleName, description }) {
24
+ if (!roleName) {
25
+ throw new Error("roleName is required.");
26
+ }
27
+ return this._post("/apps/roles/create", {
28
+ appId: this.appId,
29
+ roleName,
30
+ description,
31
+ });
32
+ },
33
+
34
+ /**
35
+ * [新增] 更新或设置一个用户在当前应用中的角色
36
+ * @param {Object} params { userId: string, role: string }
37
+ * @returns {Promise<{success: boolean, data: {userId: string, appId: string, role: string}}>}
38
+ */
39
+ async updateUserRole({ userId, role }) {
40
+ if (!userId || !role) {
41
+ throw new Error("userId and role are required.");
42
+ }
43
+ return this._post("/apps/users/update-role", {
44
+ appId: this.appId,
45
+ targetUserId: userId,
46
+ role: role,
47
+ });
48
+ },
49
+ };
@@ -0,0 +1,106 @@
1
+ export const appsClientVoiceVideoMethods = {
2
+ // ==================== 新增:语音生成 ====================
3
+
4
+ /**
5
+ * 生成语音文件和字幕(非流式)
6
+ * @param {object} options - { text, voiceId?, speed?, vol?, pitch?, projectId? }
7
+ * @returns {Promise<{audioUrl: string, fileID: string, subtitles: object, duration: number, cost: number}>}
8
+ */
9
+ async generateVoice(options = {}) {
10
+ if (!options.text) {
11
+ throw new Error("text is required for voice generation.");
12
+ }
13
+ const result = await this._post("/apps/voice/generate", {
14
+ appId: this.appId,
15
+ ...options,
16
+ });
17
+ return result.data;
18
+ },
19
+
20
+ /**
21
+ * 获取语音流式响应(流式)
22
+ * @param {object} options - { text, voiceId?, speed?, vol?, pitch?, projectId? }
23
+ * @returns {Promise<Response>} - Fetch API 的 Response 对象,其 body 是一个 ReadableStream。
24
+ */
25
+ async streamVoice(options = {}) {
26
+ if (!options.text) {
27
+ throw new Error("text is required for voice streaming.");
28
+ }
29
+ const url = `${this.config.apiBaseUrl}/apps/voice/stream`;
30
+ const headers = this._getRequestHeaders();
31
+
32
+ const response = await fetch(url, {
33
+ method: "POST",
34
+ headers,
35
+ body: JSON.stringify({
36
+ appId: this.appId,
37
+ ...options,
38
+ }),
39
+ });
40
+
41
+ if (!response.ok) {
42
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
43
+ throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
44
+ }
45
+
46
+ return response; // 直接返回 Response 对象
47
+ },
48
+
49
+ // ==================== 新增:Sora 视频生成 ====================
50
+
51
+ /**
52
+ * 创建一个 Sora 视频生成任务
53
+ * @param {object} options - { prompt, model, size, seconds }
54
+ * @returns {Promise<object>} 视频任务文档
55
+ */
56
+ async createVideoJob(options = {}) {
57
+ if (!options.prompt || !options.model) {
58
+ throw new Error("prompt and model are required for video generation.");
59
+ }
60
+ // The middleware uses appId from the body to find the organization
61
+ return this._post("/apps/sora/videos", {
62
+ appId: this.appId,
63
+ ...options,
64
+ });
65
+ },
66
+
67
+ /**
68
+ * 基于现有视频创建一个 Remix 任务
69
+ * @param {string} jobId - 原始视频任务的 wyID
70
+ * @param {object} options - { prompt }
71
+ * @returns {Promise<object>} 新的 Remix 任务初始状态的文档
72
+ */
73
+ async remixVideoJob(jobId, options = {}) {
74
+ if (!jobId) {
75
+ throw new Error("jobId of the original video is required for remixing.");
76
+ }
77
+ if (!options.prompt) {
78
+ throw new Error("A new prompt is required for remixing.");
79
+ }
80
+ return this._post(`/apps/sora/videos/${jobId}/remix`, {
81
+ appId: this.appId,
82
+ prompt: options.prompt,
83
+ });
84
+ },
85
+
86
+ /**
87
+ * 查询视频生成任务的状态
88
+ * @param {string} jobId - 任务的 wyID
89
+ * @returns {Promise<object>} 视频任务文档
90
+ */
91
+ async getVideoJob(jobId) {
92
+ if (!jobId) {
93
+ throw new Error("jobId is required.");
94
+ }
95
+ return this._get(`/apps/sora/videos/${jobId}`, { appId: this.appId });
96
+ },
97
+
98
+ /**
99
+ * 获取视频任务列表
100
+ * @param {object} [options] - { page = 1, pageSize = 10 }
101
+ * @returns {Promise<object>} 任务列表和分页信息
102
+ */
103
+ async listVideoJobs(options = {}) {
104
+ return this._get("/apps/sora/videos", { appId: this.appId, ...options });
105
+ },
106
+ };
@@ -0,0 +1,169 @@
1
+ /**
2
+ * 火山引擎实时语音对话客户端实现
3
+ * 内部类,直接集成在 AppsClient 中
4
+ */
5
+ class VolcengineImpl {
6
+ /**
7
+ * @param {AppsClient} appsClient - AppsClient 实例
8
+ */
9
+ constructor(appsClient) {
10
+ this.appsClient = appsClient;
11
+ this.ws = null;
12
+ this.sessionId = null;
13
+ this.listeners = {
14
+ asr: [],
15
+ content: [], // AI 回复的文本
16
+ audio: [], // TTS 音频数据 (Uint8Array)
17
+ sys_error: [],
18
+ close: [],
19
+ json: [], // 原始 JSON 事件
20
+ };
21
+ }
22
+
23
+ /**
24
+ * 注册事件监听器
25
+ * @param {'asr'|'content'|'audio'|'sys_error'|'close'|'json'} event
26
+ * @param {Function} callback
27
+ */
28
+ on(event, callback) {
29
+ if (this.listeners[event]) {
30
+ this.listeners[event].push(callback);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 移除事件监听器
36
+ * @param {'asr'|'content'|'audio'|'sys_error'|'close'|'json'} event
37
+ * @param {Function} callback
38
+ */
39
+ off(event, callback) {
40
+ if (this.listeners[event]) {
41
+ this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
42
+ }
43
+ }
44
+
45
+ _emit(event, data) {
46
+ if (this.listeners[event]) {
47
+ this.listeners[event].forEach(cb => {
48
+ try {
49
+ cb(data);
50
+ } catch (e) {
51
+ console.error(`[VolcengineClient] Error in ${event} listener:`, e);
52
+ }
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 开启会话
59
+ * 1. 建立 WebSocket 连接
60
+ * 2. 发送 start 指令
61
+ *
62
+ * @param {Object} options - 配置项
63
+ * @param {string} [options.dataStoreId] - 知识库 ID (用于 RAG)
64
+ * @param {string} [options.botName] - 机器人名称 (如 "客服小助手")
65
+ * @param {string} [options.systemRole] - 系统角色设定 (如 "你是一个专业的客服...")
66
+ * @param {string} [options.speakingStyle] - 说话风格 (如 "亲切、温柔")
67
+ */
68
+ async start(options = {}) {
69
+ // 1. 建立 WebSocket 连接
70
+ let baseUrl = this.appsClient.config.apiBaseUrl.replace(/^http/, "ws"); // http->ws, https->wss
71
+
72
+ // [Removed] 本地开发环境特殊处理已移除,现在通过 Vite 代理 (ws: true) 统一转发
73
+
74
+ const token = this.appsClient._getAccessToken();
75
+ const url = `${baseUrl}/apps/volcengine/connect?access_token=${token || ""}`;
76
+
77
+ this.ws = new WebSocket(url);
78
+ this.ws.binaryType = "arraybuffer"; // 关键:接收二进制音频
79
+
80
+ return new Promise((resolve, reject) => {
81
+ this.ws.onopen = () => {
82
+ console.log("[VolcengineClient] WebSocket Connected");
83
+ // 2. 连接成功后,发送 start 指令
84
+ this.ws.send(JSON.stringify({
85
+ type: "start",
86
+ ...options,
87
+ }));
88
+ };
89
+
90
+ this.ws.onmessage = event => {
91
+ const data = event.data;
92
+
93
+ // === 处理二进制音频 (TTS) ===
94
+ if (data instanceof ArrayBuffer) {
95
+ const audioData = new Uint8Array(data);
96
+ this._emit("audio", audioData);
97
+ return;
98
+ }
99
+
100
+ // === 处理 JSON 消息 ===
101
+ try {
102
+ const msg = JSON.parse(data);
103
+
104
+ if (msg.type === "started") {
105
+ this.sessionId = msg.sessionId;
106
+ resolve(msg);
107
+ } else if (msg.type === "json") {
108
+ this._emit("json", msg);
109
+ } else if (msg.type === "error") {
110
+ this._emit("sys_error", msg);
111
+ reject(new Error(msg.message || "Unknown error"));
112
+ } else if (msg.type === "close") {
113
+ this._emit("close", msg);
114
+ }
115
+
116
+ // === 业务事件分发 ===
117
+ if (msg.eventId === 451 && msg.data?.results) {
118
+ const asrText = msg.data.results[0]?.text;
119
+ if (asrText) this._emit("asr", { text: asrText });
120
+ }
121
+ if (msg.eventId === 550 && msg.data?.content) {
122
+ this._emit("content", { text: msg.data.content });
123
+ }
124
+ } catch (err) {
125
+ console.warn("[VolcengineClient] Failed to parse message:", data);
126
+ }
127
+ };
128
+
129
+ this.ws.onerror = err => {
130
+ console.error("[VolcengineClient] WebSocket error:", err);
131
+ reject(err);
132
+ };
133
+
134
+ this.ws.onclose = () => {
135
+ console.log("[VolcengineClient] WebSocket Closed");
136
+ this._emit("close", { reason: "closed" });
137
+ };
138
+ });
139
+ }
140
+
141
+ /**
142
+ * 发送音频数据 (PCM)
143
+ * @param {ArrayBuffer|Uint8Array} audioData - 16k 16bit PCM 音频数据
144
+ */
145
+ async sendAudio(audioData) {
146
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
147
+ // 直接发送二进制数据
148
+ this.ws.send(audioData);
149
+ } else {
150
+ console.warn("[VolcengineClient] Cannot send audio: WebSocket not open");
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 结束会话
156
+ */
157
+ async stop() {
158
+ if (this.ws) {
159
+ if (this.ws.readyState === WebSocket.OPEN) {
160
+ this.ws.send(JSON.stringify({ type: "stop" }));
161
+ this.ws.close();
162
+ }
163
+ this.ws = null;
164
+ this.sessionId = null;
165
+ }
166
+ }
167
+ }
168
+
169
+ export { VolcengineImpl };