🖥️ Electron 三进程 Host 实战:从架构设计到生产落地的完整指南

基于真实开源项目 hezihua/dsh-desktop(DeepSeek Harness 桌面壳)完整源码,深度拆解主进程 / 预加载 / 渲染进程的分层设计,附真实配置文件与避坑指南。


一、架构总览:为什么是"三进程 Host"模式?

dsh-desktop 不是把网页包成 exe 的普通 Electron 应用。它的核心设计是:

Electron 只负责"壳"------窗口、生命周期、系统整合;真正的业务逻辑(AI 引擎、对话、插件)全部交给独立的 Node.js 子进程 dsh web

这意味着渲染进程不直接操作文件系统、不调用 AI 模型 。官方 UI 是一个独立的 Web 服务,通过 WebContentsView 嵌入;壳层(React)只画标题栏、启动页、更新弹层。

scss 复制代码
┌─────────────────────────────────────────┐
│           主进程 (Main Process)          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │ 窗口管理 │  │ Harness │  │ 系统托盘 │ │
│  │ window.ts│  │ 引擎进程 │  │ tray.ts │ │
│  └────┬────┘  └────┬────┘  └─────────┘ │
│       │            │                    │
│  ┌────▼────────────▼────┐              │
│  │   IPC 路由中心 main.ts│              │
│  └────┬─────────────────┘              │
└───────┼─────────────────────────────────┘
        │ contextBridge (preload.ts)
┌───────▼─────────────────────────────────┐
│      预加载脚本 (Preload)                │
│  window.desktop  /  window.updater     │
└───────┬─────────────────────────────────┘
        │
┌───────▼─────────────────────────────────┐
│      渲染进程 (Renderer / 壳)            │
│  标题栏 TitleBar.tsx                    │
│  启动页 App.tsx                         │
│  官方 UI (WebContentsView, 无 preload)  │
└─────────────────────────────────────────┘

二、核心模块实战:基于真实源码的深度解析

2.1 窗口分层:无边框壳 + WebContentsView

Electron 30+ 废弃了 BrowserView,改用 WebContentsViewdsh-desktopelectron/window.ts 中的实现非常干净:

typescript 复制代码
// electron/window.ts
export class DesktopShell {
  window: BrowserWindow | null = null
  private view: WebContentsView | null = null  // 官方 UI
  private harnessUrl: string | null = null
  private overlay = false

  hideOnClose = false

  create(): BrowserWindow {
    const win = new BrowserWindow({
      width: 1280,
      height: 840,
      minWidth: 960,
      minHeight: 640,
      show: false,
      frame: false,              // 自己画标题栏
      autoHideMenuBar: true,
      backgroundColor: '#10141a',
      title: 'DeepSeek Harness',
      webPreferences: {
        preload: join(__dirname, 'preload.js'),  // 壳层有 preload
        contextIsolation: true,
        nodeIntegration: false,
        sandbox: true,
        webSecurity: true,
      },
    })

    // 壳层加载本地 React 构建产物
    if (process.env.VITE_DEV_SERVER_URL) {
      win.loadURL(process.env.VITE_DEV_SERVER_URL)
    } else {
      win.loadFile(join(__dirname, '../dist/index.html'))
    }

    // 导航锁:防止页面逃逸
    win.webContents.setWindowOpenHandler(({ url }) => {
      if (/^https?:/.test(url)) void shell.openExternal(url)
      return { action: 'deny' }
    })
    win.webContents.on('will-navigate', (event, url) => {
      if (url.startsWith('file:')) return
      if (process.env.VITE_DEV_SERVER_URL && url.startsWith(process.env.VITE_DEV_SERVER_URL)) return
      event.preventDefault()
      if (/^https?:/.test(url)) void shell.openExternal(url)
    })

    // 关闭进托盘
    win.on('close', (event) => {
      if (!this.hideOnClose) return
      event.preventDefault()
      win.hide()
    })

    return win
  }

