用 Electron + Vue 3 给 AI 一体机打造「iPad 式」工作区 Launcher
在一台预装了多个 AI 服务(大模型对话、知识库、智能问数)的 Windows 一体机上,如何让非技术用户也能「开箱即用」?我们用 Electron 33 + Vue 3.4 写了一个全屏工作区 Launcher,把多个 Web 应用整合成一个类似 iPad 的桌面体验。本文聊聊架构、关键决策和踩过的坑。
一、需求:AI 一体机的「最后一公里」
一台 AI 一体机,背后跑了五六个服务:
- AI 对话(Open WebUI,端口 5173)
- 知识库(RAG 检索系统,端口 80)
- 智能问数(数据分析,端口 8000)
- OpenClaw(开发工具平台,端口 18789)
- 文档管理(内置文件管理器)
对运维和开发者来说,记住这些端口、手动打开浏览器逐个访问,没什么难度。但对最终用户——可能是办公室行政、培训学员、一线业务员——这就不友好了。
核心诉求很明确:
- 开机后看到的不应该是 Windows 桌面 + 一堆需要手动打开的浏览器标签页,而是一个简洁的”应用桌面”
- 点图标就能用,像 iPad 一样
- 服务挂了要有提示,而不是白屏让用户猜
- 最好能把 Windows 桌面完全藏起来(Kiosk 模式)
这就是 AI Launcher 的由来。
二、技术选型:为什么是 Electron?
选型时考虑过几个方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 纯 Web(Kiosk 浏览器) | 轻量 | 无法调用系统 API(文件管理、USB 检测、电源控制) |
| WPF / WinForms | 原生体验 | 开发效率低,毛玻璃效果复杂 |
| Tauri | 体积小 | Windows API 生态不如 Electron 成熟 |
| Electron | 成熟、文档全、BrowserView 正好解决需求 | 包体积大(约 188 MB) |
最终选 Electron,核心原因是它的 BrowserView API —— 可以在一个主窗口内嵌另一个完整的 Chromium 渲染进程,完美实现「桌面 + 应用内嵌」的分层架构。
技术栈清单
| 层级 | 技术 | 版本 |
|---|---|---|
| 桌面框架 | Electron | 33.4 |
| 前端框架 | Vue 3(Composition API) | 3.4 |
| 状态管理 | Pinia | 2.1 |
| 构建工具 | Vite | 6 |
| 开发语言 | TypeScript | 5.6 |
| 打包工具 | electron-builder | 25 |
| UI 风格 | Glass-morphism(毛玻璃) | — |
三、架构设计:单窗口 + BrowserView 分层
整体架构可以用一句话概括:一个全屏无框窗口,通过 BrowserView 实现”桌面”和”应用”两种视图的切换。
┌──────────────────────────────────────────────┐
│ Electron 主进程 (Node.js) │
│ ┌───────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ConfigLoader│ │HealthMon │ │ IPC Handlers│ │
│ │(配置热加载) │ │(健康监控) │ │(文件/系统/电源)│ │
│ └───────────┘ └──────────┘ └─────────────┘ │
├──────────────────────────────────────────────┤
│ BrowserWindow (全屏、无框) │
│ ┌────────────────────────────────────────┐ │
│ │ Vue 3 渲染进程 —— 桌面视图 │ │
│ │ ┌──────┐ ┌────────────┐ │ │
│ │ │状态栏 │ │ 搜索栏 │ │ │
│ │ └──────┘ └────────────┘ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ App 图标网格 │ │ │
│ │ │ [📝][💬][📚][🦞][⚙️] │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Dock 栏 (macOS 风格) │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ BrowserView —— 应用内嵌视图 │ │
│ │ (覆盖整个窗口,显示目标 Web 应用) │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ ◀ 返回 │ AI 对话 (Web UI) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
关键设计决策:
-
单窗口,非多窗口 —— 不用
BrowserWindow打开多个独立窗口,而是用BrowserView在主窗口内覆盖渲染。这样没有任务栏多窗口切换的问题,也更贴近 iPad 的”全屏应用”体验。 -
主进程不管理业务进程生命周期 —— 后端的 AI 服务全部注册为 Windows 服务(由 SCM 管理),Launcher 只负责前端展示和健康检查。职责分离,避免 Launcher 崩溃导致所有服务跟着挂。
-
配置驱动 —— 所有应用入口、壁纸、布局参数都写在
apps.json里,修改配置后自动热加载,无需重启。
四、核心模块拆解
4.1 配置系统:热加载 + 校验 + 备份
ConfigLoader 是主进程的核心模块之一,负责加载、保存和监听 apps.json。
三个设计亮点:
原子写入防损坏
private async writeAtomic(filePath: string, content: string): Promise<void> {
const tmpPath = filePath + '.tmp';
await fs.promises.writeFile(tmpPath, content, 'utf-8');
// 先验证是合法 JSON
JSON.parse(await fs.promises.readFile(tmpPath, 'utf-8'));
await fs.promises.rename(tmpPath, filePath); // 原子替换
}
写配置时先写到临时文件,验证 JSON 合法后再 rename。Linux 下 rename 是原子操作,Windows 上也是(NTFS)。即使写入中途断电,也不会得到一个半截的配置文件。
备份恢复
主配置文件损坏时自动从 .bak 恢复,备份也损坏则降级到最小可用配置。用户永远不会看到”配置错误白屏”。
热加载 + 防抖
watch(callback: () => void) {
this.watcher = fs.watch(dir, (eventType, filename) => {
if (filename && filename.includes('apps.json')) {
// Windows 上 fs.watch 会连发多次事件,加 200ms 防抖
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.cache = null; // 清缓存,下次 load 重新读取
callback();
}, 200);
}
});
}
Windows 的 fs.watch 有个坑:修改一个文件会触发 3-4 次事件。通过 debounce 合并为一个回调,避免重复加载。
4.2 健康监控:30 秒轮询,实时状态反馈
每个 Web 应用可配置 healthCheck URL,HealthMonitor 每 30 秒并发轮询所有服务:
private async checkAll() {
const checks = this.apps.map(app => this.performCheck(app));
const results = await Promise.all(checks); // 并行,不串行等
// ... 状态变化检测与事件派发
}
并行检查而不是串行,确保不会因为一个慢服务阻塞整体检查周期。状态变化时通过 IPC 发送到前端,应用图标右下角的绿/红圆点实时更新。
容错设计:即使健康检查失败(红点),用户仍然可以点击打开应用。健康检查只是”提示”,不是”拦截”。
4.3 BrowserView 应用内嵌:注入返回按钮
这是整个项目最有意思的部分。当用户在桌面点击某个应用图标时:
- 主进程创建
BrowserView,加载目标 URL - BrowserView 覆盖整个主窗口(留出顶部导航条空间)
- 在页面中注入一个”返回”浮动按钮
注入返回按钮的实现值得细说:
appView.webContents.on('dom-ready', () => {
injectBackButton();
});
function injectBackButton() {
appView?.webContents.executeJavaScript(`
(() => {
if (document.getElementById('__launcher-back-btn')) return true;
const btn = document.createElement('button');
btn.id = '__launcher-back-btn';
btn.innerHTML = '← 返回';
btn.style.cssText = 'position:fixed;top:8px;left:8px;z-index:999999;...';
btn.addEventListener('click', () => {
window.location.href = 'launcher://close';
});
document.body.appendChild(btn);
return true;
})();
`).then((injected) => {
// SPA 场景:CSP 可能阻止注入,重试 5 次
if (!injected && injectRetries < 5) {
setTimeout(injectBackButton, 500 * (injectRetries + 1));
}
});
}
为什么这么复杂?
- 目标 Web 应用是 SPA(单页应用),
did-finish-load只在初始加载时触发一次,页面内部路由切换不触发 - 改用
dom-ready事件,每次页面导航(包括 SPA 路由变化)都会触发 - 但
dom-ready可能在 DOM 还没完全就绪时触发,所以加了一个重试机制,指数退避(500ms, 1000ms, 1500ms…) - 如果目标站有严格的 CSP(Content Security Policy),
executeJavaScript会被拒绝,重试 5 次后放弃
返回按钮点击后导航到 launcher://close(自定义协议),主进程拦截这个 URL,关闭 BrowserView。
4.4 文件管理器:安全沙箱内的文件浏览
内置的文档管理器需要访问本地文件系统,但不能让用户随意访问整个 C 盘(尤其是 C:\Windows、C:\Program Files 等敏感目录)。
安全策略:
白名单路径:
C:\Users
D:\ ~ O:\ (数据盘 + 移动设备)
黑名单路径:
C:\Windows
C:\Program Files
C:\Program Files (x86)
C:\$Recycle.Bin
System Volume Information
所有文件操作前必须经过 validatePath() 校验,防止路径穿越攻击。USB 盘符在运行时动态注册到白名单。
功能完整度:
- 磁盘列表(含 U 盘自动检测)
- 面包屑导航 + 分页加载(大目录每批 200 条)
- 文件预览(图片缩略图、文本前 2000 字符)
- Office/WPS 自动检测并关联打开
- Ctrl/Shift 多选、搜索、复制/移动/删除
4.5 Kiosk 模式:替换 Windows Shell
一体机部署时,最干净的方案是把 Windows Shell 替换为 Launcher 本身——无桌面、无任务栏、无开始菜单。
通过 Windows 的 Client-EmbeddedShellLauncher 功能实现:
# 启用 Embedded Shell Launcher 功能
Enable-WindowsOptionalFeature -Online -FeatureName Client-EmbeddedShellLauncher
# 设置 Launcher 为默认 Shell
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
-Name "Shell"
-Value "D:\launcher\AILauncher.exe"
关键:必须留好后门快捷键,否则设备锁死无法维护。
| 快捷键 | 功能 |
|---|---|
Ctrl+Shift+F1 | 启动 explorer.exe 恢复桌面 |
Ctrl+Shift+F2 | 恢复 Launcher 全屏 |
Ctrl+Shift+Esc | 打开任务管理器 |
Ctrl+Shift+R | 重启 Launcher |
Ctrl+Shift+D | 打开 DevTools |
同时拦截 Alt+F4 和 Ctrl+W,防止用户误关闭。
4.6 自动启动配套服务
Launcher 启动时可通过 launchPrograms 配置自动拉起配套的后端服务:
"launchPrograms": [
{
"id": "open-webui-backend",
"path": "D:\\tool\\open-webui\\backend\\cmd_start_windows.bat",
"delay": 5000
}
]
通过 cmd /c start /min 以最小化窗口启动,detached 模式运行,child.unref() 确保 Launcher 退出时不影响这些服务。
五、UI 设计:毛玻璃 + iPad 风格
前端采用 Glass-morphism(毛玻璃) 风格:
.app-grid-glass {
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(32px) saturate(1.5);
-webkit-backdrop-filter: blur(32px) saturate(1.5);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}
壁纸支持自定义路径 + 模糊开关。Grid 列数、图标大小、Dock 开关、搜索栏开关,全都可以通过设置面板实时调整。
底部 Dock 栏仿 macOS,鼠标悬停时图标放大并上移,支持排序和添加/移除。
六、踩过的坑
6.1 Electron 33 的 BrowserView.destroy() 被移除
Electron 30+ 移除了 BrowserView.destroy() 方法。代码里只需要 removeBrowserView() 然后把引用置 null 即可,V8 GC 会自动清理。
6.2 Windows 文件系统编码问题
从 Windows 打包/传输到 Linux 环境时,中文文件名会出现乱码(GBK 被错误解析为 UTF-8)。处理时需要把 UTF-8 解码的 mojibake 字符还原为原始字节,再用 GBK 重新解码。这也是为什么开发环境最好保持和部署环境一致的编码。
6.3 SPA 应用的路由导航拦截
BrowserView 中加载 SPA 应用时,will-navigate 事件只在页面级导航时触发(比如从 A 页面跳转到 B 页面),SPA 内部路由切换(如 #/chat → #/settings)不会触发。这也是为什么注入返回按钮必须监听 dom-ready 而不是 will-navigate。
6.4 壁纸路径的跨平台处理
配置中壁纸路径可能是绝对路径(D:\wallpaper.jpg)或相对路径(assets/wallpaper.png)。开发环境(Linux)和运行环境(Windows)的路径格式不同,需要兼容处理:
if (path.isAbsolute(wp) || wp.startsWith('/')) {
fullPath = wp; // 绝对路径直接用
} else {
const basePath = app.isPackaged ? process.resourcesPath : app.getAppPath();
fullPath = path.join(basePath, wp); // 相对路径基于资源目录解析
}
七、项目数据
| 指标 | 数值 |
|---|---|
| 源代码文件 | 40+ |
| 总代码量 | ~5,000 行 |
| 打包产物大小 | ~188 MB |
| 应用入口 | 5 个(可配置扩展) |
| 支持模式 | 普通桌面 / Kiosk 一体机 |
| 配置热更新 | ✅ 无需重启 |
| 健康监控 | ✅ 30 秒轮询 |
| USB 检测 | ✅ 轮询 + 动态白名单 |
八、总结
AI Launcher 本质上是一个企业级 Kiosk 启动器,但它没有选择传统的”锁定屏幕”路线,而是走了一条更柔和的路径——
用 iPad 式的简洁界面,降低用户的技术门槛;用 Electron 的系统能力,保障运维的可控性。
几个值得记录的设计原则:
- 配置驱动优于硬编码 —— 所有应用入口、布局参数都可配置,换一台机器只需改一个 JSON
- 职责分离 —— Launcher 不管理业务进程,只做前端聚合和健康检查
- 安全沙箱 —— 文件管理器有严格的白名单/黑名单,不能随意访问系统目录
- 永远留后门 —— Kiosk 模式下的快捷键后门,是部署时的生命线
如果你对完整源码感兴趣,或者想聊聊 Electron 桌面应用的开发经验,欢迎交流。
技术栈:Electron 33 + Vue 3.4 + TypeScript 5.6 + Vite 6
项目:Windows 11 AI 一体机工作区 Launcher