@huyooo/ai-chat-bridge-electron 0.1.4 → 0.1.8
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.cjs +243 -23
- package/dist/main/index.d.cts +31 -5
- package/dist/main/index.d.ts +31 -5
- package/dist/main/index.js +235 -24
- package/dist/preload/index.cjs +58 -4
- package/dist/preload/index.d.cts +152 -19
- package/dist/preload/index.d.ts +152 -19
- package/dist/preload/index.js +58 -4
- package/dist/renderer/index.cjs +90 -17
- package/dist/renderer/index.d.cts +282 -46
- package/dist/renderer/index.d.ts +282 -46
- package/dist/renderer/index.js +90 -17
- package/package.json +9 -4
- package/src/main/index.ts +611 -0
- package/src/preload/index.ts +457 -0
- package/src/renderer/index.ts +507 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Electron 主进程桥接
|
|
3
|
+
*
|
|
4
|
+
* 在主进程中调用,注册 IPC handlers
|
|
5
|
+
* 集成 SQLite 存储
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ipcMain, shell } from 'electron';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import {
|
|
12
|
+
HybridAgent,
|
|
13
|
+
type AgentConfig,
|
|
14
|
+
type ChatEvent,
|
|
15
|
+
type ChatOptions,
|
|
16
|
+
type ChatMode,
|
|
17
|
+
type ProviderType,
|
|
18
|
+
type ToolConfigItem,
|
|
19
|
+
MODELS,
|
|
20
|
+
DEFAULT_MODEL,
|
|
21
|
+
} from '@huyooo/ai-chat-core';
|
|
22
|
+
import { createSearchElectronBridge } from '@huyooo/ai-search/bridge/electron';
|
|
23
|
+
|
|
24
|
+
// 注意:工具解析已移至 HybridAgent 内部(asyncInit 方法)
|
|
25
|
+
// 这里不再需要提前解析,直接传递 ToolConfigItem[] 给 HybridAgent
|
|
26
|
+
|
|
27
|
+
/** 文件信息 */
|
|
28
|
+
export interface FileInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
path: string;
|
|
31
|
+
isDirectory: boolean;
|
|
32
|
+
size: number;
|
|
33
|
+
modifiedAt: Date;
|
|
34
|
+
extension: string;
|
|
35
|
+
}
|
|
36
|
+
import {
|
|
37
|
+
createStorage,
|
|
38
|
+
getDefaultStoragePath,
|
|
39
|
+
type StorageAdapter,
|
|
40
|
+
type StorageContext,
|
|
41
|
+
type SessionRecord,
|
|
42
|
+
type MessageRecord,
|
|
43
|
+
type CreateSessionInput,
|
|
44
|
+
type CreateMessageInput,
|
|
45
|
+
} from '@huyooo/ai-chat-storage';
|
|
46
|
+
|
|
47
|
+
export interface ElectronBridgeOptions extends Omit<AgentConfig, 'tools'> {
|
|
48
|
+
/** IPC channel 前缀 */
|
|
49
|
+
channelPrefix?: string;
|
|
50
|
+
/**
|
|
51
|
+
* 统一数据目录(推荐)
|
|
52
|
+
* 所有数据都存储在此目录下:
|
|
53
|
+
* - db.sqlite: 对话历史
|
|
54
|
+
* - search-data/: 搜索索引
|
|
55
|
+
* 默认: ~/.ai-chat/
|
|
56
|
+
*/
|
|
57
|
+
dataDir?: string;
|
|
58
|
+
/**
|
|
59
|
+
* SQLite 数据库路径(可选,覆盖 dataDir 设置)
|
|
60
|
+
* @deprecated 推荐使用 dataDir
|
|
61
|
+
*/
|
|
62
|
+
storagePath?: string;
|
|
63
|
+
/** 默认租户上下文 */
|
|
64
|
+
defaultContext?: StorageContext;
|
|
65
|
+
/**
|
|
66
|
+
* 工具列表(Vite 插件风格)
|
|
67
|
+
* 支持:ToolPlugin、Promise<ToolPlugin>
|
|
68
|
+
*/
|
|
69
|
+
tools?: ToolConfigItem[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 发送消息参数 */
|
|
73
|
+
interface SendMessageParams {
|
|
74
|
+
message: string;
|
|
75
|
+
images?: string[];
|
|
76
|
+
options?: ChatOptions;
|
|
77
|
+
sessionId?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 会话相关参数 */
|
|
81
|
+
interface SessionParams {
|
|
82
|
+
id?: string;
|
|
83
|
+
title?: string;
|
|
84
|
+
model?: string;
|
|
85
|
+
mode?: 'agent' | 'plan' | 'ask';
|
|
86
|
+
webSearchEnabled?: boolean;
|
|
87
|
+
thinkingEnabled?: boolean;
|
|
88
|
+
hidden?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 消息相关参数 */
|
|
92
|
+
interface MessageParams {
|
|
93
|
+
/** 消息 ID(可选,如果不传则自动生成) */
|
|
94
|
+
id?: string;
|
|
95
|
+
sessionId: string;
|
|
96
|
+
role: 'user' | 'assistant';
|
|
97
|
+
content: string;
|
|
98
|
+
/** 生成此消息时使用的模型 */
|
|
99
|
+
model?: string;
|
|
100
|
+
/** 生成此消息时使用的模式 (ask/agent) */
|
|
101
|
+
mode?: string;
|
|
102
|
+
/** 生成此消息时是否启用 web 搜索 */
|
|
103
|
+
webSearchEnabled?: boolean;
|
|
104
|
+
/** 生成此消息时是否启用深度思考 */
|
|
105
|
+
thinkingEnabled?: boolean;
|
|
106
|
+
/** 执行步骤列表 JSON */
|
|
107
|
+
steps?: string;
|
|
108
|
+
operationIds?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 更新消息参数 */
|
|
112
|
+
interface UpdateMessageParams {
|
|
113
|
+
id: string;
|
|
114
|
+
content?: string;
|
|
115
|
+
steps?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 创建 Electron IPC 桥接
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import { createElectronBridge } from '@huyooo/ai-chat-bridge-electron/main';
|
|
124
|
+
*
|
|
125
|
+
* app.whenReady().then(async () => {
|
|
126
|
+
* const { agent, storage } = await createElectronBridge({
|
|
127
|
+
* arkApiKey: 'xxx',
|
|
128
|
+
* geminiApiKey: 'xxx',
|
|
129
|
+
* });
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export async function createElectronBridge(options: ElectronBridgeOptions) {
|
|
134
|
+
const {
|
|
135
|
+
channelPrefix = 'ai-chat',
|
|
136
|
+
dataDir,
|
|
137
|
+
storagePath,
|
|
138
|
+
defaultContext = {},
|
|
139
|
+
...agentConfig
|
|
140
|
+
} = options;
|
|
141
|
+
|
|
142
|
+
// 计算实际的存储路径
|
|
143
|
+
// 优先级:storagePath > dataDir/db.sqlite > 默认路径
|
|
144
|
+
const resolvedStoragePath = storagePath
|
|
145
|
+
|| (dataDir ? `${dataDir}/db.sqlite` : undefined);
|
|
146
|
+
|
|
147
|
+
// 🔴 重要:在处理任何 tools 之前,先初始化搜索 Electron 桥接
|
|
148
|
+
// 这样当 searchPlugin 开始后台索引时,全局进度监听器已经注册好了
|
|
149
|
+
createSearchElectronBridge({
|
|
150
|
+
ipcMain,
|
|
151
|
+
channelPrefix,
|
|
152
|
+
}).init();
|
|
153
|
+
|
|
154
|
+
// 工具批准请求的等待队列(key: toolCallId, value: { resolve, reject, webContents })
|
|
155
|
+
const pendingApprovals = new Map<string, {
|
|
156
|
+
resolve: (approved: boolean) => void;
|
|
157
|
+
reject: (error: Error) => void;
|
|
158
|
+
webContents: Electron.WebContents;
|
|
159
|
+
}>();
|
|
160
|
+
|
|
161
|
+
// 创建工具批准回调
|
|
162
|
+
const toolApprovalCallback = async (toolCall: {
|
|
163
|
+
id: string;
|
|
164
|
+
name: string;
|
|
165
|
+
args: Record<string, unknown>;
|
|
166
|
+
}): Promise<boolean> => {
|
|
167
|
+
console.log('[Main] 工具批准回调被调用:', toolCall.name);
|
|
168
|
+
// 查找当前请求的 webContents(通过最近的 send 调用)
|
|
169
|
+
// 这里我们需要一个更好的方式来获取 webContents
|
|
170
|
+
// 暂时使用一个全局变量存储当前的 webContents
|
|
171
|
+
const currentWebContents = (global as { currentWebContents?: Electron.WebContents }).currentWebContents;
|
|
172
|
+
if (!currentWebContents) {
|
|
173
|
+
console.log('[Main] 警告: 没有 webContents,默认批准');
|
|
174
|
+
// 如果没有 webContents,默认批准(向后兼容)
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
179
|
+
// 存储 Promise 的 resolve/reject
|
|
180
|
+
pendingApprovals.set(toolCall.id, {
|
|
181
|
+
resolve,
|
|
182
|
+
reject,
|
|
183
|
+
webContents: currentWebContents,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 发送批准请求到前端
|
|
187
|
+
console.log('[Main] 发送工具批准请求到前端:', toolCall.name);
|
|
188
|
+
currentWebContents.send(`${channelPrefix}:toolApprovalRequest`, {
|
|
189
|
+
id: toolCall.id,
|
|
190
|
+
name: toolCall.name,
|
|
191
|
+
args: toolCall.args,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 不设置超时,让用户有足够时间思考
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// 初始化存储(在 agent 之前,因为 getAutoRunConfig 需要访问 storage)
|
|
199
|
+
const storage = await createStorage({
|
|
200
|
+
type: 'sqlite',
|
|
201
|
+
sqlitePath: resolvedStoragePath || getDefaultStoragePath(),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// 获取上下文
|
|
205
|
+
const getContext = (): StorageContext => defaultContext;
|
|
206
|
+
|
|
207
|
+
// 动态获取自动运行配置(从数据库实时读取)
|
|
208
|
+
const getAutoRunConfig = async () => {
|
|
209
|
+
try {
|
|
210
|
+
const configJson = await storage.getUserSetting('autoRunConfig', getContext());
|
|
211
|
+
if (configJson) {
|
|
212
|
+
return JSON.parse(configJson) as {
|
|
213
|
+
mode?: 'run-everything' | 'manual';
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('[Main] 获取 autoRunConfig 失败:', error);
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 创建 Agent(工具会在 HybridAgent 的 asyncInit 中自动解析)
|
|
223
|
+
const agent = new HybridAgent({
|
|
224
|
+
...agentConfig,
|
|
225
|
+
// tools 直接传递 ToolConfigItem[],HybridAgent 会在 asyncInit 中解析
|
|
226
|
+
tools: options.tools,
|
|
227
|
+
onToolApprovalRequest: toolApprovalCallback,
|
|
228
|
+
getAutoRunConfig,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ============ AI Chat API ============
|
|
232
|
+
|
|
233
|
+
// 获取可用模型
|
|
234
|
+
ipcMain.handle(`${channelPrefix}:models`, () => {
|
|
235
|
+
return MODELS;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 发送消息
|
|
239
|
+
ipcMain.handle(`${channelPrefix}:send`, async (event, params: SendMessageParams) => {
|
|
240
|
+
const webContents = event.sender;
|
|
241
|
+
const { message, images, options = {}, sessionId } = params;
|
|
242
|
+
|
|
243
|
+
// 存储当前 webContents,供工具批准回调使用
|
|
244
|
+
(global as { currentWebContents?: Electron.WebContents }).currentWebContents = webContents;
|
|
245
|
+
|
|
246
|
+
console.log('[AI-Chat] 收到消息:', { message, options, images: images?.length || 0, sessionId });
|
|
247
|
+
console.log('[AI-Chat] autoRunConfig:', JSON.stringify(options.autoRunConfig, null, 2));
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
for await (const chatEvent of agent.chat(message, options, images)) {
|
|
251
|
+
// 调试:打印事件
|
|
252
|
+
console.log('[AI-Chat] 事件:', chatEvent.type,
|
|
253
|
+
chatEvent.type === 'text_delta' ? (chatEvent.data as { content: string }).content.slice(0, 20) :
|
|
254
|
+
chatEvent.type === 'thinking_delta' ? (chatEvent.data as { content: string }).content.slice(0, 20) :
|
|
255
|
+
chatEvent.type === 'search_result' ? `搜索完成 ${(chatEvent.data as { results?: unknown[] }).results?.length || 0} 条` :
|
|
256
|
+
JSON.stringify(chatEvent.data).slice(0, 100)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// 发送事件到渲染进程(携带 sessionId 以区分不同会话)
|
|
260
|
+
if (!webContents.isDestroyed()) {
|
|
261
|
+
webContents.send(`${channelPrefix}:progress`, { ...chatEvent, sessionId });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log('[AI-Chat] 完成');
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('[AI-Chat] 错误:', error);
|
|
267
|
+
if (error instanceof Error) {
|
|
268
|
+
console.error('[AI-Chat] 错误详情:', {
|
|
269
|
+
message: error.message,
|
|
270
|
+
stack: error.stack,
|
|
271
|
+
name: error.name,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (!webContents.isDestroyed()) {
|
|
275
|
+
const errorData = error instanceof Error
|
|
276
|
+
? {
|
|
277
|
+
category: 'api',
|
|
278
|
+
message: error.message || String(error),
|
|
279
|
+
cause: error.stack,
|
|
280
|
+
}
|
|
281
|
+
: {
|
|
282
|
+
category: 'api',
|
|
283
|
+
message: String(error),
|
|
284
|
+
};
|
|
285
|
+
webContents.send(`${channelPrefix}:progress`, {
|
|
286
|
+
type: 'error',
|
|
287
|
+
data: errorData,
|
|
288
|
+
sessionId
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
// 清理当前 webContents
|
|
293
|
+
delete (global as { currentWebContents?: Electron.WebContents }).currentWebContents;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// 工具批准响应
|
|
298
|
+
ipcMain.handle(`${channelPrefix}:toolApprovalResponse`, (_event, params: { id: string; approved: boolean }) => {
|
|
299
|
+
const { id, approved } = params;
|
|
300
|
+
const pending = pendingApprovals.get(id);
|
|
301
|
+
if (pending) {
|
|
302
|
+
pendingApprovals.delete(id);
|
|
303
|
+
pending.resolve(approved);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// 取消请求
|
|
308
|
+
ipcMain.handle(`${channelPrefix}:cancel`, () => {
|
|
309
|
+
agent.abort();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ============ 用户设置 API ============
|
|
313
|
+
|
|
314
|
+
// 获取用户设置
|
|
315
|
+
ipcMain.handle(`${channelPrefix}:settings:get`, async (_event, key: string) => {
|
|
316
|
+
return storage.getUserSetting(key, getContext());
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// 设置用户设置
|
|
320
|
+
ipcMain.handle(`${channelPrefix}:settings:set`, async (_event, key: string, value: string) => {
|
|
321
|
+
await storage.setUserSetting(key, value, getContext());
|
|
322
|
+
return { success: true };
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 获取所有用户设置
|
|
326
|
+
ipcMain.handle(`${channelPrefix}:settings:getAll`, async () => {
|
|
327
|
+
return storage.getUserSettings(getContext());
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 删除用户设置
|
|
331
|
+
ipcMain.handle(`${channelPrefix}:settings:delete`, async (_event, key: string) => {
|
|
332
|
+
await storage.deleteUserSetting(key, getContext());
|
|
333
|
+
return { success: true };
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// 无状态架构:历史通过 ChatOptions.history 传入,不再需要内存历史 API
|
|
337
|
+
|
|
338
|
+
// 设置当前工作目录
|
|
339
|
+
ipcMain.handle(`${channelPrefix}:setCwd`, (_event, dir: string) => {
|
|
340
|
+
agent.setCwd(dir);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// 获取当前配置
|
|
344
|
+
ipcMain.handle(`${channelPrefix}:config`, () => {
|
|
345
|
+
return agent.getConfig();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ============ Storage API ============
|
|
349
|
+
|
|
350
|
+
// 获取会话列表
|
|
351
|
+
ipcMain.handle(`${channelPrefix}:sessions:list`, async () => {
|
|
352
|
+
return storage.getSessions(getContext());
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// 获取单个会话
|
|
356
|
+
ipcMain.handle(`${channelPrefix}:sessions:get`, async (_event, id: string) => {
|
|
357
|
+
return storage.getSession(id, getContext());
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// 创建会话
|
|
361
|
+
ipcMain.handle(`${channelPrefix}:sessions:create`, async (_event, params: SessionParams) => {
|
|
362
|
+
// 无状态架构:历史通过 ChatOptions.history 传入,不需要清空内存
|
|
363
|
+
|
|
364
|
+
const input: CreateSessionInput = {
|
|
365
|
+
id: params.id || crypto.randomUUID(),
|
|
366
|
+
title: params.title || '新对话',
|
|
367
|
+
model: params.model || DEFAULT_MODEL,
|
|
368
|
+
mode: (params.mode || 'agent') as 'agent' | 'plan' | 'ask',
|
|
369
|
+
webSearchEnabled: params.webSearchEnabled ?? true,
|
|
370
|
+
thinkingEnabled: params.thinkingEnabled ?? true,
|
|
371
|
+
hidden: params.hidden ?? false,
|
|
372
|
+
};
|
|
373
|
+
return storage.createSession(input, getContext());
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// 更新会话
|
|
377
|
+
ipcMain.handle(`${channelPrefix}:sessions:update`, async (_event, id: string, data: Partial<SessionParams>) => {
|
|
378
|
+
await storage.updateSession(id, data, getContext());
|
|
379
|
+
return storage.getSession(id, getContext());
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// 删除会话
|
|
383
|
+
ipcMain.handle(`${channelPrefix}:sessions:delete`, async (_event, id: string) => {
|
|
384
|
+
await storage.deleteSession(id, getContext());
|
|
385
|
+
return { success: true };
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// 获取消息列表
|
|
389
|
+
ipcMain.handle(`${channelPrefix}:messages:list`, async (_event, sessionId: string) => {
|
|
390
|
+
return storage.getMessages(sessionId, getContext());
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// 保存消息
|
|
394
|
+
ipcMain.handle(`${channelPrefix}:messages:save`, async (_event, params: MessageParams) => {
|
|
395
|
+
const input: CreateMessageInput = {
|
|
396
|
+
id: params.id || crypto.randomUUID(),
|
|
397
|
+
sessionId: params.sessionId,
|
|
398
|
+
role: params.role,
|
|
399
|
+
content: params.content,
|
|
400
|
+
model: params.model || null,
|
|
401
|
+
mode: params.mode || null,
|
|
402
|
+
webSearchEnabled: params.webSearchEnabled ?? null,
|
|
403
|
+
thinkingEnabled: params.thinkingEnabled ?? null,
|
|
404
|
+
steps: params.steps || null,
|
|
405
|
+
operationIds: params.operationIds || null,
|
|
406
|
+
};
|
|
407
|
+
return storage.saveMessage(input, getContext());
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// 更新消息(增量保存)
|
|
411
|
+
ipcMain.handle(`${channelPrefix}:messages:update`, async (_event, params: UpdateMessageParams) => {
|
|
412
|
+
await storage.updateMessage(params.id, {
|
|
413
|
+
content: params.content,
|
|
414
|
+
steps: params.steps,
|
|
415
|
+
}, getContext());
|
|
416
|
+
return { success: true };
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// 删除指定时间之后的消息(用于分叉)
|
|
420
|
+
ipcMain.handle(`${channelPrefix}:messages:deleteAfter`, async (_event, sessionId: string, timestamp: number) => {
|
|
421
|
+
await storage.deleteMessagesAfter(sessionId, new Date(timestamp), getContext());
|
|
422
|
+
return { success: true };
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// 获取操作日志
|
|
426
|
+
ipcMain.handle(`${channelPrefix}:operations:list`, async (_event, sessionId: string) => {
|
|
427
|
+
return storage.getOperations(sessionId, getContext());
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// 获取回收站
|
|
431
|
+
ipcMain.handle(`${channelPrefix}:trash:list`, async () => {
|
|
432
|
+
return storage.getTrashItems?.(getContext()) || [];
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// 恢复回收站项目
|
|
436
|
+
ipcMain.handle(`${channelPrefix}:trash:restore`, async (_event, id: string) => {
|
|
437
|
+
return storage.restoreFromTrash?.(id, getContext());
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ============ System API ============
|
|
441
|
+
|
|
442
|
+
// 在系统默认浏览器中打开链接
|
|
443
|
+
ipcMain.handle(`${channelPrefix}:openExternal`, async (_event, url: string) => {
|
|
444
|
+
return shell.openExternal(url);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ============ File System API ============
|
|
448
|
+
|
|
449
|
+
// 列出目录内容
|
|
450
|
+
ipcMain.handle(`${channelPrefix}:fs:listDir`, async (_event, dirPath: string): Promise<FileInfo[]> => {
|
|
451
|
+
try {
|
|
452
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
453
|
+
const files: FileInfo[] = [];
|
|
454
|
+
|
|
455
|
+
for (const entry of entries) {
|
|
456
|
+
// 跳过隐藏文件
|
|
457
|
+
if (entry.name.startsWith('.')) continue;
|
|
458
|
+
|
|
459
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
460
|
+
try {
|
|
461
|
+
const stats = fs.statSync(fullPath);
|
|
462
|
+
files.push({
|
|
463
|
+
name: entry.name,
|
|
464
|
+
path: fullPath,
|
|
465
|
+
isDirectory: entry.isDirectory(),
|
|
466
|
+
size: stats.size,
|
|
467
|
+
modifiedAt: stats.mtime,
|
|
468
|
+
extension: entry.isDirectory() ? '' : path.extname(entry.name).toLowerCase(),
|
|
469
|
+
});
|
|
470
|
+
} catch {
|
|
471
|
+
// 跳过无法访问的文件
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 排序:目录在前,文件在后,按名称排序
|
|
476
|
+
return files.sort((a, b) => {
|
|
477
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
478
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
479
|
+
return a.name.localeCompare(b.name);
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.error('[AI-Chat] 列出目录失败:', error);
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// 检查路径是否存在
|
|
488
|
+
ipcMain.handle(`${channelPrefix}:fs:exists`, async (_event, filePath: string): Promise<boolean> => {
|
|
489
|
+
return fs.existsSync(filePath);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// 获取文件信息
|
|
493
|
+
ipcMain.handle(`${channelPrefix}:fs:stat`, async (_event, filePath: string): Promise<FileInfo | null> => {
|
|
494
|
+
try {
|
|
495
|
+
const stats = fs.statSync(filePath);
|
|
496
|
+
return {
|
|
497
|
+
name: path.basename(filePath),
|
|
498
|
+
path: filePath,
|
|
499
|
+
isDirectory: stats.isDirectory(),
|
|
500
|
+
size: stats.size,
|
|
501
|
+
modifiedAt: stats.mtime,
|
|
502
|
+
extension: stats.isDirectory() ? '' : path.extname(filePath).toLowerCase(),
|
|
503
|
+
};
|
|
504
|
+
} catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// 读取文件内容(文本文件)
|
|
510
|
+
ipcMain.handle(`${channelPrefix}:fs:readFile`, async (_event, filePath: string): Promise<string | null> => {
|
|
511
|
+
try {
|
|
512
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
513
|
+
} catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// 读取文件为 base64(图片等二进制文件)
|
|
519
|
+
ipcMain.handle(`${channelPrefix}:fs:readFileBase64`, async (_event, filePath: string): Promise<string | null> => {
|
|
520
|
+
try {
|
|
521
|
+
const buffer = fs.readFileSync(filePath);
|
|
522
|
+
return buffer.toString('base64');
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// 获取用户主目录
|
|
529
|
+
ipcMain.handle(`${channelPrefix}:fs:homeDir`, async (): Promise<string> => {
|
|
530
|
+
return process.env.HOME || process.env.USERPROFILE || '/';
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// 解析路径(处理 ~)
|
|
534
|
+
ipcMain.handle(`${channelPrefix}:fs:resolvePath`, async (_event, inputPath: string): Promise<string> => {
|
|
535
|
+
if (inputPath.startsWith('~')) {
|
|
536
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '/';
|
|
537
|
+
return path.join(homeDir, inputPath.slice(1));
|
|
538
|
+
}
|
|
539
|
+
return path.resolve(inputPath);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// 获取父目录
|
|
543
|
+
ipcMain.handle(`${channelPrefix}:fs:parentDir`, async (_event, dirPath: string): Promise<string> => {
|
|
544
|
+
return path.dirname(dirPath);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// ============ File System Watch API ============
|
|
548
|
+
|
|
549
|
+
// 存储活跃的 watcher
|
|
550
|
+
const activeWatchers = new Map<string, fs.FSWatcher>();
|
|
551
|
+
|
|
552
|
+
// 监听目录变化
|
|
553
|
+
ipcMain.handle(`${channelPrefix}:fs:watchDir`, async (event, dirPath: string): Promise<boolean> => {
|
|
554
|
+
const webContents = event.sender;
|
|
555
|
+
|
|
556
|
+
// 如果已经在监听,先停止
|
|
557
|
+
if (activeWatchers.has(dirPath)) {
|
|
558
|
+
activeWatchers.get(dirPath)?.close();
|
|
559
|
+
activeWatchers.delete(dirPath);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const watcher = fs.watch(dirPath, { persistent: false }, (eventType, filename) => {
|
|
564
|
+
if (!webContents.isDestroyed()) {
|
|
565
|
+
webContents.send(`${channelPrefix}:fs:dirChange`, {
|
|
566
|
+
dirPath,
|
|
567
|
+
eventType,
|
|
568
|
+
filename,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
watcher.on('error', (error) => {
|
|
574
|
+
console.error('[AI-Chat] Watch error:', error);
|
|
575
|
+
activeWatchers.delete(dirPath);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
activeWatchers.set(dirPath, watcher);
|
|
579
|
+
return true;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
console.error('[AI-Chat] Failed to watch directory:', error);
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// 停止监听目录
|
|
587
|
+
ipcMain.handle(`${channelPrefix}:fs:unwatchDir`, async (_event, dirPath: string): Promise<void> => {
|
|
588
|
+
const watcher = activeWatchers.get(dirPath);
|
|
589
|
+
if (watcher) {
|
|
590
|
+
watcher.close();
|
|
591
|
+
activeWatchers.delete(dirPath);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return { agent, storage };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 导出类型
|
|
599
|
+
export type {
|
|
600
|
+
AgentConfig,
|
|
601
|
+
ChatEvent,
|
|
602
|
+
ChatOptions,
|
|
603
|
+
ChatMode,
|
|
604
|
+
ProviderType,
|
|
605
|
+
StorageAdapter,
|
|
606
|
+
StorageContext,
|
|
607
|
+
SessionRecord,
|
|
608
|
+
MessageRecord,
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
export { MODELS };
|