  showHarness(url: string): void {
    if (!this.view) {
      this.view = new WebContentsView({
        webPreferences: {
          contextIsolation: true,
          nodeIntegration: false,
          sandbox: true,
          webSecurity: true,
          // 🔒 注意:官方页没有 preload!
        },
      })
      this.view.setBackgroundColor('#10141a')
      
      // 官方页也有自己的导航锁
      this.view.webContents.setWindowOpenHandler(({ url: next }) => {
        if (/^https?:/.test(next) && !/^http:\/\/127\.0\.0\.1:\d+/.test(next)) {
          void shell.openExternal(next)
          return { action: 'deny' }
        }
        return { action: 'allow' }
      })
    }

    win.contentView.addChildView(this.view)
    this.layout()
    void this.view.webContents.loadURL(url)
  }

  hideHarness(): void {
    this.detachView()  // removeChildView,不是 destroy
    this.harnessUrl = null
  }

  setOverlay(visible: boolean): void {
    this.overlay = visible  // 弹层时隐藏官方页
    this.layout()
  }

  private layout(): void {
    const [width, height] = win.getContentSize()
    this.view.setBounds({
      x: 0,
      y: TITLEBAR_HEIGHT,  // 36px 标题栏
      width,
      height: Math.max(0, height - TITLEBAR_HEIGHT),
    })
  }
}

关键设计决策

  1. 壳有 preload,官方页没有 ------ 官方 UI 走标准 Web 安全模型,防止内部 API 泄露
  2. removeChildView 而非 destroy() ------ 弹层隐藏时复用 WebContents,避免重复创建
  3. 双导航锁 ------ 壳和官方页各自独立拦截外链

2.2 引擎管理:HarnessServer 的"启动-附着-重启"三态机

这是整个项目最复杂的模块。dsh web 可能由三种方式提供:

场景 行为
本机已有 dsh web(端口 3080) 附着,不杀别人的进程
全新启动 自有,退出时负责清理
安全模式 独立数据目录,不加载 ~/.dsh 插件
typescript 复制代码
// electron/harness.ts
export class HarnessServer {
  url = ''
  owned = false        // 是否自己拉起的
  private child: ChildProcess | undefined

  async startWithRetry(retries = START_RETRIES): Promise<string> {
    // 1. 先尝试附着已有服务
    if (this.options.attach !== false) {
      const attached = await this.tryAttach()
      if (attached) return attached
    }

    // 2. 自起服务,带重试和指数退避
    let lastError: unknown
    for (let attempt = 1; attempt <= retries; attempt++) {
      try {
        return await this.startOwned()
      } catch (error) {
        lastError = error
        await this.stop()
        if (attempt < retries) await sleep(1000 * attempt)
      }
    }
    throw lastError instanceof Error ? lastError : new Error(String(lastError))
  }

  private async tryAttach(): Promise<string | null> {
    const candidates = [
      ...(process.env.DSH_DESKTOP_PORT ?? '').split(',').map(Number),
      'http://127.0.0.1:3080',  // 默认端口
    ]
    
    for (const url of candidates) {
      // 校验:页面必须包含 DeepSeek Harness 特征
      if (await isDshSurface(url)) {
        this.url = url
        this.owned = false   // 附着模式:不拥有进程
        log.info(`附着到已有 Harness:${url}`)
        return url
      }
    }
    return null
  }

  private async startOwned(): Promise<string> {
    const entry = resolveDshEntry(this.options.appPath)
    const runtime = await resolveNodeRuntime(this.options.packaged)
    
    // 关键:spawn 前清理 ELECTRON_RUN_AS_NODE
    const env: NodeJS.ProcessEnv = { ...process.env }
    delete env.ELECTRON_RUN_AS_NODE
    
    this.child = spawn(runtime.executable, args, {
      cwd: homedir(),
      env,
      stdio: ['ignore', 'pipe', 'pipe'],
      windowsHide: true,
    })

    // 从 stdout 提取服务 URL
    let output = ''
    this.child.stdout?.on('data', (chunk: Buffer) => {
      output += chunk.toString()
      const extracted = extractWebUrl(output)
      if (extracted.url) this.url = extracted.url
    })

    await this.waitReady()
    await sleep(SETTLE_MS)  // 等 2.5s 稳定
    return this.url
  }

