基于真实开源项目 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,改用 WebContentsView。dsh-desktop 在 electron/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),
})
}
}
关键设计决策:
- 壳有 preload,官方页没有 ------ 官方 UI 走标准 Web 安全模型,防止内部 API 泄露
removeChildView而非destroy()------ 弹层隐藏时复用 WebContents,避免重复创建- 双导航锁 ------ 壳和官方页各自独立拦截外链
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...