基于Vue3.5和Electron构建跨平台AI聊天桌面应用实战
最近在做一个跨平台 AI 聊天桌面应用目标很简单把大模型对话能力从浏览器里解放出来变成可以常驻系统托盘、一键呼出、随手复制的桌面工具。技术栈选型时用了 Cursor 作为 AI 辅助编程工具Vue 3.5 负责界面层Electron 负责跨平台壳子大模型接口则通过 API 或者本地 Ollama 接入。整个开发过程踩了不少坑也沉淀了一套相对完整的实现路径。本文就从零开始把这个项目的搭建过程、核心代码和常见问题完整梳理出来适合已经掌握 Vue 基础、想尝试 Electron 桌面应用开发同时希望接入大模型能力的开发者参考。1. 背景与核心概念1.1 为什么需要桌面端 AI 聊天应用Web 端的大模型对话工具已经非常成熟打开浏览器就能用。但在实际使用中会遇到几个问题标签页一多聊天窗口容易被埋没浏览器上下文切换频繁复制一段代码或文字再切回聊天页面很打断心流系统级唤起不方便无法通过快捷键快速呼出。桌面应用的天然优势是常驻、隔离、可全局唤起还能读取本地文件、访问系统剪贴板、调用本地模型服务这些能力在浏览器里有较多限制。Electron 是目前构建跨平台桌面应用的主流方案之一它允许开发者使用 HTML、CSS、JavaScript 构建桌面应用。因为底层基于 Chromium 和 Node.js前端开发者可以几乎零成本迁移已有 Web 技能。Vue 3.5 负责界面层的响应式交互Electron 负责窗口、菜单、系统集成等原生能力大模型服务则作为独立的 HTTP 接口存在前后端边界非常清晰。1.2 Cursor 在项目中扮演什么角色Cursor 是一款 AI 驱动的代码编辑器基于 VS Code 的交互模式扩展而来。它在开发这个项目时主要承担三个方面的工作代码生成根据业务描述生成聊天列表组件、设置面板、消息渲染组件。问题定位Electron 启动白屏、IPC 通信不生效、打包后资源路径缺失这类问题可以直接把报错信息发给 Cursor让它分析配置和代码。代码重构把初版混乱的主进程代码拆分成独立的窗口管理、模型调用、配置管理模块。很多读者在搜索“cursor 设置中文”“cursor 汉化”。Cursor 支持通过安装中文语言包来切换界面语言打开 Cursor 后进入扩展面板搜索 Chinese Language Pack 并安装安装完成后使用快捷键打开命令面板输入 Configure Display Language 选择中文并重启即可。需要注意不同版本菜单名称可能有差异以实际安装版本为准。1.3 核心流程拆解整个应用的运行链路可以概括为用户在 Vue 页面输入消息 → 点击发送 → 渲染进程通过 preload 脚本暴露的 API 调用ipcRenderer.invoke→ Electron 主进程收到消息 → 主进程向大模型接口发起 HTTP 请求 → 拿到回复后返回给渲染进程 → Vue 更新消息列表。这个链路里最关键的设计是渲染进程不直接请求大模型接口而是把请求转发给主进程。这样做的好处有两个一是避免前端跨域问题二是 API Key 等敏感信息可以安全地保存在主进程的环境变量或系统配置中不会暴露给 UI 层。2. 环境准备与版本说明2.1 基础环境要求在开始之前先确认本机环境满足以下要求。工具建议版本说明Node.js18 及以上Electron 和 Vite 都依赖较新的 Node 运行时建议使用 LTS 版本包管理器npm / pnpm本文示例使用 npm实际项目也可以用 pnpm 提升依赖安装速度编辑器Cursor安装后建议配置中文语言包方便阅读配置界面Git2.x用于项目版本管理版本说明Vue 3.5、Electron 的版本迭代比较快具体版本号请以安装时的最新稳定版本为准本文示例代码在 Electron 30 和 Vue 3.5 环境下验证通过。如果使用更高版本API 层面的变动一般不大但注意检查 Electron 主进程的默认安全策略是否变化。2.2 大模型服务准备大模型接入部分本文使用 OpenAI 兼容的 HTTP 接口作为示例。这种方式的好处是兼容性最强因为目前很多模型服务和本地推理框架都提供 OpenAI 格式的/chat/completions接口包括 Ollama、vLLM、各类云厂商的模型服务。准备阶段只需要确认两件事有可用的模型服务地址例如https://api.openai.com/v1或者本地 Ollama 服务http://localhost:11434。如果使用需要鉴权的服务准备好 API Key。建议在.env文件中配置不要把 Key 写死在代码中。2.3 项目结构规划为了让后续的代码组织更清晰先规划一下目录结构ai-chat-app/ ├── electron/ │ ├── main.cjs # Electron 主进程入口 │ ├── preload.cjs # 预加载脚本暴露安全 API │ └── model.cjs # 大模型请求封装 ├── src/ # Vue 前端源码 │ ├── App.vue │ ├── main.js │ └── components/ ├── dist/ # 前端构建产物 ├── package.json ├── .env # 环境变量 └── vite.config.js3. 技术栈核心原理拆解3.1 Electron 的进程模型Electron 应用启动后至少有三种进程主进程负责创建窗口、管理应用生命周期、访问系统原生能力。渲染进程负责渲染页面运行 Vue 代码。preload 脚本运行在渲染进程中的隔离上下文可以在不暴露 Node.js 全部能力的情况下通过contextBridge向页面提供有限的 API。这个模型解决了一个核心安全问题默认情况下渲染进程不应该直接访问 Node.js 的fs、process等能力否则一旦页面被注入恶意脚本整个系统都会面临风险。通过 preload 暴露一个白名单 API能够把原生能力限制在最小范围内。3.2 IPC 通信机制Electron 的 IPC进程间通信用于主进程与渲染进程的消息传递。常用的模式有两种ipcRenderer.send/ipcMain.on单向异步通信。ipcRenderer.invoke/ipcMain.handle双向异步通信渲染进程发起请求并等待主进程返回 Promise 结果。本文的项目使用第二种因为聊天请求天然是一个“请求-响应”模型。渲染进程侧通过window.api.chat(messages)发起调用主进程侧通过ipcMain.handle(chat, handler)接收消息并返回大模型的回复。3.3 Vue 3.5 在项目中的使用方式Vue 3.5 的核心依然是组合式 API。文章实现的聊天页面会用到以下核心能力ref管理消息列表、输入框内容、加载状态。computed根据消息内容动态计算一些展示状态。组件通信通过 props 和 emit 封装消息项组件、输入栏组件。生命周期在组件挂载后主动获取系统语言和初始化配置。Vue 3.5 对响应式系统和defineModel等能力做了增强但如果项目里只是基础聊天功能使用常规组合式 API 就够了不需要引入过于复杂的状态管理库。3.4 大模型请求方式大模型请求有两种常见场景一次问答把历史消息数组连同用户的输入一起发给接口模型返回完整回复。流式问答模型逐字返回内容页面像打字机一样持续输出。流式体验更好但前端需要处理 SSEServer-Sent Events协议复杂度更高。本文先从一次问答开始实现这是最容易跑通闭环的方式。流式输出会在扩展思路一节给出方向不做完整实现。4. 从零搭建项目脚手架4.1 使用 Vite 创建 Vue 3.5 项目执行以下命令初始化项目npm create vitelatest ai-chat-app -- --template vue进入项目目录并安装依赖cd ai-chat-app npm install同时安装 Electron 和打包工具npm install -D electron electron-builder注意Electron 的安装包体积比较大如果下载缓慢可以使用国内镜像源例如设置ELECTRON_MIRRORhttps://npmmirror.com/mirrors/electron/具体地址以你所在网络环境为准。4.2 配置 package.json 的入口信息Electron 默认从package.json的main字段读取主进程文件。这里需要新增或修改几个字段{ name: ai-chat-app, version: 1.0.0, main: electron/main.cjs, scripts: { dev: vite, build: vite build, electron:dev: concurrently \vite\ \wait-on http://localhost:5173 electron .\, electron:build: vite build electron-builder } }electron:dev脚本的含义是先用 Vite 启动开发服务器再等待端口 5173 可访问后启动 Electron。这里的concurrently和wait-on需要额外安装npm install -D concurrently wait-on4.3 编写 Electron 主进程创建electron/main.cjs这是整个桌面应用的入口。const { app, BrowserWindow, ipcMain, shell } require(electron); const path require(path); const { handleChat } require(./model.cjs); const isDev !app.isPackaged; function createWindow() { const win new BrowserWindow({ width: 1100, height: 750, title: AI 聊天助手, webPreferences: { preload: path.join(__dirname, preload.cjs), contextIsolation: true, nodeIntegration: false, sandbox: false } }); if (isDev) { win.loadURL(http://localhost:5173); } else { win.loadFile(path.join(__dirname, ../dist/index.html)); } win.webContents.setWindowOpenHandler(({ url }) { shell.openExternal(url); return { action: deny }; }); } app.whenReady().then(() { createWindow(); app.on(activate, () { if (BrowserWindow.getAllWindows().length 0) createWindow(); }); }); app.on(window-all-closed, () { if (process.platform ! darwin) { app.quit(); } }); ipcMain.handle(chat, handleChat);关键配置逐条解释preload指定预加载脚本用于向页面暴露安全的 API。contextIsolation: true开启上下文隔离防止渲染进程直接访问 Node 能力这是现代 Electron 应用的安全基线。nodeIntegration: false禁止页面直接使用 Node.js避免 XSS 漏洞升级为远程代码执行。setWindowOpenHandler拦截页面打开的窗口统一交给系统浏览器处理避免在应用内弹出不可控的新窗口。4.4 编写 preload 脚本创建electron/preload.cjsconst { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(api, { chat: (messages) ipcRenderer.invoke(chat, messages), getLocale: () ipcRenderer.invoke(get-locale) });这样在 Vue 页面里就可以直接使用window.api.chat(messages)来发起聊天请求而不需要关心底层 IPC 是如果实现的。同时需要给 Vue 提供类型声明便于编辑器提示。在src目录下创建electron-api.d.tsinterface Window { api: { chat: (messages: { role: string; content: string }[]) Promisestring; getLocale: () Promisestring; }; }5. 实现聊天主界面5.1 定义消息数据结构消息对象需要保持与大模型接口兼容的格式{ role: user | assistant, content: 消息内容 }role表示对话角色user是用户消息assistant是模型回复。发送给大模型时直接传整个消息数组模型就能根据上下文生成回复。5.2 编写 App.vue打开src/App.vue替换为以下代码script setup import { ref } from vue; const messages ref([ { role: assistant, content: 你好我是你的 AI 助手有什么可以帮你 } ]); const inputText ref(); const loading ref(false); async function sendMessage() { const text inputText.value.trim(); if (!text || loading.value) return; messages.value.push({ role: user, content: text }); inputText.value ; loading.value true; try { const reply await window.api.chat(messages.value); messages.value.push({ role: assistant, content: reply }); } catch (error) { messages.value.push({ role: assistant, content: 请求失败 (error.message || 未知错误) }); } finally { loading.value false; } } /script template div classchat-container header classchat-header spanAI 聊天助手/span /header main classchat-body div v-for(msg, index) in messages :keyindex classmessage :classmsg.role {{ msg.content }} /div div v-ifloading classmessage assistant 正在输入... /div /main footer classchat-footer input v-modelinputText typetext placeholder输入消息按回车发送 keyup.entersendMessage / button :disabledloading clicksendMessage 发送 /button /footer /div /template style scoped .chat-container { display: flex; flex-direction: column; height: 100vh; } .chat-header { padding: 12px 16px; border-bottom: 1px solid #e5e5e5; font-weight: 600; } .chat-body { flex: 1; overflow-y: auto; padding: 16px; background: #f7f7f8; } .message { max-width: 70%; margin-bottom: 12px; padding: 10px 14px; border-radius: 8px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; } .message.user { margin-left: auto; background: #1677ff; color: #fff; } .message.assistant { background: #fff; border: 1px solid #e5e5e5; } .chat-footer { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #e5e5e5; } .chat-footer input { flex: 1; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 6px; outline: none; } .chat-footer button { padding: 8px 16px; border: none; border-radius: 6px; background: #1677ff; color: #fff; cursor: pointer; } .chat-footer button:disabled { background: #a0cfff; cursor: not-allowed; } /style这段代码虽然不长但已经构成了一个最小可用的聊天界面。loading状态的作用是防止用户连续点击发送导致重复请求消息列表使用v-for渲染新的回复会自动滚动到可见区域需要配合scrollIntoView这部分后续可以优化。5.3 生成 markdown 渲染能力大模型返回的内容往往包含 Markdown 格式直接以纯文本渲染会丢失结构。可以引入marked库将 Markdown 转为 HTML然后通过v-html渲染。但这里提醒一句v-html渲染不可信内容有 XSS 风险实际使用时要增加 DOMPurify 库进行过滤这也是工程落地时不能跳过的步骤。6. 接入大模型服务6.1 OpenAI 兼容接口请求封装创建electron/model.cjs实现大模型请求逻辑。async function handleChat(event, messages) { const apiKey process.env.AI_API_KEY; const baseUrl process.env.AI_BASE_URL || https://api.openai.com/v1; const model process.env.AI_MODEL || gpt-4o-mini; if (!apiKey) { throw new Error(未配置 AI_API_KEY); } const controller new AbortController(); const timeout setTimeout(() controller.abort(), 30 * 1000); try { const response await fetch(${baseUrl}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model, messages, temperature: 0.7 }), signal: controller.signal }); if (!response.ok) { const errorText await response.text(); throw new Error(AI 请求失败${response.status} ${errorText}); } const data await response.json(); return data.choices[0].message.content; } finally { clearTimeout(timeout); } } module.exports { handleChat };这段代码的几个细节值得展开使用AbortController实现 30 秒超时避免模型接口无响应时前端一直 loading。API Key 从环境变量process.env.AI_API_KEY读取不硬编码在代码里。接口地址支持通过AI_BASE_URL配置这样切换到不同模型服务只需要改环境变量。6.2 本地 Ollama 模型接入本地部署大模型的场景越来越常见。Ollama 是一个非常流行的本地推理工具它默认监听http://localhost:11434并且提供了 OpenAI 风格和原生两种接口。如果你不想依赖云端 API可以改用以下方式async function handleChat(event, messages) { const response await fetch(http://localhost:11434/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ model: qwen2.5, messages, stream: false }) }); if (!response.ok) { throw new Error(Ollama 请求失败${response.status}); } const data await response.json(); return data.message.content; }使用本地模型的好处是数据不出本机、无需支付 API 费用但需要电脑具备一定的显存或内存。模型的选择和部署不在本文展开只需要记住接入方式仍然是 HTTP 请求主进程的逻辑没有本质变化。6.3 在 Vue 页面中调用回到前端发送消息后只需要调用const reply await window.api.chat(messages.value);此时完整链路就打通了。为了让输入框支持回车发送Vue 模板里已经绑定了keyup.enter。启动项目后输入一句话点击发送就能看到模型回复出现在消息列表里。7. 运行验证与跨平台打包7.1 开发模式运行开发模式下需要同时启动 Vite 和 Electron。执行npm run electron:dev如果一切正常会弹出桌面窗口加载 Vue 页面。打开开发者工具可以切换到浏览器控制台查看 IPC 调用是否有报错信息。7.2 项目打包为桌面应用使用 electron-builder 进行打包。在package.json中增加简化配置{ build: { appId: com.example.aichat, productName: AIChat, files: [ dist/**/*, electron/**/*, package.json ], win: { target: nsis }, mac: { target: dmg }, linux: { target: AppImage } } }然后执行npm run electron:build打包产物会生成在release目录中。不同平台的打包需要对应的系统环境例如 macOS 的 dmg 打包需要在 macOS 上执行Windows 的 nsis 打包最好在 Windows 上执行。7.3 Electron 壳子加载本地页面还是远程 URL经常有开发者问“我想使用 Electron 把 URL 打包进去是否可行”。答案是可行的但需要区分两种场景页面资源全部本地化使用win.loadFile()加载dist/index.html优点是可以离线使用启动更快推荐聊天工具采用这种方式。加载远程 URL使用win.loadURL(https://example.com)相当于给已有网页套了个桌面壳更新网页不需要重新发布应用。但需要注意登录态、资源缓存、CSP 配置、外链跳转等细节。从用户体验和安全角度如果你的页面本身是远程 Web 服务同时需要保持在线登录态套壳是合理的但如果是纯客户端工具本地加载体验更稳定。7.4 获取系统语言和菜单定制Electron 可以轻松获取系统语言。在主进程注册一个 IPC 处理函数const { app } require(electron); ipcMain.handle(get-locale, () { return app.getLocale(); });前端调用window.api.getLocale()即可拿到当前系统语言用于切换 UI 文案。这种做法在国产系统分发场景中比较实用例如判断系统语言后调整界面默认值。菜单方面Electron 默认有系统菜单。如果希望完全自定义可以调用Menu.setApplicationMenu或Menu.buildFromTemplate。需要注意的是macOS 上应用菜单是全局菜单不能直接移除否则会导致复制粘贴快捷键失效这也是一个常见坑点。8. 常见问题与排查思路问题现象常见原因解决思路Electron 启动后白屏Vite 开发服务器未启动或加载路径错误确认electron:dev脚本中 wait-on 端口一致打包模式检查 dist 路径是否存在preload 脚本不生效路径写错或者 sandbox 配置导致 require 不可用确认 preload 使用绝对路径必要时设sandbox: false但不要同时开启 nodeIntegrationVue 页面调用window.api.chat报错preload 未正确暴露 API或类型声明缺失打印window.api看是否存在确认 contextBridge 暴露的 key 名称跨域请求大模型接口失败渲染进程直接 fetch 远程接口改为在主进程发请求或在 Vite 配置 proxy但 production 环境仍需主进程代理打包后找不到dist/index.html主进程路径基于__dirname层级不对使用path.join(__dirname, ../dist/index.html)并检查 files 配置是否包含 dist国内网络下 electron 下载慢npm 默认下载源访问慢设置镜像源例如ELECTRON_MIRROR或使用 npmmirror 镜像Cursor 界面不是中文未安装语言包或未切换显示语言安装 Chinese Language Pack通过命令面板切换显示语言9. 最佳实践与工程建议9.1 安全基线配置Electron 应用最大的安全风险来自渲染进程被注入恶意代码。因此建议从项目一开始就坚持以下配置contextIsolation: true和nodeIntegration: false必须开启。preload 只暴露最小必要 API不暴露ipcRenderer原始对象。页面中不要直接拼接 HTML 渲染大模型返回的内容必须经过过滤。后端接口地址和 API Key 放在环境变量中使用dotenv之类的库启动时加载。9.2 环境变量与配置管理开发环境使用.env文件生产环境通过系统环境变量注入。示例.envAI_API_KEYyour-api-key AI_BASE_URLhttps://api.openai.com/v1 AI_MODELgpt-4o-mini主进程启动时用dotenv加载npm install dotenvrequire(dotenv).config();注意.env文件不要提交到 Git建议在.gitignore中添加。9.3 请求失败重试和用户提示大模型接口不稳定是常态。建议在请求层增加错误分类网络错误提示“网络连接失败请检查网络”。超时错误提示“请求超时请稍后重试”。模型接口返回错误透传状态码和错误信息便于排查。重试机制需要谨慎使用只有幂等的请求比如重新生成回复才适合自动重试常规聊天消息重试会导致用户重复提交。9.4 性能优化方向聊天应用的主要性能瓶颈在长文本渲染。当历史消息超过几十条每条消息都是大字段时DOM 渲染会变得卡顿。可以从几个方向优化将消息列表拆成子组件避免整个页面重复渲染。使用虚拟列表只渲染可视区域的消息。长回复折叠默认只展示前 300 字点击展开完整内容。本地维护会话记录切换会话时延迟加载消息。9.5 国产系统分发注意点Electron 应用在国产 Linux 系统上分发时除了常规的 AppImage/deb 打包还要确认目标机器架构。当前很多国产系统设备使用 ARM 架构需要下载对应的 Electron ARM 版本否则会出现启动失败或白屏。建议在目标机器上做一轮完整的功能验证包括窗口创建、系统托盘、中文输入法、文件读写权限。如果使用系统 WebView 替代方案需要额外评估兼容性不在本文范围之内。10. 下一步学习路线到这里一个最小可用的跨平台 AI 桌面聊天应用已经跑通。回顾一下实现的完整链路Vue 3.5 负责界面交互Electron 主进程负责桌面壳子和系统级能力preload 脚本负责安全桥接大模型接口负责生成智能回复。如果继续深入可以考虑以下方向使用 SSE 实现流式回复让模型回答像打字机一样输出体验更接近 ChatGPT。增加会话管理持久化对话记录到本地 SQLite 或 JSON 文件。增加系统托盘和全局快捷键实现一键唤起。引入 RAG把本地文档作为知识库让模型基于文档内容回答。使用 Cursor 配合重构代码逐步抽取独立的model-provider、conversation-store等模块提升可维护性。桌面端 AI 应用的技术栈已经非常成熟Electron 的生态也足以支撑从工具型应用到专业级产品的落地。建议直接打开 Cursor按本文步骤创建一个项目把第一个聊天闭环跑起来再根据真实使用感受逐步迭代功能和体验。