  async stop(graceMs = STOP_GRACE_MS): Promise<void> {
    if (!this.owned) {
      this.url = ''
      return  // 附着模式:不杀别人的进程
    }
    
    this.stopping = true
    const child = this.child
    this.child = undefined
    
    // 先 SIGTERM,超时后 SIGKILL
    const exited = new Promise<void>((resolve) => child.once('exit', () => resolve()))
    if (child.pid) killProcessTree(child.pid, child)
    
    const graceful = await Promise.race([
      exited.then(() => true),
      sleep(graceMs).then(() => false),
    ])
    if (!graceful) child.kill('SIGKILL')
  }
}

崩溃恢复electron/main.ts):

typescript 复制代码
const CRASH_RESTARTS = 3
let crashRestarts = 0

async function recoverHarness(code, signal) {
  crashRestarts++
  if (crashRestarts > CRASH_RESTARTS) {
    publishStatus('error', `Harness 多次退出(code ${code}, signal ${signal})`)
    return
  }
  publishStatus('starting', `引擎退出,正在第 ${crashRestarts} 次拉起...`)
  await bootAndNavigate()
}

优雅退出

typescript 复制代码
// electron/main.ts
app.on('will-quit', (event) => {
  if (harness?.owned && harness.running) {
    event.preventDefault()
    harness.stop().finally(() => app.exit(0))
  }
})

2.3 Node 运行时:四层回退策略

dsh web 需要 Node.js 22.19+ 或 24+。electron/node-runtime.ts 实现了一套优雅降级:

markdown 复制代码
优先级:
1. DSH_NODE_PATH 环境变量(开发者指定)
2. extraResources 里的 Node 22(打包时下载,安装包自带)
3. 系统 PATH 里的 node(开发态优先)
4. Electron 二进制充当 Node(ELECTRON_RUN_AS_NODE=1,最后手段)
typescript 复制代码
// electron/node-runtime.ts
export async function resolveNodeRuntime(packaged: boolean): Promise<NodeRuntime> {
  // 1. 环境变量覆盖
  const override = process.env.DSH_NODE_PATH?.trim()
  if (override) {
    const version = await readNodeVersion(override)
    if (isSupportedNodeVersion(version)) {
      return { executable: override, source: 'override', runAsElectronNode: false }
    }
  }

  // 2. 安装包内置 Node(resources/node/node)
  const bundled = resolveBundledNodePath()
  if (packaged && bundled) {
    const runtime = await tryPath(bundled, 'bundled')
    if (runtime) return runtime
  }

  // 3. 系统 Node
  const systemNode = await resolveWhich('node')
  if (systemNode) {
    const runtime = await tryPath(systemNode, 'system')
    if (runtime) return runtime
  }

  // 4. 回退:Electron 自己当 Node 用
  if (isSupportedNodeVersion(process.versions.node)) {
    return {
      executable: process.execPath,
      source: 'electron',
      runAsElectronNode: true,  // spawn 时会设置 ELECTRON_RUN_AS_NODE=1
    }
  }

  throw new Error('找不到合格的 Node 运行时')
}

2.4 IPC 桥:最小暴露原则

typescript 复制代码
// electron/preload.ts
contextBridge.exposeInMainWorld('desktop', {
  getInfo: () => ipcRenderer.invoke('desktop:get-info'),
  restartHarness: () => ipcRenderer.invoke('desktop:restart-harness'),
  restartSafe: () => ipcRenderer.invoke('desktop:restart-safe'),
  openLog: () => ipcRenderer.invoke('desktop:open-log'),
  setOverlay: (visible: boolean) => ipcRenderer.invoke('desktop:set-overlay', visible),
  
  minimize: () => ipcRenderer.invoke('window:minimize'),
  maximize: () => ipcRenderer.invoke('window:maximize'),
  close: () => ipcRenderer.invoke('window:close'),
  isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
  
  // 事件订阅(返回取消函数,防止内存泄漏)
  onMaximizedChange: (callback) => {
    const listener = (_, maximized) => callback(maximized)
    ipcRenderer.on('window:maximized', listener)
    return () => ipcRenderer.removeListener('window:maximized', listener)
  },
  onHarnessStatus: (callback) => {
    const listener = (_, status) => callback(status)
    ipcRenderer.on('harness:status', listener)
    return () => ipcRenderer.removeListener('harness:status', listener)
  },
})

