第 6 课:Electron 主进程
主进程是什么?
Electron 应用有两种进程: - 渲染进程:跑 React 界面的(上一课讲的) - 主进程:跑 Node.js 的,负责系统级操作
主进程就像一个"管家",负责: 1. 创建窗口 2. 接收前端消息,调用 SDK 3. 管理文件(保存上传、检测输出) 4. 处理下载请求
代码在 src/main/main.ts,大约 364 行。
创建应用窗口
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1024,
height: 728,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// ↑ 预加载脚本,定义前端能用的 API
},
});
// 加载 React 界面
mainWindow.loadURL(resolveHtmlPath('index.html'));
};
preload.ts 是安全桥梁——它定义了前端能调用的 IPC 方法,避免前端直接访问 Node.js 的危险 API。
// preload.ts —— 暴露给前端的安全接口
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
sendMessage(channel: string, ...args: unknown[]) {
ipcRenderer.send(channel, ...args);
},
on(channel: string, func: (...args: unknown[]) => void) {
ipcRenderer.on(channel, (_event, ...args) => func(...args));
// 返回清理函数
return () => ipcRenderer.removeListener(channel, func);
},
invoke(channel: string, ...args: unknown[]) {
return ipcRenderer.invoke(channel, ...args);
},
},
});
处理用户上传的文件
当用户拖一个 Excel 文件到聊天框,文件是怎么到达 AI 的?
ipcMain.on('claude-code:query', async (event, { content, files }) => {
// 确保 problems 目录存在
const problemsDir = path.join(process.cwd(), 'agent', 'problems');
if (!fs.existsSync(problemsDir)) {
fs.mkdirSync(problemsDir, { recursive: true });
}
let prompt = content;
// 如果有上传文件
if (files && files.length > 0) {
for (const file of files) {
// 生成唯一文件名:原名_时间戳_随机数.扩展名
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
const ext = path.extname(file.name);
const baseName = path.basename(file.name, ext);
const uniqueName = `${baseName}_${timestamp}_${random}${ext}`;
// 保存到 agent/problems/ 目录
const filePath = path.join(problemsDir, uniqueName);
fs.writeFileSync(filePath, Buffer.from(file.data));
// 把文件路径加到提示词里
prompt += `\n\nUploaded file: ${uniqueName}`;
prompt += `\n(Saved to: ./problems/${uniqueName})`;
}
}
// 然后调用 SDK...
});
文件名加了时间戳和随机数,这样多次上传同名文件不会冲突:
用户上传:Sales.xlsx
保存为:Sales_1704747600000_a3b5c2.xlsx
│ │
原名 时间戳 + 随机数
AI 能看到文件路径,就可以用 Read 工具去读取它。
输出文件检测
AI 工作完成后,主进程需要知道"AI 生成了什么新文件":
// 开始前:记录已有的文件
const outputDir = path.join(process.cwd(), 'agent');
const initialFiles = new Set(
fs.readdirSync(outputDir)
.filter(f => f.endsWith('.xlsx') || f.endsWith('.csv'))
);
// ... AI 工作 ...
// 结束后:找出新增的文件
const finalFiles = fs.readdirSync(outputDir)
.filter(f => f.endsWith('.xlsx') || f.endsWith('.csv'));
const newFiles = finalFiles.filter(f => !initialFiles.has(f));
// 通知前端
if (newFiles.length > 0) {
const outputFiles = newFiles.map(f => ({
name: f,
path: path.join(outputDir, f),
size: fs.statSync(path.join(outputDir, f)).size,
}));
event.reply('claude-code:output-files', outputFiles);
}
逻辑很简单:开始前拍个"快照",结束后对比,多出来的就是新生成的。
文件下载处理
// 用户点击"下载"按钮时
ipcMain.handle('download-file', async (_event, filePath: string) => {
// 弹出系统"另存为"对话框
const { canceled, filePath: savePath } = await dialog.showSaveDialog({
defaultPath: path.basename(filePath),
filters: [
{ name: 'Excel', extensions: ['xlsx'] },
{ name: 'CSV', extensions: ['csv'] },
{ name: 'All Files', extensions: ['*'] },
],
});
if (!canceled && savePath) {
// 复制文件到用户选择的位置
fs.copyFileSync(filePath, savePath);
return { success: true, path: savePath };
}
return { success: false };
});
还有一个"打开输出目录"的功能:
ipcMain.handle('open-output-directory', async () => {
const outputDir = path.join(process.cwd(), 'agent');
// 在文件管理器中打开这个目录
shell.openPath(outputDir);
});
错误处理
try {
const queryIterator = query({ prompt, options: { ... } });
for await (const message of queryIterator) {
messages.push(message);
event.reply('claude-code:response', message);
}
} catch (error) {
console.error('Claude Code SDK error:', error);
// 通知前端出错了
event.reply(
'claude-code:error',
error instanceof Error ? error.message : 'Unknown error'
);
}
主进程的完整职责总结
graph TD
subgraph Main["Electron 主进程"]
W["1. 窗口管理<br/>createWindow() → 创建应用窗口"]
M["2. 消息路由<br/>前端 → IPC → 主进程 → SDK<br/>SDK → 消息流 → 主进程 → IPC → 前端"]
F["3. 文件管理<br/>上传文件 → 存到 agent/problems/<br/>AI 产出 → 检测新 .xlsx/.csv<br/>用户下载 → 弹出「另存为」"]
E["4. 错误处理<br/>SDK 报错 → IPC → 前端显示错误"]
T["5. 任务控制<br/>AbortController → 取消正在运行的任务"]
end
本课小结
- 主进程是 Electron 的"管家",负责窗口、IPC、文件、SDK 调用
- 上传文件通过唯一命名保存到 agent/problems/
- 输出文件通过前后快照对比来检测
- 下载通过系统"另存为"对话框实现
- 所有 SDK 错误都会通过 IPC 传给前端显示
课后练习
- 在 main.ts 里找到
createWindow函数,把窗口大小改成 1280x800,看看效果 - 在文件检测逻辑里加入
.pdf文件的检测 - 想一想:如果 AI 生成了一个很大的文件(100MB),当前的下载机制会不会有问题?