← 返回博客

用 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 渲染进程,完美实现「桌面 + 应用内嵌」的分层架构。

技术栈清单

层级技术版本
桌面框架Electron33.4
前端框架Vue 3(Composition API)3.4
状态管理Pinia2.1
构建工具Vite6
开发语言TypeScript5.6
打包工具electron-builder25
UI 风格Glass-morphism(毛玻璃)

三、架构设计:单窗口 + BrowserView 分层

整体架构可以用一句话概括:一个全屏无框窗口,通过 BrowserView 实现”桌面”和”应用”两种视图的切换。

┌──────────────────────────────────────────────┐
│         Electron 主进程 (Node.js)             │
│  ┌───────────┐ ┌──────────┐ ┌─────────────┐ │
│  │ConfigLoader│ │HealthMon │ │  IPC Handlers│ │
│  │(配置热加载) │ │(健康监控) │ │(文件/系统/电源)│ │
│  └───────────┘ └──────────┘ └─────────────┘ │
├──────────────────────────────────────────────┤
│         BrowserWindow (全屏、无框)            │
│  ┌────────────────────────────────────────┐ │
│  │  Vue 3 渲染进程 —— 桌面视图              │ │
│  │  ┌──────┐ ┌────────────┐              │ │
│  │  │状态栏 │ │ 搜索栏      │              │ │
│  │  └──────┘ └────────────┘              │ │
│  │  ┌──────────────────────────────┐    │ │
│  │  │      App 图标网格             │    │ │
│  │  │  [📝][💬][📚][🦞][⚙️]        │    │ │
│  │  └──────────────────────────────┘    │ │
│  │  ┌──────────────────────────────┐    │ │
│  │  │   Dock 栏 (macOS 风格)        │    │ │
│  │  └──────────────────────────────┘    │ │
│  └────────────────────────────────────────┘ │
│                                             │
│  ┌────────────────────────────────────────┐ │
│  │  BrowserView —— 应用内嵌视图            │ │
│  │  (覆盖整个窗口,显示目标 Web 应用)       │ │
│  │  ┌─────────────────────────────────┐  │ │
│  │  │ ◀ 返回 │  AI 对话 (Web UI)     │  │ │
│  │  └─────────────────────────────────┘  │ │
│  └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘

关键设计决策:

  1. 单窗口,非多窗口 —— 不用 BrowserWindow 打开多个独立窗口,而是用 BrowserView 在主窗口内覆盖渲染。这样没有任务栏多窗口切换的问题,也更贴近 iPad 的”全屏应用”体验。

  2. 主进程不管理业务进程生命周期 —— 后端的 AI 服务全部注册为 Windows 服务(由 SCM 管理),Launcher 只负责前端展示和健康检查。职责分离,避免 Launcher 崩溃导致所有服务跟着挂。

  3. 配置驱动 —— 所有应用入口、壁纸、布局参数都写在 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 应用内嵌:注入返回按钮

这是整个项目最有意思的部分。当用户在桌面点击某个应用图标时:

  1. 主进程创建 BrowserView,加载目标 URL
  2. BrowserView 覆盖整个主窗口(留出顶部导航条空间)
  3. 在页面中注入一个”返回”浮动按钮

注入返回按钮的实现值得细说:

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:\WindowsC:\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+F4Ctrl+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 的系统能力,保障运维的可控性。

几个值得记录的设计原则:

  1. 配置驱动优于硬编码 —— 所有应用入口、布局参数都可配置,换一台机器只需改一个 JSON
  2. 职责分离 —— Launcher 不管理业务进程,只做前端聚合和健康检查
  3. 安全沙箱 —— 文件管理器有严格的白名单/黑名单,不能随意访问系统目录
  4. 永远留后门 —— Kiosk 模式下的快捷键后门,是部署时的生命线

如果你对完整源码感兴趣,或者想聊聊 Electron 桌面应用的开发经验,欢迎交流。


技术栈:Electron 33 + Vue 3.4 + TypeScript 5.6 + Vite 6
项目:Windows 11 AI 一体机工作区 Launcher