contextBridge.exposeInMainWorld('updater', {
  check: () => ipcRenderer.invoke('update:check'),
  download: () => ipcRenderer.invoke('update:download'),
  install: () => ipcRenderer.invoke('update:install'),
  on: (channel, callback) => {
    const listener = (_, payload) => callback(payload)
    ipcRenderer.on(channel, listener)
    return () => ipcRenderer.removeListener(channel, listener)
  },
})

主进程路由electron/main.ts):

typescript 复制代码
function registerIpc() {
  ipcMain.handle('desktop:get-info', () => ({
    appVersion: app.getVersion(),
    dshVersion: resolveDshVersion(app.getAppPath()),
    url: harness?.url || null,
    phase: harnessPhase,
    message: harnessMessage,
    logPath: logPath(),
    safeMode: bootMode === 'safe',
  }))
  
  ipcMain.handle('desktop:restart-harness', async () => {
    await restartHarness('normal')
    return { ok: true }
  })
  
  ipcMain.handle('desktop:restart-safe', async () => {
    await restartHarness('safe')
    return { ok: true }
  })
  
  ipcMain.handle('window:minimize', () => shell.window?.minimize())
  ipcMain.handle('window:maximize', () => {
    const win = shell.window
    win?.isMaximized() ? win.unmaximize() : win?.maximize()
  })
}

2.5 壳层 UI:React 标题栏 + 启动页

tsx 复制代码
// src/TitleBar.tsx
export function TitleBar() {
  const [maximized, setMaximized] = useState(false)

  useEffect(() => {
    void window.desktop.isMaximized().then(setMaximized)
    return window.desktop.onMaximizedChange?.(setMaximized)
  }, [])

  return (
    <header className="titlebar">
      {/* 可拖拽区域 + 双击最大化 */}
      <div className="titlebar-drag" onDoubleClick={() => window.desktop?.maximize?.()}>
        <span className="titlebar-title">DeepSeek Harness</span>
      </div>
      <div className="titlebar-controls">
        <button onClick={() => window.desktop?.minimize?.()}>---</button>
        <button onClick={() => window.desktop?.maximize?.()}>
          {maximized ? '❐' : '□'}
        </button>
        <button className="titlebar-btn-close" onClick={() => window.desktop?.close?.()}>✕</button>
      </div>
    </header>
  )
}

启动页状态机(src/App.tsx):

tsx 复制代码
<div className="splash" hidden={harnessPhase === 'ready'}>
  <div className="brand">DeepSeek Harness</div>
  <div className={`status ${harnessPhase}`}>{harnessMessage}</div>
  
  {harnessPhase === 'error' ? (
    <div className="actions">
      <button onClick={restartHarness}>重试</button>
      <button onClick={restartSafe}>安全模式</button>
      <button onClick={() => window.desktop?.openLog?.()}>打开日志</button>
    </div>
  ) : null}
  
  {harnessPhase === 'error' && logTail ? (
    <pre className="log-tail">{logTail}</pre>
  ) : null}
</div>

安全模式 使用独立数据目录(userData/harness-safe),不加载 ~/.dsh 插件,用于排查第三方插件导致的启动失败。


2.6 自动更新:electron-updater + Generic 源

typescript 复制代码
// electron/updater.ts
export function setupAutoUpdater(getWindow: () => BrowserWindow | null): void {
  autoUpdater.logger = log
  autoUpdater.setFeedURL({
    provider: 'generic',
    url: process.env.UPDATE_SERVER_URL || 'http://localhost:8080/updates/',
  })
  autoUpdater.autoDownload = false  // 用户确认后再下

  autoUpdater.on('update-available', (info) => {
    send('update:available', { version: info.version })
  })
  
  autoUpdater.on('download-progress', (progress) => {
    send('update:download-progress', {
      percent: progress.percent,
      transferred: progress.transferred,
      total: progress.total,
    })
  })

  ipcMain.handle('update:check', async () => {
    if (!app.isPackaged) {
      return { ok: false, reason: 'DEV_MODE' }
    }
    const result = await autoUpdater.checkForUpdates()
    return { ok: true, updateInfo: result?.updateInfo ?? null }
  })

  ipcMain.handle('update:install', async () => {
    autoUpdater.quitAndInstall(false, true)
    return { ok: true }
  })
}

