@huyooo/ai-chat-bridge-electron 0.2.13 → 0.2.15
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/dist/main/index.d.ts +2 -0
- package/dist/main/index.js +1 -823
- package/dist/preload/index.d.ts +25 -7
- package/dist/preload/index.js +1 -136
- package/dist/renderer/index.d.cts +1 -1
- package/dist/renderer/index.d.ts +54 -15
- package/dist/renderer/index.js +1 -253
- package/package.json +4 -4
- package/src/main/index.ts +33 -0
- package/src/preload/index.ts +25 -7
- package/src/renderer/index.ts +39 -7
package/dist/main/index.js
CHANGED
|
@@ -1,823 +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}:send`, async (event, params) => {
|
|
563
|
-
const webContents = event.sender;
|
|
564
|
-
const { message, images, options: options2 = {}, sessionId } = params;
|
|
565
|
-
global.currentWebContents = webContents;
|
|
566
|
-
console.log("[AI-Chat] \u6536\u5230\u6D88\u606F:", { message, options: options2, images: images?.length || 0, sessionId });
|
|
567
|
-
console.log("[AI-Chat] autoRunConfig:", JSON.stringify(options2.autoRunConfig, null, 2));
|
|
568
|
-
try {
|
|
569
|
-
for await (const chatEvent of agent.chat(message, options2, images)) {
|
|
570
|
-
console.log(
|
|
571
|
-
"[AI-Chat] \u4E8B\u4EF6:",
|
|
572
|
-
chatEvent.type,
|
|
573
|
-
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)
|
|
574
|
-
);
|
|
575
|
-
if (!webContents.isDestroyed()) {
|
|
576
|
-
webContents.send(`${channelPrefix}:progress`, { ...chatEvent, sessionId });
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
console.log("[AI-Chat] \u5B8C\u6210");
|
|
580
|
-
} catch (error) {
|
|
581
|
-
console.error("[AI-Chat] \u9519\u8BEF:", error);
|
|
582
|
-
if (error instanceof Error) {
|
|
583
|
-
console.error("[AI-Chat] \u9519\u8BEF\u8BE6\u60C5:", {
|
|
584
|
-
message: error.message,
|
|
585
|
-
stack: error.stack,
|
|
586
|
-
name: error.name
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
if (!webContents.isDestroyed()) {
|
|
590
|
-
const errorData = error instanceof Error ? {
|
|
591
|
-
category: "api",
|
|
592
|
-
message: error.message || String(error),
|
|
593
|
-
cause: error.stack
|
|
594
|
-
} : {
|
|
595
|
-
category: "api",
|
|
596
|
-
message: String(error)
|
|
597
|
-
};
|
|
598
|
-
webContents.send(`${channelPrefix}:progress`, {
|
|
599
|
-
type: "error",
|
|
600
|
-
data: errorData,
|
|
601
|
-
sessionId
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
} finally {
|
|
605
|
-
delete global.currentWebContents;
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
ipcMain2.handle(`${channelPrefix}:toolApprovalResponse`, (_event, params) => {
|
|
609
|
-
const { id, approved } = params;
|
|
610
|
-
const pending = pendingApprovals.get(id);
|
|
611
|
-
if (pending) {
|
|
612
|
-
pendingApprovals.delete(id);
|
|
613
|
-
pending.resolve(approved);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
ipcMain2.handle(`${channelPrefix}:cancel`, () => {
|
|
617
|
-
agent.abort();
|
|
618
|
-
});
|
|
619
|
-
ipcMain2.handle(`${channelPrefix}:settings:get`, async (_event, key) => {
|
|
620
|
-
return storage.getUserSetting(key, getContext());
|
|
621
|
-
});
|
|
622
|
-
ipcMain2.handle(`${channelPrefix}:settings:set`, async (_event, key, value) => {
|
|
623
|
-
await storage.setUserSetting(key, value, getContext());
|
|
624
|
-
return { success: true };
|
|
625
|
-
});
|
|
626
|
-
ipcMain2.handle(`${channelPrefix}:settings:getAll`, async () => {
|
|
627
|
-
return storage.getUserSettings(getContext());
|
|
628
|
-
});
|
|
629
|
-
ipcMain2.handle(`${channelPrefix}:settings:delete`, async (_event, key) => {
|
|
630
|
-
await storage.deleteUserSetting(key, getContext());
|
|
631
|
-
return { success: true };
|
|
632
|
-
});
|
|
633
|
-
ipcMain2.handle(`${channelPrefix}:setCwd`, (_event, dir) => {
|
|
634
|
-
agent.setCwd(dir);
|
|
635
|
-
});
|
|
636
|
-
ipcMain2.handle(`${channelPrefix}:config`, () => {
|
|
637
|
-
return agent.getConfig();
|
|
638
|
-
});
|
|
639
|
-
ipcMain2.handle(`${channelPrefix}:sessions:list`, async () => {
|
|
640
|
-
return storage.getSessions(getContext());
|
|
641
|
-
});
|
|
642
|
-
ipcMain2.handle(`${channelPrefix}:sessions:get`, async (_event, id) => {
|
|
643
|
-
return storage.getSession(id, getContext());
|
|
644
|
-
});
|
|
645
|
-
ipcMain2.handle(`${channelPrefix}:sessions:create`, async (_event, params) => {
|
|
646
|
-
const input = {
|
|
647
|
-
id: params.id || crypto.randomUUID(),
|
|
648
|
-
title: params.title || "\u65B0\u5BF9\u8BDD",
|
|
649
|
-
model: params.model || DEFAULT_MODEL,
|
|
650
|
-
mode: params.mode || "agent",
|
|
651
|
-
webSearchEnabled: params.webSearchEnabled ?? true,
|
|
652
|
-
thinkingEnabled: params.thinkingEnabled ?? true,
|
|
653
|
-
hidden: params.hidden ?? false
|
|
654
|
-
};
|
|
655
|
-
return storage.createSession(input, getContext());
|
|
656
|
-
});
|
|
657
|
-
ipcMain2.handle(`${channelPrefix}:sessions:update`, async (_event, id, data) => {
|
|
658
|
-
await storage.updateSession(id, data, getContext());
|
|
659
|
-
return storage.getSession(id, getContext());
|
|
660
|
-
});
|
|
661
|
-
ipcMain2.handle(`${channelPrefix}:sessions:delete`, async (_event, id) => {
|
|
662
|
-
await storage.deleteSession(id, getContext());
|
|
663
|
-
return { success: true };
|
|
664
|
-
});
|
|
665
|
-
ipcMain2.handle(`${channelPrefix}:messages:list`, async (_event, sessionId) => {
|
|
666
|
-
return storage.getMessages(sessionId, getContext());
|
|
667
|
-
});
|
|
668
|
-
ipcMain2.handle(`${channelPrefix}:messages:save`, async (_event, params) => {
|
|
669
|
-
const input = {
|
|
670
|
-
id: params.id || crypto.randomUUID(),
|
|
671
|
-
sessionId: params.sessionId,
|
|
672
|
-
role: params.role,
|
|
673
|
-
content: params.content,
|
|
674
|
-
model: params.model || null,
|
|
675
|
-
mode: params.mode || null,
|
|
676
|
-
webSearchEnabled: params.webSearchEnabled ?? null,
|
|
677
|
-
thinkingEnabled: params.thinkingEnabled ?? null,
|
|
678
|
-
steps: params.steps || null,
|
|
679
|
-
operationIds: params.operationIds || null
|
|
680
|
-
};
|
|
681
|
-
return storage.saveMessage(input, getContext());
|
|
682
|
-
});
|
|
683
|
-
ipcMain2.handle(`${channelPrefix}:messages:update`, async (_event, params) => {
|
|
684
|
-
await storage.updateMessage(params.id, {
|
|
685
|
-
content: params.content,
|
|
686
|
-
steps: params.steps
|
|
687
|
-
}, getContext());
|
|
688
|
-
return { success: true };
|
|
689
|
-
});
|
|
690
|
-
ipcMain2.handle(`${channelPrefix}:messages:deleteAfter`, async (_event, sessionId, timestamp) => {
|
|
691
|
-
await storage.deleteMessagesAfter(sessionId, new Date(timestamp), getContext());
|
|
692
|
-
return { success: true };
|
|
693
|
-
});
|
|
694
|
-
ipcMain2.handle(`${channelPrefix}:operations:list`, async (_event, sessionId) => {
|
|
695
|
-
return storage.getOperations(sessionId, getContext());
|
|
696
|
-
});
|
|
697
|
-
ipcMain2.handle(`${channelPrefix}:trash:list`, async () => {
|
|
698
|
-
return storage.getTrashItems?.(getContext()) || [];
|
|
699
|
-
});
|
|
700
|
-
ipcMain2.handle(`${channelPrefix}:trash:restore`, async (_event, id) => {
|
|
701
|
-
return storage.restoreFromTrash?.(id, getContext());
|
|
702
|
-
});
|
|
703
|
-
ipcMain2.handle(`${channelPrefix}:openExternal`, async (_event, url) => {
|
|
704
|
-
return shell.openExternal(url);
|
|
705
|
-
});
|
|
706
|
-
ipcMain2.handle(`${channelPrefix}:fs:listDir`, async (_event, dirPath) => {
|
|
707
|
-
try {
|
|
708
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
709
|
-
const files = [];
|
|
710
|
-
for (const entry of entries) {
|
|
711
|
-
if (entry.name.startsWith(".")) continue;
|
|
712
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
713
|
-
try {
|
|
714
|
-
const stats = fs.statSync(fullPath);
|
|
715
|
-
files.push({
|
|
716
|
-
name: entry.name,
|
|
717
|
-
path: fullPath,
|
|
718
|
-
isDirectory: entry.isDirectory(),
|
|
719
|
-
size: stats.size,
|
|
720
|
-
modifiedAt: stats.mtime,
|
|
721
|
-
extension: entry.isDirectory() ? "" : path.extname(entry.name).toLowerCase()
|
|
722
|
-
});
|
|
723
|
-
} catch {
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return files.sort((a, b) => {
|
|
727
|
-
if (a.isDirectory && !b.isDirectory) return -1;
|
|
728
|
-
if (!a.isDirectory && b.isDirectory) return 1;
|
|
729
|
-
return a.name.localeCompare(b.name);
|
|
730
|
-
});
|
|
731
|
-
} catch (error) {
|
|
732
|
-
console.error("[AI-Chat] \u5217\u51FA\u76EE\u5F55\u5931\u8D25:", error);
|
|
733
|
-
return [];
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
ipcMain2.handle(`${channelPrefix}:fs:exists`, async (_event, filePath) => {
|
|
737
|
-
return fs.existsSync(filePath);
|
|
738
|
-
});
|
|
739
|
-
ipcMain2.handle(`${channelPrefix}:fs:stat`, async (_event, filePath) => {
|
|
740
|
-
try {
|
|
741
|
-
const stats = fs.statSync(filePath);
|
|
742
|
-
return {
|
|
743
|
-
name: path.basename(filePath),
|
|
744
|
-
path: filePath,
|
|
745
|
-
isDirectory: stats.isDirectory(),
|
|
746
|
-
size: stats.size,
|
|
747
|
-
modifiedAt: stats.mtime,
|
|
748
|
-
extension: stats.isDirectory() ? "" : path.extname(filePath).toLowerCase()
|
|
749
|
-
};
|
|
750
|
-
} catch {
|
|
751
|
-
return null;
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
ipcMain2.handle(`${channelPrefix}:fs:readFile`, async (_event, filePath) => {
|
|
755
|
-
try {
|
|
756
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
757
|
-
} catch {
|
|
758
|
-
return null;
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
ipcMain2.handle(`${channelPrefix}:fs:readFileBase64`, async (_event, filePath) => {
|
|
762
|
-
try {
|
|
763
|
-
const buffer = fs.readFileSync(filePath);
|
|
764
|
-
return buffer.toString("base64");
|
|
765
|
-
} catch {
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
ipcMain2.handle(`${channelPrefix}:fs:homeDir`, async () => {
|
|
770
|
-
return process.env.HOME || process.env.USERPROFILE || "/";
|
|
771
|
-
});
|
|
772
|
-
ipcMain2.handle(`${channelPrefix}:fs:resolvePath`, async (_event, inputPath) => {
|
|
773
|
-
if (inputPath.startsWith("~")) {
|
|
774
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || "/";
|
|
775
|
-
return path.join(homeDir, inputPath.slice(1));
|
|
776
|
-
}
|
|
777
|
-
return path.resolve(inputPath);
|
|
778
|
-
});
|
|
779
|
-
ipcMain2.handle(`${channelPrefix}:fs:parentDir`, async (_event, dirPath) => {
|
|
780
|
-
return path.dirname(dirPath);
|
|
781
|
-
});
|
|
782
|
-
const activeWatchers = /* @__PURE__ */ new Map();
|
|
783
|
-
ipcMain2.handle(`${channelPrefix}:fs:watchDir`, async (event, dirPath) => {
|
|
784
|
-
const webContents = event.sender;
|
|
785
|
-
if (activeWatchers.has(dirPath)) {
|
|
786
|
-
activeWatchers.get(dirPath)?.close();
|
|
787
|
-
activeWatchers.delete(dirPath);
|
|
788
|
-
}
|
|
789
|
-
try {
|
|
790
|
-
const watcher = fs.watch(dirPath, { persistent: false }, (eventType, filename) => {
|
|
791
|
-
if (!webContents.isDestroyed()) {
|
|
792
|
-
webContents.send(`${channelPrefix}:fs:dirChange`, {
|
|
793
|
-
dirPath,
|
|
794
|
-
eventType,
|
|
795
|
-
filename
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
watcher.on("error", (error) => {
|
|
800
|
-
console.error("[AI-Chat] Watch error:", error);
|
|
801
|
-
activeWatchers.delete(dirPath);
|
|
802
|
-
});
|
|
803
|
-
activeWatchers.set(dirPath, watcher);
|
|
804
|
-
return true;
|
|
805
|
-
} catch (error) {
|
|
806
|
-
console.error("[AI-Chat] Failed to watch directory:", error);
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
ipcMain2.handle(`${channelPrefix}:fs:unwatchDir`, async (_event, dirPath) => {
|
|
811
|
-
const watcher = activeWatchers.get(dirPath);
|
|
812
|
-
if (watcher) {
|
|
813
|
-
watcher.close();
|
|
814
|
-
activeWatchers.delete(dirPath);
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
return { agent, storage };
|
|
818
|
-
}
|
|
819
|
-
export {
|
|
820
|
-
MODELS,
|
|
821
|
-
createAsrBridge,
|
|
822
|
-
createElectronBridge
|
|
823
|
-
};
|
|
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};
|