@huyooo/ai-chat-bridge-electron 0.2.14 → 0.2.16

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.
@@ -1,843 +1 @@
1
- // src/main/index.ts
2
- import { ipcMain as ipcMain2, shell } from "electron";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- import {
6
- HybridAgent,
7
- MODELS,
8
- DEFAULT_MODEL
9
- } from "@huyooo/ai-chat-core";
10
- import { createSearchElectronBridge } from "@huyooo/ai-search/bridge/electron";
11
- import {
12
- createStorage
13
- } from "@huyooo/ai-chat-storage";
14
-
15
- // src/main/asr/protocol.ts
16
- import * as zlib from "zlib";
17
- var PROTOCOL_VERSION = 1;
18
- var HEADER_SIZE = 1;
19
- var MessageType = {
20
- /** 客户端发送:包含请求参数的完整请求 */
21
- FULL_CLIENT_REQUEST: 1,
22
- /** 客户端发送:仅包含音频数据 */
23
- AUDIO_ONLY_REQUEST: 2,
24
- /** 服务端响应:包含识别结果 */
25
- FULL_SERVER_RESPONSE: 9,
26
- /** 服务端响应:错误消息 */
27
- ERROR_RESPONSE: 15
28
- };
29
- var MessageFlags = {
30
- /** 无特殊标志 */
31
- NONE: 0,
32
- /** header 后 4 字节为正 sequence number */
33
- HAS_SEQUENCE: 1,
34
- /** 最后一包(负包) */
35
- LAST_PACKET: 2,
36
- /** 最后一包且有 sequence number */
37
- LAST_PACKET_WITH_SEQUENCE: 3
38
- };
39
- var SerializationMethod = {
40
- NONE: 0,
41
- JSON: 1
42
- };
43
- var CompressionMethod = {
44
- NONE: 0,
45
- GZIP: 1
46
- };
47
- function buildHeader(messageType, messageFlags, serialization, compression) {
48
- return [
49
- PROTOCOL_VERSION << 4 | HEADER_SIZE,
50
- messageType << 4 | messageFlags,
51
- serialization << 4 | compression,
52
- 0
53
- // reserved
54
- ];
55
- }
56
- function encodeFullClientRequest(config, useGzip = true) {
57
- const header = buildHeader(
58
- MessageType.FULL_CLIENT_REQUEST,
59
- MessageFlags.NONE,
60
- SerializationMethod.JSON,
61
- useGzip ? CompressionMethod.GZIP : CompressionMethod.NONE
62
- );
63
- const jsonPayload = JSON.stringify(config);
64
- const payloadBuffer = Buffer.from(jsonPayload, "utf-8");
65
- const compressed = useGzip ? zlib.gzipSync(payloadBuffer) : payloadBuffer;
66
- const payloadSize = Buffer.alloc(4);
67
- payloadSize.writeUInt32BE(compressed.length, 0);
68
- return Buffer.concat([
69
- Buffer.from(header),
70
- payloadSize,
71
- compressed
72
- ]);
73
- }
74
- function encodeAudioOnlyRequest(audioData, isLast = false, useGzip = true) {
75
- const header = buildHeader(
76
- MessageType.AUDIO_ONLY_REQUEST,
77
- isLast ? MessageFlags.LAST_PACKET : MessageFlags.NONE,
78
- SerializationMethod.NONE,
79
- useGzip ? CompressionMethod.GZIP : CompressionMethod.NONE
80
- );
81
- const compressed = useGzip ? zlib.gzipSync(audioData) : audioData;
82
- const payloadSize = Buffer.alloc(4);
83
- payloadSize.writeUInt32BE(compressed.length, 0);
84
- return Buffer.concat([
85
- Buffer.from(header),
86
- payloadSize,
87
- compressed
88
- ]);
89
- }
90
- function decodeServerResponse(data) {
91
- if (data.length < 4) {
92
- throw new Error("Invalid response: too short");
93
- }
94
- const byte0 = data[0];
95
- const byte1 = data[1];
96
- const byte2 = data[2];
97
- const version = byte0 >> 4 & 15;
98
- const headerSize = (byte0 & 15) * 4;
99
- const messageType = byte1 >> 4 & 15;
100
- const messageFlags = byte1 & 15;
101
- const serialization = byte2 >> 4 & 15;
102
- const compression = byte2 & 15;
103
- console.log("[ASR Protocol] \u89E3\u6790\u54CD\u5E94:", {
104
- version,
105
- headerSize,
106
- messageType: messageType.toString(2).padStart(4, "0"),
107
- messageFlags: messageFlags.toString(2).padStart(4, "0"),
108
- serialization,
109
- compression,
110
- dataLength: data.length,
111
- headerHex: data.slice(0, 4).toString("hex")
112
- });
113
- if (version !== PROTOCOL_VERSION) {
114
- throw new Error(`Unsupported protocol version: ${version}`);
115
- }
116
- let offset = headerSize;
117
- let sequence;
118
- const hasSequence = (messageFlags & 1) === 1;
119
- if (hasSequence && data.length >= offset + 4) {
120
- sequence = data.readUInt32BE(offset);
121
- offset += 4;
122
- console.log("[ASR Protocol] sequence:", sequence);
123
- }
124
- const isLast = (messageFlags & 2) === 2;
125
- if (messageType === MessageType.ERROR_RESPONSE) {
126
- const errorCode = data.readUInt32BE(offset);
127
- offset += 4;
128
- const errorMsgSize = data.readUInt32BE(offset);
129
- offset += 4;
130
- const errorMsg = data.slice(offset, offset + errorMsgSize).toString("utf-8");
131
- console.log("[ASR Protocol] \u9519\u8BEF\u54CD\u5E94:", { errorCode, errorMsg });
132
- return {
133
- type: "error",
134
- sequence,
135
- isLast: true,
136
- data: { code: errorCode, message: errorMsg }
137
- };
138
- }
139
- if (messageType === MessageType.FULL_SERVER_RESPONSE) {
140
- const payloadSize = data.readUInt32BE(offset);
141
- offset += 4;
142
- console.log("[ASR Protocol] payloadSize:", payloadSize, "offset:", offset, "remaining:", data.length - offset);
143
- if (payloadSize === 0) {
144
- return {
145
- type: "result",
146
- sequence,
147
- isLast,
148
- data: {}
149
- };
150
- }
151
- let payload = data.slice(offset, offset + payloadSize);
152
- if (compression === CompressionMethod.GZIP) {
153
- try {
154
- payload = zlib.gunzipSync(payload);
155
- } catch (e) {
156
- console.error("[ASR Protocol] gzip \u89E3\u538B\u5931\u8D25:", e);
157
- console.log("[ASR Protocol] \u539F\u59CB payload \u524D 32 \u5B57\u8282:", payload.slice(0, 32).toString("hex"));
158
- throw e;
159
- }
160
- }
161
- console.log("[ASR Protocol] \u89E3\u538B\u540E payload \u957F\u5EA6:", payload.length);
162
- let result;
163
- if (serialization === SerializationMethod.JSON) {
164
- const jsonStr = payload.toString("utf-8");
165
- console.log("[ASR Protocol] JSON \u5B57\u7B26\u4E32\u524D 200 \u5B57\u7B26:", jsonStr.slice(0, 200));
166
- try {
167
- result = JSON.parse(jsonStr);
168
- } catch (e) {
169
- console.error("[ASR Protocol] JSON \u89E3\u6790\u5931\u8D25:", e);
170
- console.log("[ASR Protocol] \u5B8C\u6574 JSON:", jsonStr);
171
- throw e;
172
- }
173
- } else {
174
- result = {};
175
- }
176
- return {
177
- type: "result",
178
- sequence,
179
- isLast,
180
- data: result
181
- };
182
- }
183
- throw new Error(`Unknown message type: ${messageType}`);
184
- }
185
-
186
- // src/main/asr/client.ts
187
- import WebSocket from "ws";
188
- import { v4 as uuidv4 } from "uuid";
189
- var AsrClient = class {
190
- config;
191
- ws = null;
192
- callbacks = {};
193
- connectId = "";
194
- isConnected = false;
195
- sessionConfig = {};
196
- constructor(config) {
197
- this.config = config;
198
- }
199
- /**
200
- * 获取 WebSocket 连接地址
201
- */
202
- getWsUrl() {
203
- if (this.config.useAsyncMode !== false) {
204
- return "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async";
205
- }
206
- return "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel";
207
- }
208
- /**
209
- * 获取资源 ID
210
- */
211
- getResourceId() {
212
- return this.config.resourceId || "volc.bigasr.sauc.duration";
213
- }
214
- /**
215
- * 连接到 ASR 服务
216
- */
217
- connect(callbacks, sessionConfig = {}) {
218
- return new Promise((resolve2, reject) => {
219
- this.callbacks = callbacks;
220
- this.sessionConfig = sessionConfig;
221
- this.connectId = uuidv4();
222
- const wsUrl = this.getWsUrl();
223
- this.ws = new WebSocket(wsUrl, {
224
- headers: {
225
- "X-Api-App-Key": this.config.appId,
226
- "X-Api-Access-Key": this.config.accessKey,
227
- "X-Api-Resource-Id": this.getResourceId(),
228
- "X-Api-Connect-Id": this.connectId
229
- }
230
- });
231
- this.ws.on("open", () => {
232
- console.log("[ASR] WebSocket \u8FDE\u63A5\u6210\u529F, connectId:", this.connectId);
233
- this.isConnected = true;
234
- this.sendFullClientRequest();
235
- this.callbacks.onConnected?.();
236
- resolve2();
237
- });
238
- this.ws.on("message", (data) => {
239
- try {
240
- const response = decodeServerResponse(data);
241
- if (response.type === "error") {
242
- const errorData = response.data;
243
- console.error("[ASR] \u670D\u52A1\u7AEF\u9519\u8BEF:", errorData);
244
- this.callbacks.onError?.(new Error(`ASR Error ${errorData.code}: ${errorData.message}`));
245
- } else {
246
- const result = response.data;
247
- this.callbacks.onResult?.(result, response.isLast);
248
- if (response.isLast) {
249
- console.log("[ASR] \u6536\u5230\u6700\u7EC8\u7ED3\u679C");
250
- }
251
- }
252
- } catch (error) {
253
- console.error("[ASR] \u89E3\u6790\u54CD\u5E94\u5931\u8D25:", error);
254
- this.callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
255
- }
256
- });
257
- this.ws.on("error", (error) => {
258
- console.error("[ASR] WebSocket \u9519\u8BEF:", error);
259
- this.callbacks.onError?.(error);
260
- reject(error);
261
- });
262
- this.ws.on("close", () => {
263
- console.log("[ASR] WebSocket \u8FDE\u63A5\u5173\u95ED");
264
- this.isConnected = false;
265
- this.ws = null;
266
- this.callbacks.onClose?.();
267
- });
268
- });
269
- }
270
- /**
271
- * 发送首包(Full Client Request)
272
- */
273
- sendFullClientRequest() {
274
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
275
- console.error("[ASR] WebSocket \u672A\u8FDE\u63A5");
276
- return;
277
- }
278
- const config = {
279
- user: {
280
- uid: "ai-chat-user"
281
- },
282
- audio: {
283
- format: this.sessionConfig.format || "pcm",
284
- rate: this.sessionConfig.sampleRate || 16e3,
285
- bits: 16,
286
- channel: 1
287
- },
288
- request: {
289
- model_name: "bigmodel",
290
- enable_itn: this.sessionConfig.enableItn ?? true,
291
- enable_punc: this.sessionConfig.enablePunc ?? true,
292
- enable_ddc: this.sessionConfig.enableDdc ?? false,
293
- show_utterances: this.sessionConfig.showUtterances ?? true,
294
- result_type: "full"
295
- }
296
- };
297
- const packet = encodeFullClientRequest(config);
298
- this.ws.send(packet);
299
- console.log("[ASR] \u5DF2\u53D1\u9001\u9996\u5305");
300
- }
301
- /**
302
- * 发送音频数据
303
- * @param audioData PCM 音频数据(16bit, 16kHz, 单声道)
304
- */
305
- sendAudio(audioData) {
306
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
307
- console.error("[ASR] WebSocket \u672A\u8FDE\u63A5\uFF0C\u65E0\u6CD5\u53D1\u9001\u97F3\u9891");
308
- return;
309
- }
310
- const packet = encodeAudioOnlyRequest(audioData, false);
311
- this.ws.send(packet);
312
- }
313
- /**
314
- * 发送最后一包并结束识别
315
- */
316
- finish() {
317
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
318
- console.error("[ASR] WebSocket \u672A\u8FDE\u63A5\uFF0C\u65E0\u6CD5\u7ED3\u675F");
319
- return;
320
- }
321
- const packet = encodeAudioOnlyRequest(Buffer.alloc(0), true);
322
- this.ws.send(packet);
323
- console.log("[ASR] \u5DF2\u53D1\u9001\u7ED3\u675F\u5305");
324
- }
325
- /**
326
- * 断开连接
327
- */
328
- disconnect() {
329
- if (this.ws) {
330
- this.ws.close();
331
- this.ws = null;
332
- }
333
- this.isConnected = false;
334
- }
335
- /**
336
- * 检查是否已连接
337
- */
338
- get connected() {
339
- return this.isConnected && this.ws?.readyState === WebSocket.OPEN;
340
- }
341
- };
342
-
343
- // src/main/asr/index.ts
344
- import { ipcMain } from "electron";
345
- function createAsrBridge(config) {
346
- const { channelPrefix = "ai-chat", appId, accessKey, resourceId } = config;
347
- const sessions = /* @__PURE__ */ new Map();
348
- function getOrCreateSession(webContentsId, webContents) {
349
- let session = sessions.get(webContentsId);
350
- if (!session) {
351
- const clientConfig = {
352
- appId,
353
- accessKey,
354
- resourceId,
355
- useAsyncMode: true
356
- // 使用双向流式优化版
357
- };
358
- const client = new AsrClient(clientConfig);
359
- session = { client, webContents };
360
- sessions.set(webContentsId, session);
361
- webContents.on("destroyed", () => {
362
- const s = sessions.get(webContentsId);
363
- if (s) {
364
- s.client.disconnect();
365
- sessions.delete(webContentsId);
366
- }
367
- });
368
- }
369
- return session;
370
- }
371
- ipcMain.handle(`${channelPrefix}:asr:start`, async (event, sessionConfig) => {
372
- const webContents = event.sender;
373
- const webContentsId = webContents.id;
374
- const session = getOrCreateSession(webContentsId, webContents);
375
- if (session.client.connected) {
376
- session.client.disconnect();
377
- }
378
- try {
379
- await session.client.connect({
380
- onConnected: () => {
381
- if (!webContents.isDestroyed()) {
382
- webContents.send(`${channelPrefix}:asr:connected`);
383
- }
384
- },
385
- onResult: (result, isLast) => {
386
- if (!webContents.isDestroyed()) {
387
- webContents.send(`${channelPrefix}:asr:result`, { result, isLast });
388
- }
389
- },
390
- onError: (error) => {
391
- if (!webContents.isDestroyed()) {
392
- webContents.send(`${channelPrefix}:asr:error`, { message: error.message });
393
- }
394
- },
395
- onClose: () => {
396
- if (!webContents.isDestroyed()) {
397
- webContents.send(`${channelPrefix}:asr:closed`);
398
- }
399
- }
400
- }, sessionConfig);
401
- return { success: true };
402
- } catch (error) {
403
- console.error("[ASR Bridge] \u542F\u52A8\u5931\u8D25:", error);
404
- return {
405
- success: false,
406
- error: error instanceof Error ? error.message : String(error)
407
- };
408
- }
409
- });
410
- ipcMain.handle(`${channelPrefix}:asr:sendAudio`, async (event, audioData) => {
411
- const webContentsId = event.sender.id;
412
- const session = sessions.get(webContentsId);
413
- if (!session || !session.client.connected) {
414
- return { success: false, error: "ASR \u4F1A\u8BDD\u672A\u542F\u52A8" };
415
- }
416
- try {
417
- const payload = audioData instanceof ArrayBuffer ? Buffer.from(audioData) : Buffer.from(new Uint8Array(audioData));
418
- session.client.sendAudio(payload);
419
- return { success: true };
420
- } catch (error) {
421
- console.error("[ASR Bridge] \u53D1\u9001\u97F3\u9891\u5931\u8D25:", error);
422
- return {
423
- success: false,
424
- error: error instanceof Error ? error.message : String(error)
425
- };
426
- }
427
- });
428
- ipcMain.handle(`${channelPrefix}:asr:finish`, async (event) => {
429
- const webContentsId = event.sender.id;
430
- const session = sessions.get(webContentsId);
431
- if (!session || !session.client.connected) {
432
- return { success: false, error: "ASR \u4F1A\u8BDD\u672A\u542F\u52A8" };
433
- }
434
- try {
435
- session.client.finish();
436
- return { success: true };
437
- } catch (error) {
438
- console.error("[ASR Bridge] \u7ED3\u675F\u4F1A\u8BDD\u5931\u8D25:", error);
439
- return {
440
- success: false,
441
- error: error instanceof Error ? error.message : String(error)
442
- };
443
- }
444
- });
445
- ipcMain.handle(`${channelPrefix}:asr:stop`, async (event) => {
446
- const webContentsId = event.sender.id;
447
- const session = sessions.get(webContentsId);
448
- if (session) {
449
- session.client.disconnect();
450
- }
451
- return { success: true };
452
- });
453
- ipcMain.handle(`${channelPrefix}:asr:status`, async (event) => {
454
- const webContentsId = event.sender.id;
455
- const session = sessions.get(webContentsId);
456
- return {
457
- connected: session?.client.connected ?? false
458
- };
459
- });
460
- ipcMain.handle(`${channelPrefix}:asr:warmup`, async (event, sessionConfig) => {
461
- const webContents = event.sender;
462
- const webContentsId = webContents.id;
463
- const session = getOrCreateSession(webContentsId, webContents);
464
- if (session.client.connected) return { success: true };
465
- try {
466
- await session.client.connect(
467
- {
468
- onConnected: () => {
469
- },
470
- onResult: () => {
471
- },
472
- onError: () => {
473
- },
474
- onClose: () => {
475
- }
476
- },
477
- sessionConfig
478
- );
479
- session.client.disconnect();
480
- return { success: true };
481
- } catch (error) {
482
- console.warn("[ASR Bridge] warmup \u5931\u8D25:", error);
483
- return {
484
- success: false,
485
- error: error instanceof Error ? error.message : String(error)
486
- };
487
- }
488
- });
489
- console.log("[ASR Bridge] IPC handlers \u5DF2\u6CE8\u518C");
490
- return {
491
- /** 清理所有会话 */
492
- cleanup: () => {
493
- for (const session of sessions.values()) {
494
- session.client.disconnect();
495
- }
496
- sessions.clear();
497
- }
498
- };
499
- }
500
-
501
- // src/main/index.ts
502
- async function createElectronBridge(options) {
503
- const {
504
- channelPrefix = "ai-chat",
505
- dataDir,
506
- defaultContext = {},
507
- ...agentConfig
508
- } = options;
509
- const resolvedStoragePath = path.join(dataDir, "db.sqlite");
510
- createSearchElectronBridge({
511
- ipcMain: ipcMain2,
512
- channelPrefix
513
- }).init();
514
- const pendingApprovals = /* @__PURE__ */ new Map();
515
- const toolApprovalCallback = async (toolCall) => {
516
- console.log("[Main] \u5DE5\u5177\u6279\u51C6\u56DE\u8C03\u88AB\u8C03\u7528:", toolCall.name);
517
- const currentWebContents = global.currentWebContents;
518
- if (!currentWebContents) {
519
- console.log("[Main] \u8B66\u544A: \u6CA1\u6709 webContents\uFF0C\u9ED8\u8BA4\u6279\u51C6");
520
- return true;
521
- }
522
- return new Promise((resolve2, reject) => {
523
- pendingApprovals.set(toolCall.id, {
524
- resolve: resolve2,
525
- reject,
526
- webContents: currentWebContents
527
- });
528
- console.log("[Main] \u53D1\u9001\u5DE5\u5177\u6279\u51C6\u8BF7\u6C42\u5230\u524D\u7AEF:", toolCall.name);
529
- currentWebContents.send(`${channelPrefix}:toolApprovalRequest`, {
530
- id: toolCall.id,
531
- name: toolCall.name,
532
- args: toolCall.args
533
- });
534
- });
535
- };
536
- const storage = await createStorage({
537
- type: "sqlite",
538
- sqlitePath: resolvedStoragePath
539
- });
540
- const getContext = () => defaultContext;
541
- const getAutoRunConfig = async () => {
542
- try {
543
- const configJson = await storage.getUserSetting("autoRunConfig", getContext());
544
- if (configJson) {
545
- return JSON.parse(configJson);
546
- }
547
- } catch (error) {
548
- console.error("[Main] \u83B7\u53D6 autoRunConfig \u5931\u8D25:", error);
549
- }
550
- return void 0;
551
- };
552
- const agent = new HybridAgent({
553
- ...agentConfig,
554
- // tools 直接传递 ToolConfigItem[],HybridAgent 会在 asyncInit 中解析
555
- tools: options.tools,
556
- onToolApprovalRequest: toolApprovalCallback,
557
- getAutoRunConfig
558
- });
559
- ipcMain2.handle(`${channelPrefix}:models`, () => {
560
- return MODELS;
561
- });
562
- ipcMain2.handle(`${channelPrefix}:getAllTools`, async () => {
563
- console.log("[Main] getAllTools \u8C03\u7528\uFF0C\u5F53\u524D\u5DE5\u5177\u6570\u91CF:", agent["tools"].size);
564
- console.log("[Main] toolConfig \u662F\u5426\u5B58\u5728:", !!agent["toolConfig"]);
565
- console.log("[Main] toolConfig \u5185\u5BB9:", agent["toolConfig"] ? `${agent["toolConfig"].length} \u4E2A\u5DE5\u5177\u914D\u7F6E` : "undefined");
566
- if (agent["tools"].size === 0 && agent["toolConfig"]) {
567
- console.log("[Main] \u5DE5\u5177\u672A\u521D\u59CB\u5316\uFF0C\u5F00\u59CB asyncInit...");
568
- await agent["asyncInit"]();
569
- console.log("[Main] asyncInit \u5B8C\u6210\uFF0C\u5DE5\u5177\u6570\u91CF:", agent["tools"].size);
570
- } else if (!agent["toolConfig"]) {
571
- console.warn("[Main] \u26A0\uFE0F toolConfig \u4E0D\u5B58\u5728\uFF0C\u5DE5\u5177\u53EF\u80FD\u672A\u6CE8\u5165\uFF01");
572
- }
573
- const tools = agent.getAllTools();
574
- console.log("[Main] getAllTools \u8FD4\u56DE:", tools.length, "\u4E2A\u5DE5\u5177", tools.map((t) => t.name));
575
- return tools;
576
- });
577
- ipcMain2.handle(`${channelPrefix}:send`, async (event, params) => {
578
- const webContents = event.sender;
579
- const { message, images, options: options2 = {}, sessionId } = params;
580
- global.currentWebContents = webContents;
581
- console.log("[AI-Chat] \u6536\u5230\u6D88\u606F:", { message, options: options2, images: images?.length || 0, sessionId });
582
- console.log("[AI-Chat] autoRunConfig:", JSON.stringify(options2.autoRunConfig, null, 2));
583
- try {
584
- for await (const chatEvent of agent.chat(message, options2, images)) {
585
- console.log(
586
- "[AI-Chat] \u4E8B\u4EF6:",
587
- chatEvent.type,
588
- chatEvent.type === "text_delta" ? chatEvent.data.content.slice(0, 20) : chatEvent.type === "thinking_delta" ? chatEvent.data.content.slice(0, 20) : chatEvent.type === "search_result" ? `\u641C\u7D22\u5B8C\u6210 ${chatEvent.data.results?.length || 0} \u6761` : JSON.stringify(chatEvent.data).slice(0, 100)
589
- );
590
- if (!webContents.isDestroyed()) {
591
- webContents.send(`${channelPrefix}:progress`, { ...chatEvent, sessionId });
592
- }
593
- }
594
- console.log("[AI-Chat] \u5B8C\u6210");
595
- } catch (error) {
596
- console.error("[AI-Chat] \u9519\u8BEF:", error);
597
- if (error instanceof Error) {
598
- console.error("[AI-Chat] \u9519\u8BEF\u8BE6\u60C5:", {
599
- message: error.message,
600
- stack: error.stack,
601
- name: error.name
602
- });
603
- }
604
- if (!webContents.isDestroyed()) {
605
- const errorData = error instanceof Error ? {
606
- category: "api",
607
- message: error.message || String(error),
608
- cause: error.stack
609
- } : {
610
- category: "api",
611
- message: String(error)
612
- };
613
- webContents.send(`${channelPrefix}:progress`, {
614
- type: "error",
615
- data: errorData,
616
- sessionId
617
- });
618
- }
619
- } finally {
620
- delete global.currentWebContents;
621
- }
622
- });
623
- ipcMain2.handle(`${channelPrefix}:toolApprovalResponse`, (_event, params) => {
624
- const { id, approved } = params;
625
- const pending = pendingApprovals.get(id);
626
- if (pending) {
627
- pendingApprovals.delete(id);
628
- pending.resolve(approved);
629
- }
630
- });
631
- ipcMain2.handle(`${channelPrefix}:cancel`, () => {
632
- agent.abort();
633
- });
634
- ipcMain2.handle(`${channelPrefix}:settings:get`, async (_event, key) => {
635
- return storage.getUserSetting(key, getContext());
636
- });
637
- ipcMain2.handle(`${channelPrefix}:settings:set`, async (_event, key, value) => {
638
- await storage.setUserSetting(key, value, getContext());
639
- return { success: true };
640
- });
641
- ipcMain2.handle(`${channelPrefix}:settings:getAll`, async () => {
642
- return storage.getUserSettings(getContext());
643
- });
644
- ipcMain2.handle(`${channelPrefix}:settings:delete`, async (_event, key) => {
645
- await storage.deleteUserSetting(key, getContext());
646
- return { success: true };
647
- });
648
- ipcMain2.handle(`${channelPrefix}:setCwd`, (_event, dir) => {
649
- agent.setCwd(dir);
650
- });
651
- ipcMain2.handle(`${channelPrefix}:config`, () => {
652
- return agent.getConfig();
653
- });
654
- ipcMain2.handle(`${channelPrefix}:sessions:list`, async () => {
655
- return storage.getSessions(getContext());
656
- });
657
- ipcMain2.handle(`${channelPrefix}:sessions:get`, async (_event, id) => {
658
- return storage.getSession(id, getContext());
659
- });
660
- ipcMain2.handle(`${channelPrefix}:sessions:create`, async (_event, params) => {
661
- const input = {
662
- id: params.id || crypto.randomUUID(),
663
- title: params.title || "\u65B0\u5BF9\u8BDD",
664
- model: params.model || DEFAULT_MODEL,
665
- mode: params.mode || "agent",
666
- webSearchEnabled: params.webSearchEnabled ?? true,
667
- thinkingEnabled: params.thinkingEnabled ?? true,
668
- hidden: params.hidden ?? false
669
- };
670
- return storage.createSession(input, getContext());
671
- });
672
- ipcMain2.handle(`${channelPrefix}:sessions:update`, async (_event, id, data) => {
673
- await storage.updateSession(id, data, getContext());
674
- return storage.getSession(id, getContext());
675
- });
676
- ipcMain2.handle(`${channelPrefix}:sessions:delete`, async (_event, id) => {
677
- await storage.deleteSession(id, getContext());
678
- return { success: true };
679
- });
680
- ipcMain2.handle(`${channelPrefix}:messages:list`, async (_event, sessionId) => {
681
- return storage.getMessages(sessionId, getContext());
682
- });
683
- ipcMain2.handle(`${channelPrefix}:messages:save`, async (_event, params) => {
684
- const input = {
685
- id: params.id || crypto.randomUUID(),
686
- sessionId: params.sessionId,
687
- role: params.role,
688
- content: params.content,
689
- images: params.images || [],
690
- model: params.model || null,
691
- mode: params.mode || null,
692
- webSearchEnabled: params.webSearchEnabled ?? null,
693
- thinkingEnabled: params.thinkingEnabled ?? null,
694
- steps: params.steps || null,
695
- operationIds: params.operationIds || null
696
- };
697
- return storage.saveMessage(input, getContext());
698
- });
699
- ipcMain2.handle(`${channelPrefix}:messages:update`, async (_event, params) => {
700
- await storage.updateMessage(params.id, {
701
- content: params.content,
702
- steps: params.steps
703
- }, getContext());
704
- return { success: true };
705
- });
706
- ipcMain2.handle(`${channelPrefix}:messages:deleteAfter`, async (_event, sessionId, timestamp) => {
707
- await storage.deleteMessagesAfter(sessionId, new Date(timestamp), getContext());
708
- return { success: true };
709
- });
710
- ipcMain2.handle(`${channelPrefix}:messages:deleteAfterMessageId`, async (_event, sessionId, messageId) => {
711
- await storage.deleteMessagesAfterMessageId(sessionId, messageId, getContext());
712
- return { success: true };
713
- });
714
- ipcMain2.handle(`${channelPrefix}:operations:list`, async (_event, sessionId) => {
715
- return storage.getOperations(sessionId, getContext());
716
- });
717
- ipcMain2.handle(`${channelPrefix}:trash:list`, async () => {
718
- return storage.getTrashItems?.(getContext()) || [];
719
- });
720
- ipcMain2.handle(`${channelPrefix}:trash:restore`, async (_event, id) => {
721
- return storage.restoreFromTrash?.(id, getContext());
722
- });
723
- ipcMain2.handle(`${channelPrefix}:openExternal`, async (_event, url) => {
724
- return shell.openExternal(url);
725
- });
726
- ipcMain2.handle(`${channelPrefix}:fs:listDir`, async (_event, dirPath) => {
727
- try {
728
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
729
- const files = [];
730
- for (const entry of entries) {
731
- if (entry.name.startsWith(".")) continue;
732
- const fullPath = path.join(dirPath, entry.name);
733
- try {
734
- const stats = fs.statSync(fullPath);
735
- files.push({
736
- name: entry.name,
737
- path: fullPath,
738
- isDirectory: entry.isDirectory(),
739
- size: stats.size,
740
- modifiedAt: stats.mtime,
741
- extension: entry.isDirectory() ? "" : path.extname(entry.name).toLowerCase()
742
- });
743
- } catch {
744
- }
745
- }
746
- return files.sort((a, b) => {
747
- if (a.isDirectory && !b.isDirectory) return -1;
748
- if (!a.isDirectory && b.isDirectory) return 1;
749
- return a.name.localeCompare(b.name);
750
- });
751
- } catch (error) {
752
- console.error("[AI-Chat] \u5217\u51FA\u76EE\u5F55\u5931\u8D25:", error);
753
- return [];
754
- }
755
- });
756
- ipcMain2.handle(`${channelPrefix}:fs:exists`, async (_event, filePath) => {
757
- return fs.existsSync(filePath);
758
- });
759
- ipcMain2.handle(`${channelPrefix}:fs:stat`, async (_event, filePath) => {
760
- try {
761
- const stats = fs.statSync(filePath);
762
- return {
763
- name: path.basename(filePath),
764
- path: filePath,
765
- isDirectory: stats.isDirectory(),
766
- size: stats.size,
767
- modifiedAt: stats.mtime,
768
- extension: stats.isDirectory() ? "" : path.extname(filePath).toLowerCase()
769
- };
770
- } catch {
771
- return null;
772
- }
773
- });
774
- ipcMain2.handle(`${channelPrefix}:fs:readFile`, async (_event, filePath) => {
775
- try {
776
- return fs.readFileSync(filePath, "utf-8");
777
- } catch {
778
- return null;
779
- }
780
- });
781
- ipcMain2.handle(`${channelPrefix}:fs:readFileBase64`, async (_event, filePath) => {
782
- try {
783
- const buffer = fs.readFileSync(filePath);
784
- return buffer.toString("base64");
785
- } catch {
786
- return null;
787
- }
788
- });
789
- ipcMain2.handle(`${channelPrefix}:fs:homeDir`, async () => {
790
- return process.env.HOME || process.env.USERPROFILE || "/";
791
- });
792
- ipcMain2.handle(`${channelPrefix}:fs:resolvePath`, async (_event, inputPath) => {
793
- if (inputPath.startsWith("~")) {
794
- const homeDir = process.env.HOME || process.env.USERPROFILE || "/";
795
- return path.join(homeDir, inputPath.slice(1));
796
- }
797
- return path.resolve(inputPath);
798
- });
799
- ipcMain2.handle(`${channelPrefix}:fs:parentDir`, async (_event, dirPath) => {
800
- return path.dirname(dirPath);
801
- });
802
- const activeWatchers = /* @__PURE__ */ new Map();
803
- ipcMain2.handle(`${channelPrefix}:fs:watchDir`, async (event, dirPath) => {
804
- const webContents = event.sender;
805
- if (activeWatchers.has(dirPath)) {
806
- activeWatchers.get(dirPath)?.close();
807
- activeWatchers.delete(dirPath);
808
- }
809
- try {
810
- const watcher = fs.watch(dirPath, { persistent: false }, (eventType, filename) => {
811
- if (!webContents.isDestroyed()) {
812
- webContents.send(`${channelPrefix}:fs:dirChange`, {
813
- dirPath,
814
- eventType,
815
- filename
816
- });
817
- }
818
- });
819
- watcher.on("error", (error) => {
820
- console.error("[AI-Chat] Watch error:", error);
821
- activeWatchers.delete(dirPath);
822
- });
823
- activeWatchers.set(dirPath, watcher);
824
- return true;
825
- } catch (error) {
826
- console.error("[AI-Chat] Failed to watch directory:", error);
827
- return false;
828
- }
829
- });
830
- ipcMain2.handle(`${channelPrefix}:fs:unwatchDir`, async (_event, dirPath) => {
831
- const watcher = activeWatchers.get(dirPath);
832
- if (watcher) {
833
- watcher.close();
834
- activeWatchers.delete(dirPath);
835
- }
836
- });
837
- return { agent, storage };
838
- }
839
- export {
840
- MODELS,
841
- createAsrBridge,
842
- createElectronBridge
843
- };
1
+ import{ipcMain as e,shell as s}from"electron";import*as t from"fs";import*as n from"path";import{HybridAgent as r,MODELS as o,DEFAULT_MODEL as a}from"@huyooo/ai-chat-core";import{createSearchElectronBridge as c}from"@huyooo/ai-search/bridge/electron";import{createStorage as i}from"@huyooo/ai-chat-storage";import*as l from"zlib";var d=1,u=2,h=9,f=15,g=0,y=2,p=0,m=1,w=0,$=1;function b(e,s,t,n){return[17,e<<4|s,t<<4|n,0]}function S(e,s=!1,t=!0){const n=b(u,s?y:g,p,t?$:w),r=t?l.gzipSync(e):e,o=Buffer.alloc(4);return o.writeUInt32BE(r.length,0),Buffer.concat([Buffer.from(n),o,r])}import E from"ws";import{v4 as C}from"uuid";var I=class{config;ws=null;callbacks={};connectId="";isConnected=!1;sessionConfig={};constructor(e){this.config=e}getWsUrl(){return!1!==this.config.useAsyncMode?"wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async":"wss://openspeech.bytedance.com/api/v3/sauc/bigmodel"}getResourceId(){return this.config.resourceId||"volc.bigasr.sauc.duration"}connect(e,s={}){return new Promise((t,n)=>{this.callbacks=e,this.sessionConfig=s,this.connectId=C();const r=this.getWsUrl();this.ws=new E(r,{headers:{"X-Api-App-Key":this.config.appId,"X-Api-Access-Key":this.config.accessKey,"X-Api-Resource-Id":this.getResourceId(),"X-Api-Connect-Id":this.connectId}}),this.ws.on("open",()=>{this.isConnected=!0,this.sendFullClientRequest(),this.callbacks.onConnected?.(),t()}),this.ws.on("message",e=>{try{const s=function(e){if(e.length<4)throw new Error("Invalid response: too short");const s=e[0],t=e[1],n=e[2],r=s>>4&15,o=4*(15&s),a=t>>4&15,c=15&t,i=n>>4&15,d=15&n;if(1!==r)throw new Error(`Unsupported protocol version: ${r}`);let u,g=o;!(1&~c)&&e.length>=g+4&&(u=e.readUInt32BE(g),g+=4);const y=!(2&~c);if(a===f){const s=e.readUInt32BE(g);g+=4;const t=e.readUInt32BE(g);return g+=4,{type:"error",sequence:u,isLast:!0,data:{code:s,message:e.slice(g,g+t).toString("utf-8")}}}if(a===h){const s=e.readUInt32BE(g);if(g+=4,0===s)return{type:"result",sequence:u,isLast:y,data:{}};let t,n=e.slice(g,g+s);if(d===$)try{n=l.gunzipSync(n)}catch(e){throw e}if(i===m){const e=n.toString("utf-8");try{t=JSON.parse(e)}catch(e){throw e}}else t={};return{type:"result",sequence:u,isLast:y,data:t}}throw new Error(`Unknown message type: ${a}`)}(e);if("error"===s.type){const e=s.data;this.callbacks.onError?.(new Error(`ASR Error ${e.code}: ${e.message}`))}else{const e=s.data;this.callbacks.onResult?.(e,s.isLast),s.isLast}}catch(e){this.callbacks.onError?.(e instanceof Error?e:new Error(String(e)))}}),this.ws.on("error",e=>{this.callbacks.onError?.(e),n(e)}),this.ws.on("close",()=>{this.isConnected=!1,this.ws=null,this.callbacks.onClose?.()})})}sendFullClientRequest(){if(!this.ws||this.ws.readyState!==E.OPEN)return;const e=function(e,s=!0){const t=b(d,g,m,s?$:w),n=JSON.stringify(e),r=Buffer.from(n,"utf-8"),o=s?l.gzipSync(r):r,a=Buffer.alloc(4);return a.writeUInt32BE(o.length,0),Buffer.concat([Buffer.from(t),a,o])}({user:{uid:"ai-chat-user"},audio:{format:this.sessionConfig.format||"pcm",rate:this.sessionConfig.sampleRate||16e3,bits:16,channel:1},request:{model_name:"bigmodel",enable_itn:this.sessionConfig.enableItn??!0,enable_punc:this.sessionConfig.enablePunc??!0,enable_ddc:this.sessionConfig.enableDdc??!1,show_utterances:this.sessionConfig.showUtterances??!0,result_type:"full"}});this.ws.send(e)}sendAudio(e){if(!this.ws||this.ws.readyState!==E.OPEN)return;const s=S(e,!1);this.ws.send(s)}finish(){if(!this.ws||this.ws.readyState!==E.OPEN)return;const e=S(Buffer.alloc(0),!0);this.ws.send(e)}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this.isConnected=!1}get connected(){return this.isConnected&&this.ws?.readyState===E.OPEN}};import{ipcMain as A}from"electron";function D(e){const{channelPrefix:s="ai-chat",appId:t,accessKey:n,resourceId:r}=e,o=new Map;function a(e,s){let a=o.get(e);if(!a){a={client:new I({appId:t,accessKey:n,resourceId:r,useAsyncMode:!0}),webContents:s},o.set(e,a),s.on("destroyed",()=>{const s=o.get(e);s&&(s.client.disconnect(),o.delete(e))})}return a}return A.handle(`${s}:asr:start`,async(e,t)=>{const n=e.sender,r=a(n.id,n);r.client.connected&&r.client.disconnect();try{return await r.client.connect({onConnected:()=>{n.isDestroyed()||n.send(`${s}:asr:connected`)},onResult:(e,t)=>{n.isDestroyed()||n.send(`${s}:asr:result`,{result:e,isLast:t})},onError:e=>{n.isDestroyed()||n.send(`${s}:asr:error`,{message:e.message})},onClose:()=>{n.isDestroyed()||n.send(`${s}:asr:closed`)}},t),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}),A.handle(`${s}:asr:sendAudio`,async(e,s)=>{const t=e.sender.id,n=o.get(t);if(!n||!n.client.connected)return{success:!1,error:"ASR 会话未启动"};try{const e=s instanceof ArrayBuffer?Buffer.from(s):Buffer.from(new Uint8Array(s));return n.client.sendAudio(e),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}),A.handle(`${s}:asr:finish`,async e=>{const s=e.sender.id,t=o.get(s);if(!t||!t.client.connected)return{success:!1,error:"ASR 会话未启动"};try{return t.client.finish(),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}),A.handle(`${s}:asr:stop`,async e=>{const s=e.sender.id,t=o.get(s);return t&&t.client.disconnect(),{success:!0}}),A.handle(`${s}:asr:status`,async e=>{const s=e.sender.id,t=o.get(s);return{connected:t?.client.connected??!1}}),A.handle(`${s}:asr:warmup`,async(e,s)=>{const t=e.sender,n=a(t.id,t);if(n.client.connected)return{success:!0};try{return await n.client.connect({onConnected:()=>{},onResult:()=>{},onError:()=>{},onClose:()=>{}},s),n.client.disconnect(),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}),{cleanup:()=>{for(const e of o.values())e.client.disconnect();o.clear()}}}async function v(l){const{channelPrefix:d="ai-chat",dataDir:u,defaultContext:h={},...f}=l,g=n.join(u,"db.sqlite");c({ipcMain:e,channelPrefix:d}).init();const y=new Map,p=await i({type:"sqlite",sqlitePath:g}),m=()=>h,w=new r({...f,tools:l.tools,onToolApprovalRequest:async e=>{const s=global.currentWebContents;return!s||new Promise((t,n)=>{y.set(e.id,{resolve:t,reject:n,webContents:s}),s.send(`${d}:toolApprovalRequest`,{id:e.id,name:e.name,args:e.args})})},getAutoRunConfig:async()=>{try{const e=await p.getUserSetting("autoRunConfig",m());if(e)return JSON.parse(e)}catch(e){}}});e.handle(`${d}:models`,()=>o),e.handle(`${d}:getAllTools`,async()=>{0===w.tools.size&&w.toolConfig?await w.asyncInit():w.toolConfig;return w.getAllTools()}),e.handle(`${d}:send`,async(e,s)=>{const t=e.sender,{message:n,images:r,options:o={},sessionId:a}=s;global.currentWebContents=t;try{for await(const e of w.chat(n,o,r))t.isDestroyed()||t.send(`${d}:progress`,{...e,sessionId:a})}catch(e){if(Error,!t.isDestroyed()){const s=e instanceof Error?{category:"api",message:e.message||String(e),cause:e.stack}:{category:"api",message:String(e)};t.send(`${d}:progress`,{type:"error",data:s,sessionId:a})}}finally{delete global.currentWebContents}}),e.handle(`${d}:toolApprovalResponse`,(e,s)=>{const{id:t,approved:n}=s,r=y.get(t);r&&(y.delete(t),r.resolve(n))}),e.handle(`${d}:cancel`,()=>{w.abort()}),e.handle(`${d}:settings:get`,async(e,s)=>p.getUserSetting(s,m())),e.handle(`${d}:settings:set`,async(e,s,t)=>(await p.setUserSetting(s,t,m()),{success:!0})),e.handle(`${d}:settings:getAll`,async()=>p.getUserSettings(m())),e.handle(`${d}:settings:delete`,async(e,s)=>(await p.deleteUserSetting(s,m()),{success:!0})),e.handle(`${d}:setCwd`,(e,s)=>{w.setCwd(s)}),e.handle(`${d}:config`,()=>w.getConfig()),e.handle(`${d}:sessions:list`,async()=>p.getSessions(m())),e.handle(`${d}:sessions:get`,async(e,s)=>p.getSession(s,m())),e.handle(`${d}:sessions:create`,async(e,s)=>{const t={id:s.id||crypto.randomUUID(),title:s.title||"新对话",model:s.model||a,mode:s.mode||"agent",webSearchEnabled:s.webSearchEnabled??!0,thinkingEnabled:s.thinkingEnabled??!0,hidden:s.hidden??!1};return p.createSession(t,m())}),e.handle(`${d}:sessions:update`,async(e,s,t)=>(await p.updateSession(s,t,m()),p.getSession(s,m()))),e.handle(`${d}:sessions:delete`,async(e,s)=>(await p.deleteSession(s,m()),{success:!0})),e.handle(`${d}:messages:list`,async(e,s)=>p.getMessages(s,m())),e.handle(`${d}:messages:save`,async(e,s)=>{const t={id:s.id||crypto.randomUUID(),sessionId:s.sessionId,role:s.role,content:s.content,images:s.images||[],model:s.model||null,mode:s.mode||null,webSearchEnabled:s.webSearchEnabled??null,thinkingEnabled:s.thinkingEnabled??null,steps:s.steps||null,operationIds:s.operationIds||null};return p.saveMessage(t,m())}),e.handle(`${d}:messages:update`,async(e,s)=>(await p.updateMessage(s.id,{content:s.content,steps:s.steps},m()),{success:!0})),e.handle(`${d}:messages:deleteAfter`,async(e,s,t)=>(await p.deleteMessagesAfter(s,new Date(t),m()),{success:!0})),e.handle(`${d}:messages:deleteAfterMessageId`,async(e,s,t)=>(await p.deleteMessagesAfterMessageId(s,t,m()),{success:!0})),e.handle(`${d}:operations:list`,async(e,s)=>p.getOperations(s,m())),e.handle(`${d}:trash:list`,async()=>p.getTrashItems?.(m())||[]),e.handle(`${d}:trash:restore`,async(e,s)=>p.restoreFromTrash?.(s,m())),e.handle(`${d}:openExternal`,async(e,t)=>s.openExternal(t)),e.handle(`${d}:fs:listDir`,async(e,s)=>{try{const e=t.readdirSync(s,{withFileTypes:!0}),r=[];for(const o of e){if(o.name.startsWith("."))continue;const e=n.join(s,o.name);try{const s=t.statSync(e);r.push({name:o.name,path:e,isDirectory:o.isDirectory(),size:s.size,modifiedAt:s.mtime,extension:o.isDirectory()?"":n.extname(o.name).toLowerCase()})}catch{}}return r.sort((e,s)=>e.isDirectory&&!s.isDirectory?-1:!e.isDirectory&&s.isDirectory?1:e.name.localeCompare(s.name))}catch(e){return[]}}),e.handle(`${d}:fs:exists`,async(e,s)=>t.existsSync(s)),e.handle(`${d}:fs:stat`,async(e,s)=>{try{const e=t.statSync(s);return{name:n.basename(s),path:s,isDirectory:e.isDirectory(),size:e.size,modifiedAt:e.mtime,extension:e.isDirectory()?"":n.extname(s).toLowerCase()}}catch{return null}}),e.handle(`${d}:fs:readFile`,async(e,s)=>{try{return t.readFileSync(s,"utf-8")}catch{return null}}),e.handle(`${d}:fs:readFileBase64`,async(e,s)=>{try{return t.readFileSync(s).toString("base64")}catch{return null}}),e.handle(`${d}:fs:homeDir`,async()=>process.env.HOME||process.env.USERPROFILE||"/"),e.handle(`${d}:fs:resolvePath`,async(e,s)=>{if(s.startsWith("~")){const e=process.env.HOME||process.env.USERPROFILE||"/";return n.join(e,s.slice(1))}return n.resolve(s)}),e.handle(`${d}:fs:parentDir`,async(e,s)=>n.dirname(s));const $=new Map;return e.handle(`${d}:fs:watchDir`,async(e,s)=>{const n=e.sender;$.has(s)&&($.get(s)?.close(),$.delete(s));try{const e=t.watch(s,{persistent:!1},(e,t)=>{n.isDestroyed()||n.send(`${d}:fs:dirChange`,{dirPath:s,eventType:e,filename:t})});return e.on("error",e=>{$.delete(s)}),$.set(s,e),!0}catch(e){return!1}}),e.handle(`${d}:fs:unwatchDir`,async(e,s)=>{const t=$.get(s);t&&(t.close(),$.delete(s))}),{agent:w,storage:p}}export{o as MODELS,D as createAsrBridge,v as createElectronBridge};