2.7 系统托盘

typescript 复制代码
// electron/tray.ts
export function createAppTray(callbacks: TrayCallbacks): Tray | null {
  try {
    const tray = new Tray(trayIcon())
    tray.setToolTip('DeepSeek Harness')
    tray.setContextMenu(
      Menu.buildFromTemplate([
        { label: '显示窗口', click: () => callbacks.show() },
        { label: '重启 Harness', click: () => callbacks.restart() },
        { type: 'separator' },
        { label: '退出', click: () => callbacks.quit() },
      ]),
    )
    tray.on('click', () => callbacks.show())
    return tray
  } catch (error) {
    log.warn('系统托盘不可用,关闭窗口将退出应用', error)
    return null
  }
}

三、建议补上的安全与能力(优先级排序)

3.1 权限拦截(🔴 P0)

当前官方页面没有权限拦截,可能被恶意脚本申请摄像头、通知、地理位置:

typescript 复制代码
// electron/main.ts - 建议添加
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
  const url = webContents.getURL()
  const allowed = ['notifications', 'clipboard-read']
  
  if (!allowed.includes(permission)) {
    console.warn(`🚫 拒绝权限 [${permission}] 来自 ${url}`)
    return callback(false)
  }
  callback(true)
})

3.2 网络隔离:webRequest 拦截

typescript 复制代码
session.defaultSession.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => {
  const url = new URL(details.url)
  const isAllowed = 
    url.hostname === '127.0.0.1' ||
    url.hostname === 'localhost'
  
  if (!isAllowed) {
    console.warn(`🚫 拦截外网请求: ${details.url}`)
    return callback({ cancel: true })
  }
  callback({ cancel: false })
})

3.3 原生文件对话框(避开 koffi 坑)

typescript 复制代码
// preload.js 添加
selectFolder: () => ipcRenderer.invoke('dialog:select-folder')

// main.ts 添加
ipcMain.handle('dialog:select-folder', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openDirectory'],
    title: '选择工作目录'
  })
  return result.filePaths[0] || null
})

3.4 系统电源管理

typescript 复制代码
const { powerMonitor } = require('electron')

powerMonitor.on('suspend', () => {
  mainWindow?.webContents.send('system:suspend')
})

powerMonitor.on('resume', () => {
  setTimeout(() => {
    mainWindow?.webContents.send('system:resume')
    checkEngineHealth()
  }, 2000)
})

3.5 全局快捷键

typescript 复制代码
app.on('ready', () => {
  globalShortcut.register('CommandOrControl+Shift+D', () => {
    shell.window?.isVisible() ? shell.window?.hide() : shell.window?.show()
  })
})

app.on('will-quit', () => globalShortcut.unregisterAll())

四、进阶优化:基于真实项目的补充建议

4.1 内存管理:窗口池与懒加载

Electron 窗口多了内存爆炸。建议实现窗口池

typescript 复制代码
class WindowPool {
  constructor(maxSize = 3) {
    this.pool = []
    this.maxSize = maxSize
  }

  acquire() {
    const win = this.pool.find(w => !w.isVisible())
    if (win) {
      win.show()
      return win
    }
    return this.create()
  }

  release(win) {
    if (this.pool.length < this.maxSize) {
      win.hide() // 不关闭,复用
      this.pool.push(win)
    } else {
      win.close()
    }
  }
}

4.2 崩溃监控:集成 Sentry

typescript 复制代码
const { crashReporter } = require('electron')

crashReporter.start({
  submitUrl: 'https://sentry.io/api/xxx/minidump/?sentry_key=xxx',
  uploadToServer: true,
  extra: { version: app.getVersion() }
})

process.on('uncaughtException', (error) => {
  console.error('主进程崩溃:', error)
  app.quit()
})

4.3 差量更新策略

electron-updater 默认全量下载,大应用体验差。可以结合 blockmap 实现差量更新,或分通道(stable/beta/canary)发布。

4.4 数据持久化:安全目录设计

typescript 复制代码
function getDataPath(isSafeMode = false) {
  const base = app.getPath('userData')
  if (isSafeMode) return join(base, 'harness-safe')
  if (!app.isPackaged) return join(base, 'dev')
  return base
}

4.5 跨平台路径与图标

typescript 复制代码
function getTrayIcon() {
  const iconName = process.platform === 'win32' 
    ? 'tray.ico' 
    : process.platform === 'darwin' 
      ? 'trayTemplate.png'
      : 'tray.png'
  return nativeImage.createFromPath(
    path.join(process.resourcesPath, 'icons', iconName)
  )
}

if (process.platform === 'win32') {
  app.setAppUserModelId('com.deepseek.desktop')
}

五、开发调试技巧

5.1 条件开启 DevTools

typescript 复制代码
if (!app.isPackaged) {
  mainWindow.webContents.openDevTools({ mode: 'detach' })
}

// 生产环境通过快捷键开启
globalShortcut.register('Shift+F12', () => {
  mainWindow.webContents.toggleDevTools()
})

5.2 Vite 热更新配置

typescript 复制代码
// vite.config.electron.ts
export default defineConfig({
  plugins: [
    react(),
    electron([
      {
        entry: 'electron/main.ts',
        onstart({ startup }) {
          delete process.env.ELECTRON_RUN_AS_NODE
          void startup()
        },
        vite: {
          build: {
            rollupOptions: {
              external: ['@deepseek-ai/dsh'],
            },
          },
        },
      },
      {
        entry: 'electron/preload.ts',
        onstart({ reload }) {
          reload()
        },
      },
    ]),
  ],
})

5.3 WSL 兼容:没有 sudo 也能跑 Electron

javascript 复制代码
// scripts/with-electron-libs.cjs
const packages = ['libnss3', 'libnspr4', 'libasound2t64']

function ensureLibs() {
  if (process.platform !== 'linux') return
  if (existsSync(join(libDir, 'libnss3.so'))) return

  // 下载 .deb 包并解压到项目目录
  execFileSync('apt-get', ['download', ...packages], { cwd: work })
  for (const deb of debs) {
    execFileSync('dpkg-deb', ['-x', deb, extractRoot])
  }
}

// 设置 LD_LIBRARY_PATH 指向项目内的库
env.LD_LIBRARY_PATH = libDir

六、完整项目结构(真实目录)

csharp 复制代码
dsh-desktop/
├── .github/
│   └── workflows/
│       └── release.yml          # CI:三平台构建 + 自动发版
├── build/
│   └── icon.icns / icon.ico
├── electron/                     # 主进程
│   ├── main.ts                   # 入口:生命周期 + IPC + 菜单
│   ├── window.ts                 # DesktopShell:窗口分层
│   ├── harness.ts                # HarnessServer:引擎管理
│   ├── node-runtime.ts           # Node 运行时四层回退
│   ├── preload.ts                # IPC 桥接
│   ├── tray.ts                   # 系统托盘
│   └── updater.ts                # 自动更新
├── src/                          # 壳层(React)
│   ├── App.tsx                   # 启动页 + 状态机
│   ├── TitleBar.tsx              # 自定义标题栏
│   ├── main.tsx                  # React 入口
│   └── style.css
├── scripts/
│   ├── prepare-node.cjs          # 下载内置 Node 22
│   └── with-electron-libs.cjs    # WSL 系统库兼容
├── resources/
│   └── node/                     # 内置 Node 二进制
├── electron-builder.json         # 打包配置
├── vite.config.electron.ts       # Vite + Electron 配置
└── package.json

七、CI/CD 与打包

7.1 electron-builder 配置

json 复制代码
{
  "appId": "com.example.dsh-desktop",
  "productName": "dsh-desktop",
  "asar": true,
  "npmRebuild": false,
  "files": ["dist/**/*", "dist-electron/**/*", "package.json"],
  "asarUnpack": [
    "node_modules/@deepseek-ai/**",
    "node_modules/node-pty/**",
    "node_modules/koffi/**",
    "node_modules/sharp/**"
  ],
  "extraResources": [
    {
      "from": "resources/node",
      "to": "node",
      "filter": ["node", "node.exe"]
    }
  ],
  "directories": { "output": "release" },
  "publish": [
    {
      "provider": "generic",
      "url": "http://localhost:8080/updates/"
    }
  ],
  "win": {
    "signAndEditExecutable": false,
    "target": [{ "target": "nsis", "arch": ["x64"] }]
  },
  "mac": {
    "identity": null,
    "category": "public.app-category.developer-tools",
    "target": ["dmg", "zip"]
  }
}

7.2 GitHub Actions 多平台构建

yaml 复制代码
# .github/workflows/release.yml
jobs:
  windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 11
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm electron:build:win
        env:
          CSC_IDENTITY_AUTO_DISCOVERY: false
      - uses: actions/upload-artifact@v4
        with:
          name: dsh-desktop-win-x64
          path: release/*.exe

  macos-arm64:
    runs-on: macos-latest
    steps:
      # ... 同上,产出 dmg + zip

  macos-x64:
    runs-on: macos-13
    steps:
      # ... 同上

  publish:
    needs: [windows, macos-arm64, macos-x64]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true
      - uses: softprops/action-gh-release@v2
        with:
          files: artifacts/*
          generate_release_notes: true

发版流程

bash 复制代码
git tag -a v0.2.1 -m "v0.2.1"
git push origin main
git push origin v0.2.1
# CI 自动构建 → 上传 artifact → 创建 GitHub Release

八、总结:路线图建议

阶段 优先级 内容
已落地 三进程架构、IPC 桥、窗口分层、Harness 三态机、Node 四层回退、asar/extraResources、自动更新、WSL 兼容
立即补 🔴 权限拦截 setPermissionRequestHandler、网络锁 webRequest、IPC 发送者校验
短期加 🟡 原生对话框、电源管理、全局快捷键、Windows appUserModelId
中期规划 🟢 utilityProcess 替代裸 spawn、差量更新、崩溃上报
长期建设 🔵 代码签名/公证(当前 identity: null)、自动化测试、性能监控

dsh-desktop 最值钱的地方不是"用了多少高级技术",而是每一个设计决策都对应一个真实场景:附着模式对应 CLI 用户、安全模式对应插件崩溃、四层回退对应环境差异、WSL 脚本对应开发便利性。这套代码是 Electron 工程化的优秀范本。


🔗 完整源码:github.com/hezihua/dsh...

相关推荐
GIS数据转换器28 分钟前
智慧林草“一张图“平台
java·大数据·服务器·前端·javascript·数据库·人工智能
2401_8858850432 分钟前
国际语音php接口代码示例:PHP使用cURL快速调用语音发送API
android·开发语言·前端·人工智能·python·php·语音识别
leoZ23133 分钟前
AI+前端提效-08 AI自动化文档:前端组件、接口、项目文档自动生成
前端·人工智能·深度学习·神经网络·目标检测·自然语言处理·自动化
lilian23338 分钟前
HarmonyOS 7 新特性(五)|ContainerReader 容器断点与自适应布局
前端·华为·harmonyos
计算机魔术师1 小时前
Google DeepMind 将 AI 科学家 Co-Scientist 扩展为实验室集成研究伙伴
前端
2601_965798471 小时前
BrandBoost WordPress Theme Review: Fast Agency Web Architecture
前端
律宏阔1 小时前
Electron 集成 Drizzle + SQLite 踩坑笔记
前端
律宏阔1 小时前
Electron preload.ts 类型无法自动推导的解决方案
前端
名字还没想好☜1 小时前
kubectl 排障实战:jsonpath 精准取值、custom-columns、events 排序与 top 速查
运维·前端·chrome·docker·kubernetes